NextGen - Noro'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: 27/243

Findings: 1

Award: $557.13

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0x3b

Also found by: 0xlemon, AvantGard, Krace, MrPotatoMagic, Noro, ZdravkoHr, fibonacci, nuthan2x, oakcobalt, trachev

Labels

bug
3 (High Risk)
partial-50
upgraded by judge
duplicate-380

Awards

557.1267 USDC - $557.13

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L181 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L196 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L240 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L252

Vulnerability details

In periodic sale model mints are limited to 1 token during each time period (e.g. minute, hour, day), this is controlled in NextGenMinterContract#mint().

function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... // control mechanism for sale option 3 if (collectionPhases[col].salesOption == 3) { uint timeOfLastMint; if (lastMintDate[col] == 0) { // for public sale set the allowlist the same time as publicsale timeOfLastMint = collectionPhases[col].allowlistStartTime - collectionPhases[col].timePeriod; } else { timeOfLastMint = lastMintDate[col]; } // uint calculates if period has passed in order to allow minting uint tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[col].timePeriod; // users are able to mint after a day passes require(tDiff>=1 && _numberOfTokens == 1, "1 mint/period"); lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1)); } }

In first period users are allowed to mint and lastMintDate is calculated for the next period mint using the circulating supply, so if an airdrop is made by one of the admins this will increase the circulating supply of the collection which will increase lastMintDate resulting in lastMintDate > block.timestamp (when the second period mint is made) and thus a revert on underflow in this line . This underflow will last until block.timestamp ≥ lastMintDate .

Proof of Concept

The PoC is conducted using foundry, to install foundry follow these installation steps .

to reproduce it :

  • In the root of the repo run forge init --force
  • open new MinterContractTest.t.sol file in test folder
  • copy the test in the MinterContractTest.t.sol file and save .
  • run forge test --mt test_DosPeriodicMint or forge test --mt test_DosPeriodicMint -vvvv for more informations .

the test :

pragma solidity ^0.8.10; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "forge-std/StdError.sol"; import "../smart-contracts/MinterContract.sol"; import "../smart-contracts/NextGenCore.sol"; import "../smart-contracts/NextGenAdmins.sol"; import "../smart-contracts/NFTdelegation.sol"; import "../smart-contracts/RandomizerNXT.sol"; import "../smart-contracts/XRandoms.sol"; import "../smart-contracts/RandomizerRNG.sol"; contract NextGenMinterContractTest is Test { NextGenAdmins public admins; NextGenCore public core; DelegationManagementContract public delegation; NextGenMinterContract public minter; NextGenRandomizerNXT public randomizer; NextGenRandomizerRNG public randomizerRNG; randomPool public poolRandom; address public globalAdmin; address public artist; address public user; function setUp() public { globalAdmin = makeAddr("globalAdmin"); artist = makeAddr("artist"); user = makeAddr("user"); vm.deal(user, 100 ether); vm.startPrank(globalAdmin); admins = new NextGenAdmins(); core = new NextGenCore("Next Gen Core", "NEXTGEN", address(admins)); delegation = new DelegationManagementContract(); minter = new NextGenMinterContract(address(core), address(delegation), address(admins)); core.addMinterContract(address(minter)); vm.stopPrank(); } function test_DosPeriodicMint() public { vm.startPrank(globalAdmin); // create colection string[] memory collectionScript = new string[](1); collectionScript[0] = "desc"; core.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", collectionScript ); // set collection data core.setCollectionData(1, artist, 10, 1000, 0); // add randomizer randomPool randoms = new randomPool(); randomizer = new NextGenRandomizerNXT(address(randoms), address(admins), address(core)); core.addRandomizer(1, address(randomizer)); // set collection costs minter.setCollectionCosts(1, 1 ether, 1 ether, 0, 3600, 3, 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B); // timePeriod for periodic minting is 3600 seconds == 1 hour . // set collection phases uint256 allowlistStartTime = 1699191666; uint256 allowlistEndTime = 1699450866; uint256 publicStartTime = 1699450866; uint256 publicEndTime = 1699710066; minter.setCollectionPhases( 1, allowlistStartTime, allowlistEndTime, publicStartTime, publicEndTime, 0xd0f7ae8bdf5c229875524ca9edf11aa2bdb42dd366ef6d0de3a02dae864736d7 ); vm.stopPrank(); skip(allowlistStartTime - 1); // skip to one second before allowlistStartTime address[] memory recipients = new address[](4); recipients[0] = makeAddr("first"); recipients[1] = makeAddr("second"); recipients[2] = makeAddr("third"); recipients[3] = makeAddr("forth"); string[] memory tokenData = new string[](4); uint256[] memory saltFun = new uint256[](4); uint256[] memory numberOfTokens = new uint256[](4); numberOfTokens[0] = 25; numberOfTokens[1] = 25; numberOfTokens[2] = 25; numberOfTokens[3] = 25; vm.prank(globalAdmin); minter.airDropTokens(recipients, tokenData, saltFun, 1, numberOfTokens); // the airdrop is made just before allowlist mint starts assertEq(core.totalSupply(), 100); // a total of 100 tokens is airdropped . skip(1); // allow list mint starts bytes32[] memory merkleProof = new bytes32[](2); merkleProof[1] = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5; vm.prank(user); minter.mint{value: 1 ether}(1, 1, 100, "", user, merkleProof, address(0), 0); // first mint does not revert because block.timestamp > timeOfLastMint assertEq(core.totalSupply(), 100 + 1); // new token minted succefully . skip(3600); // skip to next period . vm.expectRevert(stdError.arithmeticError); // mint will revert for an underflow . vm.prank(user); minter.mint{value: 1 ether}(1, 1, 100, "", user, merkleProof, address(0), 0); skip(360000); // user will be able to mint only after 363600 seconds == 101 hours == 4.2 days . vm.prank(user); minter.mint{value: 1 ether}(1, 1, 100, "", user, merkleProof, address(0), 0); assertEq(core.totalSupply(), 101 + 1); // new token minted succefully . } }

Notes:

  • 100 tokens will be airdroped .
  • airdrop is made just before allowlist start time .
  • the time period is 3600
  • user will have to wait ( 100 airdroped + 1 minted in first period mint ) 101 * 3600 == 363600 == 101 hours == 4.2 days to mint another token .

Tools Used

Foundry

  • track the minted tokens in periodic in a new mapping and calculate lastMintDate using it so it cannot be affected by an airdrop .
+ // track the number of tokens minted in periodic sale + mapping(uint256 => uint256) public tokenMintedInPeriodicSale; function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... // control mechanism for sale option 3 if (collectionPhases[col].salesOption == 3) { uint timeOfLastMint; if (lastMintDate[col] == 0) { // for public sale set the allowlist the same time as publicsale timeOfLastMint = collectionPhases[col].allowlistStartTime - collectionPhases[col].timePeriod; } else { timeOfLastMint = lastMintDate[col]; } // uint calculates if period has passed in order to allow minting uint tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[col].timePeriod; // users are able to mint after a day passes require(tDiff>=1 && _numberOfTokens == 1, "1 mint/period"); - lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1)); + tokenMintedInPeriodicSale[col] = tokenMintedInPeriodicSale[col] + 1; + lastMintDate[col] = collectionPhases[col].allowlistStartTime + + (collectionPhases[col].timePeriod * (tokenMintedInPeriodicSale[col] - 1)); } } }

Assessed type

Other

#0 - c4-pre-sort

2023-11-18T07:45:07Z

141345 marked the issue as duplicate of #486

#1 - c4-judge

2023-12-01T22:42:10Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-01T22:42:24Z

alex-ppg marked the issue as primary issue

#3 - c4-judge

2023-12-07T12:01:51Z

alex-ppg marked the issue as duplicate of #2012

#4 - c4-judge

2023-12-07T12:25:22Z

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

#5 - c4-judge

2023-12-08T21:07:29Z

alex-ppg marked the issue as partial-50

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