Abstract

This standard is an extension of ERC-721. It proposes a minimal interface for a ERC-721 NFT to be “physically backed” and owned by whoever owns the NFT’s physical counterpart.

Motivation

NFT collectors enjoy collecting digital assets and sharing them with others online. However, there is currently no such standard for showcasing physical assets as NFTs with verified authenticity and ownership. Existing solutions are fragmented and tend to be susceptible to at least one of the following:

  • The ownership of the physical item and the ownership of the NFT are decoupled.

  • Verifying the authenticity of the physical item requires action from a trusted 3rd party (e.g. StockX).

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.

Requirements

This approach requires that the physical item must have a chip attached to it that should be secure and signal authenticity:

  • The chip can securely generate and store an asymmetric key pair;
  • The chip can sign messages using the private key of the previously-generated asymmetric key pair;
  • The chip exposes the public key; and
  • The private key cannot be extracted or duplicated by design

The approach also requires that the contract uses an account-bound implementation of ERC-721 (where all ERC-721 functions that transfer must throw, e.g. the “read only NFT registry” implementation referenced in ERC-721). This ensures that ownership of the physical item is required to initiate transfers and manage ownership of the NFT, through a new function introduced in this interface described below.

Approach

Each NFT is conceptually linked to a physical chip.

When the chipId is paired to a tokenId, an event will be emitted. This lets downstream indexers know which chip addresses are mapped to which tokens for the NFT collection. The NFT cannot be minted without its token id being linked to a specific chip.

The interface includes a function called transferToken that transfers the NFT to the function caller if a valid signature signed by the chip is passed in. A valid signature must follow the schemes set forth in ERC-191 and EIP-2 (s-value restrictions), where the data to sign consists of the target recipient address (the function caller), the chip address, a block timestamp, and any extra params used for additional custom logic in the implementation.

The interface also includes other functions that let anyone validate whether the chip in the physical item is backing an existing NFT in the collection.

Interface


interface IERC5791 {
    /// @dev Returns the ERC-721 `tokenId` for a given chip address.
    ///      Reverts if `chipId` has not been paired to a `tokenId`.
    ///      For minimalism, this will NOT revert if the `tokenId` does not exist.
    ///      If there is a need to check for token existence, external contracts can
    ///      call `ERC721.ownerOf(uint256 tokenId)` and check if it passes or reverts.
    /// @param chipId The address for the chip embedded in the physical item
    ///               (computed from the chip's public key).
    function tokenIdFor(address chipId) external view returns (uint256 tokenId);

    /// @dev Returns true if `signature` is signed by the chip assigned to `tokenId`, else false.
    ///      Reverts if `tokenId` has not been paired to a chip.
    ///      For minimalism, this will NOT revert if the `tokenId` does not exist.
    ///      If there is a need to check for token existence, external contracts can
    ///      call `ERC721.ownerOf(uint256 tokenId)` and check if it passes or reverts.
    /// @param tokenId ERC-721 `tokenId`.
    /// @param data      Arbitrary bytes string that is signed by the chip to produce `signature`.
    /// @param signature EIP-191 signature by the chip to check.
    function isChipSignatureForToken(uint256 tokenId, bytes calldata data, bytes calldata signature)
        external
        view
        returns (bool);

    /// @dev Transfers the token into the address.
    ///      Returns the `tokenId` transferred.
    /// @param to                  The recipient. Dynamic to allow easier transfers to vaults.
    /// @param chipId              Chip ID (address) of chip being transferred.
    /// @param chipSignature       EIP-191 signature by the chip to authorize the transfer.
    /// @param signatureTimestamp  Timestamp used in `chipSignature`.
    /// @param useSafeTransferFrom Whether ERC-721's `safeTransferFrom` should be used,
    ///                            instead of `transferFrom`.
    /// @param extras              Additional data that can be used for additional logic/context
    ///                            when the PBT is transferred.
    function transferToken(
        address to,
        address chipId,
        bytes calldata chipSignature,
        uint256 signatureTimestamp,
        bool useSafeTransferFrom,
        bytes calldata extras
    ) external returns (uint256 tokenId);

    /// @dev Emitted when `chipId` is paired to `tokenId`.
    /// `tokenId` may not necessarily exist during assignment.
    /// Indexers can combine this event with the {ERC721.Transfer} event to
    /// infer which tokens exists and are paired with a chip ID.
    event ChipSet(uint256 indexed tokenId, address indexed chipId);
}

To aid recognition that an ERC-721 token implements physical binding via this EIP: upon calling ERC-165’s function supportsInterface(bytes4 interfaceID) external view returns (bool) with interfaceID=0x4901df9f, a contract implementing this EIP must return true.

The mint interface is up to the implementation. The minted NFT’s owner should be the owner of the physical chip (this authentication could be implemented using the signature scheme defined for transferToken).

Rationale

This solution’s intent is to be the simplest possible path towards linking physical items to digital NFTs without a centralized authority.

The interface includes a transferToken function that’s opinionated with respect to the signature scheme, in order to enable a downstream aggregator-like product that supports transfers of any NFTs that implement this EIP in the future.

The chip address is included in transferToken to allow signature verification by a smart contract. This ensures that chips in physically backed tokens are not strictly tied to implementing secp256k1 signatures, but instead may use a variety of signature schemes such as P256 or BabyJubJub.

Out of Scope

The following are some peripheral problems that are intentionally not within the scope of this EIP:

  • trusting that a specific NFT collection’s chip addresses actually map to physical chips embedded in items, instead of arbitrary EOAs that purport to be chips
  • ensuring that the chip does not deteriorate or get damaged
  • ensuring that the chip stays attached to the physical item
  • etc.

Work is being done on these challenges in parallel.

Mapping token ids to chip addresses is also out of scope. This can be done in multiple ways, e.g. by having the contract owner preset this mapping pre-mint, or by having a (tokenId, chipId) tuple passed into a mint function that’s pre-signed by an address trusted by the contract, or by doing a lookup in a trusted registry, or by assigning token ids at mint time first come first served, etc.

Additionally, it’s possible for the owner of the physical item to transfer the NFT to a wallet owned by somebody else (by sending a chip signature to that other person for use). We still consider the NFT physical backed, as ownership management is tied to the physical item. This can be interpreted as the item’s owner temporarily lending the item to somebody else, since (1) the item’s owner must be involved for this to happen as the one signing with the chip, and (2) the item’s owner can reclaim ownership of the NFT at any time.

Backwards Compatibility

This proposal is backward compatible with ERC-721 on an API level. As mentioned above, for the token to be physical-backed, the contract must use a account-bound implementation of ERC-721 (all ERC-721 functions that transfer must throw) so that transfers go through the new function introduced here, which requires a chip signature.

Reference Implementation

The following is a snippet on how to validate a chip signature in a transfer event.

import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';

/// @dev Transfers the `tokenId` assigned to `chipId` to `to`.
function transferToken(
    address to,
    address chipId,
    bytes memory chipSignature,
    uint256 signatureTimestamp,
    bool useSafeTransfer,
    bytes memory extras
) public virtual returns (uint256 tokenId) {
    tokenId = tokenIdFor(chipId);
    _validateSigAndUpdateNonce(to, chipId, chipSignature, signatureTimestamp, extras);
    if (useSafeTransfer) {
        _safeTransfer(ownerOf(tokenId), to, tokenId, "");
    } else {
        _transfer(ownerOf(tokenId), to, tokenId);
    }
}

/// @dev Validates the `chipSignature` and update the nonce for the future signature of `chipId`.
function _validateSigAndUpdateNonce(
    address to,
    address chipId,
    bytes memory chipSignature,
    uint256 signatureTimestamp,
    bytes memory extras
) internal virtual {
    bytes32 hash = _getSignatureHash(signatureTimestamp, chipId, to, extras);
    if (!SignatureCheckerLib.isValidSignatureNow(chipId, hash, chipSignature)) {
        revert InvalidSignature();
    }
    chipNonce[chipId] = bytes32(uint256(hash) ^ uint256(blockhash(block.number - 1)));
}

/// @dev Returns the digest to be signed by the `chipId`.
function _getSignatureHash(uint256 signatureTimestamp, address chipId, address to, bytes memory extras)
    internal
    virtual
    returns (bytes32)
{
    if (signatureTimestamp > block.timestamp) revert SignatureTimestampInFuture();
    if (signatureTimestamp + maxDurationWindow < block.timestamp) revert SignatureTimestampTooOld();
    bytes32 hash = keccak256(
        abi.encode(address(this), block.chainid, chipNonce[chipId], to, signatureTimestamp, keccak256(extras))
    );
    return ECDSA.toEthSignedMessageHash(hash);
}

Security Considerations

The ERC-191 signature passed to transferToken requires the function caller’s address in its signed data so that the signature cannot be used in a replay attack. It also requires a recent block timestamp so that a malicious chip owner cannot pre-generate signatures to use after a short time window (e.g. after the owner of the physical item changes). It’s recommended to use a non-deterministic chipNonce when generating signatures.

Additionally, the level of trust that one has for whether the token is physically-backed is dependent on the security of the physical chip, which is out of scope for this EIP as mentioned above.

Copyright and related rights waived via CC0.