Three Ethereum interfaces let a signature stand in for a transaction, and they are usually described as one feature. They authorize three different things. ERC-2612’s permit changes an allowance. ERC-3009 transfers a stated amount to a named address inside a time window. ERC-1271 decides nothing on its own: it asks a contract account whether that account considers a given hash and signature valid. Read the three specifications side by side and the signature turns out to be the least interesting part. What separates them is the object being authorized and the party who has to act on it.
What the signature leaves on chain
A permit call does not move tokens. Given permit(owner, spender, value, deadline, v, r, s), a compliant token will set allowance[owner][spender] to the signed value, increment nonces[owner] by one and emit an Approval event, provided the block time is at or before the deadline, the owner is not the zero address and the nonce matches. The token stores a permission. The transfer, if there is one, happens later, in a separate call made by whoever holds gas for it.
An ERC-3009 authorization skips the permission step. transferWithAuthorization carries from, to, value, validAfter, validBefore and a nonce, and the token executes the transfer once it has checked the signature and marked the nonce used. The on-chain record names the authorizer directly. There is no allowance to inspect afterwards, and no spender whose later decision determines whether funds move.
ERC-1271 defines no authorization at all. It defines one function, isValidSignature(bytes32 _hash, bytes _signature), which must return the four-byte value 0x1626ba7e when the check passes, and must do so without modifying state. The reference example returns 0xffffffff for a failed check. Everything else is left to the account that implements it.
Those three designs produce three different audit trails. A permit-based payment appears as an approval from one address and a later transfer by another, possibly in different blocks and by different parties. A 3009 payment appears once, with the authorizer’s address attached. A 1271 check appears only as a call result inside whatever transaction the verifier chose to build. If an incident review starts from transfer events alone, the first pattern shows an operator pulling funds under an approval the user granted, and the third shows a transfer whose authorization is invisible in the token’s own log.
Permit grants authority that waits
The defining property of permit is that the grant outlives the message. Once recorded, the allowance sits there until the spender uses it, someone overwrites it, or the owner revokes it. The signed structure names owner, spender, value, nonce and deadline. It does not name a moment of use, and the specification is direct about who may submit it: “the caller of the permit function can be any address”.
That has an odd consequence for front-running. Because anyone can submit, and because the message does not describe a recipient beyond the named spender, an earlier submission does not change the outcome: “The end result is the same for the Permit signer, however.” The competition to land the call first is a race over gas, not over control of the funds.
The exposure sits elsewhere. A relayer that receives a signed permit can sit on it, and the specification says so plainly: “Signed Permit messages are censorable. The relaying party can always choose to not submit the Permit after having received it, withholding the option to submit it.” The mitigation it names is a near deadline, which is a weaker guarantee than it sounds, because the same document notes that the deadline can be set to uint(-1) “to create Permits that effectively never expire”. A relayer that never submits, combined with a deadline set to uint(-1), leaves a signed permit that stays usable until the owner’s nonce is consumed. The owner can force that consumption by submitting a zero-value permit at the same nonce; otherwise the grant waits, unused and revocable only by spending the nonce.
Two further properties come from the state design rather than the copy. The allowance entry is overwritten, not increased, so a permit signed with a value of zero removes an outstanding approval, and because any address can submit, a signed revocation can be pushed on chain by a third party. The nonce, meanwhile, is sequential: nonces[owner] must equal the signed nonce and then increments by one. Two permits from the same owner signed at the same nonce cannot both land, and permits signed at consecutive nonces must land in nonce order. An operator batching payouts from a single hot wallet therefore cannot fire them off in parallel. Executing the second before the first makes it revert on the nonce check.
That last property is not a quirk of permit. It is the problem the 3009 authors set out to avoid, and their rationale explains the mechanics in terms an operator will recognise: for native transactions, a nonce that is too high waits in the pool, but for signed meta-transactions it “will revert and fail immediately, resulting in wasted gas”. Their own worked example has a user submit nonces 3, 4 and 5, a miner include them as 4, 5 and 3, and only the nonce-3 transaction succeeds.
3009 grants one transfer, and the variant decides who may submit it
Replacing the sequential nonce with a random 32-byte value removes the ordering constraint. Each authorization stands alone: it is either used or it is not, and ten independent payments can be submitted in any order or concurrently. The token keeps a per-authorizer mapping from nonce to a boolean, exposed through authorizationState, so the only shared authorization state is the set of nonces already spent.
The standard then splits into two functions that look interchangeable and are not. transferWithAuthorization lets any caller submit the signed transfer. receiveWithAuthorization adds a check that the caller is the payee. The specification explains why the second exists: “It is possible for an attacker watching the transaction pool to extract the transfer authorization and front-run the transferWithAuthorization call to execute the transfer without invoking the wrapper function. This could potentially result in unprocessed, locked up deposits.”
Read that sentence carefully and the failure mode is not theft. The transfer still moves the authorized amount to the authorized address. What breaks is the contract that intended to wrap it. A deposit contract that expects to receive tokens, credit an internal balance and then forward them will find the tokens already sitting at its address with no credit recorded, and it will not be able to replay the authorization because the nonce is spent. The signature did its job. The contract’s state machine did not. That is why the specification’s security section opens by telling developers to use receiveWithAuthorization when calling from other contracts, and why its own example checks that the decoded recipient equals address(this).
Revocation exists but is optional. An implementation may expose cancelAuthorization(authorizer, nonce, v, r, s), which consumes a signature over a cancel typehash rather than requiring the owner to send a transaction. For a treasury that signed a payment and then changed its mind, that is the only lever before the nonce is spent, and it depends on the token having implemented the optional piece.
Two status facts matter for anyone integrating this. First, ERC-3009 is still a draft: its own citation line reads “Transfer With Authorization [DRAFT]”, while the 2612 and 1271 pages carry a Final status. Second, the interface explicitly excludes contract accounts: “this ERC does not apply to smart contract accounts, as the transfer and approve/transferFrom functions from the ERC-20 standards can directly be used.” A treasury that operates through a multisig is therefore not the intended signer here, whatever its user interface suggests.
1271 moves the decision into the account
Contract accounts cannot hold a private key, so ERC-1271 does not try to make them. It gives verifiers a way to ask. The implementing contract may validate “context dependent (e.g. time based or state based), EOA dependent (e.g. signers authorization level within smart wallet), signature scheme Dependent (e.g. ECDSA, multisig, BLS)” checks. A three-of-five multisig, a session key with a spending cap and a timelocked module can all answer the same call.
That flexibility is also the reason a 1271 answer is not a settlement guarantee. The function may read state, and the specification requires that it not write any, so validity can depend on conditions that change between signing and inclusion. A signature that the account would have accepted at one block can be rejected at the next if the policy behind the answer moved: a signer removed, a module paused, a cap already spent.
The standard is also silent on the things the other two documents are careful about. It defines no nonce, no deadline and no domain separator. It validates whatever 32-byte hash the caller hands it. Replay protection therefore belongs entirely to the application that builds the hash, which means a verifier that passes a hash not bound to an amount, a recipient and an expiry is asking only whether the account would accept some signature, not whether it authorized this payment. That reading follows from the interface rather than being stated in it, but it is the practical consequence of a validation function with no message semantics.
Two operational warnings in the text deserve attention. The first is gas: because “there are no gas-limit expected for calling the isValidSignature() function”, hardcoding a gas amount when calling it can block valid signatures from complex accounts. The second is responsibility: “each contract implementing this method is responsible to ensure that the signature passed is indeed valid, otherwise catastrophic outcomes are to be expected.” A verifier that treats a magic value as proof of intent is trusting the account’s implementation, not the standard.
There is a composability gap worth naming. Neither 2612 nor 3009 requires the token to call isValidSignature. A token that only recovers ECDSA signers will reject a signature produced by a contract account, no matter how the account describes its own policy. For that treasury, permit and 3009 flows simply do not apply, which is consistent with 3009’s explicit exclusion but less obvious for permit, where the specification only notes in passing that the initial ERC-20 action must come from an externally owned account “unless the address owning the token is actually a contract wallet”.
One second, and one domain separator
The two token standards also disagree at the boundary, and the disagreement is easy to import into production code. Permit is valid while “the current blocktime is less than or equal to deadline”, an inclusive bound. The 3009 reference implementation checks now > validAfter and now < validBefore, which excludes both endpoints. A window specified as 3,600 seconds from a start time therefore admits timestamps from start+1 to end-1, which is 3,599 usable seconds, not 3,601. An integrator who reuses one deadline convention for both standards will be off by a second at each end, and will be wrong about which side.
The inequality direction is the example contract’s choice rather than a normative requirement; the interface documentation only describes validAfter as the time after which the authorization is valid and validBefore as the time before which it is. That is precisely why the check belongs against the token’s own source rather than against the interface name.
Domain separation is the other place where the standards diverge in strength. Both token standards rely on an EIP-712 domain separator to bind a signature to a chain and a contract, which is what stops it from being replayed on another network. The 2612 text treats that binding as a requirement. The 3009 text says the parameter set “is up to the implementing contract” and calls the chain and contract fields “highly recommended”, so the stronger guarantee is the one an integrator should verify rather than assume. The 2612 text notes the failure mode when that binding is frozen too early: if the separator contains the chain ID and is set at deployment rather than rebuilt per signature, “there is a risk of possible replay attacks between chains in the event of a future chain split”. The 3009 text makes the same claim as a property rather than a warning. ERC-1271 has no domain requirement at all, which returns replay protection to the application layer for contract accounts.
Who carries the risk
For a payment processor that wants to pull funds when it chooses, permit is the only one of the three that fits, because an allowance is the only object here that persists without being spent. The cost is that the operator must track approvals and expiries, and that batches from one wallet serialize on the nonce.
For a merchant accepting a payment from a payer who holds no ETH, 3009 gives a single, bounded authorization with a recipient and a window, and permit plus transferFrom gives the same result in one transaction if the calling contract builds both. In neither case is the transaction free. The submitter pays the gas, so the honest description of these flows is that the signer does not need ETH, not that the payment has no cost.
For an exchange or any deposit wrapper, the correct function is receiveWithAuthorization, with the recipient check enforced on the wrapper’s side as well. A wrapper that accepts the transfer variant can be front-run into a state where the tokens arrived and the internal ledger never moved.
For a treasury held in a contract account, the question to answer before signing anything is whether the token calls isValidSignature at all. If it does, the account’s policy governs, and the payment is only as final as that policy is stable. If it does not, the account has to transact directly and the signature standards are irrelevant to it.
What to check in the token, not the interface
- Which object the signature changes: an allowance, one transfer, or an answer to a validation call.
- Whether
permitoverwrites or increases the allowance. The 2612 backwards-compatibility section already documents deviations, including DAI’sbool allowedform that sets an approval to zero or the maximum, and a token that takesbool allowedchanges what a zero-value permit means. - Whether the deadline is inclusive or exclusive, and whether the implementation accepts an unlimited deadline.
- Whether the contract path uses the receive variant and enforces that the decoded recipient is the contract itself.
- Whether nonces are sequential or random, because that decides whether a batch can be submitted concurrently.
- Whether both signature paths reject the zero address, since a failed recovery returns zero rather than reverting.
- Whether the token calls
isValidSignaturefor contract-account signers, and what the account’s policy actually evaluates.
What would change these conclusions is specific and testable. A token whose permit increments instead of overwriting would turn a zero-value permit from a revocation into a no-op. A final 3009 with normative window bounds would end the off-by-one ambiguity. A verifier convention that required 1271 calls to receive intent-bound hashes would move replay protection back into the standard. None of the three documents establishes any of that today, and none of them reports deployment counts, audit results or gas measurements, so claims about how widely each interface is used in practice are not supported by this evidence and are not made here.
For an integrator, the shortest route through that ambiguity runs through the token’s own permit and authorization functions. They carry the inequality direction, the allowance semantics, the nonce model and, where the token accepts contract-account signers at all, the call into isValidSignature. The three specifications describe interfaces; the answers that decide an incident review live in the implementation.
