Appearance
Abstract ​
The following defines a standard interface for designating ownership of an NFT to someone while the NFT is held in escrow by a smart contract. The standard allows for the construction of a directed acyclic graph of NFTs, where the designated owner of every NFT in a given chain is the terminal address of that chain. This enables the introduction of additional functionality to pre-existing NFTs, without having to give up the authenticity of the original. In effect, this means that all NFTs are composable and can be rented, used as collateral, fractionalized, and more.
Motivation ​
Many NFTs aim to provide their holders with some utility - utility that can come in many forms. This can be the right to inhabit an apartment, access to tickets to an event, an airdrop of tokens, or one of the infinitely many other potential applications. However, in their current form, NFTs are limited by the fact that the only verifiable wallet associated with an NFT is the owner, so clients that want to distribute utility are forced to do so to an NFT's listed owner. This means that any complex ownership agreements must be encoded into the original NFT contract - there is no mechanism by which an owner can link the authenticity of their original NFT to any external contract.
The goal of this standard is to allow users and developers the ability to define arbitrarily complex ownership agreements on NFTs that have already been minted. This way, new contracts with innovative ownership structures can be deployed, but they can still leverage the authenticity afforded by established NFT contracts - in the past a wrapping contract meant brand new NFTs with no established authenticity.
Prior to this standard, wrapping an NFT inside another contract was the only way to add functionality after the NFT contract had been deployed, but this meant losing access to the utility of holding the original NFT. Any application querying for the owner of that NFT would determine the wrapping smart contract to be the owner. Using this standard, applications will have a standardized method of interacting with wrapping contracts so that they can continue to direct their utility to users even when the NFT has been wrapped.
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.
solidity
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
interface IERC4799NFT is IERC165 {
/// @dev This emits when ownership of any NFT changes by any mechanism.
/// This event emits when NFTs are created (`from` == 0) and destroyed
/// (`to` == 0). Exception: during contract creation, any number of NFTs
/// may be created and assigned without emitting Transfer. At the time of
/// any transfer, the approved address for that NFT (if any) is reset to none.
event Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
/// @notice Find the owner of an NFT
/// @dev NFTs assigned to zero address are considered invalid, and queries
/// about them throw
/// @param tokenId The identifier for an NFT
/// @return The address of the owner of the NFT
function ownerOf(uint256 tokenId) external view returns (address);
}
solidity
/// @title ERC-4799 Non-Fungible Token Ownership Designation Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-4799
/// Note: the ERC-165 identifier for this interface is [TODO].
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "./IERC4799NFT.sol";
interface IERC4799 is IERC165 {
/// @dev Emitted when a source token designates its ownership to the owner of the target token
event OwnershipDesignation(
IERC4799NFT indexed sourceContract,
uint256 sourceTokenId,
IERC4799NFT indexed targetContract,
uint256 targetTokenId
);
/// @notice Find the designated NFT
/// @param sourceContract The contract address of the source NFT
/// @param sourceTokenId The tokenId of the source NFT
/// @return (targetContract, targetTokenId) contract address and tokenId of the parent NFT
function designatedTokenOf(IERC4799NFT sourceContract, uint256 sourceTokenId)
external
view
returns (IERC4799NFT, uint256);
}
The authenticity of designated ownership of an NFT is conferred by the designating ERC-4799 contract’s ownership of the original NFT according to the source contract. This MUST be verified by clients by querying the source contract.
Clients respecting this specification SHALL NOT distribute any utility to the address of the ERC-4799 contract. Instead, they MUST distribute it to the owner of the designated token that the ERC-4799 contract points them to.
Rationale ​
To maximize the future compatibility of the wrapping contract, we first defined a canonical NFT interface. We created IERC4799NFT
, an interface implicitly implemented by virtually all popular NFT contracts, including all deployed contracts that are ERC-721 compliant. This interface represents the essence of an NFT: a mapping from a token identifier to the address of a singular owner, represented by the function ownerOf
.
The core of our proposal is the IERC4799
interface, an interface for a standard NFT ownership designation contract (ODC). ERC4799 requires the implementation of a designatedTokenOf
function, which maps a source NFT to exactly one target NFT. Through this function, the ODC expresses its belief of designated ownership. This designated ownership is only authentic if the ODC is listed as the owner of the original NFT, thus maintaining the invariant that every NFT has exactly one designated owner.
Backwards Compatibility ​
The IERC4799NFT
interface is backwards compatible with IERC721
, as IERC721
implicitly extends IERC4799NFT
. This means that the ERC-4799 standard, which wraps NFTs that implement ERC4799NFT
, is fully backwards compatible with ERC-721.
Reference Implementation ​
solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;
import "./IERC4799.sol";
import "./IERC4799NFT.sol";
import "./ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract ERC721Composable is IERC4799, IERC721Receiver {
mapping(IERC4799NFT => mapping(uint256 => IERC4799NFT)) private _targetContracts;
mapping(IERC4799NFT => mapping(uint256 => uint256)) private _targetTokenIds;
function designatedTokenOf(IERC4799NFT sourceContract, uint256 sourceTokenId)
external
view
override
returns (IERC4799NFT, uint256)
{
return (
IERC4799NFT(_targetContracts[sourceContract][sourceTokenId]),
_targetTokenIds[sourceContract][sourceTokenId]
);
}
function designateToken(
IERC4799NFT sourceContract,
uint256 sourceTokenId,
IERC4799NFT targetContract,
uint256 targetTokenId
) external {
require(
ERC721(address(sourceContract)).ownerOf(sourceTokenId) == msg.sender ||
ERC721(address(sourceContract)).getApproved(sourceTokenId) == msg.sender,
"ERC721Composable: Only owner or approved address can set a designate ownership");
_targetContracts[sourceContract][sourceTokenId] = targetContract;
_targetTokenIds[sourceContract][sourceTokenId] = targetTokenId;
emit OwnershipDesignation(
sourceContract,
sourceTokenId,
targetContract,
targetTokenId
);
}
function onERC721Received(
address,
address from,
uint256 sourceTokenId,
bytes calldata
) external override returns (bytes4) {
ERC721(msg.sender).approve(from, sourceTokenId);
return IERC721Receiver.onERC721Received.selector;
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return
(interfaceId == type(IERC4799).interfaceId ||
interfaceId == type(IERC721Receiver).interfaceId);
}
}
solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;
import "./IERC4799.sol";
import "./IERC4799NFT.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
contract DesignatedOwner {
function designatedOwnerOf(
IERC4799NFT tokenContract,
uint256 tokenId,
uint256 maxDepth
) public view returns (address owner) {
owner = tokenContract.ownerOf(tokenId);
if (ERC165Checker.supportsInterface(owner, type(IERC4799).interfaceId)) {
require(maxDepth > 0, "designatedOwnerOf: depth limit exceeded");
(tokenContract, tokenId) = IERC4799(owner).designatedTokenOf(
tokenContract,
tokenId
);
return designatedOwnerOf(tokenContract, tokenId, maxDepth - 1);
}
}
}
Security Considerations ​
Long/Cyclical Chains of Ownership ​
The primary security concern is that of malicious actors creating excessively long or cyclical chains of ownership, leading applications that attempt to query for the designated owner of a given token to run out of gas and be unable to function. To address this, clients are expected to always query considering a maxDepth
parameter, cutting off computation after a certain number of chain traversals.
Copyright ​
Copyright and related rights waived via CC0.