Solidity ERC721 Metadata
ERC721 metadata is a string stored per token id that describes that item. The tokenURI function returns the string for one id.
TokenURI reads back the minted string
Minting id 7 with a description stores that string. Reading it back returns the same string.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ItemWithMetadata {
address public owner;
mapping(uint256 => address) private owners;
mapping(uint256 => string) private uris;
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 tokenId, string memory uri) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
uris[tokenId] = uri;
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(owners[tokenId] != address(0), "no such token");
return uris[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(owners[tokenId] == msg.sender, "only owner moves it");
require(from == owners[tokenId], "wrong sender");
require(to != address(0), "zero receiver");
owners[tokenId] = to;
}
}A missing id has no string
Asking for the string of an id that was never minted reverts. Only minted ids carry a string.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ItemWithMetadata {
address public owner;
mapping(uint256 => address) private owners;
mapping(uint256 => string) private uris;
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 tokenId, string memory uri) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
uris[tokenId] = uri;
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(owners[tokenId] != address(0), "no such token");
return uris[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(owners[tokenId] == msg.sender, "only owner moves it");
require(from == owners[tokenId], "wrong sender");
require(to != address(0), "zero receiver");
owners[tokenId] = to;
}
}Different ids carry different strings
Two minted ids store two different strings. Each read returns only its own string.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ItemWithMetadata {
address public owner;
mapping(uint256 => address) private owners;
mapping(uint256 => string) private uris;
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 tokenId, string memory uri) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
uris[tokenId] = uri;
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(owners[tokenId] != address(0), "no such token");
return uris[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(owners[tokenId] == msg.sender, "only owner moves it");
require(from == owners[tokenId], "wrong sender");
require(to != address(0), "zero receiver");
owners[tokenId] = to;
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ItemWithMetadata {
address public owner;
mapping(uint256 => address) private owners;
mapping(uint256 => string) private uris;
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 tokenId, string memory uri) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
uris[tokenId] = uri;
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(owners[tokenId] != address(0), "no such token");
return uris[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(owners[tokenId] == msg.sender, "only owner moves it");
require(from == owners[tokenId], "wrong sender");
require(to != address(0), "zero receiver");
owners[tokenId] = to;
}
}Key points
- TokenURI: the function that returns the stored string for one token id.
- Per-id string: each token id carries its own description string.
- Mint sets the string: the string is stored when the token is minted.
- Missing id: asking for a string for an id that was never minted reverts.
- Changing hands: a transfer moves ownership and leaves the stored string with the token id.
In this example, two items are minted with different description strings. Transferring the first item changes its owner, and its string stays attached to the same token id.
Note
tokenURI only returns a string, so this lesson fetches nothing from outside the chain.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
What does tokenURI return?