🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Token Standards Compared

Token standards differ in whether units are identical, unique, or grouped by type. ERC20, ERC721, and ERC1155 each fit a different kind of thing.

ERC20 counts identical amounts

An ERC20 transfer moves 10 identical units. The receiver balance grows by 10 and the units are interchangeable.

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

contract SimpleCoin {
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    event Transfer(address indexed from, address indexed to, uint256 value);

    constructor() {
        totalSupply = 1000;
        balanceOf[msg.sender] = 1000;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero receiver");
        require(balanceOf[msg.sender] >= amount, "not enough balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

ERC721 tracks one owner per id

An ERC721 mint gives id 1 exactly one owner. No amount is counted, because the id itself is the item.

// 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);
    }
}

ERC1155 holds many types in one contract

One ERC1155 contract mints 100 of id 1 and 5 of id 2. One holder carries both types at once.

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

contract MultiToken {
    address public owner;
    mapping(uint256 => mapping(address => uint256)) private balances;
    event TransferBatch(address operator, address from, address to, uint256[] ids, uint256[] amounts);

    constructor() {
        owner = msg.sender;
    }

    function mint(address to, uint256 id, uint256 amount) public {
        require(msg.sender == owner, "only owner mints");
        require(to != address(0), "zero receiver");
        balances[id][to] += amount;
    }

    function balanceOf(address account, uint256 id) public view returns (uint256) {
        return balances[id][account];
    }

    function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view returns (uint256[] memory) {
        require(accounts.length == ids.length, "length mismatch");
        uint256[] memory result = new uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; i++) {
            result[i] = balances[ids[i]][accounts[i]];
        }
        return result;
    }

    function transferBatch(address to, uint256[] memory ids, uint256[] memory amounts) public {
        require(to != address(0), "zero receiver");
        require(ids.length == amounts.length, "length mismatch");
        for (uint256 i = 0; i < ids.length; i++) {
            require(balances[ids[i]][msg.sender] >= amounts[i], "not enough balance");
            balances[ids[i]][msg.sender] -= amounts[i];
            balances[ids[i]][to] += amounts[i];
        }
        emit TransferBatch(msg.sender, msg.sender, to, ids, amounts);
    }
}

Example

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

contract MiniCoin {
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    constructor() {
        totalSupply = 1000;
        balanceOf[msg.sender] = 1000;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "not enough balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

contract MiniItem {
    mapping(uint256 => address) private owners;
    function mint(address to, uint256 tokenId) public {
        require(owners[tokenId] == address(0), "already minted");
        owners[tokenId] = to;
    }
    function ownerOf(uint256 tokenId) public view returns (address) {
        require(owners[tokenId] != address(0), "no such token");
        return owners[tokenId];
    }
}

contract MiniPack {
    mapping(uint256 => mapping(address => uint256)) private balances;
    function mint(address to, uint256 id, uint256 amount) public {
        balances[id][to] += amount;
    }
    function balanceOf(address account, uint256 id) public view returns (uint256) {
        return balances[id][account];
    }
}

Key points

  • ERC20: identical units counted as amounts in a balance.
  • ERC721: unique ids with exactly one owner per id.
  • ERC1155: many token types in one contract, counted as amounts per id.
  • Fungible: interchangeable, so one unit can replace another without any difference.
  • Choosing: identical units fit a currency, unique items fit a collectible, and grouped types fit a game inventory.

In this example, three small contracts sit side by side. The coin counts 10 identical units, the item gives id 1 one owner, and the pack holds 100 of one type and 5 of another.

Note

the three standards share the transfer idea, but they answer a different question about what a unit is.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

Which standard fits a game with swords and shields in one contract?