ERC-2612 Approving token use with a signature (Permit) (Permit Extension for EIP-20 Signed Approvals)
Final · Tokens · created 2020-04-13
Handles the approval (approve) of EIP-20 token use with a single signature instead of a transaction. The token owner can approve without holding ETH, and the approval and the actual operation can be combined into one.
Why it is needed
One reason EIP-20 tokens are so widely used is the combination of approve and transferFrom. It lets an app contract take and use your tokens under set conditions, not just lets you send tokens to someone else. However, approve itself works on the basis of the address that sent the transaction (msg.sender). So the first action to use a token must always be a transaction sent directly by the owner. Interacting with a contract takes two transactions, one approve and one actual call. Even when simply sending tokens to someone else, you must separately hold ETH to pay gas fees. This standard solves this one problem in the smallest way by adding a single permit function that lets an allowance be set by a signed message.
What to implement
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external: After verifying the owner's signature (v, r, s), it sets allowance[owner][spender] to value and increases the nonce by 1. The caller can be anyone.function nonces(address owner) external view returns (uint): Returns the sequence number the owner must use in the next Permit signature. It increases by 1 each time permit succeeds.function DOMAIN_SEPARATOR() external view returns (bytes32): Returns the EIP-712 domain separator. It must be unique per contract and chain to prevent the signature from being reused elsewhere.
In depth
The three added functions
A compliant contract must implement three functions in addition to the EIP-20 functions. permit(owner, spender, value, deadline, v, r, s) takes a signature and sets allowance[owner][spender] to value. nonces(owner) returns the number the owner must sign next, and it increases by 1 each time permit succeeds. DOMAIN_SEPARATOR() is the EIP-712 domain separator, and it must be unique to this contract and chain. In the signature, value and deadline are uint, and the signature is passed as uint8 v and bytes32 r, s.
The four conditions for permit to succeed
A call to permit(owner, spender, value, deadline, v, r, s) sets allowance[owner][spender] to value, increments nonces[owner] by 1 and emits an Approval event only if all four of the following conditions hold. First, the current block timestamp must be less than or equal to deadline. Second, owner must not be the zero address. Third, nonces[owner] before the state change must equal the nonce in the message. Fourth, r, s and v must be a valid secp256k1 signature from owner over the specified message. If any condition fails, permit must revert. This revert requirement was added after the standard was already widely deployed, but it matched every implementation that had been checked at that time.
Structure of the signed message
The signing target is in EIP-712 format, of the form keccak256(abi.encodePacked(hex"1901", DOMAIN_SEPARATOR, struct hash)). The struct hash is the keccak256 of the type hash keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") together with owner, spender, value, nonce and deadline, combined with abi.encode. In terms of typed data, primaryType is Permit, the domain contains the token name (name), version, chainId and the token address (verifyingContract), and the message contains owner, spender, value, nonce and deadline. Because msg.sender appears nowhere in this definition, the caller of permit can be any address.
Computing DOMAIN_SEPARATOR
DOMAIN_SEPARATOR follows the EIP-712 requirements, but it must be unique to each contract and chain so that replay attacks from other domains are prevented. There are no other constraints. A common computation is the keccak256 of the type hash keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') together with keccak256(bytes(name)), keccak256(bytes(version)), chainid and address(this), combined with abi.encode.
The roles of nonce and deadline
The nonces mapping is a mechanism to prevent replay, where the same signature is used twice. A common usage is for a relayer to submit the Permit on behalf of the owner, and in that case the relayer gets a free option to choose whether to submit it or not. If this is a concern, the owner can limit the validity period of the Permit by setting the deadline to the near future. Conversely, setting deadline to uint(-1) makes the Permit effectively non-expiring. The permit function alone can make any operation that handles an EIP-20 token payable in the token itself instead of ETH.
Why the scope is limited to permit alone
Many proposals to let people use Ethereum without ETH have existed for a long time, but most saw little adoption. Those that were adopted, such as EIP-777, had many features and caused unexpected behavior in existing contracts. This standard addresses only one problem, the lack of abstraction in EIP-20 approve, in the smallest possible way. There are two reasons it does not add a signature version (*_by_signature) to every EIP-20 function. Details such as the fee policy or batching method of transfer_by_signature differ by use case, and such features can be built by combining permit with helper contracts without losing generality. Contract wallets solve a similar problem, but adoption is still low, and because the owner EOA and the wallet contract are separate, the UI must be designed separately, whereas permit gets the same benefit with almost no UI change.
Differences from existing implementations
Before this standard, permit functions already existed in several tokens, and the best known is the implementation in dai.sol. dai takes a bool allowed instead of value, sets the approval only to 0 or uint(-1), and calls the deadline expiry. The difference in names also changes the contents of the signed message, so it is not a mere difference in notation. The Stake token (0x0Ae055097C6d159879521C384F1D2123D1f195e6) has the same ABI as dai but different semantics, and it issues an expiring approval under which transferFrom works only while expiry >= block.timestamp. The specification in this document matches the Uniswap V2 implementation.
Things to watch out for
- Front running: Even if the signer expects a specific party to submit it, someone else can call permit first. From the signer's perspective, however, the result is the same.
- Checking that owner is not the zero address: The ecrecover precompile fails silently when given an invalid message and returns the zero address as the signer. You must check owner != address(0) to prevent an approval from being created over zombie funds held by the zero address.
- Censorship: A relayer can receive a Permit and not submit it. The deadline is one mitigation. If the signer has ETH, they can submit it themselves, but doing so may invalidate the Permits they signed earlier.
- Approval race condition (SWC-114): The standard race condition in EIP-20 approve (SWC-114) applies to permit as well.
- Replay on chain splits: DOMAIN_SEPARATOR includes chainId, and if it is fixed at deployment time instead of being recomputed for each signature, a cross-chain replay attack can occur later if the chain forks.