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 ID | Address | value |
1 | Alice | 100 |
Bob | 200 |
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 ID | Address | value |
2 | Charlie | 1 |
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 thefrom
account. - The
from
account has approved themsg.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;
Name | ID | Amount |
GOLD | 0 | 10**18 (one quintillion) |
SILVER | 1 | 10**27 (one octillion) |
THORS_HAMMER | 2 | 1 |
SWORD | 3 | 10**9 (one billion) |
SHIELD | 4 | 10**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.
- EIP-1155: Multi Token Standard
- ERC-1155 (enjin.io)
- ERC-1155 NON-FUNGIBLE TOKEN STANDARD (ethereum.org)
- ERC 1155 (OpenZeppelin docs)
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.