Skip to content

fix(underwriter): bind the source-deposit hash to the caller's target_amount - #556

Merged
heifner merged 1 commit into
masterfrom
fix/source-deposit-hash-uses-target-amount
Aug 10, 2026
Merged

fix(underwriter): bind the source-deposit hash to the caller's target_amount#556
heifner merged 1 commit into
masterfrom
fix/source-deposit-hash-uses-target-amount

Conversation

@heifner

@heifner heifner commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Split out of #543, which fixes this as part of a broader UIC-prevalidation refactor. It is isolated here because it is a live defect on master with a small, independently-reviewable fix, and #543 still has open design discussion on unrelated sysio.uwrit reject semantics.

The defect

Both outposts fold the user's accepted target_amount into the SwapDeposit correlation hash:

  • EVM — ReserveManagerLib.hashSwapDeposit(sender, sourceAmountDepot, args) packs args.targetAmount
  • SVM — swap_correlation_hash(..., target_amount, target_tolerance_bps)

The underwriter's source-deposit verifier recomputed that preimage from uwreq.dst_amount.

Those were the same value until #550 split them. dst_amount is now the depot's AMM quote, re-priced at ingestion; sysio.uwrit.hpp states it is "never the caller's SwapRequest.target_amount". The recomputed hash therefore matches only when the caller's target happens to equal the depot's quote exactly — and a gap between the two is the expected case, which is what variance_tolerance_bps exists to bound. Any swap with a non-zero gap fails verification, the UWREQ never leaves PENDING, and no underwriter commits.

Observed

A live ETH→SOL underwritten swap (target_amount 98039214, dst_amount 97747972) produced 148 consecutive rejections against a single stable hash pair, until the flow timed out waiting for a CONFIRMED commitment:

underwriter: source-deposit verify failed for uwreq 11 — SwapDeposit hash mismatch (id=1):
  on-chain=fd8f16aad2443acf2847c6f49c41feac6f32feadc87682df25835ae328a76695
  recomputed=758ed0f49ec37030baebfdc586a4d57a36f88492c24fcb2758d8f5b4d8ffa2ff

Recomputing that preimage both ways confirms the split precisely:

preimage destination amount keccak256
target_amount = 98039214 fd8f16aa… the hash ReserveManager emitted
dst_amount = 97747972 758ed0f4… what the verifier computed

The fix

Carry target_amount on the plugin's uw_request, read it from the uwreqs row, and use it in the preimage.

The packing was duplicated across the EVM and SVM verifiers, which is how one wrong field became two. Both now call a single source_deposit_hash helper (lifted verbatim from #543, so that PR's rebase is a no-op) whose input struct names the field target_amount — the depot's mutable quote is no longer reachable from the preimage by accident. The EVM leg also gains the explicit depositor-size check the SVM leg already had; previously it was only implied by a post-hoc buffer-length assertion the helper makes unnecessary.

Coverage

Two cases pin the preimage to the production vector above — the recomputed hash equals the SwapDeposit hash ReserveManager actually emitted, and the settlement quote demonstrably does not reproduce it.

Note the tests pin the helper's preimage layout and field semantics, not the two call sites' argument selection; the naming (target_amount vs dst_amount on the input struct) is what guards those.

Verification

  • test_underwriter_plugin — 50 cases, *** No errors detected
  • plugin_test*** No errors detected
  • nodeop links clean

No contracts/** changes, so contracts_unit_test is not implicated.

…_amount

Both outposts fold the user's accepted `target_amount` into the `SwapDeposit`
correlation hash — `ReserveManagerLib.hashSwapDeposit` on EVM,
`swap_correlation_hash` on SVM. The underwriter's source-deposit verifier
recomputed that preimage from `uwreq.dst_amount` instead.

Those were the same value until #550 split them: `dst_amount` is now the depot's
AMM quote, re-priced at ingestion, and `sysio.uwrit` documents it as "never the
caller's `target_amount`". So the recomputed hash matches only when the caller's
target happens to equal the depot's quote exactly — and a gap between the two is
the expected case, bounded by `variance_tolerance_bps`. Every other swap stalls:
verification fails, the UWREQ never leaves PENDING, and no underwriter commits.

Observed on a live cluster (ETH→SOL, target 98039214 vs quote 97747972): 148
consecutive `SwapDeposit hash mismatch` rejections against one stable hash pair,
until the flow timed out waiting for a CONFIRMED commitment.

The packing was duplicated across the EVM and SVM verifiers, which is how one
wrong field became two. Both now call a single `source_deposit_hash` helper whose
input struct names the field `target_amount`, so the depot's mutable quote is not
reachable from the preimage by accident. The EVM leg gains the explicit
depositor-size check the SVM leg already had — previously it was implied by a
post-hoc buffer-length assertion the helper makes unnecessary.

Regression coverage pins the preimage to the production vector above: the
recomputed hash equals the `SwapDeposit` hash `ReserveManager` actually emitted,
and the settlement quote demonstrably does not reproduce it.

Change-Id: Ib2365213ae59d495182360477b26dd5e16ff0062

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. As the PR description notes, these tests pin the helper's preimage rather than the two production call sites. The call-site regression test—using distinct target_amount and dst_amount values and asserting that the EVM/SVM verifier input selects target_amount—should be added in #543, which owns the broader verifier refactor.

@heifner
heifner merged commit bd479f1 into master Aug 10, 2026
13 checks passed
@heifner
heifner deleted the fix/source-deposit-hash-uses-target-amount branch August 10, 2026 17:54
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.

2 participants