Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Burning a ERC1155Enumerable token doesn't remove it from the enumeration #248

Open
code423n4 opened this issue Jan 27, 2023 · 6 comments
Open
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working M-02 primary issue Highest quality submission among a set of duplicates satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-01-timeswap/blob/main/packages/v2-token/src/base/ERC1155Enumerable.sol#L81-L101

Vulnerability details

The ERC1155Enumerable base contract used in the TimeswapV2Token and TimeswapV2LiquidityToken tokens provides a functionality to enumerate all token ids that have been minted in the contract.

The logic to remove the token from the enumeration if the last token is burned is implemented in the _afterTokenTransfer hook:

https://github.com/code-423n4/2023-01-timeswap/blob/main/packages/v2-token/src/base/ERC1155Enumerable.sol#L81-L101

function _afterTokenTransfer(address, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory) internal virtual override {
    for (uint256 i; i < ids.length; ) {
        if (amounts[i] != 0) _removeTokenEnumeration(from, to, ids[i], amounts[i]);

        unchecked {
            ++i;
        }
    }
}

/// @dev Remove token enumeration list if necessary.
function _removeTokenEnumeration(address from, address to, uint256 id, uint256 amount) internal {
    if (to == address(0)) {
        if (_idTotalSupply[id] == 0 && _additionalConditionRemoveTokenFromAllTokensEnumeration(id)) _removeTokenFromAllTokensEnumeration(id);
        _idTotalSupply[id] -= amount;
    }

    if (from != address(0) && from != to) {
        if (balanceOf(from, id) == 0 && _additionalConditionRemoveTokenFromOwnerEnumeration(from, id)) _removeTokenFromOwnerEnumeration(from, id);
    }
}

The _removeTokenEnumeration condition to check if the supply is 0 happens before the function decreases the burned amount. This will _removeTokenFromAllTokensEnumeration from being called when the last token(s) is(are) burned.

Impact

The token isn't removed from the enumeration since _removeTokenFromAllTokensEnumeration will never be called. This will cause the enumeration to always contain a minted token even though it is burned afterwards. The function totalSupply and tokenByIndex will report wrong values.

This will also cause the enumeration to contain duplicate values or multiple copies of the same token. If the token is minted again after all tokens were previously burned, the token will be re added to the enumeration.

PoC

The following test demonstrates the issue. Alice is minted a token and that token is then burned, the token is still present in the enumeration. The token is minted again, causing the enumeration to contain the token by duplicate.

// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.8;

import "forge-std/Test.sol";
import "../src/base/ERC1155Enumerable.sol";

contract TestERC1155Enumerable is ERC1155Enumerable {
    constructor() ERC1155("") {
    }

    function mint(address to, uint256 id, uint256 amount) external {
        _mint(to, id, amount, "");
    }

    function burn(address from, uint256 id, uint256 amount) external {
        _burn(from, id, amount);
    }
}

contract AuditTest is Test {
    function test_ERC1155Enumerable_BadRemoveFromEnumeration() public {
        TestERC1155Enumerable token = new TestERC1155Enumerable();
        address alice = makeAddr("alice");
        uint256 tokenId = 0;
        uint256 amount = 1;

        token.mint(alice, tokenId, amount);

        // tokenByIndex and totalSupply are ok
        assertEq(token.tokenByIndex(0), tokenId);
        assertEq(token.totalSupply(), 1);

        // now we burn the token
        token.burn(alice, tokenId, amount);

        // tokenByIndex and totalSupply still report previous values
        // tokenByIndex should throw index out of bounds, and supply should return 0
        assertEq(token.tokenByIndex(0), tokenId);
        assertEq(token.totalSupply(), 1);

        // Now we mint it again, this will re-add the token to the enumeration, duplicating it
        token.mint(alice, tokenId, amount);
        assertEq(token.totalSupply(), 2);
        assertEq(token.tokenByIndex(0), tokenId);
        assertEq(token.tokenByIndex(1), tokenId);
    }
}

Recommendation

Decrease the amount before checking if the supply is 0.

function _removeTokenEnumeration(address from, address to, uint256 id, uint256 amount) internal {
    if (to == address(0)) {
        _idTotalSupply[id] -= amount;
        if (_idTotalSupply[id] == 0 && _additionalConditionRemoveTokenFromAllTokensEnumeration(id)) _removeTokenFromAllTokensEnumeration(id);
    }

    if (from != address(0) && from != to) {
        if (balanceOf(from, id) == 0 && _additionalConditionRemoveTokenFromOwnerEnumeration(from, id)) _removeTokenFromOwnerEnumeration(from, id);
    }
}
@code423n4 code423n4 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Jan 27, 2023
code423n4 added a commit that referenced this issue Jan 27, 2023
@c4-judge c4-judge closed this as completed Feb 2, 2023
@c4-judge
Copy link
Contributor

c4-judge commented Feb 2, 2023

Picodes marked the issue as duplicate of #228

@c4-judge
Copy link
Contributor

Picodes marked the issue as selected for report

@c4-judge c4-judge reopened this Feb 12, 2023
@c4-judge c4-judge added primary issue Highest quality submission among a set of duplicates selected for report This submission will be included/highlighted in the audit report labels Feb 12, 2023
@c4-judge
Copy link
Contributor

Picodes changed the severity to QA (Quality Assurance)

@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Feb 12, 2023
@c4-judge
Copy link
Contributor

This previously downgraded issue has been upgraded by Picodes

@c4-judge c4-judge added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value and removed downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax labels Feb 12, 2023
@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Feb 12, 2023
@c4-judge
Copy link
Contributor

Picodes marked the issue as satisfactory

@vhawk19
Copy link

vhawk19 commented Feb 17, 2023

Resolved in PR

@C4-Staff C4-Staff added the M-02 label Feb 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working M-02 primary issue Highest quality submission among a set of duplicates satisfactory satisfies C4 submission criteria; eligible for awards selected for report This submission will be included/highlighted in the audit report
Projects
None yet
Development

No branches or pull requests

4 participants