Preserve the synchronous reserve-time pin (fix quote↔settlement rate divergence)#451
Open
anderdc wants to merge 1 commit into
Open
Preserve the synchronous reserve-time pin (fix quote↔settlement rate divergence)#451anderdc wants to merge 1 commit into
anderdc wants to merge 1 commit into
Conversation
The event watcher re-read the miner's commitment at the reservation's on-chain inclusion block and overwrote the pin the reserve handler had already written. When a miner moves its rate between the handler's quote-validation read and the inclusion of vote_reserve, the re-read captures a different rate than the one the on-chain to_amount was reserved against. That settlement rate then diverges from the reserved amount and the user is short-changed at confirm (swap 2405: reserved at 370, pinned to 280, settled ~24% low). The synchronous pin is written at the instant the user's quote was validated, so it is the authoritative reserve-time rate. Make the watcher's read a pure backfill: only write the settlement pin when none exists. A NOTE/TODO documents that this relies on the single-validator invariant (one authoritative synchronous pin) and that the multi-validator fix is to bind (reserve_block, rate) into the reservation at quorum and verify it against CommitmentOf(block) within slippage, which requires a contract iteration. Scoring-overlay pin events are intentionally left reading the canonical block (separate scoring workstream).
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.
Problem
A user reserved a swap at one rate but settled at a worse one — silently, well beyond the protocol fee. Concretely, swap 2405 (btc→tao): reserved/quoted at rate 370 (
to_amount = 315,610,000), but the swap settled at rate 280 (deliveredAmount = 236,451,600) — the user received ~24% less than the swap's owndestAmount, with no slippage check in between.Root cause
The reservation pin is written twice:
handle_swap_reservewrites a synchronous pin at the instant it reads the miner's commitment to validate the user's quote. This is the rate the on-chainto_amountwas reserved against — i.e. correct.event_watcher.record_reservation_pinthen re-reads the commitment at the reservation's on-chain inclusion block and overwrites the pin (INSERT OR REPLACE, keyed by miner).When the miner moves its rate between the handler's read and the inclusion of
vote_reserve, those two reads see different rates. For 2405 the miner was oscillating 370↔280 every ~2 blocks, and the inclusion block landed on a 280 tick:At
handle_swap_confirm, the divergence is baked in:to_amount(370-based) comes from the on-chain reservation, whileratecomes from the (overwritten) pin (280).expected_swap_amounts— the single source of truth for both miner delivery and validator verification — keys offrate, so the miner delivered the 280 amount and the validator confirmed it. Nothing anywhere compares the 370 amount to the 280 rate.Fix
The synchronous pin is the authoritative reserve-time rate. Make the watcher's read a pure backfill: only write the settlement pin when none already exists. This stops the overwrite that introduced the stale rate, so confirm settles at the rate the user reserved against.
NOTE / TODO(contract-v2, multi-validator)documents the scope: this relies on the single-validator invariant (exactly one authoritative synchronous pin). With multiple validators, each synchronous pin is read at its own instant and isn't deterministic across the set; the correct fix then is to bind(reserve_block, rate)into the reservation at quorum and verifyrate == CommitmentOf(reserve_block)within the user's slippage band — which needs the reserve hash +Reservationstruct to carry the rate, i.e. a smart-contract iteration. Until v2 lands, we back off to the synchronous pin.Why this is safe now
There is currently a single whitelisted validator, so the synchronous pin is always present and authoritative — preferring it fully closes the divergence. The change degrades gracefully: if the synchronous write ever failed, the watcher still backfills from the inclusion block (current behavior).
Tests
test_existing_synchronous_pin_is_not_overwritten— asserts a pre-existing 370 pin survives aMinerReservedre-read that sees 280.test_miner_reserved_writes_expected_pinstill passes (fresh state → backfill writes the pin).Not in scope