Implementing tokenURI as a separate contract
The metadata extension of ERC721 includes tokenURI()
that returns the metadata for a specific token ID:
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
/// @notice A descriptive name for a collection of NFTs in this contract
function name() external view returns (string _name);
/// @notice An abbreviated name for NFTs in this contract
function symbol() external view returns (string _symbol);
/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
/// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
/// 3986. The URI may point to a JSON file that conforms to the "ERC721
/// Metadata JSON Schema".
function tokenURI(uint256 _tokenId) external view returns (string);
}
And the Openzeppelin implementation or ERC721 implements it like this (practically, it returns "baseURI + tokenId")
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
For NiftyWalls, is decided to implement tokenURI as a separate contract.
So, the NiftyWalls implementation looks like more or less this
interface ITokenURI {
function tokenURI(uint256 tokenId) public view returns (string memory) ;
}
contract NiftyWalls is Ownable, ERC721 {
address public metadata;
...
...
function tokenURI(uint256 tokenId) public view override(ERC721)returns (string memory) {
require(_exists(tokenId), "NiftyWalls: URI query for nonexistent token");
IToknURI meta = ITokenURI(metadata);
string memory json = meta.idToJson(tokenId);
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
}
...
...
function setMetadataContract(address _metadata) public onlyOwner {
metadata = _metadata;
}
A very simple implementation of the TokenURI contract would be something like this.
contract TokenURI {
string private baseURI = 'http://mysite/' ;
function tokenURI(uint256 tokenId) public view returns (string memory) {
return string(abi.encodePacked(baseURI, tokenId.toString())) ;
}
}
The nice thing with this approach is that it allows you to upgrade the TokenURI contract to do interesting things in the future, without touching the NFT contract. For example:
- You may use
setMetadataContract()
to "activate" metadata only after all tokens are minted. - Or you could make the TokenURI contract return different metadata after a date or a block number is reached.
- Or deploy a new TokenURI contract (and use
setMetadataContract()
to point your contract to it) that implements on-chain metadata. - Or deploy a new TokenURI contract that holds the metadata on-chain and allows for changes (for example, the owner of a token can change the name of the token).
- Or even use on-chain events or an oracle to update metadata.
The cost of implementing tokenURI
as a separate contract is minimal, and the advantages are significant, so I expect more ERC721 contracts to implement it this way.