OpenZeppelin's ERC1155: A User Guide

OpenZeppelin's ERC1155: A User Guide
⚠️
OpenZeppelin version introduced: v5.0.0

In this post, we'll explore the ERC1155 standard, commonly used in blockchain games, alongside OpenZeppelin. We'll delve into the inner workings of ERC1155, which adheres to the EIP1155 specification, and discuss its usage and considerations. (For detailed EIP1155 specifications, click here).

What is ERC1155?

ERC1155 is a new token standard that allows for efficient management of multiple token types (ERC721 and ERC20) within a single smart contract. This multi-token standard is particularly suitable for use in games and digital assets, offering features such as batch transfer capabilities that reduce transaction costs and improve efficiency.

Inside ERC1155

Here's a brief look at the ERC1155 implementation:

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/ERC1155.sol)

pragma solidity ^0.8.20;

import {IERC1155} from "./IERC1155.sol";
import {IERC1155Receiver} from "./IERC1155Receiver.sol";
import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol";
import {Context} from "../../utils/Context.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
import {Arrays} from "../../utils/Arrays.sol";
import {IERC1155Errors} from "../../interfaces/draft-IERC6093.sol";

abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors {
    using Arrays for uint256[];
    using Arrays for address[];

    mapping(uint256 => mapping(address => uint256)) private _balances;
    mapping(address => mapping(address => bool)) private _operatorApprovals;
    string private _uri;

    constructor(string memory uri_) {
        _setURI(uri_);
    }

    // Other functions and implementations...
}

Now, let's explain some key functions!

_update

function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual {
    if (ids.length != values.length) {
        revert ERC1155InvalidArrayLength(ids.length, values.length);
    }

    address operator = _msgSender();

    for (uint256 i = 0; i < ids.length; ++i) {
        uint256 id = ids[i];
        uint256 value = values[i];

        if (from != address(0)) {
            uint256 fromBalance = _balances[id][from];
            if (fromBalance < value) {
                revert ERC1155InsufficientBalance(from, id, fromBalance, value);
            }
            _balances[id][from] = fromBalance - value;
        }

        if (to != address(0)) {
            _balances[id][to] += value;
        }
    }

    if (ids.length == 1) {
        emit TransferSingle(operator, from, to, ids[0], values[0]);
    } else {
        emit TransferBatch(operator, from, to, ids, values);
    }
}

The _update function is used internally for minting, burning, and transferring tokens. Unlike ERC20 or ERC721, which handle one token at a time, ERC1155's _update function can handle multiple tokens simultaneously due to its batch processing capability.

_updateWithAcceptanceCheck

function _updateWithAcceptanceCheck(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory values,
    bytes memory data
) internal virtual {
    // Execute _update
    _update(from, to, ids, values);
    // If 'to' is not the zero address, it's a mint or transfer
    if (to != address(0)) {
        // Get the address of the executor
        address operator = _msgSender();
        // If the number of ids is 1
        if (ids.length == 1) {
            // Get the id and value
            uint256 id = ids.unsafeMemoryAccess(0);
            uint256 value = values.unsafeMemoryAccess(0);
            // Check if it was received properly (single)
            _doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
        } else {
            // Check if it was received properly (multiple)
            _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
        }
    }
}

This function executes _update and also checks if the mint or transfer was properly received from the sender to the receiver.

Note that there is a caution:

💡
"Don't override this function due to the risk of reentrancy from the receiver. Updating the contract state after this function would break the check-effect-interaction pattern and pose a hacking risk. If you consider overriding, think about _update instead."

_safeTransferFrom & _safeBatchTransferFrom

function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
    // Revert if 'to' is the zero address
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    // Revert if 'from' is the zero address
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }
    // Convert a single id and value into arrays
    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
    // Execute _updateWithAcceptanceCheck
    _updateWithAcceptanceCheck(from, to, ids, values, data);
}

These internal functions handle the safe transfer of single and multiple tokens, respectively, ensuring that the recipient is capable of receiving ERC1155 tokens.

_mint & _mintBatch

function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
    // Revert if 'to' is the zero address
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    // Convert the id and value into arrays
    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
    // Execute _updateWithAcceptanceCheck to mint
    _updateWithAcceptanceCheck(address(0), to, ids, values, data);
}

The internal function _mint is used for minting a single token.

function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
    // Revert if 'to' is the zero address
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    // Execute _updateWithAcceptanceCheck to mint multiple tokens
    _updateWithAcceptanceCheck(address(0), to, ids, values, data);
}

The internal function _mintBatch is used for minting multiple tokens.

_burn & _burnBatch

function _burn(address from, uint256 id, uint256 value) internal {
    // Revert if 'from' is the zero address
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }
    // Convert 'id' and 'value' into arrays
    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
    // Execute _updateWithAcceptanceCheck to burn
    _updateWithAcceptanceCheck(from, address(0), ids, values, "");
}

The internal function '_burn' is used to burn a single token.

function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
    // Revert if 'from' is the zero address
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }
    // Execute _updateWithAcceptanceCheck to burn multiple tokens
    _updateWithAcceptanceCheck(from, address(0), ids, values, "");
}

The internal function '_burnBatch' is used to burn multiple tokens at once.

Example Code

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

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";

contract GameItems is ERC1155, Ownable, ERC1155Burnable {
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant THORS_HAMMER = 2;
    uint256 public constant SWORD = 3;
    uint256 public constant SHIELD = 4;

    constructor(
        address initialOwner
    ) ERC1155("https://example.com/{id}.json") Ownable(initialOwner) {
        _mint(initialOwner, GOLD, 10 ** 18, "");
        _mint(initialOwner, SILVER, 10 ** 27, "");
        _mint(initialOwner, THORS_HAMMER, 1, "");
        _mint(initialOwner, SWORD, 10, "");
        _mint(initialOwner, SHIELD, 10, "");
    }

    function setURI(string memory newuri) public onlyOwner {
        _setURI(newuri);
    }

    function mint(
        address account,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public onlyOwner {
        _mint(account, id, amount, data);
    }

    function mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public onlyOwner {
        _mintBatch(to, ids, amounts, data);
    }
}

In this example, minting and burning are restricted to the owner only, which is a common approach for managing game tokens.

The tokens in this example are:

  • GOLD (gold coins)
  • SILVER (silver coins)
  • THORS_HAMMER (Thor's Hammer, a super rare item with only one available)
  • SWORD (swords)
  • SHIELD (shields)

The initial minting of these tokens is set up in the constructor, which allows for a fixed number of items from the start. This setup makes it easier to distinguish between rare and common items in the game.

Of course, the implementation can be customized according to the specific requirements of the game.

Summary


OpenZeppelin's ERC1155 is a multi-token standard that efficiently manages various token types in blockchain games and digital asset domains. By using this standard, developers can handle multiple tokens within a single smart contract and reduce transaction costs with batch transfer functionality. Utilizing OpenZeppelin allows for the implementation of tokens that are both secure and convenient.