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
Rank: 27/243
Findings: 1
Award: $557.13
🌟 Selected for report: 0
🚀 Solo Findings: 0
557.1267 USDC - $557.13
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
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
.
The PoC is conducted using foundry, to install foundry follow these installation steps .
to reproduce it :
forge init --force
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:
Foundry
+ // 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)); } } }
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