Back to Overview

TotalSupply Inconsistency in ERC1155 NFT Tokens

November 21, 2021

On November 1st, 2021, we reported a vulnerability to OpenZeppelin in which the totalSupply can appear lower than it actually is for ERC1155 NFT tokens. This affects projects relying on totalSupply for calculations, e.g. when voting or determining market caps. All tokens utilizing the ERC1155Supply.sol extension prior to v4.3.2 are affected. The vulnerability was promptly fixed and publicly disclosed on November 12th.

We want to thank the OpenZeppelin team around Francisco Giordano for their excellent collaboration and responsiveness on this issue.

Background — ERC1155 inner workings

ERC1155 tokens implement the “Multi Token Standard” in which both fungible (ERC20-like) and non-fungible (ERC721-like) tokens can be handled by a single contract. The standard allows to create non-fungible tokens with unique IDs, but allowing each one to have multiple instances, making them fungible among themselves.

The EIP-1155 standard does not contain a need to track totalSupply for the fungible tokens on the smart contract level and opts to suggest projects to track events to keep a count of the total number of tokens existing. While this works well for some use-cases, others require to know on-chain the total amount of tokens issued. To facilitate this, OpenZeppelin provides an extension to ERC1155 called ERC1155Supply.sol which adds the totalSupply field known from ERC20 tokens to ERC1155 tokens. This field is updated whenever tokens are minted or burned.

Another feature of ERC1155 is to allow receivers of tokens to be notified, which can enable interesting use-cases in the composable Ethereum universe. The downside is that control flow is handed over to untrusted parties, allowing the infamous reentrancy attacks. Due to this, every project handling ERC1155 tokens (and ERC712 tokens) must be careful to protect against these attacks.

The ERC1155Supply vulnerability explained

The following code snippet presents a simplified view of the functions involved in minting new tokens:

pragma solidity ^0.8.0;

contract ERC1155 {
    function _mint(
        address account,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        ...

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

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

        _doSafeTransferAcceptanceCheck(operator, address(0), account, id, amount, data);
    }
    
    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non ERC1155Receiver implementer");
            }
        }
    }
}

abstract contract ERC1155Supply is ERC1155 {
    mapping(uint256 => uint256) private _totalSupply;

    function _mint(
        address account,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual override {
        super._mint(account, id, amount, data);
        _totalSupply[id] += amount;
    }
}

ERC1155 first does some checks in beforeTokenTransfer (L12), then mints the new tokens to the receiver (L14) who is allowed to do an acceptance check (L17). The acceptance check is implemented in L29 by calling onERC1155Received on the receiver smart contract, handing over control flow to the receiver.

ERC1155Supply adds tracking of totalSupply by first calling ERC1155 mint method and then increasing the totalSupply. The crucial part here is that before the totalSupply is increased, control flow is given to the receiver as part of the normal ERC1155 minting. At this stage, an attack already has tokens which they can e.g. use to vote, but the totalSupply has not yet been increased, potentially allowing the attack to pretend to have a higher share of tokens than they really should have (including having a share bigger than the totalSupply which could lead to other attacks if applications assume that totalSupply <= balance[any_user] ). Technically, this is a light form of a re-entrancy attack as the attack would just re-enter the totalSupply view function and not modify any state. This is frequently seen as a “benign reentrancy”, but in this case can lead to real attacks.

What to do

For the attack to be exploitable, the mint function must be called with untrusted receivers. While this is usually the case for dynamically mintable tokens, be aware that this can also happen in case the token isn’t generally minted to untrusted receivers, but bridged to side-chains like Polygon. In this case a transfer of tokens from Ethereum to the side-chain usually triggers mint on the EIP1155 contracts on the side-chain.

If you are in charge of an ERC1155 token implementing ERC1155Supply, consider upgrading to the newly released version of OpenZeppelin v4.3.3 or newer. If upgrading is not possible, add a warning to your documentation for integrators to make them aware of the Security Advisory.

If you are in charge of a platform handling ERC1155 tokens which implements the ERC1155Supply extension and which allows minting from anyone, you cannot rely on totalSupply from these tokens anymore and need to prepare an alternative implementation.

Any project utilizing OpenZeppelin or other commonly used libraries should make sure that they report on which libraries are used in the Smart Contract Security Registry and also make sure that their security contact details are up to date.

If you are a holder of an ERC1155 token affected by this vulnerability, no immediate action is needed as your tokens remain safe as long as they aren’t used in a platform vulnerable to this attack. If you aren’t sure about the safety of the platform, withdraw your tokens and reach out to the platform to ask them to confirm if they are safe against this vulnerability.

About us

ChainSecurity’s mission is to build trust within the blockchain ecosystem, to allow this emerging technology to reach its potential among established organizations, governments and blockchain companies alike.

If you have questions, don’t hesitate to reach out to contact@chainsecurity.com for general requests including requests for audits, and to help@chainsecurity.com for questions about this or other vulnerabilities. Also, visit us on chainsecurity.com