Appearance
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 fulfills the following requirements:
- The chip can securely generate and store an ECDSA secp256k1 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
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 NFT is minted, it must also emit an event that includes the corresponding chip address (20-byte address derived from the chip's public key). 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 transferTokenWithChip
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) and a recent blockhash (the level of recency is up to 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
solidity
interface IERC5791 {
/// @notice Returns the token id for a given chip address.
/// @dev Throws if there is no existing token for the chip in the collection.
/// @param chipAddress The address for the chip embedded in the physical item (computed from the chip's public key).
/// @return The token id for the passed in chip address.
function tokenIdFor(address chipAddress) external view returns (uint256);
/// @notice Returns true if the chip for the specified token id is the signer of the signature of the payload.
/// @dev Throws if tokenId does not exist in the collection.
/// @param tokenId The token id.
/// @param payload Arbitrary data that is signed by the chip to produce the signature param.
/// @param signature Chip's signature of the passed-in payload.
/// @return Whether the signature of the payload was signed by the chip linked to the token id.
function isChipSignatureForToken(uint256 tokenId, bytes calldata payload, bytes calldata signature)
external
view
returns (bool);
/// @notice Transfers the token into the message sender's wallet.
/// @param signatureFromChip An EIP-191 signature of (msgSender, blockhash), where blockhash is the block hash for blockNumberUsedInSig.
/// @param blockNumberUsedInSig The block number linked to the blockhash signed in signatureFromChip. Should be a recent block number.
/// @param useSafeTransferFrom Whether EIP-721's safeTransferFrom should be used in the implementation, instead of transferFrom.
///
/// @dev The implementation should check that block number be reasonably recent to avoid replay attacks of stale signatures.
/// The implementation should also verify that the address signed in the signature matches msgSender.
/// If the address recovered from the signature matches a chip address that's bound to an existing token, the token should be transferred to msgSender.
/// If there is no existing token linked to the chip, the function should error.
function transferTokenWithChip(
bytes calldata signatureFromChip,
uint256 blockNumberUsedInSig,
bool useSafeTransferFrom
) external;
/// @notice Calls transferTokenWithChip as defined above, with useSafeTransferFrom set to false.
function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) external;
/// @notice Emitted when a token is minted
event PBTMint(uint256 indexed tokenId, address indexed chipAddress);
/// @notice Emitted when a token is mapped to a different chip.
/// Chip replacements may be useful in certain scenarios (e.g. chip defect).
event PBTChipRemapping(uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress);
}
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 transferTokenWithChip
).
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 transferTokenWithChip
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.
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
- ensuring that the chip does not deterioriate 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, chipAddress)
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 recover a chip address from a signature.
solidity
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
function getChipAddressFromChipSignature(
bytes calldata signatureFromChip,
uint256 blockNumberUsedInSig
) internal returns (TokenData memory) {
if (block.number <= blockNumberUsedInSig) {
revert InvalidBlockNumber();
}
unchecked {
if (block.number - blockNumberUsedInSig > getMaxBlockhashValidWindow()) {
revert BlockNumberTooOld();
}
}
bytes32 blockHash = blockhash(blockNumberUsedInSig);
bytes32 signedHash = keccak256(abi.encodePacked(_msgSender(), blockHash))
.toEthSignedMessageHash();
address chipAddr = signedHash.recover(signatureFromChip);
}
Security Considerations
The ERC-191 signature passed to transferTokenWithChip
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 blockhash 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).
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
Copyright and related rights waived via CC0.