fix(dispute-resolution): extend instance TTL from every entry point - #78
Conversation
The Admin, EscrowContract, Version and NextDisputeId keys are written once at initialize and never touched again, and nothing in the contract ever called extend_ttl on instance storage. Measured in a test, the instance entry's TTL sat at 4095 ledgers (~5.7h at 5s/ledger) after initialize and only decayed from there. Once archived, get_escrow_contract and get_admin fail with NotInitialized, which bricks raise_dispute — the contract's only fully implemented mutating entry point — until someone submits a RestoreFootprint operation. Add extend_instance_ttl to storage.rs, mirroring campaign-escrow's helper and reusing the same ~30-day bump/threshold pair, and call it from every public function in lib.rs. The read-only paths (get_dispute, version) bump too: nothing writes to instance storage after initialize, so leaving reads out would let the config expire on a contract that is being actively read. Tests assert the TTL is bumped to the full window after initialize, and re-extended after raise_dispute and version once it has decayed under the threshold. All three fail against the previous behaviour with an observed TTL of 4095. Closes Ads-Bazaar#59
`cargo fmt --all -- --check` has been failing on main since 43ba011, 7c41d08 and 748ba2f landed on 2026-08-25, so the CI fmt job is red on every open PR rather than on anything those PRs changed. Pure rustfmt output on the four reported hunks — two call-site rewrappings in lib.rs and two assert_eq! expansions in test.rs. No semantic change. Unrelated to the dispute-resolution TTL fix in this branch; kept as its own commit so it can be dropped or cherry-picked independently if the maintainers would rather land it separately.
|
Pushed 3bc11bc to fix the red fmt job. To be clear about attribution, since it looked like this PR's fault: the four failing hunks are all in The commit is pure rustfmt output, kept separate from the TTL fix so it can be dropped or cherry-picked on its own. Also worth noting All four gates green locally: fmt clean, clippy |
Problem
Closes #59.
dispute-resolutionhas no instance-TTL bump anywhere. TheAdmin,EscrowContract,VersionandNextDisputeIdkeys are written once byinitializevia plainenv.storage().instance().set(...)and never touched again —storage.rsextends TTL for its two persistent keys (Dispute,OpenDispute) but has noextend_instance_ttlhelper at all, and no function inlib.rsever calledenv.storage().instance().extend_ttl(...).This turned out to be sharper than the issue estimates. Instrumenting it in a test, the instance entry's TTL sits at 4095 ledgers immediately after
initialize— roughly 5.7 hours at 5s/ledger — and only decays from there. It isn't a slow leak that bites an under-used deployment eventually; a freshly initialized contract is inside a day of archival on day one.Once archived,
get_escrow_contractfails withError::NotInitialized, which breaks the cross-contract reads (get_campaign_business,freeze_for_dispute) that everyraise_disputecall depends on — bricking the contract's only fully implemented mutating entry point until someone submits aRestoreFootprintoperation.get_adminfails the same way forassign_arbiterandupgrade.Fix
storage.rs— addedextend_instance_ttl, mirroringcampaign-escrow's helper of the same name, with a matchingINSTANCE_BUMP_LEDGERS/INSTANCE_LIFETIME_THRESHOLDpair (518_400 / 500_000). Kept as separate constants from the persistent pair for the same reason escrow keeps them separate: instance and persistent TTL are tracked independently by the ledger even when the numbers happen to coincide.lib.rs— called from every public#[contractimpl]function:initialize,raise_dispute,assign_arbiter,resolve_dispute,close_dispute,get_dispute,version,upgrade. Also fromclose_dispute's early-return branch (the no-open-record no-op), since that path is still a real invocation that read instance storage to authorize its caller.Why the read-only paths bump too
get_disputeandversionmutate nothing, so bumping TTL there may look out of place. It's deliberate, and it's the difference between fixing this and half-fixing it: nothing writes to instance storage afterinitialize. If only the write paths bumped, a deployment being actively read — an indexer pollingget_dispute, a client checkingversion— would still let its config expire, which is exactly the failure mode this issue describes. This follows the precedent already set bycampaign-escrow::get_protocol_config, which bumps on a pure read for the same reason. Reads bump after the underlying read succeeds, soversionstill returnsNotInitializedbefore initialization rather than bumping a nonexistent entry.Scope notes
resolve_disputeis stilltodo!(). The bump is placed above it so the invariant holds for whoever implements the arbitration flow, rather than leaving one entry point to be remembered later.campaign-escrowalone entirely — its own missingextend_instance_ttlcall sites are bug: persistent storage entries have no TTL bump — live campaigns will expire off-ledger #14's scope, and this issue's failure mode is independent of it.Test coverage added
New
test_instance_ttlmodule indispute-resolution/src/test.rs:initialize_extends_instance_ttl— TTL is the full 518_400 window afterinitialize, not the 4095 default.raise_dispute_re_extends_decayed_instance_ttl— advances the ledger sequence past the extend threshold, asserts the TTL actually decayed under it (so the bump is genuinely exercised rather than being a no-op), then assertsraise_disputerestores the full window.read_only_version_call_extends_instance_ttl— same decay-then-restore, through a pure read, covering the case argued above.All three were verified to fail against the pre-fix code with an observed TTL of 4095 vs. an expected 518400 — they're regression guards, not assertions that pass either way.
Test plan
cargo test --workspace— 147 pass (98 escrow + 18 integration + 31 dispute-resolution; dispute-resolution up from 28)cargo clippy --workspace --all-targets -- -D warnings— cleancargo build --workspace --target wasm32v1-none --release— buildsSecond commit:
cargo fmtoncampaign-escrow(unrelated to the fix)cargo fmt --all -- --checkhas been failing onmainsince 43ba011, 7c41d08 and 748ba2f landed on 2026-08-25 — four hunks, all incampaign-escrow(src/lib.rs:268,src/lib.rs:1033,src/test.rs:1800,src/test.rs:1826). None of them are in the three files this fix touches;dispute-resolutionis fmt-clean on its own. The effect is that the CI fmt job is red on every open PR against this repo, not on anything those PRs changed.I originally left it alone to keep this diff focused, but since it blocks this PR's CI I've applied it as a separate second commit (3bc11bc) rather than folding it into the fix. It's pure rustfmt output — two call-site rewrappings and two
assert_eq!expansions, no semantic change — so it can be dropped or cherry-picked independently if you'd rather land it as its own PR. Happy to split it out if you prefer; just say so and I'll rebase.With it applied, all four CI gates pass locally.