feat: fix token bridge wbal ttl - #72
Open
stephenowoh43-collab wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
token_bridge: TTL-extend per-holder WBAL persistent entries
Problem
token_bridgestores each holder's wrapped-token balance under(KEY_WBAL, holder)inpersistent storage, but never extended the entry's TTL. Soroban persistent entries have a
finite lifetime, so a balance left untouched long enough would be archived. A subsequent
persistent-storage
get()on an archived entry traps before returning, so the existingunwrap_or(0)fallback did not protect against anything: a user who wrapped tokens and wentdormant could no longer query, top up, or unwrap their own balance.
A related subtlety: once an entry is archived it is unrecoverable from inside the contract
(
extend_ttlon an expired entry errors too), so protection had to happen before expiry,not just on writes.
Changes (
token_bridge/src/lib.rs)No shared TTL policy existed anywhere in this repository (verified: no prior
extend_ttlusage), so contract-local constants were introduced following the standard Soroban
ledger-day convention:
(Values satisfy the host constraints
threshold <= extend_to <= max_entry_ttl, and thethreshold exceeds the default minimum so any touch immediately refreshes a stale entry.)
get_wrapped_balance_internal()now extends the entry's TTL whenever anexisting balance is successfully read (used by both the public
get_wrapped_balancequery and
unwrap's balance check). A holder with no entry still returns the default0with no extension attempt against a nonexistent key.
credit_wrapped()(thewrap()credit path)unwrap()Amounts, validation, events, error behavior, and all other storage are unchanged; on the
normal non-archival path behavior is identical apart from entry expirations being pushed
forward.
Tests (
token_bridge/src/test.rs)Added four regression tests reproducing the archival scenario using ledger-sequence
advancement (test env: fresh persistent entries live until
sequence + 4095):test_dormant_balance_survives_ttl_window_and_read_refreshes_ttl— wraps, advances intothe entry's final stretch, reads (refreshing its TTL), then jumps far past the original
expiry point: no trap, correct balance returned, entry stays available for further reads.
test_returning_holder_can_wrap_after_ttl_advancement— top-up near expiry, then wrapagain past original expiry; protects
credit_wrapped's read-before-write path.test_returning_holder_can_unwrap_after_ttl_advancement— same for the unwrap debit path,including correctness of underlying payouts across both unwraps.
test_get_wrapped_balance_for_unknown_holder_is_zero_without_ttl_extension— missingentries keep returning
0with no invalid extension attempt.All three archival-scenario tests were verified to fail with traps without the fix and
pass with it.
Test-harness note:
restore_bridge_instanceIn SDK v21.2.1, updating an existing persistent entry preserves its original
live_until_ledger_seq, so nothing in these flows keeps the contract's own instance/codeentries alive across large sequence jumps — on the real network an archived instance blocks
invocation entirely until a restore operation is run. The tests add a small harness helper
that mirrors that restore (instance + code entries only) so the tests exercise exactly the
issue's scenario: contract invokable, WBAL entry dormant. It deliberately never touches
ContractDataentries, leaving WBAL state entirely to the contract's own TTL logic.Existing test snapshots changed only in recorded
live_until_ledger_seqvalues — balances,events, and all other state are identical.
Verification
cargo test -p escrow -p fee_collector -p stellar_send -p token_bridge: all pass(18/18 in token_bridge).
factory's test build fails to compile due to missingWASM artifacts for
contractimport!; it reproduces identically on a clean tree and isout of scope for this change.
Closes #55