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: 239/243
Findings: 1
Award: $0.00
🌟 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
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L116 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L116
The vulnerability arises from a reentrancy issue in the interaction between the claimAuction
and cancelAllBids
functions. The critical flaw lies in allowing a participant, who has become the highest bidder and won the auction, to initiate a reentrancy attack during the block where block.timestamp == minter.getAuctionEndTime(_tokenid)
.
claimAuction
function is designed to transfer the token to the winner and refund the other participants. Simultaneously.
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}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
notice that it's doesn't set auctionInfoData[_tokenid][i].status
to false
after sending bids to the bidders.
cancelAllBids
function allows a bidder to cancel all of his bids it send eth to the biddes and set auctionInfoData[_tokenid][i].status
to false
for each bid get cancled. and it also can be called when block.timestamp == minter.getAuctionEndTime(_tokenid)
function cancelAllBids(uint256 _tokenid) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); for (uint256 i = 0; i < auctionInfoData[_tokenid].length; i++) { >> if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true) { auctionInfoData[_tokenid][i].status = false; (bool success,) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit CancelBid(msg.sender, _tokenid, i, success, auctionInfoData[_tokenid][i].bid); } else {} } }
notice that for order to cancle bids the status should be true.
claimAuction
when block.timestamp == minter.getAuctionEndTime(_tokenid)
, and he have other bids beside the higher bid. he can reenter the contract when the contract send him eth . and call cancleAllBids
which will refund him again.block.timestamp == minter.getAuctionEndTime(_tokenid)
and in the same block will do the following :
claimAuction
function .eth
. and when the contracts reach the last bidder (which always will be the higher bid) it send the amont of bid to the owner of token
, and send the token
to this higher bidder
.cancelAllBids
.auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true
So the attacker will get refunded two times for his 10 bids , and 1 time highest bid.block.timestamp == minter.getAuctionEndTime(_tokenid)
, he will just revert the Tx , and he still get refunded his bids when the winner or the admin calls claimAuction
.vs code
claimAuction
make this change :function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { require( // set change this to > instead of >= : block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true ); ...... ...... }
Reentrancy
#0 - c4-pre-sort
2023-11-20T14:25:18Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:40Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:12:22Z
alex-ppg marked the issue as partial-50
#3 - alex-ppg
2023-12-08T18:12:54Z
This is the same submission as #862 by the same Warden, perhaps either should be removed/nullified.
🌟 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#L116 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L116
The vulnerability arises from a reentrancy issue in the interaction between the claimAuction
and cancelAllBids
functions. The critical flaw lies in allowing a participant, who has become the highest bidder and won the auction, to initiate a reentrancy attack during the block where block.timestamp == minter.getAuctionEndTime(_tokenid)
.
claimAuction
function is designed to transfer the token to the winner and refund the other participants. Simultaneously.
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}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
notice that it's doesn't set auctionInfoData[_tokenid][i].status
to false
after sending bids to the bidders.
cancelAllBids
function allows a bidder to cancel all of his bids it send eth to the biddes and set auctionInfoData[_tokenid][i].status
to false
for each bid get cancled. and it also can be called when block.timestamp == minter.getAuctionEndTime(_tokenid)
function cancelAllBids(uint256 _tokenid) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); for (uint256 i = 0; i < auctionInfoData[_tokenid].length; i++) { >> if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true) { auctionInfoData[_tokenid][i].status = false; (bool success,) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit CancelBid(msg.sender, _tokenid, i, success, auctionInfoData[_tokenid][i].bid); } else {} } }
notice that for order to cancle bids the status should be true.
claimAuction
when block.timestamp == minter.getAuctionEndTime(_tokenid)
, and he have other bids beside the higher bid. he can reenter the contract when the contract send him eth . and call cancleAllBids
which will refund him again.block.timestamp == minter.getAuctionEndTime(_tokenid)
and in the same block will do the following :
claimAuction
function .eth
. and when the contracts reach the last bidder (which always will be the higher bid) it send the amont of bid to the owner of token
, and send the token
to this higher bidder
.cancelAllBids
.auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true
So the attacker will get refunded two times for his 10 bids , and 1 time highest bid.block.timestamp == minter.getAuctionEndTime(_tokenid)
, he will just revert the Tx , and he still get refunded his bids when the winner or the admin calls claimAuction
.vs code
claimAuction
make this change :function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { require( // set change this to > instead of >= : block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true ); ...... ...... }
Reentrancy
#0 - c4-pre-sort
2023-11-20T14:25:36Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:42Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:11:58Z
alex-ppg marked the issue as partial-50