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: 188/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/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L254 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L227-L232
Due to a reentrancy in mint()/_mintProcessing()
a user can mint more items than allowed potentially not giving other users a chance to mint. This can lead to poor UI and frustrated users and unfairly punishing a user who wanted to mint an item.
The minting functionality has a classic reentrancy but it is requires some digging to see. Let's break it down in three places.
In the mint()
function MinterContract.sol if we are able to mint it happens within one of two phases but in either case we are first checking to make sure the caller has not minted their "max" amount already. If we are in "phase1" we use a call like this
require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit"); or require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, _delegator) + _numberOfTokens, "AL limit");
If we are in "phase2" we use a check like this
require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");
So, only if a user has a 'mint allowance' can we ultimately call gencore.mint()
Now this will trigger a call to the mint()
function in the NextGenCore contract where we can see that the updates to a users 'mint allowance' happen AFTER the call to _mintProcessing()
_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; }
This might be fine in some situations but _mintProcessing()
ultimately uses a call to _safeMint()
function _mintProcessing(uint256 _mintIndex, address _recipient, string memory _tokenData, uint256 _collectionID, uint256 _saltfun_o) internal { tokenData[_mintIndex] = _tokenData; collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o); tokenIdsToCollectionIds[_mintIndex] = _collectionID; _safeMint(_recipient, _mintIndex); }
While safeMint()
makes sure the item is transferred to an account that can receive it it does not prevent reentrancy if token is sent to a malicious contract.
A malicious contract can mint their max amounts of items and call back in to the MinterContract on receive and keep minting their 'max' amount of items until they are gone. This is possibly because it all happens BEFORE their 'allowance' is updated.
Essentially a single user can buy more items than allotted or even all of the items. While no money is lost this is certainly a horrible UX for every other potential buyer.
Manual Inspection
Follow CEI pattern function and/or use a reentrancy guard in the mint()
function in NextGenCore.sol. We can swap the code in step 2 like this
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-19T13:45:52Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:02:46Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:32:21Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:33:34Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:15Z
alex-ppg marked the issue as satisfactory
#5 - c4-judge
2023-12-09T00:18:52Z
alex-ppg changed the severity to 3 (High Risk)