Skip to content

fix(bridge): re-land reserved-redemption (#1121) with escrow P0s fixed - #1129

Draft
piotr-roslaniec wants to merge 2 commits into
reservations-upgradefrom
m2/reserved-redemption-fixed
Draft

piotr-roslaniec wants to merge 2 commits into
reservations-upgradefrom
m2/reserved-redemption-fixed

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Re-lands the reserved-redemption and veto surface from #1121 (reverted
via #1122 for shipping two P0 escrow bugs found in the #1116 multi-agent
review), with both bugs fixed and regression-tested before re-landing.

Scope note: this is milestone-2 work, not part of the milestone-1 A-H
decomposition.
This branch was originally based on reconciliation commit
52bf2822. The current M1 tip is 9f8f5ef1 and has diverged from dev
again; this PR is therefore currently conflicting with reservations-upgrade.
The pending M1 review fix is #1161. Do not merge this PR ahead of milestone 1
landing; it remains a draft for that reason.

What changed

Commit 1 (f362d6b3) re-applies #1121's original diff unmodified, as the
base for the fixes below.

Commit 2 (d826c1ec) fixes two P0 escrow issues:

  1. unwindPendingAction missing Redemption branch. The function
    handled Reanchor and Acceptance action types when unwinding a
    superseded pending generation, but had no branch for Redemption.
    A pending redemption superseded by a late acceptance, late reanchor,
    or late redemption settlement on the same position was marked
    Superseded with its escrowed mintedAmount never refunded -
    permanently stranding it as unbacked Bank balance on the Bridge.
    Fixed by adding the missing refund branch
    (self.bank.transferBalance(pendingAction.redeemer, pendingAction.amount)),
    matching what notifyReservationRedemptionTimedOut already does on an
    ordinary timeout.

  2. Late-settlement double-claim on redemption timeout.
    loadSettleableAction allowed a TimedOut Redemption action to be
    settled late, same as Acceptance/Reanchor. But
    notifyReservationRedemptionTimedOut already refunds the full
    escrowed claim to the redeemer and returns the reservation to
    Active on timeout - unlike Acceptance/Reanchor, whose timeouts
    leave the position open for late resolution. Permitting a late proof
    afterward let a wallet that confirms the redemption transaction after
    the timeout deliver the BTC while the redeemer keeps the refunded
    TBTC, burning nothing. This is reachable via ordinary slow Bitcoin
    confirmation - no attacker required, honest parties trigger it too.

    Fixed by restricting loadSettleableAction so a Redemption action
    must still be Pending to settle; a TimedOut redemption can never
    be proven again (a wallet that broadcasts anyway just produces an
    output the protocol no longer recognizes - it becomes a fresh anchor
    requiring a new acceptance). This removed the vulnerable code path
    entirely rather than adding new Bank-integration machinery to
    force-collect from an already-refunded redeemer (the Bank's
    decreaseBalance only burns the caller's own balance, so the Bridge
    cannot force-burn a third party without new Bank surface). Deleted
    the now-unreachable resolveLateRedemptionAgainstPending and the
    dead late-settlement branch of submitReservedRedemptionProof.

  3. Test coverage. submitReservedRedemptionProof had zero test
    coverage before this PR (across both ReservationProofs.test.ts and
    Bridge.ReservedRedemption.test.ts). Added regression tests for both
    fixes above at the loadSettleableAction/unwindPendingAction unit
    level, plus a dispatcher-level test proving the timeout guard is
    wired through the real submitReservationProof production entry
    point.

Verification

  • hardhat compile: clean.
  • Full reservation test surface (ReservationProofs, Bridge.ReservedRedemption,
    Reservation, Bridge.Reservation*, RedemptionWatchtower,
    Bridge.Redemption, ReservationVault): 592 passing, 0 failing.
  • prettier --check and solhint on touched files: clean (no new
    warnings beyond 3 pre-existing, unrelated ordering warnings already
    present in the re-landed baseline).

Not in scope

Everything else already covered by the #1116 review of #1121 remains as
originally landed; this PR only touches the two P0 escrow findings and
adds the missing settlement-proof test coverage.

Re-applies the reverted #1121 diff (requestReservedRedemption,
notifyReservedRedemptionVeto, extendReservation, RedemptionWatchtower)
onto the current reservations-upgrade tip, unmodified, as the base for
the P0 fixes identified in the #1116 multi-agent review before re-landing.
Two P0 escrow bugs found in the #1116 multi-agent review of #1121
(reverted via #1122 pending a fix), fixed before re-landing:

1. unwindPendingAction had no branch for ActionType.Redemption: a
   pending redemption superseded by a late acceptance/reanchor/
   redemption settlement on the same position was marked Superseded
   with its escrowed mintedAmount never refunded, permanently
   stranding it as unbacked Bank balance on the Bridge. Added the
   missing refund branch, matching notifyReservationRedemptionTimedOut.

2. loadSettleableAction allowed late (post-TimedOut) settlement of a
   Redemption action. notifyReservationRedemptionTimedOut already
   refunds the full escrowed claim to the redeemer and returns the
   reservation to Active on timeout; permitting a late proof afterward
   let a wallet that confirms the redemption transaction after the
   timeout deliver the BTC while the redeemer keeps the refunded TBTC,
   burning nothing. Reachable via ordinary slow confirmation, no
   attacker required. Restricted loadSettleableAction so a Redemption
   action must still be Pending to settle; a TimedOut redemption can
   never be proven again. Deleted the now-unreachable
   resolveLateRedemptionAgainstPending and the dead late-branch of
   submitReservedRedemptionProof.

Adds the escrow-path test coverage that was previously zero for
submitReservedRedemptionProof: regression tests for both fixes at the
loadSettleableAction/unwindPendingAction unit level and through the
real submitReservationProof dispatcher entry point.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@piotr-roslaniec

Copy link
Copy Markdown
Contributor Author

External audit (V12) targeted this exact head

V12 audited d826c1ecfbbb6e9185f35cc1269cf3e971b684ac, the current head of this PR, and reported 11 findings. All 11 have been line-verified against that commit. None were false positives, though several severities moved.

The two escrow P0s this commit set out to fix are confirmed fixed. Verified against the parent:

No spurious burn fires, and no escrow is stranded on supersession.

The important scoping point

git diff d826c1e^ d826c1e touches only ReservationProofs.sol plus its test and helper (+68/-98). Reservation.sol, ReservationVault.sol, RedemptionWatchtower.sol, BridgeState.sol, and all deploy scripts are byte-identical to the parent.

So 10 of the 11 findings are inherited, not introduced here, and most cannot be fixed in this PR since their defect sites are outside its diff. Notably this PR does not touch WalletProposalValidator.sol, so the validator gap below has to land elsewhere. Full routing is in the comment on #1116.

The one finding that concerns this PR's own change: 184199

Reported Critical, "Timeout race enables BTC and TBTC double claim". Assessment splits three ways:

  1. On-chain accounting: closed here, by the two fixes above.
  2. Economic outcome: unchanged. The timeout path still refunds the full escrow, restores Active, mints a retry credit, and slashes the wallet (Reservation.sol#L998-L1048). A wallet whose Bitcoin transaction confirms after timeoutAt has delivered real BTC to a redeemer-chosen script and receives no settlement credit.
  3. But this matches shipped behavior. On main, a late-delivered output is matched against timedOutRedemptions, deleted, and contributes zero burnable value: Redemption.sol#L1049-L1077, with the comment "that also means the wallet was already punished for the inactivity."

Refund-plus-delivered-BTC with nothing burned is therefore a pre-existing, deliberately accepted property of tBTC redemptions, not something reservations introduced. That reframes 184199 away from "new Critical" and onto three narrower reservation-specific deltas:

  • No late-recognition branch. Main recognizes and bypasses the late output so the remainder of the proof still processes. The reserved path reverts the whole proof, so a wallet has no way to account for BTC it already delivered. Worth considering a timedOutReservationActions equivalent.
  • No reuse cap. Main moves the wallet out of Live on timeout and requestRedemption requires Live (Redemption.sol#L515-L518), which caps the loop. The reserved gate accepts MovingFunds (Reservation.sol#L1112-L1118), so it does not.
  • Added redeemer upside. The reserved timeout also mints a retry credit on a wallet-fault timeout. Main has no analogue.

This looks like a design decision rather than a patch. The rationale comment says a post-timeout broadcast "becomes a fresh anchor requiring a new acceptance", but the output script is redeemer-chosen at request time, so the protocol cannot reclaim that UTXO, and nothing constrains a wallet from broadcasting close to timeoutAt.

Also worth noting

This PR is currently CONFLICTING against reservations-upgrade, so it is probably not the right place to absorb fixes for the inherited findings even where the files overlap.

Findings inventory with per-finding permalinks and routing: see the comment on #1116.

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.

1 participant