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: 77/243
Findings: 2
Award: $35.61
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
AuctionDemo#claimAuction did not set auctionInfoData.status to false, the auction is still valid after the claim and the user can call cancelBid
to get a 2x refund.
The claimAuction
function finds the user with the highest bid and buys the NFT, and the other users make a refund.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true; uint256 highestBid = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); 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}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); } else if (auctionInfoData[_tokenid][i].status == true) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); //@audit status has not been reset emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
Since auctionInfoData[_tokenid][i].status is not set to false, the user within the same block.timestamp can call cancelBid after claimAuction to get a 2x refund.
cancelBid
verifies whether the Auction is out of date and auctionInfoData.status is true, if claimAuction
and cancelBid
are within the same block.timestamp, block.timestamp == minter.getAuctionEndTime(_tokenid), the time verification will pass, and since auctionInfoData.status is always true, the claimAuction
and cancelBid
functions will both be called successfully, and the user will get 2x For a refund.
function cancelBid(uint256 _tokenid, uint256 index) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); auctionInfoData[_tokenid][index].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
There are 3 ways a malicious user can carry out an attack:
cancelBid
in bidder's fallback, this is not a reentrant attack, it just makes cancelBid
execute in the same block after the claimAuction
execution.claimAuction
and then cancelBid
to double the refund on one or more of his unwinning auctions.claimAuction
function to initiate cancelBid
, which is then executed in the same block after claimAuction
.vscode manual
Set status to false after claimAuction
Other
#0 - c4-pre-sort
2023-11-15T00:44:23Z
141345 marked the issue as primary issue
#1 - c4-sponsor
2023-11-22T15:27:40Z
a2rocket (sponsor) disputed
#2 - c4-pre-sort
2023-11-27T10:40:13Z
141345 marked the issue as sufficient quality report
#3 - c4-judge
2023-12-06T21:29:02Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T18:03:05Z
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
There is a reentrant attack in AuctionDemo#claimAuction, users can auction for tokens at zero cost.
The claimAuction function sends tokens to the winning bidder via IERC721(gencore).safetransferfrom.
bidder can be a contract account,
bidder implements onERC721Received, calls cancelBid
in onERC721Received to get a refund, and bidder can get the token with zero payment.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true; uint256 highestBid = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); 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) { //@audit reentrant ,calling `cancelBid` in the `onERC721Received` callback function 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) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
vscode manual
Add openzeppelin reentrancy guard.
Reentrancy
#0 - c4-pre-sort
2023-11-14T10:54:45Z
141345 marked the issue as duplicate of #1370
#1 - c4-pre-sort
2023-11-14T14:21:05Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-04T21:41:14Z
alex-ppg marked the issue as duplicate of #1323
#3 - c4-judge
2023-12-08T18:02:53Z
alex-ppg marked the issue as partial-25
35.614 USDC - $35.61
block.timestamp = collectionPhases.allowlistStartTime || block.timestamp = collectionPhases.publicEndTime
getPrice
returns the wrong price
When salesOption==2
in the getPrice
function, there is no case where the time is equal to the start/end time.
When block.timestamp is equal to allowlistStartTime
or publicEndTime
, the default price is returned instead of the price over time.
It may be correct to return the default time for allowlistStartTime
, but it is wrong to return the default price for publicEndTime
.
function getPrice(uint256 _collectionId) public view returns (uint256) { .... } else if ( collectionPhases[_collectionId].salesOption == 2 && //@audit missing = block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime )
In NextGenMinterContract#mint the judgment of time is: >=
<=
So when block.timestamp = publicEndTime/allowlistStartTime, the mint parameter is set correctly.
function mint( uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o ) public payable { .... if ( block.timestamp >= collectionPhases[col].allowlistStartTime && block.timestamp <= collectionPhases[col].allowlistEndTime ) { .... } else if ( block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime ) { .... require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH"); for (uint256 i = 0; i < _numberOfTokens; i++) { uint256 mintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); gencore.mint( mintIndex, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase ); } ....
vscode manual
function getPrice(uint256 _collectionId) public view returns (uint256) { .... } else if ( collectionPhases[_collectionId].salesOption == 2 && block.timestamp >= collectionPhases[_collectionId].allowlistStartTime && block.timestamp <= collectionPhases[_collectionId].publicEndTime )
Other
#0 - c4-pre-sort
2023-11-15T12:55:48Z
141345 marked the issue as primary issue
#1 - c4-sponsor
2023-11-23T14:52:52Z
a2rocket (sponsor) confirmed
#2 - c4-pre-sort
2023-11-26T12:24:07Z
141345 marked the issue as sufficient quality report
#3 - c4-judge
2023-12-05T21:23:42Z
alex-ppg marked issue #1275 as primary and marked this issue as a duplicate of 1275
#4 - c4-judge
2023-12-08T21:40:41Z
alex-ppg marked the issue as partial-50