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: 144/243
Findings: 2
Award: $2.77
🌟 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/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L113 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L125 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105
Reentrancy in claimAuction()
allows a user to obtain an auctioned NFT for free by reentering cancelBid()
while claiming the auction, essentially allowing an attacker to obtain the auctioned NFT, and then immediately obtaining its their amount refunded.
When a user is the highest bidder, the claimAuction()
must be triggered so that the NFT can be claimed, and the bid amount can be transferred to the contract owner:
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) { ... } else {} } }
We can see how the previous block of code allows block.timestamp
to be higher or equal to the minter.getAuctionEndTime(_tokenid)
. If now we analyze the cancelBid()
method, we'll see that block.timestamp
is required to be smaller or equal to minter.getAuctionEndTime(_tokenid)
. This essentially means that cancelBid()
and claimAuction()
can be called in the same block.
An attacker can leverage the fact that such timestamp checks are poorly performed in order to craft a reentrancy attack, where the bid can be cancelled after obtaining the NFT for free, but before actually transferring the bid funds to the contract owner. The following steps illustrate how such attack would take place:
onERC721Received()
transfer hook) executes the claimAuction()
function so that it can obtain the NFT, and such execution is triggered exactly at timestamp minter.getAuctionEndTime(_tokenid)
safeTransferFrom()
transfer is performed, sending the NFT to the malicious highest bidder contract.onERC721Received()
transfer hook will then execute and reenter the cancelBid()
function. Because block.timestamp
is equal to minter.getAuctionEndTime(_tokenid)
, the function will execute gracefully, effectively refunding the highest bidder with its bid ETH amount.cancelBid()
ends, execution will move back to the initial claimAuction()
flow. Execution jumped when transferring the NFT from claiming the auction to cancelling the bid, so now that execution has been returned to claiming the auction, next step is to refund the owner. Because the contract holds ETH from other users and auctions, the owner will correctly obtain their corresponding highestBid amount, and execution will finish gracefully, effectively claiming an auctioned NFT while also obtaining the highest bid funds backManual review
Add a reentrancy guard for claimAuction()
. An effective way to add such checks is by using OpenZeppelin's Reentrancy guard.
Reentrancy
#0 - c4-pre-sort
2023-11-16T14:46:42Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:42:29Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T17:42:27Z
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
2.7688 USDC - $2.77
It is possible to cause a denial of service in the claimAuction()
function by bidding with a contract that fully consumes the gas allocated for the transaction. Because claimAuction()
loops over all bidders to refund bidders who lost or transfer the NFT to the auction winner, this function can be completely DoS'ed:
function claimAuction( uint256 _tokenid ) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { ... 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 {} } }
Suppose a malicious contract bids an amount in an auction. With this amountonly being of 1 wei the attack can be performed.
The auction takes place, with several bidders bidding, and eventually it reaches an end. claimAuction()
should now be executed so that the highest bidder can obtain the auctioned NFT, and all other bidders can get their bid amounts back.
Because claimAuction()
will try to refund all bidders (our malicious contract included), a malicious contract can be crafted with a fallback()
function that tries to consume all gas allocated for the call when receiving the refunded ETH. If this occurs, the claim transaction will ALWAYS
run out of gas due to the fact that our malicious contract will be successfully consuming all the allocated gas.
This will prevent the auction from ever being claimed, effectively preventing loser bidders from such auction from ever obtaining their bidded ETH back, while also making the highest bidder not capable of getting their corresponding NFT for winning the bid.
Manual review
It is recommended to incorporate the "Pull over push" pattern. Essentially, the idea is to make users execute a transaction to obtain their own ETH or NFT back, instead of looping through all bidders to "push" the amounts to them
DoS
#0 - c4-pre-sort
2023-11-16T14:47:16Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:07:01Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:07:08Z
alex-ppg marked the issue as primary issue
#3 - c4-judge
2023-12-04T19:35:00Z
alex-ppg marked issue #734 as primary and marked this issue as a duplicate of 734
#4 - c4-judge
2023-12-08T20:46:23Z
alex-ppg marked the issue as satisfactory