Skip to content

Bind Solana swap execution to the persisted request intent - #512

Merged
kome12 merged 6 commits into
mainfrom
fix/verify-solana-swap-outcome
Aug 26, 2026
Merged

Bind Solana swap execution to the persisted request intent#512
kome12 merged 6 commits into
mainfrom
fix/verify-solana-swap-outcome

Conversation

@kome12

@kome12 kome12 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Wire the existing intent-binding checks (assertQuoteMatchesRequest, a new assertCompleteSolanaRequestIntent) into all three Solana signing paths (local, Privy, WalletConnect), closing the gap where Solana previously signed the aggregator's transaction verbatim with no validation. This is the Solana counterpart to PR Harden EVM swap signing: scope approvals and validate target #494's EVM hardening.
  • --swap-mode exactOut now requires --max-input on Solana too (previously EVM-only). A cap auto-derived from the API's own quote response can't independently guard against an inflated one — an explicit, user-supplied ceiling is required on every chain now, matching EVM.

Known follow-up (not in scope here)

While doing live e2e testing, found a pre-existing bug on main, unrelated to this diff: Relay-sourced Solana bridge quotes return raw, uncompiled instructions ({instructions, addressLookupTableAddresses}) rather than a ready-to-sign transaction, and the signing code has no branch for that shape — crashes with ALL_QUOTES_FAILED. Filed separately as API-353; confirmed via git diff origin/main that the affected code is untouched by this PR.

Two smaller/cosmetic review findings also left as follow-up (not correctness bugs): duplicated assertCompleteEvmRequestIntent/assertCompleteSolanaRequestIntent (copy-paste instead of a shared helper), and duplicated native-SOL-mint literals across trading.js/trade-validation.js (to avoid a circular import).

Test plan

  • npm test — 2105 passed, 2 skipped
  • npm run lint — clean
  • Live mainnet e2e (bridge-test wallet): SOL ↔ USDC same-chain round-trip via the local-wallet signing path this PR changes — both legs succeeded, net cost ~143,584 lamports (fees/slippage only)
  • Code review pass; one confirmed correctness bug found and fixed (the self-referential maxInputAmount cap), see commit c1b8ffb

🤖 Generated with Claude Code

@nansen-pr-reviewer

nansen-pr-reviewer Bot commented Aug 21, 2026

Copy link
Copy Markdown

pr-reviewer Summary for #f5c4051

No issues found

The code review completed successfully with no findings.

Review effort: 4/5 (Complex)

Summary

This PR correctly closes the intent-binding gap for Solana swap signing. The implementation is well-structured and the security properties are sound. No actionable bugs found.

Overall assessment: This is a solid security hardening PR. The three signing paths (local, Privy, WalletConnect) all have assertCompleteSolanaRequestIntent + assertQuoteMatchesRequest inserted at the right point — before any signing call — with explicit fail-closed guards on the wallet address resolution. The --max-input enforcement for Solana exactOut is correctly placed before any API call and the self-referential cap bug mentioned in the PR description was already fixed. Tests are comprehensive: adversarial scenarios cover tampered token pairs, inflated inputs, wrong wallet binding, and missing intent across all three signer paths, plus a regression test confirming benign quotes still complete successfully. The changeset is present with a correct minor bump, and schema.json is updated to match the new behavior.


Token usage: 2,878 input, 10,233 output, 2,312,641 cache read, 68,071 cache write | Usage Guide

New pushes are reviewed automatically with a 10-minute cooldown between reviews. To request a review at any time, comment @nansen-pr-reviewer re-review.

@gulshngill

Copy link
Copy Markdown
Contributor

Follow-up review findings:

  1. src/trading.js:2053-2056 and :2277-2279 — The comments overstate the protection provided by the new validation. The implementation validates persisted request/quote metadata, but does not inspect the serialized Solana transaction instructions. Please narrow the wording so it does not imply that the signed transaction’s contents are fully bound to the persisted intent.

  2. src/trading.js:2274 — The local Solana signing path does not apply the same fail-closed wallet-address check used by the Privy path. Please add the equivalent guard so all signing paths consistently reject an unavailable signer address before intent validation/signing.

  3. src/__tests__/trading.test.js:1580-1731 and :4557-4737 — There is some redundant coverage between the Solana validation suites. Please consolidate the overlapping cases while retaining the distinct signer-path coverage.

  4. Please rebase the branch onto the current base branch and remove any duplicate hunk that is already present upstream before merging.

kome12 and others added 6 commits August 26, 2026 08:00
Solana previously signed the aggregator's serialized transaction verbatim,
with no validation against what the user actually requested. Wire the
existing assertQuoteMatchesRequest/completeness checks (already used on EVM)
into all three Solana signing paths (local, Privy, WalletConnect) so a
compromised or buggy quote can't swap in a different token pair, inflate
the input, or target a different signer. exactOut on Solana now also
persists a derived spend ceiling so the check always has a bound, without
requiring --max-input.

Also fixes a false-positive this surfaced: some aggregators report native
SOL as the System Program ID rather than the wrapped-SOL mint, which the
new token-pair check would have rejected as a mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The auto-derived maxInputAmount for Solana exactOut was computed from the
same quotes it was later checked against, so assertInputWithinMax could
never reject anything — a compromised or buggy quote could inflate the
input by 10x and the "cap" would just inflate along with it. Require an
explicit --max-input on Solana too, matching EVM, so the ceiling is an
independently-supplied value instead of the API checking itself.

Also makes the assertInputWithinMax over-cap error chain-aware, since
Solana now exercises it for real (no ERC-20 "approval" concept to
reference in the message).
Adds a fail-closed null check for the Privy wallet address before the
quote-binding assertions run, so the signer-binding check cannot be
silently skipped when address resolution fails. Updates the surrounding
comments for clarity, and rewords the changeset description to be more
user-facing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The description still said "optional on Solana" after the previous commit
made it required on Solana too, matching EVM behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
assertInputWithinMax picked the EVM-worded (approval/native-value) over-cap
message for a Solana swap whenever request.chain was persisted in non-lowercase
form (e.g. `--chain Solana`), since request.chain stores the raw --chain input
verbatim. Normalize the case before choosing the message. Wording-only; the
throw itself was already correct.

Add integration coverage proving assertQuoteMatchesRequest /
assertCompleteSolanaRequestIntent are actually wired into all three Solana
signing paths (local, Privy, WalletConnect): each test poisons a different
quote field (output token, inflated input, swapped signer) and asserts the
execute path refuses before signing. Mutation-verified as non-vacuous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…signer, dedupe tests

Comments at the Privy and local Solana signing paths implied the intent-binding
checks validate the serialized transaction itself; they only validate persisted
request/quote metadata. Reworded to say so explicitly.

The local wallet signing branch resolved solanaWalletAddress without checking it,
unlike the Privy path's existing fail-closed guard — an unresolved address would
silently skip the signer-binding comparison. Added the matching check.

Consolidated the "Solana execute binds to persisted request intent" and "Solana
intent binding (adversarial)" describe blocks: several cases (local/WalletConnect
signer-swap) were tested near-identically in both. Merged the distinct cases
(local output-token mismatch, Privy inflated-input) into the adversarial suite
and removed the duplicate block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kome12
kome12 force-pushed the fix/verify-solana-swap-outcome branch from 7b59fac to f5c4051 Compare August 25, 2026 23:12
@kome12

kome12 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up review findings:

  1. src/trading.js:2053-2056 and :2277-2279 — The comments overstate the protection provided by the new validation. The implementation validates persisted request/quote metadata, but does not inspect the serialized Solana transaction instructions. Please narrow the wording so it does not imply that the signed transaction’s contents are fully bound to the persisted intent.
  2. src/trading.js:2274 — The local Solana signing path does not apply the same fail-closed wallet-address check used by the Privy path. Please add the equivalent guard so all signing paths consistently reject an unavailable signer address before intent validation/signing.
  3. src/__tests__/trading.test.js:1580-1731 and :4557-4737 — There is some redundant coverage between the Solana validation suites. Please consolidate the overlapping cases while retaining the distinct signer-path coverage.
  4. Please rebase the branch onto the current base branch and remove any duplicate hunk that is already present upstream before merging.

@gulshngill Thanks addressed your findings. Rebased from the latest main as well.

In terms of scope, this PR only addresses the metadata binding and not the transaction instructions. Follow up PRs will address this.

@kome12
kome12 requested a review from gulshngill August 25, 2026 23:34
@kome12
kome12 merged commit be512b6 into main Aug 26, 2026
9 checks passed
@kome12
kome12 deleted the fix/verify-solana-swap-outcome branch August 26, 2026 03:41
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
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