EIP4494: Extending ERC2612-style permits to ERC721 NFTs

This thread is intended for an upcoming ERC based around EIP-2612-style approvals for ERC721 NFTs. The development work has been done by @dievardump , and is a result of conversation’s in @anettrolikova 's NFT Standards Working Group. There’s a tentative implementation here.

ERC2612 accomplishes this by creating a signed message involving the addresses of the owner and proposed spender of an ERC20 token, in addition to the amount being approved, a deadline, and a signature. This ERC (despite being Stagnant at the time of this writing) enjoys a large amount of traction, and, for example, is leveraged by Uniswap wherever available.

There are a few things we’d like community feedback on regarding the best way to set this up for ERC721 NFTs.

1) owner-based or tokenId-based nonces

While trying to apply this formula to ERC721 NFTs, there is an additional optimization allowing for more flexibility than ERC2612 owing to the unique architecture of ERC721. ERC2612 takes the value being approved as an argument, and increments a nonce after each call to the permit function in order to prevent replay attacks. Since each ERC721 token is discrete, it allows for having the nonce based not on the owner's address or calls to permit, but rather to tie the nonce to tokenId and to increment on each transfer of the NFT. One gain from this pattern is that allows an owner to create multiple permits for the same NFT, since the nonce will only be incremented if the NFT is transferred.

(For comparison, 2612 needs to focus on the owner, and the owner needs to either only give one permit at a time, or make sure that they are used sequentially.)

Another advantage to this setup is that 2612 permits can only be signed by the owner of the tokens, but not by parties approved by the owner. This setup should allow approved parties to create permits for NFTs that they have been approved on too.

tokenId seems to us to be the best way to handle nonces, though otoh 2612 is owner-based, as are Uniswap v3’s position NFTs. We’re interested in community feedback about this!

2) v,r,s vs full signatures

EIP2612 takes v, r, and s arguments, which are the three parts of a signature. The full signature is needed to verify the message, though. The author of 2612, Martin Lundfall, told me that his reasoning was that v, r, and s are all fixed length (uint8, bytes32, and bytes32), whereas the full signature would need to be a dynamically-sized array (if I understood him right, apologies if I missed something there), though if we kept v, r, and s as arguments (as in 2612), likely every function would need to concatenate them (using abi.encodePacked) in order to use ecrecover to verify.

@Amxx has a repo with a singleton contract that “wraps” ERC20, 721, or 1155 tokens in a permit structure which was a big inspiration in this project here, and you can see his use of OZ utilities for verification - all of which take a full signature.

We’d like to keep things as similar to 2612 as possible, but are interested in community feedback if there’s a preference towards keeping the three elements separate or maybe towards ingesting the signature whole.

There are likely other major conversation points here, this is meant to kick things off, and we’re looking forward to what people say!

2 Likes

About this, I would like to add:

since version 4.1, ECDSA from OpenZeppelin’s contracts repository implements EIP-2098 which allows signature of length 64.

We will not be able to support this easily with (r, s, v) as parameters since encodePacked(bytes32, bytes32, uint8) will always return a signature of 65 length.
This is why for the moment I went with the full bytes memory signature in the example of implementation, letting the library manage what EIP to follow according to the length of the bytes.

1 Like

Seems like I expressed myself in a confusing manner:

ECDSA supports 2 types of signature: (r, s, v) which is standard and 65 bytes long, and (r, vs) the “compact form” (eip 2098) which is 64 bytes long.

if we use (r, s, v) as parameters, it will be more complicated to support EIP 2098.

if we use bytes memory signature, ECDSA can be used to automatically detect what type of signature it is, before recovering the address.

I can’t say if it’s better or not, but I suppose other EIPs might come later with other signature types/lengths, so maybe using the full bytes and let the Libraries handle the detection is a good habit to take.

2 Likes

Great to see an EIP for that!

One minor suggestion: the name nonces is generic but has been used so far to represent account based nonce.

Also it is possible for an ERC721 contract to want to have both type of nonces
example 1): account nonce would be more fitting for approveForAll permit for example
example 2): DAO based on ERC721 votes have the need for signature nonce for delegation and these use account based nonce

Renaming nonces to be tokenNonces would be preferable in my opinion to avoid such conflict.

2 Likes

Actually just realised that since nonces(uint256) is not the same as nonces(address) there is no conflict

1 Like

Starting to wonder if it’s worth it to add a transferWithPermit function that does a safeTransfer using a signature - I originally felt like maybe not since we were trying to keep things as close to 2612 as possible, but am starting to have my doubts.

In addition, I’m beginning to doubt how I did the EIP165 inegration - currently the interface ID is taken from the interface for 4494 + 165. The thing is that ERC721 already requires 721, meaning that if someone would build their contract strictly as described in the EIP, they’d run into circular inheritance problems from EIP165 being in both 721 and 4494. This makes me think that I should probably have the 4494 interface inherit the ERC721 interface, and then take the interface ID from there, but I wanted to open this up for conversation first.

Hello.

First of all, I’d like to say that I welcome this standardization effort. This is really needed.

For the record, I am responsible for OZ supporting EIP-2098, and I am also the author of GitHub - Amxx/Permit. That might make you think I would favor

bytes sign

over

uint8 v, bytes32 r, bytes32 s

That is however not where I stand! While I hate early implementers forcing the hand of later standardization effort, I really think the permit being implemented by UniswapV3 sets a strong precedent in favor of sticking with uint8, bytes32, bytes32.

AFAIK, bytes is more expensive, because it requires an additional slot to store the length. It is thus however more versatile in some cases, particularly when dealing with signatures that are produced by multisig and are then verified using 1271 (see this library that, if possible, should be preferred to ECDSA)

The question then becomes:

  • Do we want to future proof the standard with support for smart wallets (and other smart contracts)
  • Do we want to stick with simple, EOA based signature, that would be cheaper to process

I don’t think there is an easy answer, particularly when smart wallets (and other smart contracts) can already implement meta-tx / signature based methods for calling approve (or any other functions). They don’t really need to be supported here.

One should keep in mind that using this permit interface for EOA would be irrelevant if/when EIP-3074 becomes available.

2 Likes

This was the idea, because NFTs are more and more put in Vaults or managed by multisig and their usage, imo, will continue to grow, which is why supporting them.

However, we should be able to use Contract.isValidSignature with r,s,v the same way Uniswap does it now. Supporting 2098 came mostly because this implementation here was inspired by yours.
But if people are more in favor of sticking with 3 parameters and forgetting the bytes form, then let’s go!

In the example implementation, I did not use SignatureChecker first because I am also checking if the signer is approved, which needs to recover the address.
I added SignatureChecker (following your implementation) as the second condition, if the first does not work.

I think your interface id is the right one. I used type(Interface).interfaceId and the tests are passing.

Usually, for extensions, we try to have only the methods specific to the extension in the interface Id. So no need to add nor EIP165, nor 721.

I strongly disagree with that.

We should never assume that the bytes part of ERC1271’s isValidSignature is a 64 or 65 bytes long ECDSA signature. It would be anyone, including the concatenation of multiple ECDSA signatures, or even non ECDSA based signatures.

Taking 3 values, packing them, and passing them to isValidSignature might work in some case, but possibly lose a lot of the generality offered by ERC1271.

Again, I also want to point that these smart-contract based wallets don’t need permit as much as EOA do. In particular, they (often) include batching mechanism that allow to atomically execute an approve call, and a call to another contract. They also (often) include meta-tx relaying mechanism, allowing the owner to sign this batch of operations and letting someone else pay for the gas.
These are two cases where permit is essential to EOA but not needed for smart wallets

1 Like