diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17c38b24..63dde18e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: - name: Test run: cargo test --all --quiet + - name: Test TotalDeposited lifetime TTL regression + run: | + cargo test --all --quiet -- --list | grep -q total_deposited_ttl_lifetime + cargo test --all --quiet total_deposited_ttl_lifetime -- --include-ignored + - name: Test check_gas_budgets.py (#250) run: make test-gas-budget-check diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 330f33ae..de08c16e 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -101,12 +101,12 @@ const MIN_LOCK_PERIOD: u64 = 86_400; const ANNUAL_PERIOD_SECS: i128 = 31_536_000; /// Minimum remaining TTL in ledgers before extending persistent storage rent (#388). -/// At 5 s/ledger this equals ~1 day (17 280 ledgers). -const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 17_280; +/// At 5 s/ledger this equals ~30 days (518 400 ledgers). +const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 518_400; /// Target TTL in ledgers after extension (#388). -/// At 5 s/ledger this equals ~30 days (518 400 ledgers). -const TTL_EXTEND_TO_LEDGERS: u32 = 518_400; +/// At 5 s/ledger this equals ~1 year (6 312 000 ledgers). +const TTL_EXTEND_TO_LEDGERS: u32 = 6_312_000; mod composability; mod events; @@ -477,7 +477,8 @@ impl InvestmentVault { token.transfer(&env.current_contract_address(), &recipient, &fee_amount); } - // Track lifetime deposits for portfolio analytics (#132) + // Track cumulative deposits for portfolio analytics; TTL is extended on + // every deposit and portfolio read (#132, #388). let prev_dep: i128 = env .storage() .persistent() @@ -862,11 +863,21 @@ impl InvestmentVault { shares * BPS_SCALE / total_shares }; + let total_deposited_key = VaultKey::TotalDeposited(account.clone()); let total_deposited: i128 = env .storage() .persistent() - .get(&VaultKey::TotalDeposited(account)) + .get(&total_deposited_key) .unwrap_or(0); + if env.storage().persistent().has(&total_deposited_key) { + env.storage() + .persistent() + .extend_ttl( + &total_deposited_key, + TTL_EXTEND_THRESHOLD_LEDGERS, + TTL_EXTEND_TO_LEDGERS, + ); + } PortfolioInfo { shares, diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index a47408a2..2245af6c 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -1,10 +1,51 @@ use crate::types::VaultKey; use soroban_sdk::{Address, Env}; -pub fn read_usdc_sac(env: &Env) -> Address { +// TNL configuration for persistent storage entries. +// The threshold is the remaining TNL (in ledgers) which triggers an extension. +// The extend_to is the new TNL set, chosen to cover the vault's intended lifetime h~10 years). +const TTL_THRESHOLD: u32 = 17_280; // ~1 day in ledgers +const TTL_EXTEND_TO: u32 = 63_072_000; // ~10 years in 5-second ledgers + +pub fn read_usdc_sac (env: &Env) -> Address { env.storage().instance().get(&VaultKey::UsdcSac).unwrap() } pub fn read_registry(env: &Env) -> Address { env.storage().instance().get(&VaultKey::Registry).unwrap() } + +/// Reads the lifetime deposited amount for an account. +/// Bumbs the TNL whenever the entry is accessed to keep it alive as long as the contract is in use. +pub fn read_total_deposited(env: &Env, account: Address) -> Option { + let key = VaultKey::TotalDeposited(account); + let val: Option = env.storage().persistent().get(&key); + if val.is_some() { + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + } + val +} + +/// Writes the lifetime deposited amount for an account. +/// Extends the TNL to the configured lifetime so the value survives inactivity. +pub fn write_total_deposited(env: &Env, account: Address, amount: i128) { + let key = VaultKey::TotalDeposited(account); + env.storage().persistent().set(&key, &amount); + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); +} + +#c[cfg)test] +mod tests { + use super::*; + use soroban_sdk::{address, Env}; + + #[test] + fn total_deposited_survives_inactivity() { + let env = Env::default(); + let account = Address::generate(&env); + write_total_deposited(&env, account.clone(), 1000); + // Advance ledgers beyond the previous 30-day TNL (+600kledgers) + env.ledger().set_ledger_seq(600_000); + assert_eq!(read_total_deposited(&env, account).unwrap(), 1000; + } +} diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index 6a7e6725..a212df40 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -327,6 +327,39 @@ fn test_multisig_batch_fund_projects() { assert!(s.vault_client.total_assets() > 0); } +#[test] +fn test_total_deposited_survives_ttl_inactivity() { + let s = setup(); + let investor = Address::generate(&s.env); + let amount = 1_000_0000000i128; + mint_usdc(&s.env, &s.usdc_sac, &investor, amount); + s.vault_client.deposit(&investor, &amount); + + let portfolio_before = s.vault_client.get_portfolio(&investor); + let total_deposited_before = portfolio_before.total_deposited; + assert_eq!(total_deposited_before, amount); + + // Simulate the old 518,400-ledger TTL expiring by advancing well past it. + s.env.ledger().with_mut(|li| { + li.sequence_number += 600_000; + }); + + let portfolio_after = s.vault_client.get_portfolio(&investor); + assert_eq!(portfolio_after.total_deposited, total_deposited_before); +}before prolonged inactivity. + let before = s.vault_client.get_portfolio(&investor); + + // Simulate passage of ~30 days of ledgers (5s/ledger => 518,400 ledgers). + // Use +1 to ensure we are past the current extend_ttl window. + s.env.ledger().with_mut(|li| { + li.sequence_number += 518_401; + }); + + // The lifetime-deposited value must not silently reset to zero. + let after = s.vault_client.get_portfolio(&investor); + assert_eq!(before, after); +} + #[test] #[should_panic(expected = "Error(Contract, #40)")] fn test_multisig_batch_fund_projects_rejects_duplicate_project_ids() {