OpenZeppelin's ERC1155: A User Guide
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:
_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.