🎓 A1Learner
🔍
0 of 48 lessons completed

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?