Organization
- @coinbase
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
4 findings
4 fixed
0 acknowledged
Low Risk3 findings
Signature griefing/frontrunning in x402UptoPermit2Proxy
State
Severity
- Severity: Low
Submitted by
red-swan
Description
In the
x402UptoPermit2Proxy, a user signs a permit for a maximum amount (permit.permitted.amount). The facilitator then callssettle()orsettleWithPermit()with a specificamountthat is less than or equal to the maximum.The Permit2 signature covers the
permitand thewitness(destination, timestamp), but it does not cover theamountparameter passed tosettle().function settle(..., uint256 amount, ...) external nonReentrant { if (amount > permit.permitted.amount) revert AmountExceedsPermitted(); _settle(permit, amount, owner, witness, signature);}This gives anyone monitoring the mempool (or even a malicious/incompetent facilitator) the opportunity to attempt to settle the payment with a different amount. This could be an amount near zero or for the full permissioned amount. The funds will still reach the correct recipient, but this disrupts the intended payment, and the payer/payee may be forced to settle for an unintended amount.
Recommendation
Require that the payer sign over the facilitator's address and they be the caller of this function so that only they have the intended power to choose the final settlement amount.
Coinbase: Fixed in 373ff0e6e — add facilitator to Witness struct to prevent settlement frontrunning and amount.
Cantina: Verified fix. But the fixes introduced a centralization risk for the facilitator.
Unbounded length for extra data in witness struct
State
Severity
- Severity: Low
Submitted by
red-swan
Description
The Witness struct in
x402BasePermit2Proxy.soldefines an extra field as a dynamically-sized bytes type with no upper-bound length validation:struct Witness { address to; uint256 validAfter; bytes extra; }During settlement, the
_settle()function computeskeccak256(witness.extra)as part of the witness hash reconstruction:bytes32 witnessHash = keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.validAfter, keccak256(witness.extra)));The keccak256 precompile costs
30 gas base + 6 gas per 32-byte word. A malicious client can craft a payment payload with an arbitrarily large extra field (e.g., several hundred kilobytes), sign it, and submit it to the facilitator.Since the facilitator must pass the exact extra value that was signed to reconstruct a valid witness hash, they are forced to:
- Relay the bloated calldata (16 gas per non-zero byte, 4 gas per zero byte)
- Pay for the keccak256 computation over the full extra length
In an automated facilitator system that does not pre-validate payload sizes, a malicious client can grief the facilitator into spending significantly more gas than expected, while the actual payment amount remains minimal. The facilitator bears the cost because they are the transaction broadcaster per the X402 protocol design.
Recommendation
Consider enforcing a maximum length for
witness.extrain the_settle()function.Coinbase: Fixed in 0ff7cffbd. Removed the extra bytes field from Witness entirely, along with its keccak256 hashing, since it served no on-chain purpose and allowed arbitrarily large calldata to grief facilitators.
Cantina: Verified fix.
Absence of zero-amount validation allows no-op settlements that consume nonces
State
Severity
- Severity: Low
Submitted by
red-swan
Description
Neither
x402BasePermit2Proxy._settle()nor the derived contracts (x402ExactPermit2Proxy, x402UptoPermit2Proxy) validate that the settlement amount is non-zero.This has two distinct exploitation paths:
-
Exact variant — Client griefs Facilitator: A client signs a permit with
permitted.amount = 0and submits it as payment. An automated facilitator that does not pre-validate the amount off-chain may grant the resource, settle on-chain, and receive zero tokens. The transaction succeeds, aSettled()event is emitted, and the facilitator bears the gas cost for a no-op transfer. -
Upto variant — Facilitator griefs Client: A facilitator receives a valid signature from a client for amount X, but calls settle() with amount = 0. The Permit2 nonce is permanently consumed, invalidating the client's signature. No tokens are transferred, yet the client cannot reuse that signature to pay another facilitator. The facilitator effectively burns the client's payment authorization without settling.
In both cases, the
Settled()event is emitted, creating misleading on-chain records that suggest a successful payment occurred.Recommendation
Consider adding a zero-amount check in
_settle()to reject economically meaningless settlements.Coinbase: Fixed in 04b844da6. Added InvalidAmount error and amount == 0 check in _settle() to block economically meaningless settlements that waste gas and consume Permit2 nonces.
Cantina: Verified fix.
Informational4 findings
Replace tx.origin with msg.sender in constructor to fully support multisig deployment
State
Severity
- Severity: Informational
Submitted by
red-swan
Description
The
x402BasePermit2Proxycontract usestx.originin the constructor to identify the_deployer, who is the only address authorized to callinitialize(). This is problematic because the deployment strategy involves deploying and initializing it from a Multisig.tx.originin this context will be the EOA that initiated the transaction, not the Multisig contract itself. Consequently, the Multisig will not be the authorized_deployer.Recommendation
Remove the reference to
tx.originin the constructor, and bundle the deployment and initialization into a single multi-call transaction to prevent front-running.Coinbase: Fixed in 7e154ff and 0042512. For deployment reasons we've removed initialize and moved the permit2 assignment to the constructor.
Cantina: Verified fix.
Non-standard variable naming
State
Severity
- Severity: Informational
Submitted by
red-swan
Description
The variable
PERMIT2is capitalized like a constant but it is a state variable set duringinitialize(). Per the Solidity style guide, it should use lowercase (e.g.,permit2).Recommendation
Consider making
PERMIT2lowercase.Coinbase: Fixed in 2ce095d
Cantina: Verified fix.
Lack of observability for EIP-2612 permit failures
State
Severity
- Severity: Informational
Submitted by
red-swan
Description
The
_executePermit()function inx402BasePermit2Proxy.solwraps the EIP-2612permit()call in a try/catch that silently discards all errors. This design is intentional — if the Permit2 allowance already exists, the EIP-2612 permit is unnecessary and its failure is harmless.The subsequent
_settle()call will succeed regardless. However, this creates two observability problems:-
Obscured root cause on revert: When the EIP-2612 permit fails and no prior Permit2 allowance exists, the transaction reverts inside permitWitnessTransferFrom() with a generic Permit2 INSUFFICIENT_ALLOWANCE error. The actual root cause — an invalid EIP-2612 signature, an expired permit deadline, or a non-compliant token — is lost. Facilitators and off-chain monitoring systems cannot distinguish between "token lacks Permit2 allowance" and "EIP-2612 permit was submitted but rejected."
-
Silent success ambiguity: When the transaction succeeds, there is no way to determine on-chain whether the EIP-2612 permit was actually executed or silently skipped. The SettledWithPermit() event is emitted in both cases, making it indistinguishable whether the allowance was freshly granted via EIP-2612 or pre-existed. This hinders debugging, monitoring, and accounting
Recommendation
Emit a distinct event on permit failure to preserve the error context without reverting the transaction.
Coinbase: Added more granular error checking in commit 9e95d29
Cantina: Verified fix. However, if the token is malicious, a gas-greiving attack on the facilitator can occur via a return data bomb. Facilitators should consider this risk before relaying signed user transactions.
Ambiguous parameter naming in _settle() function
State
Severity
- Severity: Informational
Submitted by
red-swan
Description
In
x402BasePermit2Proxy, the parameter nameamountin_settlecould be confused with thepermitted.amountin the permit struct.Recommendation
Consider renaming the parameter to a name more accurate to its role in the system.
Coinbase: Fixed in 5cddb02
Cantina: Verified fix.