Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
419b85a
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
9a0163e
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
c41c40c
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
da6b60a
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
8ac6ac6
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
82503de
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
8ab57dc
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
3f3bc31
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
65b5904
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
f5ab70e
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
a596565
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
7a0d170
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
4ccd091
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
8cac5f4
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
847a28f
fix: TotalDeposited lifetime-analytics entry can silently reset t (#316)
Andreas2410 Aug 31, 2026
0879abc
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
7875b5f
fix(ci): resolve failing checks for #316
Andreas2410 Aug 31, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 17 additions & 6 deletions investment_vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 42 additions & 1 deletion investment_vault/src/storage.rs
Original file line number Diff line number Diff line change
@@ -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<i128> {
let key = VaultKey::TotalDeposited(account);
let val: Option<i128> = 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;
}
}
33 changes: 33 additions & 0 deletions investment_vault/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading