spec(auth-capture/evm): require trace-level simulation + gas cap for smart contract captureAuthorizer#60
Conversation
…smart contract captureAuthorizer
Adds a "Smart Contract captureAuthorizer" section to the EVM scheme
spec that distinguishes the EOA path from the contract path and lays
out the additional facilitator checks required on the contract path:
- Trace-level simulation (eth_simulateV1 or equivalent), not just
revert-only.
- Escrow event verification: the trace MUST contain the expected
AuthCaptureEscrow event emitted by AUTH_CAPTURE_ESCROW_ADDRESS, with
paymentInfoHash matching the payer-agnostic hash from verification
step 12.
- Asset-delta verification: reconstruct ERC-20 Transfer deltas for
requirements.asset and bound them to {payer, receiver, feeReceiver,
escrow}, with a fee split within [minFeeBps, maxFeeBps].
- Gas cap on both simulation and broadcast. Recommended cap: 400k gas,
~2x the worst-case legitimate charge path through the audited escrow
with the ERC-3009 collector (measured at ~181k on Base).
Also adds operational hardening guidance (gas-only hot wallet, no
native value, cap applies pre-broadcast not just in simulation),
updates the verification + settlement steps to point at the new
section, and adds four new error codes covering the new failure
modes.
This captures concrete requirements behind the existing prose-level
"facilitators MAY accept smart contract captureAuthorizers"
allowance, so a compliant facilitator cannot reduce contract-path
safety to simulateContract().revertOrOk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ionale The cap paragraph was citing "~181k measured on Base via the upstream gas benchmark," which is an implementation-side observation rather than a protocol fact. Keeps the concrete 400k recommendation and the rest of the rationale (covers both assetTransferMethod values with thin-wrapper headroom; bounds facilitator gas exposure) but removes the specific measurement reference so the spec reads as a generic EVM binding rather than something derived from one stack's profiling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Reviewed the spec end-to-end against the deployed escrow ( 1. Gas cap is a DoS bound, not a correctness bound — make this explicitThe spec implies the 400k cap helps prevent the wrapper from misbehaving. It doesn't. EIP-150's 63/64 rule means a cap on the outer call doesn't bound the inner escrow call — a wrapper can OOG the escrow call internally and still return success. The reason this is safe today is the event-presence check: if escrow ran out of gas, no Worth a sentence in the "Required checks" section saying explicitly:
This matters because someone implementing this from the spec might assume "gas cap + revert-only sim" is enough and skip the event check, which would silently re-open the original attack. 2. Promote the gas-only hot wallet to MUST on the contract pathUnder "Operational hardening" the three items are SHOULD. The "gas-only wallet, no token approvals" one is the only mitigation that survives a check bypass — if the trace checks all pass but the wrapper does something unexpected at broadcast time (state-dependent on `block.number`, `block.timestamp`, oracle reads, etc.), funds aren't extractable as long as the submitter EOA holds no balances or approvals. The other two SHOULDs are tripwires; this one is the wall. This is the same philosophy 0x's Settler uses — security from architecture, not from verifying the callee. 3. `eth_simulateV1` cross-client behavior isn't uniformAs of late 2025, observable divergence: Geth issues 30950 + 32852 (log timestamps, ethclient bindings), Nethermind 8412 (`validation:true` + multi-tx), Reth 8281 + 13421. viem's `simulateCalls` wraps whichever client the RPC runs. Two facilitators running different providers can see different gating on the same broadcast. The impl already restates checks in terms of decoded events (good). Spec should make that explicit:
4. Headroom on 400k is genuinely tight110% over the measured ~181k worst-case covers a passthrough or simple multisig threshold check. A 3-of-5 multisig that recovers + verifies signatures on-chain, or anything that does an oracle read, eats 150k+ on its own and won't fit. The "MAY raise per-authorizer" provision handles this — but spec might want to flag that the default cap is calibrated for thin wrappers and that arbiter-class authorizers will routinely need raised caps. 5. Out of scope but worth flagging: structural alternativeThe spec is the right interim defense given the escrow can't be changed. The structurally correct long-term fix is moving the smart-contract authorization off the wrapper-call path entirely — e.g. an EIP-1271 attestation from the smart contract, with the facilitator submitting directly to escrow as the operator. No wrapper call, no trace simulation, no gas DoS surface. Not for this PR, but worth noting in the doc as a future direction so this isn't read as the terminal design. What's solid
|
Four changes addressing review feedback: 1. Clarify gas cap is DoS bound, not correctness. EIP-150's 63/64 rule means the outer cap doesn't strictly bound the inner escrow call; a wrapper can pre-burn gas so escrow OOGs internally and still return success. The escrow event check (2) is what catches that case — make it explicit so implementers don't drop the event check thinking the gas cap suffices. 2. Promote gas-only hot wallet from SHOULD to MUST (with "MUST partition" wording so paymaster-style submitters that hold non-asset balances are still allowed). This is the architectural defense that survives a check bypass; the trace checks are tripwires, the wallet partition is the wall. 3. Add MUST language for expressing checks (2) and (3) in terms of decoded Transfer and escrow events rather than raw trace structure. eth_simulateV1 layout is not uniform across execution clients; decoded events stay portable. 4. Raise default cap from 400_000 to 3_000_000 gas. Covers the audited escrow path plus on-chain logic up to and including modern zk verifier circuits (Groth16, PLONK, Halo2, most STARK constructions). The previous 400k cap forced almost any non-trivial captureAuthorizer (multi-oracle, zk-anchored) to negotiate higher caps off-chain, but there is no protocol mechanism for that — drop the "MAY raise per-authorizer" line so the spec stops endorsing a mechanism it doesn't define. Per-attack DoS exposure at 3M gas remains modest (~$0.10 on Base, ~$0.32 on L1 at 30 gwei) and is intentionally backstopped by the gas-only wallet partition (operational hardening), not the cap itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks, agreed on most. Pushed 1. Gas cap is a DoS bound, not correctness. Agreed and added explicit language under the "Required checks" section calling out EIP-150's 63/64 rule and naming the event check as the correctness primitive. Someone implementing "gas cap + revert-only sim" and dropping (2) would silently reopen the original attack — worth being noisy about. 2. Gas-only hot wallet → MUST. Promoted, with "MUST partition" wording rather than "MUST hold zero" so paymaster-style submitters (need non-asset balances for gas sponsoring) aren't precluded. Architectural defense vs. tripwires framing made explicit. 3. 4. Cap headroom. Recalibrated and went further: raised the cap to 3,000,000 gas. After tighter math on realistic arbiter-class wrappers — 3-of-5 ECDSA multisig (~30k), refund arbiter with Chainlink read (~50k), composite condition trees (~30k each) — 400k actually has ~50-70% headroom for typical cases, not the 30% I initially worried about. But moving to 3M means the cap also covers on-chain zk verifiers (Groth16 ~300k, PLONK ~500k, Halo2 ~1M, most Plonky2/STARK constructions). Per-attack DoS exposure at 3M is ~$0.10 on Base / ~$0.32 on L1 at 30 gwei — trivially absorbable with rate limiting, especially given (2) above said the cap is just a blast-radius bound anyway. Also dropped the "MAY raise per-authorizer" line. As you noted there's no protocol mechanism for discovering which authorizers a facilitator supports at higher caps — it would have to be off-chain coordination, and the spec was endorsing a "mechanism" it didn't actually define. At 3M the escape hatch is theoretical for all realistic deployments; if a facilitator wants to support truly pathological wrappers (recursive STARK aggregators), that's their call but the spec doesn't need to bless it. 5. EIP-1271 attestation as long-term direction. Skipping. This spec is scoped to the deployed Companion impl fixup landing on #61 shortly (cap value + matching test threshold). |
|
Round-2 follow-up. Items 1, 2, 3 from the original review landed cleanly. Two spec-impl divergences introduced in the round-2 rewording need fixing before this is final, plus three smaller items. 1. (blocking) Spec says "escrow" in the asset-delta allowed set; impl correctly uses the operator's TokenStore
A second implementer following the spec literally would whitelist the escrow address and reject every honest contract-path trace. Fix: replace "escrow" with "the operator's TokenStore ( 2. (blocking) Spec doesn't cover the
|
Summary
Adds a "Smart Contract
captureAuthorizer" section toscheme_auth_capture_evm.mdthat lays out concrete facilitator requirements whenextra.captureAuthorizeris a smart contract (e.g. an arbiter / multisig / passthrough wrapper).Today the spec leaves the contract path under-specified: a facilitator can
simulateContract(escrow.authorize), see a non-revert, and broadcast. That's insufficient because:AuthCaptureEscrowwith the signedPaymentInfo.minFeeBps/maxFeeBps, allowed recipients).The spec PR makes the contract path's required behavior explicit:
eth_simulateV1or equivalent), not just revert-only.AuthCaptureEscrowevent emitted byAUTH_CAPTURE_ESCROW_ADDRESS, withpaymentInfoHashmatching the payer-agnostic hash from verification step 12.Transferdeltas forrequirements.assetand bound them to{payer, receiver, feeReceiver, escrow}, with the split implied by somefeeBps ∈ [minFeeBps, maxFeeBps].chargepath through the audited escrow with the ERC-3009 collector (measured at ~181k on Base via the upstream gas benchmark inbase/commerce-payments).Also adds operational hardening guidance (gas-only hot wallet with no token balances or approvals, reject
value > 0, gas cap applies pre-broadcast not just in simulation), and four new error codes (capture_authorizer_escrow_call_missing,capture_authorizer_payment_info_mismatch,capture_authorizer_asset_divergence,capture_authorizer_gas_exceeded).Why now
This is preparatory work before bringing the same hardening to the upstream
@x402/evmauth-capture mechanism (x402-foundation/x402 PR 2308). Settling the spec here first means the TS implementation can land referencing a published normative source instead of inventing the contract.Scope discipline
This PR deliberately does NOT touch:
typediscriminator.serverAuthorizationandextra.serverAuthorizationRequired.paymentInfotraveling in the payload (vs being reconstructed).Those are landing through a separate upstream effort and are still in flux; locking them into our spec now would be premature.
Test plan
pnpm buildruns as part of the pre-commit hook and passed.packages/mechanisms/evm/src/auth-capture/facilitator/scheme.ts.