🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity ERC721 Mechanics

An ERC721 token is a unique item where each token id has exactly one owner. Two tokens from the same contract can still differ, because each id is its own item.

Mint gives one id one owner

Minting id 1 to a receiver makes that receiver its only owner. The receiver balance becomes 1.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleItem {
    address public owner;
    mapping(uint256 => address) private owners;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 tokenId) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "zero receiver");
        require(owners[tokenId] == address(0), "already minted");
        owners[tokenId] = to;
        balanceOf[to] += 1;
        emit Transfer(address(0), to, tokenId);
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        require(owners[tokenId] != address(0), "no such token");
        return owners[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;
        balanceOf[from] -= 1;
        balanceOf[to] += 1;
        emit Transfer(from, to, tokenId);
    }
}

Transfer replaces the owner

The owner moves id 1 to a second address. The new address owns it and the old owner balance falls to 0.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleItem {
    address public owner;
    mapping(uint256 => address) private owners;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 tokenId) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "zero receiver");
        require(owners[tokenId] == address(0), "already minted");
        owners[tokenId] = to;
        balanceOf[to] += 1;
        emit Transfer(address(0), to, tokenId);
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        require(owners[tokenId] != address(0), "no such token");
        return owners[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;
        balanceOf[from] -= 1;
        balanceOf[to] += 1;
        emit Transfer(from, to, tokenId);
    }
}

A non-owner transfer reverts

A stranger cannot move a token that belongs to someone else. The call reverts and the owner stays the same.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleItem {
    address public owner;
    mapping(uint256 => address) private owners;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 tokenId) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "zero receiver");
        require(owners[tokenId] == address(0), "already minted");
        owners[tokenId] = to;
        balanceOf[to] += 1;
        emit Transfer(address(0), to, tokenId);
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        require(owners[tokenId] != address(0), "no such token");
        return owners[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;
        balanceOf[from] -= 1;
        balanceOf[to] += 1;
        emit Transfer(from, to, tokenId);
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleItem {
    address public owner;
    mapping(uint256 => address) private owners;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 tokenId) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "zero receiver");
        require(owners[tokenId] == address(0), "already minted");
        owners[tokenId] = to;
        balanceOf[to] += 1;
        emit Transfer(address(0), to, tokenId);
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        require(owners[tokenId] != address(0), "no such token");
        return owners[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;
        balanceOf[from] -= 1;
        balanceOf[to] += 1;
        emit Transfer(from, to, tokenId);
    }
}

Key points

  • Unique item: one token id that stands for one distinct thing.
  • Token id: the number that names one item in the contract.
  • OwnerOf: the function that reports the single owner of one token id.
  • Balance: the number of token ids one address owns.
  • TransferFrom: the function that moves one token id from its owner to a receiver.
  • Transfer rule: only the owner may move a token in this minimal version.

In this example, one holder receives ids 1 and 2. Moving id 1 to a second holder changes only that id, so the first holder still owns id 2.

Note

a token id has exactly one owner at a time, so a transfer replaces the old owner instead of splitting the item.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

Can one ERC721 token id have two owners at once?