diff --git a/contract/vault/soroban/README.md b/contract/vault/soroban/README.md index 5f2794673..3b685a1b7 100644 --- a/contract/vault/soroban/README.md +++ b/contract/vault/soroban/README.md @@ -218,3 +218,10 @@ Parity tests check behavioral equivalence across the shared kernel and chain exe - Soroban share-token transfers are user-authorized (`from.require_auth()`). - The vault can still transfer shares for internal flows (escrow/payout effects). + +## Share Token TTL and Archival Recovery + +- Share-token instance storage is refreshed by every public share-token entrypoint, including SEP-41 read-only methods (`total_supply`, `balance`, `allowance`, `decimals`, `name`, and `symbol`) and the custom `admin` / `vault` getters. +- The admin-only `extend_ttl(caller)` entrypoint is the explicit keeper path for proactive instance maintenance. Operators should schedule it well before the instance reaches the TTL threshold; if the instance is archived, restore the contract instance through the Stellar/Soroban archival restore flow first, then call `extend_ttl` as the configured admin. +- Per-holder balances are persistent entries owned by the upstream `stellar-tokens` implementation. Balance reads and balance-changing writes refresh the specific holder balance that is touched; the share token intentionally does not maintain an enumerable holder index or perform unbounded global balance refreshes from `extend_ttl`. +- Allowances are temporary entries bounded by their explicit `live_until_ledger`. They are not extended beyond that caller-selected expiry by the share-token keeper path; owners should renew approvals when continued delegated spending is desired. diff --git a/contract/vault/soroban/share-token/src/lib.rs b/contract/vault/soroban/share-token/src/lib.rs index 25701ee16..83a44a636 100644 --- a/contract/vault/soroban/share-token/src/lib.rs +++ b/contract/vault/soroban/share-token/src/lib.rs @@ -25,14 +25,17 @@ impl FungibleToken for SorobanShareTokenContract { type ContractType = Base; fn total_supply(e: &Env) -> i128 { + extend_instance_ttl(e); Base::total_supply(e) } fn balance(e: &Env, account: Address) -> i128 { + extend_instance_ttl(e); Base::balance(e, &account) } fn allowance(e: &Env, owner: Address, spender: Address) -> i128 { + extend_instance_ttl(e); Base::allowance(e, &owner, &spender) } @@ -52,14 +55,17 @@ impl FungibleToken for SorobanShareTokenContract { } fn decimals(e: &Env) -> u32 { + extend_instance_ttl(e); Base::decimals(e) } fn name(e: &Env) -> String { + extend_instance_ttl(e); Base::name(e) } fn symbol(e: &Env) -> String { + extend_instance_ttl(e); Base::symbol(e) } } diff --git a/contract/vault/soroban/share-token/src/tests.rs b/contract/vault/soroban/share-token/src/tests.rs index 60f5fe87e..430e547c0 100644 --- a/contract/vault/soroban/share-token/src/tests.rs +++ b/contract/vault/soroban/share-token/src/tests.rs @@ -1,4 +1,5 @@ use super::*; +use soroban_sdk::testutils::storage::Instance; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::{Events, Ledger, LedgerInfo}; use soroban_sdk::xdr::{ContractEventBody, ScVal}; @@ -467,6 +468,139 @@ fn admin_cannot_change_metadata_after_deployment() { ); } +#[test] +fn read_only_entrypoints_cover_share_token_ttl_maintenance_surface() { + let (env, _admin, vault, token) = setup(); + let user = Address::generate(&env); + let spender = Address::generate(&env); + + env.as_contract(&vault, || { + VaultCaller::mint(env.clone(), token.clone(), user.clone(), 1000); + VaultCaller::approve( + env.clone(), + token.clone(), + user.clone(), + spender.clone(), + 250, + 300, + ); + }); + + let supply: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "total_supply"), + ().into_val(&env), + ); + let balance: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "balance"), + (&user,).into_val(&env), + ); + let allowance: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "allowance"), + (&user, &spender).into_val(&env), + ); + let name: String = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "name"), + ().into_val(&env), + ); + let symbol: String = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "symbol"), + ().into_val(&env), + ); + let decimals: u32 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "decimals"), + ().into_val(&env), + ); + + assert_eq!(supply, 1000); + assert_eq!(balance, 1000); + assert_eq!(allowance, 250); + assert_eq!(name, String::from_str(&env, "Templar Share")); + assert_eq!(symbol, String::from_str(&env, "tvSHARE")); + assert_eq!(decimals, 7); + + env.ledger().set(LedgerInfo { + timestamp: 100, + protocol_version: 25, + sequence_number: 2_592_100, + max_entry_ttl: 3_110_400, + ..Default::default() + }); + let ttl_before_read = env.as_contract(&token, || env.storage().instance().get_ttl()); + let refreshed_supply: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "total_supply"), + ().into_val(&env), + ); + let ttl_after_read = env.as_contract(&token, || env.storage().instance().get_ttl()); + + assert_eq!(refreshed_supply, 1000); + assert!(ttl_after_read > ttl_before_read); +} + +#[test] +fn admin_extend_ttl_preserves_holder_balances_and_allowance_expiry_semantics() { + let (env, admin, vault, token) = setup(); + let user = Address::generate(&env); + let spender = Address::generate(&env); + + env.as_contract(&vault, || { + VaultCaller::mint(env.clone(), token.clone(), user.clone(), 1000); + VaultCaller::approve( + env.clone(), + token.clone(), + user.clone(), + spender.clone(), + 400, + 150, + ); + }); + + env.invoke_contract::<()>( + &token, + &soroban_sdk::Symbol::new(&env, "extend_ttl"), + (&admin,).into_val(&env), + ); + + let balance: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "balance"), + (&user,).into_val(&env), + ); + let allowance_before_expiry: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "allowance"), + (&user, &spender).into_val(&env), + ); + assert_eq!(balance, 1000); + assert_eq!(allowance_before_expiry, 400); + + env.ledger().set(LedgerInfo { + timestamp: 101, + protocol_version: 25, + sequence_number: 151, + max_entry_ttl: 1_000, + ..Default::default() + }); + + env.invoke_contract::<()>( + &token, + &soroban_sdk::Symbol::new(&env, "extend_ttl"), + (&admin,).into_val(&env), + ); + let allowance_after_expiry: i128 = env.invoke_contract( + &token, + &soroban_sdk::Symbol::new(&env, "allowance"), + (&user, &spender).into_val(&env), + ); + assert_eq!(allowance_after_expiry, 0); +} + #[test] fn total_supply_tracks_mint_and_burn() { let (env, _admin, vault, token) = setup();