Skip to content

Tokens: ERC‐1155

Ananthan edited this page May 25, 2024 · 6 revisions
ERC-1155 is a multi-token standard. It has adopted some properties from ERC-20 & ERC-721. In the case of ERC-20 and ERC-721, one contract can represent only one type of token. Multiple contract deployments are necessary to create tokens of different types. However, we can create multiple types of tokens in ERC-1155 with a single smart contract. Similar to NFT, it will have a unique ID but allows multiple copies of tokens with a particular ID. ERC-1155 allows batch operations where a batch of tokens can be transferred at once.

A predominant use of ERC-1155 is in online games. This standard can monitor numerous items within a game, each possessing its distinct attributes.

ERC-1155

ERC-1155 was proposed as an Ethereum Improvement Proposal by Witek Radomski and the team in the year 2018. The optimized contract helped in reducing gas costs.

function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

function balanceOf(address _owner, uint256 _id) external view returns (uint256);

function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

function setApprovalForAll(address _operator, bool _approved) external;

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

ERC- 1155 contract must implement all the functions present in the interface.

safeTransferFrom

This Function is used to safely transfer tokens with a particular ID from owner to recipient.

safeBatchTransferFrom

This function is used to transfer a batch of tokens from owner to recipient. Since ERC-1155 creates fungible tokens, this function is useful to transfer a set of tokens as a batch rather than transferring them one by one.

balanceOf

This function returns the number of tokens with a specific id, that the given address holds.

balanceOfBatch

This function returns the balance of multiple account/token pairs

setApprovalForAll

This function is used to enable or disable approval for third party to manage tokens.

isApprovedForAll

This function is used to know the approval status of an operator. An operator is an address that has been authorized to manage tokens on behalf of an owner.

Events on ERC-1155

event TransferSingle(address indexed operator, address indexed from, address indexed to,
                          uint256 id, uint256 value);

event TransferBatch(address indexed operator,address indexed from,address indexed to,
                         uint256[] ids,uint256[] values);

event ApprovalForAll(address indexed account, address indexed operator, bool approved);

event URI(string value, uint256 indexed id);
  • TransferSingle: This event is emitted when value is transferred from the owner to recipient by an operator

  • TransferBatch: This event is emitted when a set of tokens gets transferred from owner to recipient by an operator

  • ApprovalForAll: This event is emitted when the owner allow/disallow an operator to manage the owner's tokens

  • URI: This event is emitted when change happens on Metadata URI

Contract

Every ERC-1155 compliant contract must implement the ERC1155 and ERC165 interfaces. ERC165 interface is used to publish and detect interfaces used in a smart contract.

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

The supportsInterface function receives a single argument representing interfaceID (bytes4) of an interface and returns true (bool) if that interface is supported.

ERC-1155 Contract

Let us take a look at the implementation of ERC1155 by OpenZeppelin. We are going to inherit multiple contracts in this contract.

Here we will be importing 6 contracts

  1. IERC1155 interface

  2. ERC165 interface

  3. IERC1155MetadataURI interface

  4. ERC1155Utils.sol

  5. Context.sol

  6. Arrays.sol

  7. IERC1155Errors interface

We can copy these contracts from the respective links, create files in the Remix IDE and import them in our code.

import {IERC1155} from "./IERC1155.sol";
import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol";
import {ERC1155Utils} from "./utils/ERC1155Utils.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";

Here Arrays.sol library is used to perform some array-specific operations like sort, swap etc. Context.sol contract provides information about the current execution context, including the sender of the transaction and data through msg.sender and msg.data.

Next, we can inherit our imported contracts in the ERC-1155 contract and we can define mappings and state variables used in this contract.

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

    // mapping from token ID to owner's balance
    mapping(uint256 id => mapping(address account => uint256)) private _balances;

    //Mapping from owner to operator approval
    mapping(address account => mapping(address operator => bool)) private _operatorApprovals;

    // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-  domain/{id}.json
    string private _uri;
}

The _balances mapping is used to track the number of tokens of a specific id, held by an address. The _operatorApprovals keeps track of operator approvals.

Next, we have a constructor for setting URI of a token.

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

Let us go through the functions used in the ERC-1155 contract implementation.

supportsInterface

The supportInterface function is inherited from ERC-165, and it will check whether the provided interface-id is implemented in that contract or not.

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
    return
    interfaceId == type(IERC1155).interfaceId ||
    interfaceId == type(IERC1155MetadataURI).interfaceId ||
    super.supportsInterface(interfaceId);
}

uri

This function will return the uri of token type with given id.

function uri(uint256 id) public view virtual returns (string memory) {
    return _uri;
}

balanceOf

This function is used to get the number of tokens with specific id, the given address holds.

function balanceOf(address account, uint256 id) public view virtual returns (uint256) {
    return _balances[id][account];
}

balanceOfBatch

This function is used to get the balance of a set of tokens held by a set of addresses.

function balanceOfBatch(address[] memory accounts, uint256[] memory ids ) public view virtual {
    returns (uint256[] memory) {
        if (accounts.length != ids.length) {
            revert ERC1155InvalidArrayLength(ids.length, accounts.length);
        }

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

        return batchBalances;

    }
}

setApprovalforAll

This function sets/resets permission for third parties to use caller's tokens. Such authorised third parties are called operators.

function setApprovalForAll(address operator, bool approved) public virtual {
    _setApprovalForAll(_msgSender(), operator, approved);

}

isApprovedForAll

This function checks whether a given operator is under the specific owner. It returns a true/false value.

function isApprovedForAll(address account, address operator) public view virtual returns (bool) {
    return _operatorApprovals[account][operator];
}

safeTransferFrom

This function transfers tokens from one address to another and checks whether the receiver is an EOA or a contract account. If it is a contract account, it checks whether it is a valid ERC1155 receiver contract.

function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) public virtual {
    address sender = _msgSender();
    if (from != sender && !isApprovedForAll(from, sender)) {
        revert ERC1155MissingApprovalForAll(sender, from);
    }

    _safeTransferFrom(from, to, id, value, data);
}

The safeBatchTransferFrom is similar to safeTransferFrom, except that it allows batch transfer.

function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory values,  bytes memory data ) public virtual {
    address sender = _msgSender();
    if (from != sender && !isApprovedForAll(from, sender)) {
        revert ERC1155MissingApprovalForAll(sender, from);
    }

    _safeBatchTransferFrom(from, to, ids, values, data);
}

_update

This function is used to transfer tokens from owner to recipient. The number of tokens is specified by 'values' input paramater. This will also mint new tokens, if the sender ('from' parameter) is the zero address and may burn new tokens, if the recipient ('to' parameter)is a zero address. The zero address (null address/zero account) refers to the Ethereum address 0x0000000000000000000000000000000000000000. The zero address usually denotes the absence of a valid Ethereum address.

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.unsafeMemoryAccess(i);
        uint256 value = values.unsafeMemoryAccess(i);
        if (from != address(0)) {
            uint256 fromBalance = _balances[id][from];
            if (fromBalance < value) {
                revert ERC1155InsufficientBalance(from, fromBalance, value, id);
            }
            unchecked {
                // Overflow not possible: value <= fromBalance
                _balances[id][from] = fromBalance - value;
            }
        }

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

    }

    if (ids.length == 1) {
        uint256 id = ids.unsafeMemoryAccess(0);
        uint256 value = values.unsafeMemoryAccess(0);
        emit TransferSingle(operator, from, to, id, value);
    } else {
        emit TransferBatch(operator, from, to, ids, values);
    }

}

_updateWithAcceptanceCheck

This function is similar to _update function. but includes an additional check on whether the contract receivers are aware of the ERC-1155 standard.

function _updateWithAcceptanceCheck(address from, address to, uint256[] memoryids, uint256[] memory values,bytes memory data ) internal virtual {
    _update(from, to, ids, values);

    if (to != address(0)) {
        address operator = _msgSender();
        if (ids.length == 1) {
            uint256 id = ids.unsafeMemoryAccess(0);
            uint256 value = values.unsafeMemoryAccess(0);
            ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, data);
        } else {
            ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
        }
    }
}

_safeTransferFrom

This function safely transfers tokens from sender to receiver by performing some extra checks. It also checks whether the contract receivers are knowledgeable of the ERC1155 standard.

function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }

    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
     _updateWithAcceptanceCheck(from, to, ids, values, data);
}

_safeBatchTransferFrom

This function is just like the _safeTransferFrom function, here we can transfer a batch of tokens.

function _safeBatchTransferFrom( address from,  address to, uint256[] memory ids,
    uint256[] memory values, bytes memory data  ) internal {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }

    _updateWithAcceptanceCheck(from, to, ids, values, data);
}

_setURI

This function is used to set new URI.

function _setURI(string memory newuri) internal virtual {
    _uri = newuri;
}

_mint

This function is used to mint a set of new tokens with a particular id.

function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }

    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
    _updateWithAcceptanceCheck(address(0), to, ids, values, data);
}

_mintBatch

This function is used to mint a group of tokens to a group of addresses.

function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }

    _updateWithAcceptanceCheck(address(0), to, ids, values, data);
}

_burn

The _burn function is used if you want to remove a set of tokens of specific token ID from circulation.

function _burn(address from, uint256 id, uint256 value) internal {
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }

    (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
    _updateWithAcceptanceCheck(from, address(0), ids, values, "");
}

_burnBatch

This is a function to remove a set of tokens with a given set of IDs.

function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }

    _updateWithAcceptanceCheck(from, address(0), ids, values, "");
}

_asSingletonArrays

Used to create array and manage the array. It is used in situations where a given variable needs to be represented as an array

function _asSingletonArrays(uint256 element1, uint256 element2) private pure returns (uint256[] memory array1, uint256[] memory array2) {
    /// @solidity memory-safe-assembly
    assembly {
        // Load the free memory pointer
        array1 := mload(0x40)
        // Set array length to 1
        mstore(array1, 1)
        // Store the single element at the next word after the length (where content starts)
        mstore(add(array1, 0x20), element1)
        // Repeat for next array locating it right after the first array
        array2 := add(array1, 0x40)
        mstore(array2, 1)
        mstore(add(array2, 0x20), element2)
        // Update the free memory pointer by pointing after the second array
        mstore(0x40, add(array2, 0x40))
    }
}

The complete contract is given below.

// 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 {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol";
import {ERC1155Utils} from "./utils/ERC1155Utils.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 id => mapping(address account => uint256)) private _balances;

    mapping(address account => mapping(address operator => bool)) private _operatorApprovals;


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


    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
            super.supportsInterface(interfaceId);
    }


    function uri(uint256 /* id */) public view virtual returns (string memory) {
        return _uri;
    }


    function balanceOf(address account, uint256 id) public view virtual returns (uint256) {
        return _balances[id][account];
    }


    function balanceOfBatch(
        address[] memory accounts,
        uint256[] memory ids
    ) public view virtual returns (uint256[] memory) {
        if (accounts.length != ids.length) {
            revert ERC1155InvalidArrayLength(ids.length, accounts.length);
        }

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

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

        return batchBalances;
    }


    function setApprovalForAll(address operator, bool approved) public virtual {
        _setApprovalForAll(_msgSender(), operator, approved);
    }


    function isApprovedForAll(address account, address operator) public view virtual returns (bool) {
        return _operatorApprovals[account][operator];
    }


    function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) public virtual {
        address sender = _msgSender();
        if (from != sender && !isApprovedForAll(from, sender)) {
            revert ERC1155MissingApprovalForAll(sender, from);
        }
        _safeTransferFrom(from, to, id, value, data);
    }


    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) public virtual {
        address sender = _msgSender();
        if (from != sender && !isApprovedForAll(from, sender)) {
            revert ERC1155MissingApprovalForAll(sender, from);
        }
        _safeBatchTransferFrom(from, to, ids, values, data);
    }


    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.unsafeMemoryAccess(i);
            uint256 value = values.unsafeMemoryAccess(i);

            if (from != address(0)) {
                uint256 fromBalance = _balances[id][from];
                if (fromBalance < value) {
                    revert ERC1155InsufficientBalance(from, fromBalance, value, id);
                }
                unchecked {
                    // Overflow not possible: value <= fromBalance
                    _balances[id][from] = fromBalance - value;
                }
            }

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

        if (ids.length == 1) {
            uint256 id = ids.unsafeMemoryAccess(0);
            uint256 value = values.unsafeMemoryAccess(0);
            emit TransferSingle(operator, from, to, id, value);
        } else {
            emit TransferBatch(operator, from, to, ids, values);
        }
    }


    function _updateWithAcceptanceCheck(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) internal virtual {
        _update(from, to, ids, values);
        if (to != address(0)) {
            address operator = _msgSender();
            if (ids.length == 1) {
                uint256 id = ids.unsafeMemoryAccess(0);
                uint256 value = values.unsafeMemoryAccess(0);
                ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, data);
            } else {
                ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
            }
        }
    }


    function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        if (from == address(0)) {
            revert ERC1155InvalidSender(address(0));
        }
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(from, to, ids, values, data);
    }


    function _safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory values,
        bytes memory data
    ) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        if (from == address(0)) {
            revert ERC1155InvalidSender(address(0));
        }
        _updateWithAcceptanceCheck(from, to, ids, values, data);
    }


    function _setURI(string memory newuri) internal virtual {
        _uri = newuri;
    }


    function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }


    function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
        if (to == address(0)) {
            revert ERC1155InvalidReceiver(address(0));
        }
        _updateWithAcceptanceCheck(address(0), to, ids, values, data);
    }

       function _burn(address from, uint256 id, uint256 value) internal {
        if (from == address(0)) {
            revert ERC1155InvalidSender(address(0));
        }
        (uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
        _updateWithAcceptanceCheck(from, address(0), ids, values, "");
    }


    function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
        if (from == address(0)) {
            revert ERC1155InvalidSender(address(0));
        }
        _updateWithAcceptanceCheck(from, address(0), ids, values, "");
    }


    function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
        if (operator == address(0)) {
            revert ERC1155InvalidOperator(address(0));
        }
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }


    function _asSingletonArrays(
        uint256 element1,
        uint256 element2
    ) private pure returns (uint256[] memory array1, uint256[] memory array2) {
        /// @solidity memory-safe-assembly
        assembly {
            // Load the free memory pointer
            array1 := mload(0x40)
            // Set array length to 1
            mstore(array1, 1)
            // Store the single element at the next word after the length (where content starts)
            mstore(add(array1, 0x20), element1)

            // Repeat for next array locating it right after the first array
            array2 := add(array1, 0x40)
            mstore(array2, 1)
            mstore(add(array2, 0x20), element2)

            // Update the free memory pointer by pointing after the second array
            mstore(0x40, add(array2, 0x40))
        }
    }
}


Clone this wiki locally