draft: UTXO reservations — segregated custody with in-kind redemption - #1088
Draft
mswilkison wants to merge 19 commits into
Draft
draft: UTXO reservations — segregated custody with in-kind redemption#1088mswilkison wants to merge 19 commits into
mswilkison wants to merge 19 commits into
Conversation
Adds the Reservation library implementing segregated, in-kind-redeemable custody of deposited UTXOs. A deposit revealed with the designated reservation vault is anchored by the wallet -- a 1-input-1-output spend into a fresh wallet-controlled output with no refund path -- instead of being swept. Balance is credited gross only on the SPV proof of the anchor, mirroring the sweep's refund-disabling role while dropping its consolidating role. The registry tracks the anchor outpoint through re-anchor hops (wallet migration) until an in-kind reserved redemption burns the gross claim, or term+grace expiry lets the wallet dissolve the anchor into its main UTXO. All four lifecycle SPV proofs share one Bridge entry point (submitReservationProof) to preserve the EIP-170 margin; Bridge compiles to 23.9 KB with a runs=100 optimizer override (baseline 24.1 KB at runs=1000 with only 0.5 KB of headroom). Safety wiring: acceptance marks the deposit swept (blocking sweeps and enabling fraud-challenge defeats); consumed anchors are recorded in spentMainUTXOs so the existing defeat path recognizes them; sweeps targeting the reservation vault revert; wallets cannot finalize closing while custodying reservations; reserved redemptions pass the redemption watchtower isSafeRedemption gate and reuse the redemption-timeout slashing machinery.
Liability-side companion of the reservation core: receives the gross acceptance credit, mints TBTC gross to the reservation owner, and collects all protocol fees as explicit TBTC transfers (claims are never netted, so the surrendered claim always equals the sats earmarked on-chain). Draft fee schedule: 40 bps all-in at initiation (mint leg + first custody term), 20 bps per extension, 20 bps at redemption -- strictly dominating the pooled path's 20+20 bps round trip at every holding horizon.
Covers parameter governance, the reserved-deposit sweep guard (recorded sweep proof against a reservation-routed deposit), term extension, reserved redemption request/timeout bookkeeping with wallet-slashing reuse, and the vault's gross-mint fee split and redemption surrender flow. The SPV proof validators (acceptance, redemption, re-anchor, dissolution) are exercised only up to proof validation and need Bitcoin-fixture coverage as a follow-up.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
The deploy-script-85 unit test mocks the deployments registry and the rebate recovery test links BridgeStub libraries explicitly; both needed to learn about the new Reservation library.
Hoists the TBTCVault mint out of the per-depositor loop (one aggregate mint, per-depositor transfers), which also resolves the Slither calls-inside-a-loop findings. Adds retryRedeemReservation so an owner left holding Bank balance by a reserved-redemption timeout can re-request without manually re-minting TBTC.
Mirrors Redemption.notifyRedemptionVeto for reserved redemptions: the watchtower detains the surrendered gross balance, the pending request is cleared, and the reservation returns to Active -- the anchor outpoint was not spent, so the in-kind claim survives. The guardian-side flow in the RedemptionWatchtower contract is a follow-up; this is the Bridge hook it needs.
Adds a grouped begin/finalize pair to BridgeGovernance staging all reservation parameters (including the reservation vault) behind the standard governance delay, applied atomically via the Bridge's single updateReservationParameters call.
Crafts structurally-valid Bitcoin transactions and regtest-difficulty headers in pure TypeScript (SPV validation checks structure, merkle inclusion, and header work -- not script signatures) and exercises all four reservation proof validators end to end: anchor acceptance with gross vault credit, in-kind redemption with gross burn, re-anchoring to a second wallet, and post-grace dissolution into the main UTXO. Also covers the watchtower veto, the vault retry path, and the governance-delayed parameters flow.
Adds raiseReservedObjection mirroring raiseObjection for pending reserved redemptions, reusing the existing VetoProposal/objections storage keyed by reservation key -- no storage layout changes, so the watchtower upgrade is layout-safe. Three objections finalize the veto: the surrendered gross amount is detained via the Bridge's notifyReservedRedemptionVeto hook, the penalty fee is burned, the redeemer is banned, and the reservation returns to Active on the Bridge. Also exposes getReservedRedemptionDelay for wallet-side coordination and extends the IRedemptionWatchtower interface accordingly.
Teaches WalletProposalValidator the reservation lifecycle: rejects sweep proposals containing reservation-vault deposits, and adds proposal validation for anchors (deposit eligibility, fee bounds, minimum anchor amount, refund safety margin), reserved redemptions (pending state, watchtower delay, timeout safety margin, snapshotted fee bound), re-anchors (Active state, Live target) and dissolutions (term + grace elapsed). Restores the grouped Bridge.reservationParameters() getter the validator consumes; Bridge stays under the EIP-170 limit (24.175 KB).
Covers the full guardian objection flow (three objections, veto finalization through the Bridge hook, 100% penalty burn, redeemer ban blocking re-requests via the isSafeRedemption gate) and the validator's sweep exclusion, anchor, reserved redemption, re-anchor and dissolution proposal rules.
Prettier reflowed the multi-line if condition, detaching the no-await-in-loop disable comment from the awaited call.
Adopts the custody-style pay-to-hold schedule: 40 bps all-in at initiation (mint leg + first-year custody), 20 bps per extension year, free redemption. Never cheaper than the pooled 20+20 bps round trip (parity at a one-year hold, premium beyond), exit-neutral where the product's in-kind promise lives, and it removes the fee re-charge on retries after wallet-fault timeouts. The redemptionFeeBps parameter is retained for governance; the minimum reservation size -- not the fee schedule -- is the dial that keeps carry covering per-position lifecycle costs as FROST lowers ceremony economics.
The endpoints are priced at parity with the pooled path -- a 20 bps mint leg inside the 40 bps initiation fee and a 20 bps redemption fee -- so the only premium being purchased is the 20 bps/yr custody fee (the remainder of the initiation fee prepays the first year). An N-year holding pays 40 + 20N bps against the pooled 40 bps round trip: strictly premium at every horizon. The redemption fee is not re-charged on retries after wallet-fault timeouts: it was collected by the original request, and the retry only exists because the wallet failed.
Two correctness fixes surfaced by external review of the proof validators: 1. Redemption liveness. The redemption range check subtracted redemptionTxMaxFee (a governable parameter, not a tx-derived value) from anchorAmount, which reverts on underflow in Solidity 0.8 when the fee bound exceeds the anchor -- reachable via a governance fee increase against an existing anchor. That would make every redemption proof for the affected reservation revert, stranding the coins until dissolution. Reformulated as the underflow-safe equivalent (outputValue <= anchorAmount && anchorAmount - outputValue <= maxFee). 2. Re-anchor floor. Re-anchors bounded only the per-hop miner fee, never the resulting anchor, so repeated network-scheduled hops could grind the anchor toward the fee bound. Now require the re-anchored amount to stay >= reservationMinAmount; since the minimum is required to exceed the tx max fee, this also keeps the anchor clear of the redemption fee bound, reinforcing fix (1) across migrations. Adds regression tests for both.
…100 override A runs=1000-vs-100 gas diff over the deposit/redemption/reservation hot paths measures at 0-8 gas (noise): the Bridge is a dispatch shell over linked libraries that stay at runs=1000, so the size-preserving override carries no meaningful runtime cost.
External review found that the previous `>= reservationMinAmount` re-anchor floor, combined with the proposal validator's positive-fee requirement, left no compliant re-anchor for an exactly-minimum-sized reservation — pinning a retiring wallet, which contradicts mandatory migration. The floor's redemption-liveness purpose is already covered by the underflow-safe redemption range check, so relax it to a dust floor (`> reservationTxMaxFee`) that keeps anchors clear of dust while leaving minimum-sized reservations migratable. Bounding cumulative Byzantine re-anchor grinding is deferred to the authorized-action model (migration request with nonce + owner/target authorization + cumulative fee budget), tracked as a follow-up. Regression test asserts a minimum-sized reservation migrates.
mswilkison
added a commit
that referenced
this pull request
Aug 10, 2026
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.
What this is
Draft implementation of UTXO reservations: a deposit lane where a depositor's coins are custodied by the threshold network without ever commingling with the pooled supply, and redemption returns exactly their coin lineage (unbroken 1-input-1-output spends from deposit to redemption). Motivations: bailment-style fact pattern for tax treatment of bridging, and provable client-level fund segregation for institutions — with the side benefit that reserved clients neither inherit nor contribute UTXO taint.
Wallet-side companion: threshold-network/keep-core#4238.
Design
Commingling in tBTC happens at exactly one place: the deposit sweep merges deposit UTXOs into the wallet's main UTXO, and balances are credited only there. A reservation is a deposit that is never merged:
ReservationVault(reveal.vaultdoubles as the reservation flag).Claims are minted gross, never netted — all protocol fees are explicit TBTC transfers collected by the vault (schedule: 40 bps initiation + 20 bps/yr extension + 20 bps redemption, decomposing as endpoint parity with the pooled path — a 20 bps mint leg and the standard 20 bps redemption fee — plus a 20 bps/yr custody fee that is the actual premium being purchased, with the first year prepaid inside the initiation fee. An N-year holding pays 40 + 20N bps against the pooled 40 bps round trip: strictly premium at every horizon. The redemption fee is waived on retries after wallet-fault timeouts. The schedule is intended to be stable across the FROST transition: the minimum reservation size, not the fee legs, is the governance dial that keeps carry covering per-position lifecycle costs).
Safety wiring
sweptAt; consumed anchors are recorded inspentMainUTXOs— fraud-defeat coverage with zero Fraud.sol changes.isSafeRedemption, and guardians can veto pending reserved redemptions viaRedemptionWatchtower.raiseReservedObjection(same three-objection flow, freeze, penalty burn, and redeemer ban; reuses the existing veto storage keyed by reservation key — no layout changes, upgrade-safe). Reserved vetoes deliberately share the pooled veto parameters and semantics (penalty divisor, freeze period, ban); parity is intended, not inherited by accident. Timeouts reusenotifyWalletRedemptionTimeoutslashing.beginReservationParametersUpdate/finalizeReservationParametersUpdate).Testing
26 tests in the reservation suite; full repo suite green (3000+). Includes Bitcoin SPV fixtures for all four proof validators (structurally-valid transactions + regtest-difficulty headers crafted in TypeScript; SPV checks structure/merkle/PoW, not scripts), the watchtower veto e2e (three guardian objections through the Bridge hook, penalty burn, ban blocking re-requests), validator coverage, vault fee math, retry path, and the governance-delayed parameter flow.
Decision point 1: EIP-170 strategy
Main's Bridge compiles to 24.093 KB at runs=1000 — 483 bytes under the 24.576 limit. No usable reservation API fits in that. This PR consolidates all four lifecycle proofs behind one
submitReservationProofentry point, trims the API to essentials, and gives Bridge.sol a runs=100 optimizer override (a contract-specific override — the same technique BridgeGovernance uses, though it runs at 200, not the same number), landing at 24.175 KB with the full surface including the veto hook and parameters getter.Recommendation: ship the override short-term; converge on the activation track's router architecture as the durable fix rather than inventing a parallel one. The override is one line, reversible, and precedented; the router is the right end-state but should happen once, in one shape.
Decision point 2: re-anchor miner-fee policy
Re-anchor (wallet migration) miner fees ride in-kind: they reduce the on-chain
anchorAmountwhile the gross claim (mintedAmount) is unchanged, so accumulated fees are settled by the owner at redemption (gross burn vs. reduced payout). Migrations happen on the network's schedule, so this charges clients for events they don't control — but the protocol invariant stays maximally clean ("claims burn gross; miner fees are the only in-kind deduction"), and the amounts are negligible: a re-anchor costs roughly 500–2,000 sats, i.e. sub-0.01 bp on a 10 BTC reservation, versus 20 bps/yr custody.Recommendation: keep owner-borne in the protocol; disclose in the custody agreement that all Bitcoin miner fees — including network-scheduled re-anchors — ride in-kind, and handle any client-specific rebates commercially. The design principle that rotation must not be disincentivized is preserved where it matters: operator economics are untouched.
A full design+implementation review (Codex gpt-5.6-sol, max effort) found 1 Critical, 8 High, 9 Medium, 1 Low. The SPV proof paths, replay guards, and storage layout are sound; the findings are about the settlement state machine and economic model. Root cause: the reservation lifecycle is single-phase (
request → prove) over long-lived per-position UTXOs, but tBTC settlement needs a two-phase authorize-then-prove model with request nonces and terminal settlement records (as the main redemption path has viatimedOutRedemptions). The headline bug (C-01) is a claim double-spend: a timeout/veto racing a confirmed reserved-redemption tx refunds the claim after the BTC was already paid.Critical: C-01 claim double-spend (timeout/veto races a confirmed redemption tx -> BTC paid + claim refunded).
High: H-01 re-anchor/dissolution lack on-chain authorization + proof-type ambiguity; H-02 capacity/lifecycle checked at proof time not reserved before signing (confirmed spends become unprovable); H-03 watchtower delay enforced only by the advisory validator, not the Bridge proof path; H-04 dissolution permanently underbacks by cumulative in-kind fees (treasury transfers do not burn liability); H-05 requests against Closing wallets permanently lock owner+wallet and post-grace requests defeat the stranding bound; H-06 forced termination with live anchors enables unchallengeable unbacked mint; H-07 concurrent no-main-UTXO dissolutions race; H-08 FIXED (re-anchor-floor regression).
Medium (9): retry fee-bypass, per-generation veto keying, vault-swap sweep leak, anchor-not-bound-to-named-wallet, cap semantics, term/fee bounds, instant vault fee changes + un-transferred ownership, release completeness. Low (1): stale close data.
The Critical/High settlement-race items are a deliberate follow-up redesign (two-phase authorize-then-prove with request nonces + terminal settlement records), not ad-hoc patches. Full triaged detail with file:line and fixes is maintained in the local design docs.
External review notes (Codex gpt-5.6-sol, max effort)
An independent read of the proof validators surfaced two correctness bugs, now fixed in this branch with regression tests:
redemptionTxMaxFeefromanchorAmount, reverting on underflow when a governance fee increase pushes the bound above an existing anchor — which would strand that reservation. Reformulated underflow-safe.reservationMinAmount.Open items it raised, carried as review/governance decisions (not blockers):
mintedAmount − anchorAmount + dissolutionTxFee, andreservationTotalAmounttracks current anchor assets rather than gross minted liability, so the total-reserved cap does not bound cumulative backing leakage across many dissolved positions. Immaterial at the sat-scale magnitudes and the launch cap, but worth an explicit acknowledgement (or a small burn-the-shortfall step) before the cap is widened.ReservationVault.updateFeesapplies immediately to live positions (each leg ≤ 500 bp). If the custody agreement promises schedule stability, this needs a timelock/snapshot or an explicit governance-change clause.Remaining follow-ups (all blocked on sequencing, not design)
@keep-network/tbtc-v2npm artifacts, so reservation methods cannot typecheck until this PR merges and publishes. The work itself is mechanical (vault routing inDepositsService+ a thin reservations service).