Add EVM verify transaction simulation (#1377)#1446
Add EVM verify transaction simulation (#1377)#1446ruhil6789 wants to merge 3 commits intocoinbase:mainfrom
Conversation
🟡 Heimdall Review Status
|
|
@ruhil6789 is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
…nbase#1377) Wire simulateContract into @x402/evm verify path: dry-run transferWithAuthorization / settle before settle() to reject payloads that would revert (expired auth, invalid signature, etc.). Includes revert reason decoding, extension signer simulateContract type, and tests.
|
@ryanRfox |
ryanRfox
left a comment
There was a problem hiding this comment.
Great work responding to my feedback, @ruhil6789! The core simulation logic in verifyEIP3009 and verifyPermit2 is exactly what #1377 needed — pre-settlement dry-runs that catch doomed transactions before gas is spent. The revert reason decoding and test coverage are solid.
A few suggestions inline, none blocking. Approving as-is.
| } | ||
| } | ||
|
|
||
| function decodePermit2RevertReason(error: unknown): string { |
There was a problem hiding this comment.
Suggestion (non-blocking): decodePermit2RevertReason and decodeEip3009RevertReason are structurally identical — same viem error unwrapping logic, same regex patterns. The only difference is which KNOWN_REVERT_REASONS map gets consulted via the normalize function.
Consider extracting a shared helper:
function decodeRevertReason(error: unknown, knownReasons: Record<string, string>): stringThis would live in a shared utils file and be called from both paths. Reduces ~60 lines of duplication and ensures future error-parsing improvements apply to both mechanisms.
| * Used by facilitators to perform eth_call-based transaction simulation | ||
| * during verification to detect potential settlement reverts. | ||
| */ | ||
| simulateContract(args: { |
There was a problem hiding this comment.
Question: simulateContract is added as a required field on FacilitatorEvmSigner. Any existing code that constructs a signer without it will now fail to compile.
Was this intentional? If so, the minor changeset bump is appropriate for the new capability, but it's worth calling out in the changeset description that this is a breaking type change for existing facilitator implementations.
Alternatively, making it optional (simulateContract?:) and having verify gracefully skip simulation when absent would preserve backwards compatibility. The verify path would still work for existing integrations — they'd just not get the simulation safety net until they upgrade their signer.
There was a problem hiding this comment.
Question:
simulateContractis added as a required field onFacilitatorEvmSigner. Any existing code that constructs a signer without it will now fail to compile.Was this intentional? If so, the
minorchangeset bump is appropriate for the new capability, but it's worth calling out in the changeset description that this is a breaking type change for existing facilitator implementations.Alternatively, making it optional (
simulateContract?:) and having verify gracefully skip simulation when absent would preserve backwards compatibility. The verify path would still work for existing integrations — they'd just not get the simulation safety net until they upgrade their signer.
Great point on the type surface area.
Yes, making simulateContract required on FacilitatorEvmSigner was intentional so that every facilitator wired into the @x402/evm verify path actually gets the simulation safety net. Given that verify now relies on pre-settlement dry-runs to protect against doomed settlements, I felt it was better to make this a required capability rather than silently skipping simulation.
I agree this is a meaningful type change and I’ll update the changeset description to explicitly call out that:
FacilitatorEvmSignernow requires asimulateContractmethod, and- custom facilitator implementations need to be updated to provide it (typically by wiring to
viemClient.simulateContract).
If you’d prefer a softer migration path (optional method + feature-detection with a log/metric when simulation is absent), I’m happy to follow up with a compatibility shim, but for now I’d like to keep the verify path assuming simulation is available.
There was a problem hiding this comment.
I’m happy with it as-is, so I defer to maintainers on these points. Again, nice work.
| const parsedSig = parseSignature(signature); | ||
| callArgs = [ | ||
| ...baseArgs, | ||
| (parsedSig.v as number | undefined) || parsedSig.yParity, |
There was a problem hiding this comment.
Nit: This uses || which would fall through on any falsy value (including v=0). While valid EIP-155 v values are 27/28 and viem's parseSignature guards against malformed input, using nullish coalescing (??) makes the intent explicit and is more defensive:
(parsedSig.v as number | undefined) ?? parsedSig.yParityNote: the existing settlement code at line ~421 uses the same || pattern, so this isn't a simulation-vs-settlement divergence — but both could benefit from ??.
There was a problem hiding this comment.
Nit: This uses
||which would fall through on any falsy value (includingv=0). While valid EIP-155 v values are 27/28 and viem'sparseSignatureguards against malformed input, using nullish coalescing (??) makes the intent explicit and is more defensive:(parsedSig.v as number | undefined) ?? parsedSig.yParityNote: the existing settlement code at line ~421 uses the same
||pattern, so this isn't a simulation-vs-settlement divergence — but both could benefit from??.
Agreed — ?? is the better fit here.
Even though viem’s parseSignature shouldn’t produce v = 0 for valid inputs and the existing settle path used ||, using nullish coalescing makes the intent explicit and removes the “falsy” edge case. I’ll update both the simulation and settlement call sites to:
(parsedSig.v as number | undefined) ?? parsedSig.yParity
so they stay aligned and a bit more defensive.
| .registerV1("solana-devnet" as Network, new ExactSvmSchemeV1(svmSigner)); | ||
|
|
||
| // Optionally register Aptos if configured | ||
| // Optionally register Aptos if configured (dynamic import avoids requiring optional deps at load time) |
There was a problem hiding this comment.
Observation (non-blocking): The Aptos dynamic import and Extensions.* namespace change are nice cleanups but unrelated to #1377. Might be worth a one-liner in the PR description noting these as incidental housekeeping so reviewers can focus on the simulation logic. Not worth splitting at this point.
|
Review Error for ryanRfox @ 2026-03-05 10:59:09 UTC |
|
@ryanRfox For this PR I kept the functions local to keep the diff focused on wiring up simulation, but I agree this should be a shared helper. I’ll follow up with a small refactor that introduces something like: decodeRevertReason(error: unknown, knownReasons: Record<string, string>) in a shared utils module and have both |
Closes #1377.
This PR adds pre-settlement transaction simulation to the EVM facilitator verify path so that payloads which would revert at settlement time are rejected before the facilitator spends gas.
Concretely:
verifyEIP3009now calls a newsimulateEip3009Settlementhelper that:simulateContractto runtransferWithAuthorizationagainst the target ERC‑20.v,r,s) and bytes signatures.AuthorizationAlreadyUsed,InvalidSignature) into clearinvalidReasonmessages.verifyPermit2now simulates the final settlement call:settleWithPermit; otherwise it simulatessettle.InvalidNonce,InvalidSignature) intoinvalidReasonon verification failure.simulateContractmethod, and wires them to viem’ssimulateContract.site/app/facilitator/index.tsto:@aptos-labs/ts-sdk,@x402/aptos) so Aptos remains fully optional.@x402/extensionsnamespace and registerEIP2612_GAS_SPONSORING/createErc20ApprovalGasSponsoringExtensioncorrectly.Tests
pnpm --filter @x402/evm test -- test/unit/exact/facilitator.test.ts test/unit/v1/facilitator.test.tspnpm --filter @x402/extensions run buildpnpm --filter @x402/aptos run buildpnpm --filter site lint:check(passes, includingsite/app/facilitator/index.ts)Checklist