- Points: 200
- Labels: contract, storage, ttl, reliability
Background
DataKey::ActiveCount is stored in instance storage, which in Soroban shares its TTL with the contract instance itself. However, DataKey::FeeCollector, DataKey::FeeBps, DataKey::GracePeriod, DataKey::WhitelistEnabled, DataKey::Admin, and DataKey::SchemaVersion are all also in instance storage and share the same TTL. The contract currently never explicitly calls extend_ttl on instance storage. On Soroban mainnet, instance storage TTL is bounded — if the contract goes dormant (no transactions) for long enough, all instance-level data expires. A contract that loses its Admin or Token key is effectively bricked: charge will fail on Token lookup, and admin functions will fail on Admin lookup.
Task
Add a bump_instance_ttl(env: &Env) private helper in lib.rs that calls env.storage().instance().extend_ttl(SUBSCRIPTION_TTL_LEDGERS / 2, SUBSCRIPTION_TTL_LEDGERS). Call bump_instance_ttl at the start of every state-mutating public function: subscribe, charge, pay_per_use, cancel, pause, resume, set_fee, set_grace_period, set_whitelist_enabled, add_merchant_to_whitelist, remove_merchant_from_whitelist, and batch_charge. This ensures any active use of the contract continuously refreshes its instance TTL without a separate keeper.
Key Files
-
contract/src/lib.rs — add fn bump_instance_ttl(env: &Env) and call it at the start of all write-path public functions
-
contract/src/test.rs — add a test that verifies env.storage().instance().get_ttl() is $\ge$ SUBSCRIPTION_TTL_LEDGERS / 2 after a subscribe call
Edge Cases
- Read-only functions (
get_protocol_stats, get_subscription, next_charge_at, etc.) must NOT call bump_instance_ttl — they should not write to storage
bump_instance_ttl uses threshold = SUBSCRIPTION_TTL_LEDGERS / 2 so it only extends when needed, avoiding unnecessary writes on every single call
initialize should call bump_instance_ttl since it is the first write and sets the initial TTL
Acceptance Criteria
Background
DataKey::ActiveCountis stored in instance storage, which in Soroban shares its TTL with the contract instance itself. However,DataKey::FeeCollector,DataKey::FeeBps,DataKey::GracePeriod,DataKey::WhitelistEnabled,DataKey::Admin, andDataKey::SchemaVersionare all also in instance storage and share the same TTL. The contract currently never explicitly callsextend_ttlon instance storage. On Soroban mainnet, instance storage TTL is bounded — if the contract goes dormant (no transactions) for long enough, all instance-level data expires. A contract that loses its Admin or Token key is effectively bricked: charge will fail on Token lookup, and admin functions will fail on Admin lookup.Task
Add a
bump_instance_ttl(env: &Env)private helper inlib.rsthat callsenv.storage().instance().extend_ttl(SUBSCRIPTION_TTL_LEDGERS / 2, SUBSCRIPTION_TTL_LEDGERS). Callbump_instance_ttlat the start of every state-mutating public function:subscribe,charge,pay_per_use,cancel,pause,resume,set_fee,set_grace_period,set_whitelist_enabled,add_merchant_to_whitelist,remove_merchant_from_whitelist, andbatch_charge. This ensures any active use of the contract continuously refreshes its instance TTL without a separate keeper.Key Files
contract/src/lib.rs— addfn bump_instance_ttl(env: &Env)and call it at the start of all write-path public functionscontract/src/test.rs— add a test that verifiesenv.storage().instance().get_ttl()isSUBSCRIPTION_TTL_LEDGERS / 2after asubscribecallEdge Cases
get_protocol_stats,get_subscription,next_charge_at, etc.) must NOT callbump_instance_ttl— they should not write to storagebump_instance_ttlusesthreshold = SUBSCRIPTION_TTL_LEDGERS / 2so it only extends when needed, avoiding unnecessary writes on every single callinitializeshould callbump_instance_ttlsince it is the first write and sets the initial TTLAcceptance Criteria
bump_instance_ttlis called in all write-path functions listed abovebump_instance_ttlenv.storage().instance().get_ttl() >= SUBSCRIPTION_TTL_LEDGERS / 2aftersubscribeinitializesets a non-zero instance TTLcargo testpasses with no regressions