Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions contract/vault/soroban/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions contract/vault/soroban/share-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}
}
Expand Down
134 changes: 134 additions & 0 deletions contract/vault/soroban/share-token/src/tests.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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);
}
Comment on lines +471 to +544

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

TTL regression test is value-only and does not prove TTL refresh.

On Line 471, this test name states TTL maintenance coverage, but it only asserts token values. It would still pass even if the read-only extend_instance_ttl calls were removed. Please add an assertion that verifies observable instance-TTL movement before/after at least one of these read-only calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contract/vault/soroban/share-token/src/tests.rs` around lines 470 - 525, The
test read_only_entrypoints_cover_share_token_ttl_maintenance_surface currently
only asserts token values and must also verify TTL movement: read and store the
contract instance TTL for token before calling the read-only entrypoint(s) (use
env to query the instance TTL), invoke the read-only extend_instance_ttl
entrypoint via env.invoke_contract (Symbol::new(&env, "extend_instance_ttl") or
the specific read-only method you want to exercise), then read the instance TTL
again and assert it has advanced/been refreshed; update the test to perform this
before/after TTL assertion using the existing token variable and the test
function read_only_entrypoints_cover_share_token_ttl_maintenance_surface.


#[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();
Expand Down