From 419b85ad69583de029f74ac81c852978e5cc9d60 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:36:01 +0100 Subject: [PATCH 01/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 330f33ae..c275feca 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -105,8 +105,8 @@ const ANNUAL_PERIOD_SECS: i128 = 31_536_000; const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 17_280; /// 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 ~10 years (63 072 000 ledgers). +const TTL_EXTEND_TO_LEDGERS: u32 = 63_072_000; mod composability; mod events; From 9a0163e2724e7ffd72774c9fda5cff14636f4b7b Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:36:03 +0100 Subject: [PATCH 02/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/test.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index 6a7e6725..56a8d3ba 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -3191,6 +3191,35 @@ fn test_get_portfolio_zero_for_nondepositor() { assert_eq!(portfolio.total_deposited, 0); } +#[test] +fn test_total_deposited_survives_ttl_window_when_read() { + let s = setup(); + let investor = Address::generate(&s.env); + let amount = 1_000_0000000i128; + mint_usdc(&s.env, &s.usdc_sac, &investor, amount); + + // Use a known ledger sequence so the original 518,400-ledger TTL expiry + // is deterministic. + s.env.ledger().with_mut(|li| { + li.sequence_number = 100; + }); + s.vault_client.deposit(&investor, &amount); + assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); + + // Move to just before the original TTL would expire; get_portfolio must + // refresh the entry's lifetime so a later read still sees it. + s.env.ledger().with_mut(|li| { + li.sequence_number = 100 + 518_400 - 1; + }); + assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); + + // Advance just past the original expiry and confirm the value is intact. + s.env.ledger().with_mut(|li| { + li.sequence_number = 100 + 518_400 + 1; + }); + assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); +} + #[test] fn test_insurance_fund_balance_starts_at_zero() { let s = setup(); From c41c40c6d9deb9f55b3db6a38198afc9803f3775 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:36:05 +0100 Subject: [PATCH 03/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/storage.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index a47408a2..e6abe5f2 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -1,5 +1,5 @@ -use crate::types::VaultKey; -use soroban_sdk::{Address, Env}; +use crate:::Types::VaultKey; +use soroban_sdk::{:Address, Env}; pub fn read_usdc_sac(env: &Env) -> Address { env.storage().instance().get(&VaultKey::UsdcSac).unwrap() @@ -8,3 +8,7 @@ pub fn read_usdc_sac(env: &Env) -> Address { pub fn read_registry(env: &Env) -> Address { env.storage().instance().get(&VaultKey::Registry).unwrap() } + +pub fn bump_total_deposited_ttl(env: &Env, account: &Address) { + env.storage().persistent().extend_ttl(&VaultKey::TotalDeposited(account.clone()), 17280, u32::MAX); +} \ No newline at end of file From da6b60afdf4612ed98687601f3943d42d735c2ba Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:49:14 +0100 Subject: [PATCH 04/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index c275feca..b72511e6 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -862,11 +862,17 @@ impl InvestmentVault { shares * BPS_SCALE / total_shares }; + let key = VaultKey::TotalDeposited(account); let total_deposited: i128 = env .storage() .persistent() - .get(&VaultKey::TotalDeposited(account)) + .get(&key) .unwrap_or(0); + if env.storage().persistent().has(&key) { + env.storage() + .persistent() + .extend_ttl(&key, TTL_EXTEND_THRESHOLD_LEDGERS, TTL_EXTEND_TO_LEDGERS); + } PortfolioInfo { shares, From 8ac6ac64e142b90becf2802542ecee0f385f4671 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:49:16 +0100 Subject: [PATCH 05/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/test.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index 56a8d3ba..f51dadb9 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -282,6 +282,26 @@ fn test_receive_yield_with_approvals_after_multisig_enabled() { assert!(s.vault_client.claimable_yield(&investor) > 0); } +#[test] +fn test_total_deposited_persists_after_ttl_elapses() { + 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 before = s.vault_client.get_portfolio(&investor); + assert!(before.total_deposited > 0); + + // Advance past the ~30-day TTL (518,400 ledgers) to expose the bug. + s.env.ledger().with_mut(|li| { + li.sequence_number += 518_401; + }); + + let after = s.vault_client.get_portfolio(&investor); + assert_eq!(before.total_deposited, after.total_deposited); +} + #[test] fn test_multisig_batch_fund_projects() { let s = setup(); From 82503de1de7c2131b3f1fc3f56804143e6d79502 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 22:49:17 +0100 Subject: [PATCH 06/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/storage.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index e6abe5f2..6278ea1a 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -1,5 +1,5 @@ -use crate:::Types::VaultKey; -use soroban_sdk::{:Address, Env}; +use crate::Types::VaultKey; +use soroban_sdk:{Address, Env}; pub fn read_usdc_sac(env: &Env) -> Address { env.storage().instance().get(&VaultKey::UsdcSac).unwrap() @@ -11,4 +11,23 @@ pub fn read_registry(env: &Env) -> Address { pub fn bump_total_deposited_ttl(env: &Env, account: &Address) { env.storage().persistent().extend_ttl(&VaultKey::TotalDeposited(account.clone()), 17280, u32::MAX); +} + +#c[cfg(test)] +mod test { + use super::*; + use soroban_sdk:{Env, Address, testutils::Address as _}; + + #[test] + fn test_total_deposited_ttl_extended_to_max() { + let env = Env::default(); + let account = Address::generate(&env); + let key = VaultKey::TotalDeposited(account.clone()); + env.storage().persistent().set(&key, &123i128); + bump_total_deposited_ttl(&env, &account); + let current = env.ledger().sequence(); + env.ledger().with_mutable(|li| li.sequence = current + 600_000); + let value: i128 = env.storage().persistent().get(&key).unwap(); + assert_eq(!value, 123); + } } \ No newline at end of file From 8ab57dcddc70eb23f9e0486b19841da3c6b2dce6 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:05:46 +0100 Subject: [PATCH 07/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/lib.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index b72511e6..1931d29d 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 ~1 year (the protocol maximum). +const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 6_312_000; /// Target TTL in ledgers after extension (#388). -/// At 5 s/ledger this equals ~10 years (63 072 000 ledgers). -const TTL_EXTEND_TO_LEDGERS: u32 = 63_072_000; +/// At 5 s/ledger this equals ~1 year (the protocol maximum). +const TTL_EXTEND_TO_LEDGERS: u32 = 6_312_000; mod composability; mod events; @@ -477,7 +477,7 @@ impl InvestmentVault { token.transfer(&env.current_contract_address(), &recipient, &fee_amount); } - // Track lifetime deposits for portfolio analytics (#132) + // Track cumulative deposits for portfolio analytics (#132) let prev_dep: i128 = env .storage() .persistent() @@ -863,16 +863,15 @@ impl InvestmentVault { }; let key = VaultKey::TotalDeposited(account); - let total_deposited: i128 = env - .storage() - .persistent() - .get(&key) - .unwrap_or(0); - if env.storage().persistent().has(&key) { - env.storage() - .persistent() - .extend_ttl(&key, TTL_EXTEND_THRESHOLD_LEDGERS, TTL_EXTEND_TO_LEDGERS); - } + let total_deposited: i128 = match env.storage().persistent().get(&key) { + Some(value) => { + env.storage() + .persistent() + .extend_ttl(&key, TTL_EXTEND_THRESHOLD_LEDGERS, TTL_EXTEND_TO_LEDGERS); + value + } + None => 0, + }; PortfolioInfo { shares, From 3f3bc3181ae3e8e3cc0939e43946e86115d1318f Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:05:47 +0100 Subject: [PATCH 08/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/test.rs | 48 +++++++++--------------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index f51dadb9..a03ca4f5 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -282,26 +282,6 @@ fn test_receive_yield_with_approvals_after_multisig_enabled() { assert!(s.vault_client.claimable_yield(&investor) > 0); } -#[test] -fn test_total_deposited_persists_after_ttl_elapses() { - 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 before = s.vault_client.get_portfolio(&investor); - assert!(before.total_deposited > 0); - - // Advance past the ~30-day TTL (518,400 ledgers) to expose the bug. - s.env.ledger().with_mut(|li| { - li.sequence_number += 518_401; - }); - - let after = s.vault_client.get_portfolio(&investor); - assert_eq!(before.total_deposited, after.total_deposited); -} - #[test] fn test_multisig_batch_fund_projects() { let s = setup(); @@ -3212,32 +3192,26 @@ fn test_get_portfolio_zero_for_nondepositor() { } #[test] -fn test_total_deposited_survives_ttl_window_when_read() { +fn test_total_deposited_survives_past_ttl_window() { let s = setup(); let investor = Address::generate(&s.env); let amount = 1_000_0000000i128; - mint_usdc(&s.env, &s.usdc_sac, &investor, amount); - // Use a known ledger sequence so the original 518,400-ledger TTL expiry - // is deterministic. - s.env.ledger().with_mut(|li| { - li.sequence_number = 100; - }); + mint_usdc(&s.env, &s.usdc_sac, &investor, amount); s.vault_client.deposit(&investor, &amount); - assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); + assert_eq!( + s.vault_client.get_portfolio(&investor).total_deposited, + amount + ); - // Move to just before the original TTL would expire; get_portfolio must - // refresh the entry's lifetime so a later read still sees it. + // Advance beyond the maximum TTL (518,400 ledgers) currently applied to + // TotalDeposited. The lifetime value must not reset to zero. s.env.ledger().with_mut(|li| { - li.sequence_number = 100 + 518_400 - 1; + li.sequence_number += 518_400 + 1; }); - assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); - // Advance just past the original expiry and confirm the value is intact. - s.env.ledger().with_mut(|li| { - li.sequence_number = 100 + 518_400 + 1; - }); - assert_eq!(s.vault_client.get_portfolio(&investor).total_deposited, amount); + let portfolio = s.vault_client.get_portfolio(&investor); + assert_eq!(portfolio.total_deposited, amount); } #[test] From 65b59041d2aee169ab8ec12efb4495a38f2ab53a Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:05:49 +0100 Subject: [PATCH 09/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/storage.rs | 52 ++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index 6278ea1a..57d5dcce 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -1,7 +1,13 @@ -use crate::Types::VaultKey; -use soroban_sdk:{Address, Env}; +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 (~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() } @@ -9,25 +15,37 @@ pub fn read_registry(env: &Env) -> Address { env.storage().instance().get(&VaultKey::Registry).unwrap() } -pub fn bump_total_deposited_ttl(env: &Env, account: &Address) { - env.storage().persistent().extend_ttl(&VaultKey::TotalDeposited(account.clone()), 17280, u32::MAX); +/// Reads the lifetime deposited amount for an account. +/// Bumps 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 } -#c[cfg(test)] -mod test { +/// 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); +} + +[cfg(test)] +mod tests { use super::*; - use soroban_sdk:{Env, Address, testutils::Address as _}; + use soroban_sdk::{Address, Env}; #[test] - fn test_total_deposited_ttl_extended_to_max() { + fn total_deposited_survives_inactivity() { let env = Env::default(); let account = Address::generate(&env); - let key = VaultKey::TotalDeposited(account.clone()); - env.storage().persistent().set(&key, &123i128); - bump_total_deposited_ttl(&env, &account); - let current = env.ledger().sequence(); - env.ledger().with_mutable(|li| li.sequence = current + 600_000); - let value: i128 = env.storage().persistent().get(&key).unwap(); - assert_eq(!value, 123); + 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); } -} \ No newline at end of file +} From f5ab70e9a044c457152f2b2a5ac0e8dcc4b62f5e Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:25:52 +0100 Subject: [PATCH 10/17] fix(ci): resolve failing checks for #316 --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) 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 From a59656504123586a5185346126b953a5eed6d572 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:25:57 +0100 Subject: [PATCH 11/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 1931d29d..1efe72d4 100644 --- a/investment_vault/src/lib.rs +++ b/investment_vault/src/lib.rs @@ -838,6 +838,10 @@ impl InvestmentVault { // ── Portfolio analytics (#132) ───────────────────────────────────────────── /// Return a full on-chain portfolio snapshot for `account`. + /// + /// Note: `total_deposited` is cumulative but not guaranteed indefinitely; the + /// persistent entry's TTL is extended on deposits and reads, but after + /// `TTL_EXTEND_TO_LEDGERS` ledgers (~1 year) without activity it may read 0. pub fn get_portfolio(env: Env, account: Address) -> PortfolioInfo { require_current_state(&env); let shares = Base::balance(&env, &account); From 7a0d170a859ad1804e6f8b42657f2a754972a87d Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:26:01 +0100 Subject: [PATCH 12/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/test.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index a03ca4f5..aac8beb2 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -3204,10 +3204,23 @@ fn test_total_deposited_survives_past_ttl_window() { amount ); - // Advance beyond the maximum TTL (518,400 ledgers) currently applied to - // TotalDeposited. The lifetime value must not reset to zero. + // Advance to just inside the TTL threshold (17,280 ledgers remaining) so a + // read from get_portfolio must renew the TotalDeposited entry. s.env.ledger().with_mut(|li| { - li.sequence_number += 518_400 + 1; + li.sequence_number += 518_400 - 17_280 + 1; + }); + + // This read exercises the TTL bump on get_portfolio: the value must still + // be visible at this point, and the read should extend the entry's life. + assert_eq!( + s.vault_client.get_portfolio(&investor).total_deposited, + amount + ); + + // Advance far beyond the original 518,400-ledger TTL window. The lifetime + // value must not reset to zero. + s.env.ledger().with_mut(|li| { + li.sequence_number += 400_000; }); let portfolio = s.vault_client.get_portfolio(&investor); From 4ccd0913e53a4176e5b42ed4f699033966da699d Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:26:04 +0100 Subject: [PATCH 13/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/storage.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/investment_vault/src/storage.rs b/investment_vault/src/storage.rs index 57d5dcce..2245af6c 100644 --- a/investment_vault/src/storage.rs +++ b/investment_vault/src/storage.rs @@ -3,7 +3,7 @@ use soroban_sdk::{Address, Env}; // 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 (~10 years). +// 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 @@ -16,7 +16,7 @@ pub fn read_registry(env: &Env) -> Address { } /// Reads the lifetime deposited amount for an account. -/// Bumps the TNL whenever the entry is accessed to keep it alive as long as the contract is in use. +/// 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); @@ -34,10 +34,10 @@ pub fn write_total_deposited(env: &Env, account: Address, amount: i128) { env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); } -[cfg(test)] +#c[cfg)test] mod tests { use super::*; - use soroban_sdk::{Address, Env}; + use soroban_sdk::{address, Env}; #[test] fn total_deposited_survives_inactivity() { @@ -46,6 +46,6 @@ mod tests { 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); + assert_eq!(read_total_deposited(&env, account).unwrap(), 1000; } } From 8cac5f42578a19186e857144b2a3621a01d1d6f4 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:33:45 +0100 Subject: [PATCH 14/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/lib.rs | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index 1efe72d4..a3676f3a 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 year (the protocol maximum). -const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 6_312_000; +/// At 5 s/ledger this equals ~1 day (17 280 ledgers). +const TTL_EXTEND_THRESHOLD_LEDGERS: u32 = 17_280; /// Target TTL in ledgers after extension (#388). -/// At 5 s/ledger this equals ~1 year (the protocol maximum). -const TTL_EXTEND_TO_LEDGERS: u32 = 6_312_000; +/// At 5 s/ledger this equals ~30 days (518 400 ledgers). +const TTL_EXTEND_TO_LEDGERS: u32 = 518_400; mod composability; mod events; @@ -477,7 +477,7 @@ impl InvestmentVault { token.transfer(&env.current_contract_address(), &recipient, &fee_amount); } - // Track cumulative deposits for portfolio analytics (#132) + // Track lifetime deposits for portfolio analytics (#132) let prev_dep: i128 = env .storage() .persistent() @@ -838,10 +838,6 @@ impl InvestmentVault { // ── Portfolio analytics (#132) ───────────────────────────────────────────── /// Return a full on-chain portfolio snapshot for `account`. - /// - /// Note: `total_deposited` is cumulative but not guaranteed indefinitely; the - /// persistent entry's TTL is extended on deposits and reads, but after - /// `TTL_EXTEND_TO_LEDGERS` ledgers (~1 year) without activity it may read 0. pub fn get_portfolio(env: Env, account: Address) -> PortfolioInfo { require_current_state(&env); let shares = Base::balance(&env, &account); @@ -866,16 +862,21 @@ impl InvestmentVault { shares * BPS_SCALE / total_shares }; - let key = VaultKey::TotalDeposited(account); - let total_deposited: i128 = match env.storage().persistent().get(&key) { - Some(value) => { - env.storage() - .persistent() - .extend_ttl(&key, TTL_EXTEND_THRESHOLD_LEDGERS, TTL_EXTEND_TO_LEDGERS); - value - } - None => 0, - }; + let total_deposited_key = VaultKey::TotalDeposited(account.clone()); + let total_deposited: i128 = env + .storage() + .persistent() + .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, From 847a28f2825c40153b3f4e7e296ffc5e0d2f4d42 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:33:48 +0100 Subject: [PATCH 15/17] fix: TotalDeposited lifetime-analytics entry can silently reset t (#316) --- investment_vault/src/test.rs | 58 ++++++++++++++---------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index aac8beb2..c7900d17 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -327,6 +327,28 @@ 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); + + // Capture the portfolio 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() { @@ -3191,42 +3213,6 @@ fn test_get_portfolio_zero_for_nondepositor() { assert_eq!(portfolio.total_deposited, 0); } -#[test] -fn test_total_deposited_survives_past_ttl_window() { - 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); - assert_eq!( - s.vault_client.get_portfolio(&investor).total_deposited, - amount - ); - - // Advance to just inside the TTL threshold (17,280 ledgers remaining) so a - // read from get_portfolio must renew the TotalDeposited entry. - s.env.ledger().with_mut(|li| { - li.sequence_number += 518_400 - 17_280 + 1; - }); - - // This read exercises the TTL bump on get_portfolio: the value must still - // be visible at this point, and the read should extend the entry's life. - assert_eq!( - s.vault_client.get_portfolio(&investor).total_deposited, - amount - ); - - // Advance far beyond the original 518,400-ledger TTL window. The lifetime - // value must not reset to zero. - s.env.ledger().with_mut(|li| { - li.sequence_number += 400_000; - }); - - let portfolio = s.vault_client.get_portfolio(&investor); - assert_eq!(portfolio.total_deposited, amount); -} - #[test] fn test_insurance_fund_balance_starts_at_zero() { let s = setup(); From 0879abcafeb898040d5d8e038fae2b14e7866fd4 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:58:29 +0100 Subject: [PATCH 16/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/investment_vault/src/lib.rs b/investment_vault/src/lib.rs index a3676f3a..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() From 7875b5fb2c1f680a9ebe6c0df29c44aecb81cc95 Mon Sep 17 00:00:00 2001 From: Andreas2410 Date: Mon, 31 Aug 2026 23:58:30 +0100 Subject: [PATCH 17/17] fix(ci): resolve failing checks for #316 --- investment_vault/src/test.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/investment_vault/src/test.rs b/investment_vault/src/test.rs index c7900d17..a212df40 100644 --- a/investment_vault/src/test.rs +++ b/investment_vault/src/test.rs @@ -335,7 +335,18 @@ fn test_total_deposited_survives_ttl_inactivity() { mint_usdc(&s.env, &s.usdc_sac, &investor, amount); s.vault_client.deposit(&investor, &amount); - // Capture the portfolio before prolonged inactivity. + 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).