Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions contracts/claims-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ enum StorageKey {
/// Configurable delay in seconds between claim approval and payout (u64).
/// 0 = immediate payout (default behavior).
PayoutDelay,
/// Identity attestation requirement: product_id → Symbol (id_type required).
IdentityRequirement(u128),
}

// ─── Errors ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -158,6 +160,8 @@ pub enum Error {
ClaimDeadlinePassed = 19,
/// Payout delay has not yet elapsed — the claim cannot be settled now.
PayoutDelayNotElapsed = 20,
/// Identity verification required for this claim category but not verified.
IdentityVerificationRequired = 21,
}

/// Approximate Stellar ledger close time in seconds, used to convert
Expand Down Expand Up @@ -1475,6 +1479,89 @@ impl ClaimsProcessor {
overdue
}

// ── Identity Verification ───────────────────────────────────────────────

/// Admin-only: mark a product/category as requiring identity verification.
/// Claimants must have verified identity (via oracle-verifier attestation)
/// to claim payouts for this product.
pub fn require_identity_for_category(
env: Env,
admin: Address,
product_id: u128,
id_type: Symbol,
) {
Self::require_admin(&env, &admin);

let key = StorageKey::IdentityRequirement(product_id);
env.storage().instance().set(&key, &id_type);

env.events().publish(
(Symbol::new(&env, "identity_requirement_set"),),
(product_id, id_type),
);
}

/// Admin-only: remove identity verification requirement for a product.
pub fn remove_identity_requirement(env: Env, admin: Address, product_id: u128) {
Self::require_admin(&env, &admin);

let key = StorageKey::IdentityRequirement(product_id);
env.storage().instance().remove(&key);

env.events().publish(
(Symbol::new(&env, "identity_requirement_removed"),),
product_id,
);
}

/// Admin-only: manually verify a claimant's identity for a claim.
/// Used when off-chain identity verification is completed or approved by DAO.
pub fn verify_claimant_identity(
env: Env,
admin: Address,
claim_id: u128,
id_type: Symbol,
) {
Self::require_admin(&env, &admin);

let mut claim: Claim = env
.storage()
.persistent()
.get(&StorageKey::Claim(claim_id))
.unwrap_or_else(|| panic_with_error!(&env, Error::ClaimNotFound));

let now = env.ledger().timestamp();
claim.identity_verified = true;
claim.verification_type = Some(id_type.clone());
claim.verification_time = Some(now);

let key = StorageKey::Claim(claim_id);
env.storage().persistent().set(&key, &claim);
Self::extend_claim_ttl(&env, &key);

env.events().publish(
(Symbol::new(&env, "identity_verified"),),
(claim_id, id_type, now),
);
}

/// Get the identity requirement for a product (if set).
pub fn get_identity_requirement(env: Env, product_id: u128) -> Option<Symbol> {
env.storage()
.instance()
.get(&StorageKey::IdentityRequirement(product_id))
}

/// Check if a claimant's identity is verified for a particular ID type.
/// This is a utility for off-chain systems to verify attestation status.
pub fn is_identity_verified(env: Env, claim_id: u128) -> bool {
env.storage()
.persistent()
.get(&StorageKey::Claim(claim_id))
.map(|claim: Claim| claim.identity_verified)
.unwrap_or(false)
}

// ── Internal helpers ─────────────────────────────────────────────────────

/// Evaluate a pending claim and settle it against the configured oracle trigger.
Expand Down
6 changes: 6 additions & 0 deletions contracts/claims-processor/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ pub struct Claim {
pub partial_payout_bps: Option<u32>,
/// Installment payout configuration for large claims.
pub installments: Option<InstallmentSchedule>,
/// Whether the claimant's identity was verified (optional, for Sybil protection).
pub identity_verified: bool,
/// Type of identity verification performed (e.g., "kyc", "accreditation").
pub verification_type: Option<Symbol>,
/// Timestamp when identity verification occurred.
pub verification_time: Option<u64>,
/// Timestamp at which payout becomes available (issue #432).
/// `None` means payout is immediate or not applicable.
pub payout_ready_at: Option<u64>,
Expand Down
123 changes: 123 additions & 0 deletions contracts/governance-dao/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ enum StorageKey {
RiskPool,
/// On-chain audit trail record for executed proposal — proposal_id -> ExecutionAuditRecord.
ExecutionAudit(u64),
/// Proposal comment by ID — comment_id -> ProposalComment.
ProposalComment(u128),
/// Next comment ID counter for a proposal — proposal_id -> u128.
NextCommentId(u64),
}


Expand Down Expand Up @@ -157,6 +161,8 @@ pub enum Error {
/// `vote_batch` was called with an empty proposal list.
NoProposals = 40,
InvalidInput = 41,
/// Proposal passed but execution deadline has expired without execution.
ExecutionDeadlineExpired = 42,
}

#[contract]
Expand Down Expand Up @@ -303,6 +309,7 @@ impl GovernanceDao {
created_at: now,
vote_end,
execution_time: 0,
execution_deadline: vote_end.saturating_add(config.finalize_delay).saturating_add(7 * 24 * 3600),
total_supply: config.total_supply,
kind: ProposalKind::Standard,
impact_analysis,
Expand Down Expand Up @@ -406,6 +413,7 @@ impl GovernanceDao {
created_at: now,
vote_end,
execution_time: 0,
execution_deadline: vote_end.saturating_add(config.finalize_delay).saturating_add(7 * 24 * 3600),
total_supply: config.total_supply,
kind: ProposalKind::Upgrade,
impact_analysis,
Expand Down Expand Up @@ -1091,6 +1099,14 @@ impl GovernanceDao {
if env.ledger().timestamp() < proposal.execution_time {
panic_with_error!(&env, Error::TimelockNotExpired);
}
// Check execution deadline: if passed, prevent execution to keep proposals fresh
if env.ledger().timestamp() > proposal.execution_deadline {
proposal.status = ProposalStatus::Expired;
env.storage()
.persistent()
.set(&StorageKey::Proposal(proposal_id), &proposal);
panic_with_error!(&env, Error::ExecutionDeadlineExpired);
}

// Validate target address is a valid Stellar address before execution
// This is a defense-in-depth check to prevent targeting invalid contracts
Expand Down Expand Up @@ -1140,6 +1156,113 @@ impl GovernanceDao {
.unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound))
}

// ── On-Chain Discussion ──────────────────────────────────────────────────

/// Post a comment on a proposal during the Discussion or Active phase.
/// Comments are stored on-chain for transparent discussion.
/// `reply_to` is optional and allows threading of comments.
pub fn add_comment(
env: Env,
commenter: Address,
proposal_id: u64,
text: Bytes,
reply_to: Option<u128>,
) -> u128 {
commenter.require_auth();

// Validate text length (max 1024 bytes)
if text.is_empty() || text.len() > 1024 {
panic_with_error!(&env, Error::InvalidInput);
}

// Verify proposal exists
let proposal: Proposal = env
.storage()
.persistent()
.get(&StorageKey::Proposal(proposal_id))
.unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound));

// Only allow comments during Discussion or Active phases
if proposal.status != ProposalStatus::Discussion && proposal.status != ProposalStatus::Active {
panic_with_error!(&env, Error::InvalidInput);
}

// If reply_to is specified, verify that comment exists
if let Some(reply_id) = reply_to {
if !env
.storage()
.persistent()
.has(&StorageKey::ProposalComment(reply_id))
{
panic_with_error!(&env, Error::ProposalNotFound);
}
}

// Generate comment ID
let comment_id: u128 = env
.storage()
.instance()
.get(&StorageKey::NextCommentId(proposal_id))
.unwrap_or(1u128);

let now = env.ledger().timestamp();
let comment = ProposalComment {
id: comment_id,
proposal_id,
author: commenter.clone(),
text,
created_at: now,
reply_to,
};

let comment_key = StorageKey::ProposalComment(comment_id);
env.storage().persistent().set(&comment_key, &comment);
env.storage()
.persistent()
.extend_ttl(&comment_key, TTL_THRESHOLD, TTL_EXTEND_TO);

env.storage()
.instance()
.set(&StorageKey::NextCommentId(proposal_id), &(comment_id.saturating_add(1)));

env.events().publish(
(Symbol::new(&env, "comment_added"),),
ProposalCommentAdded {
proposal_id,
comment_id,
author: commenter,
reply_to,
created_at: now,
},
);

comment_id
}

/// Retrieve a comment by its ID.
pub fn get_comment(env: Env, comment_id: u128) -> ProposalComment {
env.storage()
.persistent()
.get(&StorageKey::ProposalComment(comment_id))
.unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound))
}

/// Admin-only: delete a comment (for moderation of spam/abuse).
pub fn delete_comment(env: Env, admin: Address, comment_id: u128) {
Self::require_admin(&env, &admin);

let comment_key = StorageKey::ProposalComment(comment_id);
if !env.storage().persistent().has(&comment_key) {
panic_with_error!(&env, Error::ProposalNotFound);
}

env.storage().persistent().remove(&comment_key);

env.events().publish(
(Symbol::new(&env, "comment_deleted"),),
comment_id,
);
}

/// Admin-only: cancel an Active proposal before voting closes.
///
Expand Down
29 changes: 29 additions & 0 deletions contracts/governance-dao/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ pub enum ProposalStatus {
Executed,
/// Cancelled by admin before vote close
Cancelled,
/// Passed but execution deadline expired without execution
Expired,
}

/// On-chain comment on a proposal for discussion and feedback.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposalComment {
pub id: u128,
pub proposal_id: u64,
pub author: Address,
pub text: Bytes,
pub created_at: u64,
/// Optional: ID of the comment this replies to, for threaded discussion
pub reply_to: Option<u128>,
}

/// Vote direction cast by a token holder.
Expand Down Expand Up @@ -83,6 +98,9 @@ pub struct Proposal {
pub vote_end: u64,
/// Timelock expiration timestamp for execution.
pub execution_time: u64,
/// Timestamp after which a passed proposal can no longer be executed.
/// Defaults to vote_end + finalize_delay + 7 days. Prevents stale proposals from executing.
pub execution_deadline: u64,
/// Total supply captured at proposal creation time for quorum calculation.
/// This prevents admin manipulation of total_supply during active votes.
pub total_supply: i128,
Expand Down Expand Up @@ -467,3 +485,14 @@ pub struct ExecutionVerificationFailed {
pub callback: Symbol,
pub error: Symbol,
}

/// Emitted when a comment is posted on a proposal for discussion.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposalCommentAdded {
pub proposal_id: u64,
pub comment_id: u128,
pub author: Address,
pub reply_to: Option<u128>,
pub created_at: u64,
}
Loading