Permit for ERC-721 NFTs
Abstract
The “Permit” approval flow outlined in ERC-2612 has proven a very valuable advancement in UX by creating gasless approvals for ERC20 tokens. This EIP extends the pattern to ERC-721 NFTs. This EIP borrows heavily from ERC-2612.
This requires a separate EIP due to the difference in structure between ERC-20 and ERC-721 tokens. While ERC-20 permits use value (the amount of the ERC-20 token being approved) and a nonce based on the owner’s address, ERC-721 permits focus on the tokenId
of the NFT and increment nonce based on the transfers of the NFT.
Motivation
The permit structure outlined in ERC-2612 allows for a signed message (structured as outlined in ERC-712) to be used in order to create an approval. Whereas the normal approval-based pull flow generally involves two transactions, one to approve a contract and a second for the contract to pull the asset, which is poor UX and often confuses new users, a permit-style flow only requires signing a message and a transaction. Additional information can be found in ERC-2612.
ERC-2612 only outlines a permit architecture for ERC-20 tokens. This ERC proposes an architecture for ERC-721 NFTs, which also contain an approve architecture that would benefit from a signed message-based approval flow.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Three new functions MUST be added to ERC-721:
pragma solidity 0.8.10;
import "./IERC165.sol";
///
/// @dev Interface for token permits for ERC-721
///
interface IERC4494 is IERC165 {
/// ERC165 bytes to add to interface array - set in parent contract
///
/// _INTERFACE_ID_ERC4494 = 0x5604e225
/// @notice Function to approve by way of owner signature
/// @param spender the address to approve
/// @param tokenId the index of the NFT to approve the spender on
/// @param deadline a timestamp expiry for the permit
/// @param sig a traditional or EIP-2098 signature
function permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) external;
/// @notice Returns the nonce of an NFT - useful for creating permits
/// @param tokenId the index of the NFT to get the nonce of
/// @return the uint256 representation of the nonce
function nonces(uint256 tokenId) external view returns(uint256);
/// @notice Returns the domain separator used in the encoding of the signature for permits, as defined by EIP-712
/// @return the bytes32 domain separator
function DOMAIN_SEPARATOR() external view returns(bytes32);
}
The semantics of which are as follows:
For all addresses spender
, uint256
s tokenId
, deadline
, and nonce
, and bytes
sig
, a call to permit(spender, tokenId, deadline, sig)
MUST set spender
as approved on tokenId
as long as the owner of tokenId
remains in possession of it, and MUST emit a corresponding Approval
event, if and only if the following conditions are met:
- the current blocktime is less than or equal to
deadline
- the owner of the
tokenId
is not the zero address nonces[tokenId]
is equal tononce
sig
is a validsecp256k1
or EIP-2098 signature from owner of thetokenId
:keccak256(abi.encodePacked( hex"1901", DOMAIN_SEPARATOR, keccak256(abi.encode( keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"), spender, tokenId, nonce, deadline)) ));
where
DOMAIN_SEPARATOR
MUST be defined according to EIP-712. TheDOMAIN_SEPARATOR
should be unique to the contract and chain to prevent replay attacks from other domains, and satisfy the requirements of EIP-712, but is otherwise unconstrained. A common choice forDOMAIN_SEPARATOR
is:DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), keccak256(bytes(name)), keccak256(bytes(version)), chainid, address(this) ));
In other words, the message is the following ERC-712 typed structure:
{ "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "Permit": [ { "name": "spender", "type": "address" }, { "name": "tokenId", "type": "uint256" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" } ], "primaryType": "Permit", "domain": { "name": erc721name, "version": version, "chainId": chainid, "verifyingContract": tokenAddress }, "message": { "spender": spender, "value": value, "nonce": nonce, "deadline": deadline } }}
In addition:
- the
nonce
of a particulartokenId
(nonces[tokenId]
) MUST be incremented upon any transfer of thetokenId
- the
permit
function MUST check that the signer is not the zero address
Note that nowhere in this definition do we refer to msg.sender
. The caller of the permit
function can be any address.
This EIP requires EIP-165. EIP165 is already required in ERC-721, but is further necessary here in order to register the interface of this EIP. Doing so will allow easy verification if an NFT contract has implemented this EIP or not, enabling them to interact accordingly. The interface of this EIP (as defined in EIP-165) is 0x5604e225
. Contracts implementing this EIP MUST have the supportsInterface
function return true
when called with 0x5604e225
.
Rationale
The permit
function is sufficient for enabling a safeTransferFrom
transaction to be made without the need for an additional transaction.
The format avoids any calls to unknown code.
The nonces
mapping is given for replay protection.
A common use case of permit has a relayer submit a Permit on behalf of the owner. In this scenario, the relaying party is essentially given a free option to submit or withhold the Permit. If this is a cause of concern, the owner can limit the time a Permit is valid for by setting deadline to a value in the near future. The deadline argument can be set to uint(-1) to create Permits that effectively never expire.
ERC-712 typed messages are included because of its use in ERC-2612, which in turn cites widespread adoption in many wallet providers.
While ERC-2612 focuses on the value being approved, this EIP focuses on the tokenId
of the NFT being approved via permit
. This enables a flexibility that cannot be achieved with ERC-20 (or even ERC-1155) tokens, enabling a single owner to give multiple permits on the same NFT. This is possible since each ERC-721 token is discrete (oftentimes referred to as non-fungible), which allows assertion that this token is still in the possession of the owner
simply and conclusively.
Whereas ERC-2612 splits signatures into their v,r,s
components, this EIP opts to instead take a bytes
array of variable length in order to support EIP-2098 signatures (64 bytes), which cannot be easily separated or reconstructed from r,s,v
components (65 bytes).
Backwards Compatibility
There are already some ERC-721 contracts implementing a permit
-style architecture, most notably Uniswap v3.
Their implementation differs from the specification here in that:
- the
permit
architecture is based onowner
- the
nonce
is incremented at the time thepermit
is created - the
permit
function must be called by the NFT owner, who is set as theowner
- the signature is split into
r,s,v
instead ofbytes
Rationale for differing on design decisions is detailed above.
Test Cases
Basic test cases for the reference implementation can be found here.
In general, test suites should assert at least the following about any implementation of this EIP:
- the nonce is incremented after each transfer
permit
approves thespender
on the correcttokenId
- the permit cannot be used after the NFT is transferred
- an expired permit cannot be used
Reference Implementation
A reference implementation has been set up here.
Security Considerations
Extra care should be taken when creating transfer functions in which permit
and a transfer function can be used in one function to make sure that invalid permits cannot be used in any way. This is especially relevant for automated NFT platforms, in which a careless implementation can result in the compromise of a number of user assets.
The remaining considerations have been copied from ERC-2612 with minor adaptation, and are equally relevant here:
Though the signer of a Permit
may have a certain party in mind to submit their transaction, another party can always front run this transaction and call permit
before the intended party. The end result is the same for the Permit
signer, however.
Since the ecrecover precompile fails silently and just returns the zero address as signer
when given malformed messages, it is important to ensure ownerOf(tokenId) != address(0)
to avoid permit
from creating an approval to any tokenId
which does not have an approval set.
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 deadline
parameter is one mitigation to this. If the signing party holds ETH they can also just submit the Permit
themselves, which can render previously signed Permit
s invalid.
The standard ERC-20 race condition for approvals applies to permit
as well.
If the DOMAIN_SEPARATOR
contains the chainId
and is defined at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split.
Copyright
Copyright and related rights waived via CC0.