DCIPs/EIPS/eip-4494.md

208 lines
11 KiB
Markdown
Raw Normal View History

---
eip: 4494
title: Permit for ERC-721 NFTs
description: ERC-712-singed approvals for ERC-721 NFTs
author: Simon Fremaux (@dievardump), William Schwab (@wschwab)
discussions-to: https://ethereum-magicians.org/t/eip-extending-erc2612-style-permits-to-erc721-nfts/7519/2
status: Draft
type: Standards Track
category: ERC
created: 2021-11-25
requires: 165, 712, 721
---
## Abstract
The "Permit" approval flow outlined in [ERC-2612](./eip-2612.md) 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](./eip-2612.md) allows for a signed message (structured as outlined in [ERC-712](./eip-712.md)) 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](./eip-2612.md).
[ERC-2612](./eip-2612.md) 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](./eip-721.md):
```solidity
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 to `nonce`
* `sig` is a valid `secp256k1` or [EIP-2098](./eip-2098.md) signature from owner of the `tokenId`:
```
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](./eip-712.md). The `DOMAIN_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 for `DOMAIN_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:
```json
{
"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 particular `tokenId` (`nonces[tokenId]`) MUST be incremented upon any transfer of the `tokenId`
* 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](./eip-165.md). EIP165 is already required in [ERC-721](./eip-721.md), 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](./eip-2612.md), 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](./eip-1155.md)) 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](./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 on `owner`
* the `nonce` is incremented at the time the `permit` is created
* the `permit` function must be called by the NFT owner, who is set as the `owner`
* the signature is split into `r,s,v` instead of `bytes`
Rationale for differing on design decisions is detailed above.
## Test Cases
Basic test cases for the reference implementation can be found [here](https://github.com/dievardump/erc721-with-permits/tree/main/test).
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 the `spender` on the correct `tokenId`
* 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](https://github.com/dievardump/erc721-with-permits).
## 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](./eip-2612.md) 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](https://swcregistry.io/docs/SWC-114) 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](../LICENSE.md).