🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity ERC1155 Mechanics

An ERC1155 token is a single contract that holds many token types at once. Each type has its own id, and one address can hold different amounts of each type.

One type has one balance per holder

Minting 100 units of id 1 gives the receiver a balance of 100 for that id. Other ids stay at 0.

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

Two types have different balances

Minting 100 of id 1 and 5 of id 2 gives the same holder two different balances. Each id is counted on its own.

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

Batch balances read together

One batch call reads both balances at once. The returned list holds 100 for id 1 and 5 for id 2, in order.

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

Key points

  • Token type: one id in the contract that stands for one kind of item.
  • BalanceOf pair: a balance read needs both an address and a token id.
  • Batch balance: one call that reads several balances at once.
  • Batch transfer: one call that moves several token types at once.
  • Amounts: each type is counted in units, so one holder can own many copies of one type.

In this example, one holder owns 100 units of id 1 and 5 units of id 2. One batch transfer moves 40 of the first type and all 5 of the second type, and a batch with mismatched lists reverts.

Note

one contract holds every type, so a game can keep swords and shields in the same contract instead of deploying one contract per type.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

What does balanceOf need in ERC1155?