Skip to content

feat: fix token bridge wbal ttl - #72

Open
stephenowoh43-collab wants to merge 1 commit into
StellarSend:mainfrom
stephenowoh43-collab:fix/token-bridge-wbal-ttl-55
Open

feat: fix token bridge wbal ttl#72
stephenowoh43-collab wants to merge 1 commit into
StellarSend:mainfrom
stephenowoh43-collab:fix/token-bridge-wbal-ttl-55

Conversation

@stephenowoh43-collab

Copy link
Copy Markdown

token_bridge: TTL-extend per-holder WBAL persistent entries

Problem

token_bridge stores each holder's wrapped-token balance under (KEY_WBAL, holder) in
persistent 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 existing
unwrap_or(0) fallback did not protect against anything: a user who wrapped tokens and went
dormant 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_ttl on 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_ttl
usage), so contract-local constants were introduced following the standard Soroban
ledger-day convention:

const DAY_IN_LEDGERS: u32 = 17_280;
const WBAL_TTL_THRESHOLD: u32  = 7  * DAY_IN_LEDGERS; // extend when < ~1 week left
const WBAL_TTL_EXTEND_TO: u32  = 31 * DAY_IN_LEDGERS; // extend to ~1 month from now

(Values satisfy the host constraints threshold <= extend_to <= max_entry_ttl, and the
threshold exceeds the default minimum so any touch immediately refreshes a stale entry.)

  1. Read pathget_wrapped_balance_internal() now extends the entry's TTL whenever an
    existing balance is successfully read (used by both the public get_wrapped_balance
    query and unwrap's balance check). A holder with no entry still returns the default 0
    with no extension attempt against a nonexistent key.
  2. Write paths — after every successful balance write, the entry's TTL is extended:
    • credit_wrapped() (the wrap() credit path)
    • the debit path inside 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 into
    the 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 wrap
    again 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 — missing
    entries keep returning 0 with 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_instance

In 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/code
entries 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
ContractData entries, leaving WBAL state entirely to the contract's own TTL logic.

Existing test snapshots changed only in recorded live_until_ledger_seq values — 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).
  • Snapshot diffs confirm zero behavioral drift on the non-archival path.
  • Pre-existing unrelated failure: factory's test build fails to compile due to missing
    WASM artifacts for contractimport!; it reproduces identically on a clean tree and is
    out of scope for this change.

Closes #55

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

token_bridge: per-holder WBAL entries never TTL-extended — a dormant wrapped-token balance risks archival and traps on read

2 participants