A Caplane order is signed once. The trick is what the signature’s nonce is: nonce = keccak256(abi.encode(order)). That one value ties the payment to the adapter, the params, the amount, the payee, and the fee at the same time. This page explains why, and why USDC itself rejects a tampered order.
The gap a plain EIP-3009 signature leaves
EIP-3009 transferWithAuthorization lets a consumer authorize a token transfer with an off-chain signature. But that authorization binds only (from, to, value, validAfter, validBefore, nonce). It says nothing about which adapter runs or which params it runs with. A relayer holding that signature could point the payment at a different capability than the one the consumer intended. The payment is bound; the order is not.
The fix: make the nonce the order hash
Caplane closes the gap by setting the EIP-3009 authorization nonce to the hash of the whole order:
nonce = keccak256(abi.encode(order))
The order carries everything that matters, in this exact Solidity field order:
struct Order {
address consumer; // payer + capability recipient; must equal the EIP-3009 signer
address adapter; // the ICapabilityAdapter that grants the capability
bytes params; // opaque, adapter-specific parameters
address payToken; // the EIP-3009 token used to settle (TestUSDC on testnet)
uint256 amount; // total charged to the consumer
uint16 feeBps; // relayer fee in basis points of amount; at most 10000
address payee; // the provider receiving amount minus fee
uint256 nonce; // a consumer-chosen uniqueness salt (NOT the EIP-3009 nonce)
}
The consumer computes orderHash, then signs an EIP-3009 authorization with nonce = orderHash, to = the broker, and value = amount.
order.nonce and the EIP-3009 nonce are two different things. order.nonce is a consumer-chosen salt that lives inside the order so two otherwise-identical orders hash differently. The EIP-3009 nonce is the whole orderHash. They are easy to confuse; keep them distinct.
Why USDC itself rejects tampering
The check is enforced by the token, not by Caplane’s good behaviour:
- The relayer hands the broker the
order. The broker recomputes orderHash = keccak256(abi.encode(order)) from that order. It never trusts a client-supplied hash.
- The broker passes the recomputed hash to
transferWithAuthorization as the nonce.
- USDC verifies the consumer’s signature against that nonce. If the relayer altered any order field, the recomputed
orderHash no longer matches what the consumer signed, the signature check fails, and transferWithAuthorization reverts. The whole brokerage reverts with it.
So a relayer cannot redirect the payment, swap the adapter, change the amount, or raise the fee. Any change breaks the signature at the token contract. The relayer’s only choice is to relay the order verbatim or to decline it.
What this buys you
One consumer signature ties payment to order to adapter to params to amount to payee. There is no facilitator to trust, because there is nothing a facilitator could change without invalidating the signature. This is the difference from pay-then-trust designs: the payment cannot be separated from the exact capability it was signed for.
Next