ERC-6956 Asset-bound NFT (Asset-bound Non-Fungible Tokens)

Review · NFTs & multi-token · created 2023-04-29

By attaching a unique identifier called an anchor to a physical or digital asset and binding it 1:1 with an NFT, and having an oracle prove that it actually holds the asset, the token can be moved without requiring the existing owner's signature.

Why it is needed

When ERC-721 is used to represent an off-chain asset like a physical good, the identifier of the asset is usually written into the metadata, but since the metadata mostly lives off-chain and can change, the link between the token and the asset stays weak. Storing a hash on-chain to fix it in place then makes it impossible to update metadata that is supposed to change, such as traits or play time, and there have been incidents where the external storage that tokenURI points to simply disappeared. Direction is the bigger problem. Even if ownership of the physical item changes hands, if the existing NFT owner does not cooperate or loses access to their account, there is no way to move on-chain ownership. Actually holding the asset does not translate into on-chain ownership, so the token cannot keep up with the life cycle of an asset whose owner may change thousands of times.

What to implement

In depth

1:1 binding between anchor and token

The anchor is a value that uniquely identifies the off-chain asset, and the contract must have a bidirectional mapping of anchorByToken(tokenId) and tokenByAnchor(anchor), with at most one token per anchor. tokenId=0 and anchor=0x0 are defined as invalid, which takes advantage of the property that Solidity mappings return 0 for a nonexistent key, letting the contract skip a separate existence-check state variable to save gas. Transfers with a batch size greater than 1 are prohibited, and every transfer must emit an AnchorTransfer(from, to, anchor, tokenId) event. For tokenURI(tokenId), an anchor-based URI of the form baseURI/anchor is recommended, since the mapping between anchor and tokenId is unknown before the anchor is first used. Even for mass-produced items, each individual asset has its own ownership history, so an ERC-721 extension was chosen rather than a multi-token standard.

Attestation format and verification

An attestation ABI-encodes the to address, the anchor, the signing time attestationTime, the validity start time validStartTime, and the validity end time validEndTime, and attaches a 65-byte ETH signature that the oracle created over attestationHash = keccak256([to, anchor, attestationTime, validStartTime, validEndTime]). decodeAttestationIfValid(attestation, data) decodes this and returns to, anchor, and attestationHash, but must fail if the signature is not from a trusted oracle, if it is outside the validity period, or if the same attestationHash has already been used. Since this function does not change state, it can also be used solely to check validity without consuming the attestation. Any operation that changes state must mark the attestation as used within the same transaction (it is recommended to store the used attestationHash), increment attestationsUsedByAnchor by 1, and emit an AttestationUse event.

Permissionless transfer, approval, and burn

transferAnchor, approveAnchor, and burnAnchor are all permissionless functions. Neither the current owner (from) nor the recipient (to) needs to sign, anyone can submit the transaction, and authorization comes solely from an attestation signed by the oracle. transferAnchor follows the same behavior and events as ERC-721.safeTransferFrom, except that it mints a new NFT if tokenByAnchor(anchor) is 0, and fails if attestation.to is already the owner. Approval and burn authorization are each set to one of the combinations in the Authorization enum: OWNER (token owner), ISSUER (issuing contract), or ASSET (attestation), and can be queried via burnAuthorization() and approveAuthorization(). This design ensures that even if a token ends up in an arbitrary account whose private key is unknown, or in a contract that does not support ERC-721, approval and burning remain possible through proof of the asset alone.

Three optional extensions

The AttestationLimited extension caps the number of attestation uses per anchor. It implements attestationLimit(anchor) and attestationUsagesLeft(anchor), and fixes the direction in which the limit can be updated to one of IMMUTABLE, INCREASE_ONLY, DECREASE_ONLY, or FLEXIBLE, exposed via attestationLimitPolicy(). The Floatable extension allows a token to be temporarily floated. When floating(anchor) is true, that token can be transferred through the normal ERC-721 method (isApprovedOrOwner approval) without an attestation, and the authority to start and stop floating is kept separate via floatStartAuthorization and floatStopAuthorization. The ValidAnchors extension records the set of valid anchors on-chain as a Merkle tree root hash and verifies them with anchorValid(anchor, proof). Only the maintainer can update the root, so even if the oracle is compromised, it cannot slip in a new anchor.

Locked by default, compatible with ERC-721

Anchors must be unreleased by default, and ordinary ERC-721 transfers must all fail for tokens whose anchor has not been released. In other words, by default a token can only move via attestation. Adding the Floatable extension turns this into familiar ERC-721 behavior with an attestation authorization mechanism layered on top, making it fully compatible with ERC-721. Since the commonly used _beforeTokenTransfer() hook is called for every transfer, including attestation-based ones, checking lock extensions such as ERC-5192 or ERC-6982 in this same hook can block both ordinary transfers and attestation-based ones. For marketplaces, it is also recommended to add interfaces that signal transferability, such as ERC-6454 and ERC-5484.

Additional requirements for physical assets

When the asset is physical, the oracle must prove physical proximity (typically a few millimeters to a few meters) between the input device specifying the to address and the physical anchor, and the method used must be resistant to attempts to clone or reproduce the anchor and to spoofing (presentation attacks). It must also be implemented under the assumption that whoever specifies the to address could be a malicious party trying to obtain a false attestation without ever having had access to the physical item. The physical asset itself must have an anchor serving as a unique identifier (hashing the serial number is recommended over plaintext) along with a security feature or marking proving physical presence, and anchor technologies with non-clonable security properties are recommended. Technologies that are easily copied, physically or digitally, such as barcodes or ordinary NFC chips, are not recommended.

A proposal still under review

This proposal is in Review status, so the interface or requirements may change during review, and it is advisable to check the latest source before implementing. The ERC-165 identifier for the standard interface is 0xa9cf7635, AttestationLimited is 0x75a2e933, Floatable is 0xf82773f7, and ValidAnchors is 0x051c9bd8. The source provides a minimal implementation covering only the standard interface, a full implementation supporting all three extensions, a TypeScript example for generating attestations, and tests for both configurations.

Things to watch out for

Original ERC · ← ERCs