How Do ERC-1155 Tokens Work?

Rate this post
How Do ERC-1155 Tokens Work?

What is an ERC-1155 Token?

An ERC-1155 token is a smart contract on Ethereum that implements the methods and events specified in the  EIP-1155: Multi Token Standard. As the name suggests, it is designed to represent any number of fungible and non-fungible token types in a single smart contract. 

In the ERC-20 and ERC-721 standards, a smart contract can only represent one token (ERC-20) or collection (ERC-721). So, if there is a requirement to create multiple tokens or collections, it is necessary to create multiple smart contracts, resulting in a lot of duplicates and high operation costs. The ERC-1155 tokens can solve this problem.

This article explains the ERC-1155’s functionality by comparing it with ERC-20 and ERC-721. If you’re not familiar with them, you might find it helpful to check my previous articles below.

ERC-20:

ERC-721

Overview of a Basic ERC-1155 Token

This article will look at the ERC-1155 token implementation by OpenZeppelin. We can find the Solidity code in their Github repository. In this article, we use the latest version (v4.4.1) at the time of writing. 

_balances state variable

The core of an ERC-1155 token is the _balances private variable. It is a map of a map variable, as shown below. 

// Mapping from token ID to account balances
mapping(uint256 => mapping(address => uint256)) private _balances;

(Line 23 – 24)

This variable is like merging the _owners variable in ERC-721 and the balances variable in ERC-20. The first mapping (id to address) is the same as the _owners variable in ERC-721, keeping the information about which token ID is owned by which account. The second mapping (address to value) is the same as the balances variable in ERC-20, keeping which account has how many tokens. 

For example, when Alice has 100 units of the token ID 1 and Bob has 200 units of the same token, the variable would contain the following entries.

_balances 

Token IDAddressvalue
1Alice100
Bob200

This token must be fungible because there is more than one same token. 

When Charlie has a non-fungible token (ID 2), the variable would contain the following entry:

_balances 

Token IDAddressvalue
2Charlie1

When a token is non-fungible, there is always only one token by definition. Therefore, for non-fungible tokens, ERC-1155’s _balances variable always contains the value 1, effectively making it the same as ERC-721’s _owner variable.

As we can see, this variable can hold multiple token IDs. Therefore, an ERC-1155 smart contract can represent multiple tokens (hence the name “Multi Token Standard”).

The balanceOf() function looks up this variable and returns a corresponding entry for the specified token ID if it finds one.

/**
 * @dev See {IERC1155-balanceOf}.
 *
 * Requirements:
 *
 * - `account` cannot be the zero address.
 */
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
    require(account != address(0), "ERC1155: balance query for the zero address");
    return _balances[id][account];
}

(Line 63 – 73)

Create a new token (minting)

The _mint() internal function creates (or mints) a new token. It updates the _balances variable so that the specified token ID’s address has the specified amount.

/**
 * @dev Creates `amount` tokens of token type `id`, and assigns them to `to`.
 *
 * Emits a {TransferSingle} event.
 *
 * Requirements:
 *
 * - `to` cannot be the zero address.
 * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
 * acceptance magic value.
 */
function _mint(
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: mint to the zero address");

    address operator = _msgSender();

    _beforeTokenTransfer(operator, address(0), to, _asSingletonArray(id), _asSingletonArray(amount), data);

    _balances[id][to] += amount;
    emit TransferSingle(operator, address(0), to, id, amount);

    _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}

(Line 249 – 276)

It is very similar to ERC-20’s _mint() function, although there is no totalSupply variable to keep track of the amount of the token’s total supply in ERC-1155.

The _mint() function in ERC-20 (Line 252 – 262)

function _mint(address account, uint256 amount) internal virtual {
    require(account != address(0), "ERC20: mint to the zero address");

    _beforeTokenTransfer(address(0), account, amount);

    _totalSupply += amount;
    _balances[account] += amount;
    emit Transfer(address(0), account, amount);

    _afterTokenTransfer(address(0), account, amount);
}

 Transfer workflow

Like ERC-20 and ERC-721, ERC-1155 has two workflows to transfer tokens from one address to another; one when the owner transfers tokens and the other when a third party transfers tokens. But, like ERC-721, the safeTransferFrom() function handles both workflows in ERC-1155.

/**
 * @dev See {IERC1155-safeTransferFrom}.
 */
function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) public virtual override {
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: caller is not owner nor approved"
    );
    _safeTransferFrom(from, to, id, amount, data);
}

(Line 114 – 129)

The safeTransferFrom() function checks if any of the following conditions is true or not:

  • The msg.sender (who is calling this function) is the from account.
  • The from account has approved the msg.sender to transfer all their tokens.

The mechanism is very similar to ERC-721, which I looked at in the ERC-721 article below, so I will not repeat it in this article.

In addition, like ERC-721, it performs the safe transfer check to ensure that the to account can receive an ERC-1155 token.

Batch operations

One of the distinct features of ERC-1155 is to perform batch operations. It makes it efficient and cost-effective to check the balance of multiple tokens or transfer multiple tokens in a single transaction.

The following is the balanceOfBatch() function.

/**
 * @dev See {IERC1155-balanceOfBatch}.
 *
 * Requirements:
 *
 * - `accounts` and `ids` must have the same length.
 */
function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
    public
    view
    virtual
    override
    returns (uint256[] memory)
{
    require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");

    uint256[] memory batchBalances = new uint256[](accounts.length);

    for (uint256 i = 0; i < accounts.length; ++i) {
        batchBalances[i] = balanceOf(accounts[i], ids[i]);
    }

    return batchBalances;
}

(Line 75 – 98)

The function takes an array of accounts as the first parameter and another array of token IDs as the second parameter and returns an array of balances. Internally, it loops through the arrays and runs the balanceOf() function for each element of the arrays.

The following is the safeBatchTransferFrom() function. With this function, we can transfer multiple tokens from the from account to the to account.

/**
 * @dev See {IERC1155-safeBatchTransferFrom}.
 */
function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public virtual override {
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: transfer caller is not owner nor approved"
    );
    _safeBatchTransferFrom(from, to, ids, amounts, data);
}

(Line 131 – 146)

The _safeBatchTransferFrom() internal function loops through the array of token IDs and the amounts and performs the transfer (i.e. updates the _balances variable) one by one, as shown below.

/**
 * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}.
 *
 * Emits a {TransferBatch} event.
 *
 * Requirements:
 *
 * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
 * acceptance magic value.
 */
function _safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {
    require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
    require(to != address(0), "ERC1155: transfer to the zero address");

    address operator = _msgSender();

    _beforeTokenTransfer(operator, from, to, ids, amounts, data);

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

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;
    }

    emit TransferBatch(operator, from, to, ids, amounts);

    _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}

(Line 185 – 224)

The OpenZeppelin’s ERC-1155 token also implements the _mintBatch() and _burnBatch() internal functions in a similar manner, although the ERC-1155 standard does not specify them.

ERC-1155 metadata extension

Like ERC-721, the ERC-1155 specification defines the optional metadata extension (ERC1155Metadata), where we can implement the uri() function. It takes the _id parameter (token ID) and returns the URI for the token. 

The specification states that if the function returns the text string {id} in the URI, clients must replace it with the actual token ID in hexadecimal form padded with zero to 64 hex characters length. 

Example of such a URI: https://token-cdn-domain/{id}.json would be replaced with https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json if the client is referring to token ID 314592/0x4CCE0.

OpenZeppelin’s ERC-1155 implements it as follows:

/**
 * @dev See {IERC1155MetadataURI-uri}.
 *
 * This implementation returns the same URI for *all* token types. It relies
 * on the token type ID substitution mechanism
 * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP].
 *
 * Clients calling this function must replace the `\{id\}` substring with the
 * actual token type ID.
 */
function uri(uint256) public view virtual override returns (string memory) {
    return _uri;
}

(Line 49 – 61)

ERC-1155 Token with Brownie

This section will create an ERC-1155 token using an example in the OpenZeppelin documentation and try some functions.

Set up the Brownie environment

If you haven’t installed Brownie, you can install it by following the tutorial below:

After installing Brownie, go to a new directory and run the brownie init command.

[~/erc1155_test]$ brownie init
Brownie v1.17.1 - Python development framework for Ethereum
SUCCESS: A new Brownie project has been initialized at /Users/mikio/erc1155_test

Then install the OpenZeppelin library. 

[~/erc1155_test]$ brownie pm install OpenZeppelin/openzeppelin-contracts@4.4.1                  
Brownie v1.17.1 - Python development framework for Ethereum

1.71MiB [00:00, 11.2MiB/s]
New compatible solc version available: 0.8.10
Compiling contracts...
  Solc version: 0.8.10
  Optimizer: Enabled  Runs: 200
  EVM Version: Istanbul
Generating build data...
 - AccessControl
 - AccessControlEnumerable
...
...
...

SUCCESS: Package 'OpenZeppelin/openzeppelin-contracts@4.4.1' has been installed

Create the configuration file brownie-config.yaml in the current directory and add the following content.

brownie-config.yaml

dependencies:
  - OpenZeppelin/openzeppelin-contracts@4.4.1
compiler:
    solc:
        remappings:
          - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.4.1"

Set up accounts

In this example, we will use the accounts alice, bob, charlie, which we created in my previous article. If you don’t have these accounts, you can create them in Brownie, as shown below. When prompted, type a password. Remember it, as we need it in the later steps.

[~/erc1155_test]$ brownie accounts generate alice
Brownie v1.17.1 - Python development framework for Ethereum

Generating a new private key...
mnemonic: 'xxxx xxxx ...'
Enter the password to encrypt this account with: 
SUCCESS: A new account '0x32d506BccD1367e779c53A2887070C611cC40Aa8' has been generated with the id 'alice'
[~/erc1155_test]$ brownie accounts generate bob  
Brownie v1.17.1 - Python development framework for Ethereum

Generating a new private key...
mnemonic: 'yyyy yyyy ....'
Enter the password to encrypt this account with: 
SUCCESS: A new account '0x80b8EFE085A0671931606eB1cF1314cf4dcc9062' has been generated with the id 'bob'
[~/erc1155_test]$ brownie accounts generate charlie
Brownie v1.17.1 - Python development framework for Ethereum

Generating a new private key...
mnemonic: 'zzzz zzzz ....'
Enter the password to encrypt this account with: 
SUCCESS: A new account '0xCa31275A8eF4a16cfaC4f7317aBeeDe31F73558D' has been generated with the id 'charlie'

Deploy smart contract

We use the following example for demonstration purposes, which I took from the OpenZeppelin documentation. Save this file as GameItems.sol in the contracts directory.

contracts/GameItems.sol

// contracts/GameItem.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {
    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() ERC1155("https://game.example/api/item/{id}.json") {
        _mint(msg.sender, GOLD, 10**18, "");
        _mint(msg.sender, SILVER, 10**27, "");
        _mint(msg.sender, THORS_HAMMER, 1, "");
        _mint(msg.sender, SWORD, 10**9, "");
        _mint(msg.sender, SHIELD, 10**9, "");
    }
}

The smart contract inherits from ERC1155 in the OpenZeppelin library. 

The constructor creates the following five different tokens;

NameIDAmount
GOLD010**18 (one quintillion)
SILVER110**27 (one octillion)
THORS_HAMMER21
SWORD310**9 (one billion)
SHIELD410**9 (one billion)

The amount of THORS_HAMMER is 1, which means it is a non-fungible token. The other tokens are fungible.

Let’s deploy this smart contract in the local development environment to interact with it using the Brownie console. 

[~/erc1155_test]$ brownie console
Brownie v1.17.1 - Python development framework for Ethereum

New compatible solc version available: 0.8.10
Compiling contracts...
  Solc version: 0.8.10
  Optimizer: Enabled  Runs: 200
  EVM Version: Istanbul
Generating build data...
 - OpenZeppelin/openzeppelin-contracts@4.4.1/ERC1155
 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC1155
 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC1155Receiver
 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC1155MetadataURI
 - OpenZeppelin/openzeppelin-contracts@4.4.1/Address
 - OpenZeppelin/openzeppelin-contracts@4.4.1/Context
 - OpenZeppelin/openzeppelin-contracts@4.4.1/ERC165
 - OpenZeppelin/openzeppelin-contracts@4.4.1/IERC165
 - GameItems

Erc1155TestProject is the active project.

Launching 'ganache-cli – accounts 10 – hardfork istanbul – gasLimit 12000000 – mnemonic brownie – port 8545'...
Brownie environment is ready.

Then, load the accounts.

>>> alice = accounts.load('alice')
Enter password for "alice": 
>>> bob = accounts.load('bob')
Enter password for "bob": 
>>> charlie = accounts.load('charlie')
Enter password for "charlie": 

Now, let’s deploy the smart contract using the alice account.

>>> gameItems = GameItems.deploy({'from': alice})
Transaction sent: 0x22eb146fc32a1ca83ffeaf0ee5133a203b392f572933da0357ade51845597247
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  GameItems.constructor confirmed   Block: 1   Gas used: 1385548 (11.55%)
  GameItems deployed at: 0x37B39bE2E35caA706CF1a80b28Df9FE412008508

check the balances

Let’s check Alice’s balance of the token 1 using the balanceOf() function.

>>> gameItems.balanceOf(alice, 1)
1000000000000000000000000000

We can also check multiple token’s balances using the balanceOfBatch() function.

>>> gameItems.balanceOfBatch([alice]*5, [0, 1, 2, 3, 4])
(1000000000000000000, 1000000000000000000000000000, 1, 1000000000, 1000000000)

Transfer the tokens

We can use the safeTransferFrom() function to transfer the token 2 from Alice to Bob.

>>> tx1 = gameItems.safeTransferFrom(alice, bob, 2, 1, '', {'from': alice})
Transaction sent: 0x9044fd79b368067f78efc03848ab93e2eb728cfa8e3bbf5e589392405644ea2c
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 1
  GameItems.safeTransferFrom confirmed   Block: 2   Gas used: 39442 (0.33%)

>>> tx1.info()
Transaction was Mined 
---------------------
Tx Hash: 0x9044fd79b368067f78efc03848ab93e2eb728cfa8e3bbf5e589392405644ea2c
From: 0x8023f551705AF01849986a5Ea4625610c4946255
To: 0x37B39bE2E35caA706CF1a80b28Df9FE412008508
Value: 0
Function: GameItems.safeTransferFrom
Block: 2
Gas Used: 39442 / 12000000 (0.3%)

Events In This Transaction
--------------------------
└── GameItems (0x37B39bE2E35caA706CF1a80b28Df9FE412008508)
    └── TransferSingle
        ├── operator: 0x8023f551705AF01849986a5Ea4625610c4946255
        ├── from: 0x8023f551705AF01849986a5Ea4625610c4946255
        ├── to: 0x1271Fa44960382D26168e76ef9bd62a141f0EB38
        ├── id: 2
        └── value: 1

>>> gameItems.balanceOf(alice, 2)
0
>>> gameItems.balanceOf(bob, 2)
1

We can also batch transfer multiple tokens using the safeBatchTransferFrom() function. The following example transfers fungible tokens (0, 1, 3) from Alice to Charlie.

>>> gameItems.safeBatchTransferFrom(alice, charlie, [0, 1, 3], [50, 100, 1], '', {'from': alice})

>>> gameItems.balanceOfBatch([charlie]*3, [0, 1, 3])
(50, 100, 1)

Summary

In this article, we looked at the basic functionality of the ERC-1155 token. 

An ERC-1155 token is a smart contract on Ethereum that implements the methods and events specified in the  EIP-1155: Multi Token Standard. As the name suggests, it is designed to represent any number of fungible and non-fungible token types in a single smart contract. 

We can say that the ERC-1155 token standard is like a hybrid of the ERC-20 and ERC-721 token standards. Unique IDs identify token types, and the smart contract holds the addresses and the balances for each token.

It specifies batch operations to make it possible to transfer multiple tokens in a single transaction efficiently and cost-effectively. 

I hope this overview has been helpful to understand how ERC-1155 tokens work. 

You can find more in the following resources.


Learn Solidity Course

Solidity is the programming language of the future.

It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.

In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.

NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.

This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.