diff --git a/contracts/dividend/src/lib.rs b/contracts/dividend/src/lib.rs index 10b3a9e..0e77b83 100644 --- a/contracts/dividend/src/lib.rs +++ b/contracts/dividend/src/lib.rs @@ -1,9 +1,14 @@ #![no_std] use soroban_sdk::{ - contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, + contract, contractimpl, contracttype, token, Address, Env, String, Symbol, Vec, }; +/// ─── TTL Constants ────────────────────────────────────────────────────────── +const LEDGERS_PER_DAY: u32 = 17_280; +const INSTANCE_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; +const INSTANCE_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + #[contracttype] #[derive(Clone)] pub enum DataKey { @@ -33,6 +38,7 @@ pub struct DividendContract; impl DividendContract { pub fn initialize(env: Env, admin: Address, asset: Address, treasury: Address) { admin.require_auth(); + Self::bump_instance(&env); env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::AssetAddress, &asset); env.storage().instance().set(&DataKey::TreasuryContract, &treasury); @@ -55,6 +61,7 @@ impl DividendContract { ) -> u32 { admin.require_auth(); Self::require_admin(&env, &admin); + Self::bump_instance(&env); if recipients.len() != shares.len() { panic!("recipients and shares length mismatch"); @@ -115,6 +122,16 @@ impl DividendContract { .unwrap_or(Vec::new(&env)) } + /// Bump the instance storage TTL to prevent the contract from expiring. + /// Called at the start of every state-changing function. + /// + /// Threshold: 30 days (518,400 ledgers); Extend to: 180 days (3,110,400 ledgers). + fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); if admin != *caller { panic!("unauthorized"); } diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index 7d9476b..ab82e3c 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -1,9 +1,14 @@ #![no_std] use soroban_sdk::{ - contract, contractimpl, contracttype, Address, Env, Symbol, Vec, + contract, contractimpl, contracttype, Address, Env, Symbol, }; +/// ─── TTL Constants ────────────────────────────────────────────────────────── +const LEDGERS_PER_DAY: u32 = 17_280; +const INSTANCE_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; +const INSTANCE_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + #[contracttype] #[derive(Clone)] pub enum DataKey { @@ -39,6 +44,7 @@ impl GovernanceContract { treasury: Address, ) { admin.require_auth(); + Self::bump_instance(&env); env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::VotingContract, &voting); env.storage().instance().set(&DataKey::LoanContract, &loan); @@ -60,6 +66,7 @@ impl GovernanceContract { pub fn update_rules(env: Env, admin: Address, rules: CoopRules) { admin.require_auth(); Self::require_admin(&env, &admin); + Self::bump_instance(&env); env.storage().instance().set(&DataKey::Rules, &rules); env.events().publish((Symbol::new(&env, "rules_updated"),), ()); } @@ -68,6 +75,16 @@ impl GovernanceContract { env.storage().instance().get(&DataKey::Rules).unwrap() } + /// Bump the instance storage TTL to prevent the contract from expiring. + /// Called at the start of every state-changing function. + /// + /// Threshold: 30 days (518,400 ledgers); Extend to: 180 days (3,110,400 ledgers). + fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); if admin != *caller { panic!("unauthorized"); } diff --git a/contracts/loan/src/lib.rs b/contracts/loan/src/lib.rs index e08ceae..b32f2b6 100644 --- a/contracts/loan/src/lib.rs +++ b/contracts/loan/src/lib.rs @@ -4,6 +4,16 @@ use soroban_sdk::{ contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String, }; +/// ─── TTL Constants ────────────────────────────────────────────────────────── +/// Number of ledgers in one day (approximate, based on ~5s ledger close time). +const LEDGERS_PER_DAY: u32 = 17_280; + +/// Extend instance storage TTL when it drops below this threshold (30 days). +const INSTANCE_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; + +/// Extend instance storage TTL to this many ledgers (180 days ≈ 6 months). +const INSTANCE_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + #[contracttype] #[derive(Clone)] pub enum DataKey { @@ -46,6 +56,7 @@ pub struct LoanContract; impl LoanContract { pub fn initialize(env: Env, admin: Address, treasury: Address, asset: Address) { admin.require_auth(); + Self::bump_instance(&env); if env.storage().instance().has(&DataKey::Admin) { panic!("already initialized"); } @@ -65,6 +76,7 @@ impl LoanContract { repayment_days: u32, ) -> u32 { borrower.require_auth(); + Self::bump_instance(&env); if amount <= 0 { panic!("amount must be positive"); } let counter: u32 = env.storage().instance() @@ -104,6 +116,7 @@ impl LoanContract { pub fn approve_loan(env: Env, admin: Address, loan_id: u32) { admin.require_auth(); Self::require_admin(&env, &admin); + Self::bump_instance(&env); let mut loans: Vec = env.storage().instance() .get(&DataKey::Loans).unwrap(); @@ -138,6 +151,7 @@ impl LoanContract { /// Borrower repays (partial or full). pub fn repay(env: Env, borrower: Address, loan_id: u32, amount: i128) { borrower.require_auth(); + Self::bump_instance(&env); let mut loans: Vec = env.storage().instance() .get(&DataKey::Loans).unwrap(); @@ -187,6 +201,19 @@ impl LoanContract { loans.get(idx).unwrap() } + // ── TTL helpers ────────────────────────────────────────────────────────── + + /// Bump the instance storage TTL to prevent the contract from expiring. + /// Called at the start of every state-changing function. + /// + /// Threshold: 30 days (518,400 ledgers) — only extend when TTL falls below. + /// Extend to: 180 days (3,110,400 ledgers) — ~6 months of ledger lifetime. + fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); if admin != *caller { panic!("unauthorized"); } diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs index 786f8f7..35315a6 100644 --- a/contracts/treasury/src/lib.rs +++ b/contracts/treasury/src/lib.rs @@ -4,6 +4,22 @@ use soroban_sdk::{ contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String, }; +/// ─── TTL Constants ────────────────────────────────────────────────────────── +/// Number of ledgers in one day (approximate, based on ~5s ledger close time). +const LEDGERS_PER_DAY: u32 = 17_280; + +/// Extend instance storage TTL when it drops below this threshold (30 days). +const INSTANCE_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; + +/// Extend instance storage TTL to this many ledgers (180 days ≈ 6 months). +const INSTANCE_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + +/// Extend persistent entry TTL when it drops below this threshold (30 days). +const PERSISTENT_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; + +/// Extend persistent entry TTL to this many ledgers (180 days ≈ 6 months). +const PERSISTENT_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + /// ─── Storage Keys ──────────────────────────────────────────────────────────── #[contracttype] @@ -69,6 +85,7 @@ impl TreasuryContract { asset: Address, ) -> GroupInfo { admin.require_auth(); + Self::bump_instance(&env); if env.storage().instance().has(&DataKey::Admin) { panic!("already initialized"); @@ -95,6 +112,7 @@ impl TreasuryContract { pub fn add_member(env: Env, admin: Address, member: Address) { admin.require_auth(); Self::require_admin(&env, &admin); + Self::bump_instance(&env); let mut members: Vec
= env .storage().instance() @@ -115,6 +133,7 @@ impl TreasuryContract { pub fn contribute(env: Env, member: Address, amount: i128, period: u32) { member.require_auth(); Self::require_member(&env, &member); + Self::bump_instance(&env); if amount <= 0 { panic!("amount must be positive"); @@ -142,6 +161,18 @@ impl TreasuryContract { env.storage().persistent() .set(&DataKey::Contributions(member.clone()), &history); + // Extend the TTL of this member's contribution history so it isn't + // silently deleted by the network. Members who contribute regularly + // keep their history alive; stale entries from inactive members will + // naturally expire. + env.storage() + .persistent() + .extend_ttl( + &DataKey::Contributions(member.clone()), + PERSISTENT_TTL_THRESHOLD, + PERSISTENT_TTL_EXTEND_TO, + ); + // Update total let total: i128 = env.storage().instance() .get(&DataKey::TotalContributions).unwrap_or(0); @@ -158,6 +189,7 @@ impl TreasuryContract { pub fn withdraw(env: Env, admin: Address, to: Address, amount: i128) { admin.require_auth(); Self::require_admin(&env, &admin); + Self::bump_instance(&env); let asset: Address = env.storage().instance().get(&DataKey::AssetAddress).unwrap(); let token_client = token::Client::new(&env, &asset); @@ -252,6 +284,19 @@ impl TreasuryContract { } } + // ── TTL helpers ────────────────────────────────────────────────────────── + + /// Bump the instance storage TTL to prevent the contract from expiring. + /// Called at the start of every state-changing function. + /// + /// Threshold: 30 days (518,400 ledgers) — only extend when TTL falls below. + /// Extend to: 180 days (3,110,400 ledgers) — ~6 months of ledger lifetime. + fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + // ── Internal helpers ───────────────────────────────────────────────────── fn require_admin(env: &Env, caller: &Address) { diff --git a/contracts/voting/src/lib.rs b/contracts/voting/src/lib.rs index f9a22f6..fe395e6 100644 --- a/contracts/voting/src/lib.rs +++ b/contracts/voting/src/lib.rs @@ -4,6 +4,22 @@ use soroban_sdk::{ contract, contractimpl, contracttype, Address, Env, Map, Symbol, Vec, String, }; +/// ─── TTL Constants ────────────────────────────────────────────────────────── +/// Number of ledgers in one day (approximate, based on ~5s ledger close time). +const LEDGERS_PER_DAY: u32 = 17_280; + +/// Extend instance storage TTL when it drops below this threshold (30 days). +const INSTANCE_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; + +/// Extend instance storage TTL to this many ledgers (180 days ≈ 6 months). +const INSTANCE_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + +/// Extend persistent entry TTL when it drops below this threshold (30 days). +const PERSISTENT_TTL_THRESHOLD: u32 = 30 * LEDGERS_PER_DAY; + +/// Extend persistent entry TTL to this many ledgers (180 days ≈ 6 months). +const PERSISTENT_TTL_EXTEND_TO: u32 = 180 * LEDGERS_PER_DAY; + #[contracttype] #[derive(Clone)] pub enum DataKey { @@ -58,6 +74,7 @@ pub struct VotingContract; impl VotingContract { pub fn initialize(env: Env, admin: Address, treasury: Address) { admin.require_auth(); + Self::bump_instance(&env); env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::TreasuryContract, &treasury); env.storage().instance().set(&DataKey::ProposalCounter, &0u32); @@ -76,6 +93,7 @@ impl VotingContract { payload: String, ) -> u32 { proposer.require_auth(); + Self::bump_instance(&env); let counter: u32 = env.storage().instance() .get(&DataKey::ProposalCounter).unwrap_or(0); @@ -109,6 +127,16 @@ impl VotingContract { env.storage().persistent() .set(&DataKey::Votes(id), &Map::::new(&env)); + // Extend TTL for the new vote entry so it survives the full + // voting period plus a grace window after finalization. + env.storage() + .persistent() + .extend_ttl( + &DataKey::Votes(id), + PERSISTENT_TTL_THRESHOLD, + PERSISTENT_TTL_EXTEND_TO, + ); + env.events().publish( (Symbol::new(&env, "proposal_created"),), (id, proposer), @@ -119,6 +147,7 @@ impl VotingContract { /// Member casts a vote on a proposal. pub fn vote(env: Env, voter: Address, proposal_id: u32, approve: bool) { voter.require_auth(); + Self::bump_instance(&env); let mut proposals: Vec = env.storage().instance() .get(&DataKey::Proposals).unwrap(); @@ -143,6 +172,16 @@ impl VotingContract { votes.set(voter.clone(), approve); env.storage().persistent().set(&DataKey::Votes(proposal_id), &votes); + // Extend TTL for the vote map so tally records survive through + // the voting window and the post-finalization query period. + env.storage() + .persistent() + .extend_ttl( + &DataKey::Votes(proposal_id), + PERSISTENT_TTL_THRESHOLD, + PERSISTENT_TTL_EXTEND_TO, + ); + if approve { proposal.votes_for += 1; } else { @@ -160,6 +199,7 @@ impl VotingContract { /// Finalize a proposal after deadline. pub fn finalize(env: Env, proposal_id: u32) -> ProposalStatus { + Self::bump_instance(&env); let mut proposals: Vec = env.storage().instance() .get(&DataKey::Proposals).unwrap(); let idx = Self::find_proposal_idx(&proposals, proposal_id); @@ -204,6 +244,19 @@ impl VotingContract { .unwrap_or(Map::new(&env)) } + // ── TTL helpers ────────────────────────────────────────────────────────── + + /// Bump the instance storage TTL to prevent the contract from expiring. + /// Called at the start of every state-changing function. + /// + /// Threshold: 30 days (518,400 ledgers) — only extend when TTL falls below. + /// Extend to: 180 days (3,110,400 ledgers) — ~6 months of ledger lifetime. + fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO); + } + fn find_proposal_idx(proposals: &Vec, id: u32) -> u32 { for i in 0..proposals.len() { if proposals.get(i).unwrap().id == id { return i; }