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: 180/243
Findings: 1
Award: $0.15
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: btk
Also found by: 00xSEV, 0x175, 0x180db, 0x3b, 0xAlix2, 0xJuda, 0xpiken, 0xraion, 3th, 836541, Al-Qa-qa, AvantGard, Aymen0909, Beosin, ChrisTina, DarkTower, DeFiHackLabs, EricWWFCP, Kose, Kow, KupiaSec, MrPotatoMagic, Neo_Granicen, PENGUN, PetarTolev, Ruhum, Soul22, SovaSlava, SpicyMeatball, Talfao, The_Kakers, Toshii, Tricko, VAD37, Viktor_Cortess, ZdravkoHr, _eperezok, alexxander, audityourcontracts, ayden, bird-flu, bronze_pickaxe, codynhat, critical-or-high, danielles0xG, degensec, droptpackets, evmboi32, fibonacci, flacko, gumgumzum, ilchovski, immeas, innertia, jacopod, joesan, ke1caM, kk_krish, mojito_auditor, nuthan2x, phoenixV110, pontifex, r0ck3tz, sces60107, seeques, sl1, smiling_heretic, stackachu, t0x1c, trachev, turvy_fuzz, ubl4nk, ustas, xAriextz, xuwinnie, y4y
0.152 USDC - $0.15
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L196-L255 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/NextGenCore.sol#L189-L200 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/NextGenCore.sol#L227-L232
When the MinterContract.mint
function is called it mints new tokens to the user by calling _safeMint
(in NextGenCore._mintProcessing
) before updating the amount of tokens minted per address associated with the caller, thus a malicious actor can use the ERC721OnReceived
callback to reenter into the mint
function to mint multiple tokens and bypass the limit per address.
The mint
function does check if the caller will remain below the minting limit per address after either an allowList or public mint by performing the following checks :
require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit");
require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");
Then assuming we are below the max token index of the collection, the function calls gencore.mint
(function from NextGenCore
) multiple time in the for loop to mint all the tokens to the user:
for (uint256 i = 0; i < _numberOfTokens; i++) { uint256 mintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); gencore.mint( mintIndex, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase ); }
The NextGenCore.mint
function shown below :
function mint( uint256 mintIndex, address _mintingAddress, address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase ) external { require( msg.sender == minterContract, "Caller is not the Minter Contract" ); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID] .collectionCirculationSupply + 1; if ( collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply ) { _mintProcessing( mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o ); //@audit mints new token before updating the tokens minted amount if (phase == 1) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][ _mintingAddress ] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } } }
As it can be seen the function after incrementing the circulating supply proceed to mint the token by calling _mintProcessing
which under the hood is invoking the _safeMint
function, and only after that the amount of token minted per address is increased for the caller.
So this introduces a reentrancy risk where the caller could be a contract that uses the ERC721OnReceived
callback to reenter the MinterContract.mint
function multiple times to mint new tokens bypassing the minting limit checks mentioned above as its tokensMintedPerAddress amount (either allowList or Public) is only incremented at the end of each call and thus will not affect the overall transaction.
To illustrate this attack let's take the following scenario:
We assume that Bob is using a contract that implements the ERC721OnReceived
callback and that he wants to use it to mint tokens from the collection named colA
.
We also assume that we are in the public minting period and we have the following :
gencore.viewMaxAllowance(colA) = 10
tokensMintedPerAddress[colA][Bob] = 4
Bob calls the MinterContract.mint
function to mint 6 tokens, this will allow him to pass the minting limit check as :
tokensMintedPerAddress[colA][Bob] + 6 = 10 <= gencore.viewMaxAllowance(colA)
Then when the gencore.mint
will eventually call _mintProcessing
which under the hood invokes _safeMint
, the ERC721OnReceived
callback will be triggered and in its logic it will call MinterContract.mint
function again to mint another token, because tokensMintedPerAddress[colA][Bob]
was not yet updated it's still equal to 4 and thus the the minting limit check will not revert.
A new token will be minted to Bob, and let's suppose that he wants to reenter a single time (using a counter in its contract to only execute the reentrancy once) thus his tokensMintedPerAddress[colA][Bob]
will now be incremented by 1 resulting in tokensMintedPerAddress[colA][Bob] = 5
.
Now that the reentrancy call is over the original call can resume minting the first 6 tokens requested (we are still in the for loop so the minting limit check will not be verified again), and incrementing each time the value of tokensMintedPerAddress[colA][Bob]
by 1 resulting in :
tokensMintedPerAddress[colA][Bob] + 6 = 5 + 6 = 11
As the minting limit limit was checked the first time and not updated, we see that Bob was able to mint more tokens and is now above the minting limit set for the collection.
It should also be noted that in this scenario it was supposed that Bob wanted only to reenter once (using a specific logic in the ERC721OnReceived
callback) but this can be done multiple times even minting the entire collection, provided Bob can cover the total minting cost.
Even though the given scenario is illustrated in public period, the same attack can be conducted during the allowList period as both use the same logic.
Important: this attack is not possible for the collections with a periodic sale model salesOption == 3
as in that case only one token can be minted per period and thus mint call will revert in case of minting multiple tokens, but it is feasible for all other sale models
Manual review
To avoid these attack i recommend to update the amount of token minted per address before calling _mintProcessing
which ensure that the limit checks will not allow reentrancy.
The NextGenCore.mint
function can be modified as follows :
function mint( uint256 mintIndex, address _mintingAddress, address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase ) external { require( msg.sender == minterContract, "Caller is not the Minter Contract" ); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID] .collectionCirculationSupply + 1; if ( collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply ) { //@audit update amount of tokens minted before calling _safeMint 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 ); } }
Reentrancy
#0 - c4-pre-sort
2023-11-20T06:12:41Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:04:03Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:19:14Z
alex-ppg marked the issue as partial-50
#3 - c4-judge
2023-12-08T16:19:23Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-08T16:44:37Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T19:17:03Z
alex-ppg marked the issue as satisfactory