Alert Source Discuss
đźš§ Stagnant Standards Track: SRC

SRC-4494: Permit for SRC-721 NFTs

SRC-712-singed approvals for SRC-721 NFTs

Authors Simon Fremaux (@dievardump), William Schwab (@wschwab)
Created 2021-11-25
Discussion Link https://sila-magicians.org/t/sip-extending-src2612-style-permits-to-src721-nfts/7519/2
Requires SIP-165, SIP-712, SIP-721

Abstract

The “Permit” approval flow outlined in SRC-2612 has proven a very valuable advancement in UX by creating gasless approvals for SRC20 tokens. This SIP extends the pattern to SRC-721 NFTs. This SIP borrows heavily from SRC-2612.

This requires a separate SIP due to the difference in structure between SRC-20 and SRC-721 tokens. While SRC-20 permits use value (the amount of the SRC-20 token being approved) and a nonce based on the owner’s address, SRC-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 SRC-2612 allows for a signed message (structured as outlined in SRC-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 SRC-2612.

SRC-2612 only outlines a permit architecture for SRC-20 tokens. This SRC proposes an architecture for SRC-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 SRC-721:

pragma solidity 0.8.10;

import "./ISRC165.sol";

///
/// @dev Interface for token permits for SRC-721
///
interface ISRC4494 is ISRC165 {
  /// SRC165 bytes to add to interface array - set in parent contract
  ///
  /// _INTERFACE_ID_SRC4494 = 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 SIP-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 SIP-712
  /// @return the bytes32 domain separator
  function DOMAIN_SEPARATOR() external view returns(bytes32);
}

The semantics of which are as follows:

For all addresses spender, uint256s 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 SIP-2098 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 SIP-712. The DOMAIN_SEPARATOR should be unique to the contract and chain to prevent replay attacks from other domains, and satisfy the requirements of SIP-712, but is otherwise unconstrained. A common choice for DOMAIN_SEPARATOR is:

    DOMAIN_SEPARATOR = keccak256(
      abi.encode(
          keccak256('SIP712Domain(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 SRC-712 typed structure:

    {
    "types": {
      "SIP712Domain": [
        {
          "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": src721name,
        "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 SIP requires SIP-165. SIP165 is already required in SRC-721, but is further necessary here in order to register the interface of this SIP. Doing so will allow easy verification if an NFT contract has implemented this SIP or not, enabling them to interact accordingly. The interface of this SIP (as defined in SIP-165) is 0x5604e225. Contracts implementing this SIP 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.

SRC-712 typed messages are included because of its use in SRC-2612, which in turn cites widespread adoption in many wallet providers.

While SRC-2612 focuses on the value being approved, this SIP focuses on the tokenId of the NFT being approved via permit. This enables a flexibility that cannot be achieved with SRC-20 (or even SRC-1155) tokens, enabling a single owner to give multiple permits on the same NFT. This is possible since each SRC-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 SRC-2612 splits signatures into their v,r,s components, this SIP opts to instead take a bytes array of variable length in order to support SIP-2098 signatures (64 bytes), which cannot be easily separated or reconstructed from r,s,v components (65 bytes).

Backwards Compatibility

There are already some SRC-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.

In general, test suites should assert at least the following about any implementation of this SIP:

  • 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.

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 SRC-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 SIL they can also just submit the Permit themselves, which can render previously signed Permits invalid.

The standard SRC-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 and related rights waived via CC0.

Citation

Please cite this document as:

Simon Fremaux (@dievardump), William Schwab (@wschwab), "SRC-4494: Permit for SRC-721 NFTs [STAGNANT]," Sila Improvement Proposals, no. 4494, November 2021. Available: https://sips.sila.org/SIPS/sip-4494.