trade execute: bind EVM receipt confirmation to the locally-derived tx hash - #519
trade execute: bind EVM receipt confirmation to the locally-derived tx hash#519kome12 wants to merge 5 commits into
Conversation
trade execute confirmed a broadcast transaction by polling waitForReceipt with whatever txHash the broadcaster reported, without checking it against the transaction the CLI actually signed. A compromised or buggy broadcaster could report success for a different transaction, and the CLI would proceed to the next step (e.g. broadcasting a swap after a "confirmed" allowance revoke that never happened on-chain). Add evmTxHash (keccak256 of the signed tx bytes) and confirmEvmBroadcast, which polls the receipt on our own locally-derived hash and fails closed if the broadcaster's reported hash disagrees. Apply it at every EVM executeTransaction/waitForReceipt pair in trade execute (swap, approval, and revoke-then-reapprove, across the Privy/WalletConnect/local-key paths), with a carve-out for gasless swaps where the Relay solver broadcasts its own transaction and the returned hash is legitimately different. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pr-reviewer Summary for #65fc4c8📝 1 finding Review completed. Please address the findings below. Findings by Severity
Review effort: 4/5 (Complex) SummaryThis PR correctly addresses a real security gap: the CLI was polling the receipt for whatever hash the broadcaster reported, meaning a compromised broadcaster could supply a hash for a different transaction and the CLI would proceed as if its own transaction confirmed. The fix — deriving the canonical hash locally from the signed bytes and failing closed on a mismatch — is well-reasoned, correctly applied across all three signing paths (Privy, WalletConnect fallthrough, local-key), and the gasless carve-out is sound. Test coverage is thorough. Changeset level ( Findings (1 medium)
|
…path Fold the duplicated TXHASH_MISMATCH check (confirmEvmBroadcast plus the two inline WalletConnect broadcast sites) into a single assertTxHashMatch helper that returns the locally-derived hash and fails closed on a mismatch. No behavior change — the WalletConnect sites already bound their receipt wait to the local hash; this just removes the copy-pasted error message. Add a unit test for guarantee #2: when the broadcaster returns no hash, confirmEvmBroadcast polls OUR locally-derived hash, never a foreign hash that happens to have a receipt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses pr-reviewer's finding on #519: the four confirmEvmBroadcast success-log lines printed the broadcaster's reported txHash, while the receipt was actually polled on our locally-derived hash. They are identical whenever the equality check passes (always, today), so the log was never wrong — but it showed a value we hadn't independently verified. Return the local hash from confirmEvmBroadcast alongside the receipt and log that instead, so the success line always names the transaction we confirmed landed. Extend the guarantee-#2 test to assert the returned hash is our local hash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The main EVM swap block dropped its `&& result.txHash` guard so the non-gasless path can fall back to polling our locally-derived hash (guarantee #2). But the gasless branch has no local hash to bind to — the Relay solver broadcasts its own tx — so a gasless success with no reported hash would call waitForReceipt(chain, undefined), poll eth_getTransactionReceipt([undefined]) for the full 180s timeout, and then falsely report a revert. Restore the pre-existing skip for the gasless branch only: poll a receipt when the solver reports a hash, otherwise skip. Non-gasless behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
trade executeconfirmed a broadcast EVM transaction by pollingwaitForReceiptwith whatevertxHashthe broadcaster reported, without ever checking it against the transaction the CLI actually signed. A compromised or buggy broadcaster could report success for a different transaction, and the CLI would proceed to the next step (e.g. broadcasting a swap after a "confirmed" allowance revoke that never happened on-chain).evmTxHash(keccak256 over the raw signed tx bytes) andconfirmEvmBroadcast, which polls the receipt on our own locally-derived hash and fails closed with a clear, actionable error if the broadcaster's reported hash disagrees.executeTransaction/waitForReceiptpair intrade execute— the swap broadcast, ordinary approvals, and the revoke-then-reapprove flow — across the Privy, WalletConnect, and local-key signing paths.TXHASH_MISMATCHis now fatal (re-thrown) everywhere it can occur, instead of being swallowed into "try the next quote" — a broadcaster-integrity failure shouldn't be treated like a bad quote.src/bridge.jsunchanged (out of scope — no raw signed bytes available in the former; different broadcast mechanism in the latter).Test plan
npm test— 2127 tests passing, including newevmTxHashunit tests (known-vector, hex normalization, rejection cases, EIP-1559/legacy signing round-trips), a fail-closed mismatch test, and a gasless-regression testnpm run lintexecutetest mocks updated to echo the real hash of the signed bytes, as a correct broadcaster wouldnode src/index.js trade quote/trade execute) against a local mock trading-api + RPC server: a correct broadcaster confirms normally, a broadcaster returning a mismatched hash is rejected with the fail-closed error, before any success is reported🤖 Generated with Claude Code