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: 228/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/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105-L106 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L126
In the claimAuction and cancelBid Function, their is a single opportunity that at the right timing a malicious winner that won the auction, can prepare both the two function transactions, before the auction end time, then send the transactions to the mempool, which will get exactly mined at the right time the windows open with this kind of vulnerability, but with the claimAuction executing their functions, first and then the cancelBid following. If all this process happens, then the user can both claim The NFT and also get paid in a refund for the same NFT they just claimed. A user could potentially execute claimAuction right after the auction ends (thus transferring the NFT to themselves and sending the highest bid to the owner) and then execute cancelBid (thus refunding their highest bid amount) if both transactions are mined in the same block
Let's Explore how this Vulnerability can happen and what it will take to actually make it happen
Analysis of claimAuction Function
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 {} } }
Checks: It requires that the auction has ended, the auction hasn't been previously claimed, and the auction is still active require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true)
.
Claim Process: If the conditions are met, it marks the auction as claimed, transfers the NFT to the highest bidder, and sends the highest bid amount to the owner of the contract.
Analysis of cancelBid Function
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); }
Checks The function requires that the auction hasn't ended yet block.timestamp <= minter.getAuctionEndTime(_tokenid)
and that the sender is the bidder of the specific bid they are trying to cancel, and the bid is active.
Cancellation Process: If the conditions are met, it sets the bid status to false (canceled) and refunds the bid amount to the bidder.
Potential Exploit Scenario Timing: executing claimAuction just after the auction ends and then immediately executing cancelBid while still within the same block, assuming the timestamp hasn't updated yet.
Outcome: If both transactions are included in the same block and if cancelBid doesn't check whether the auction has already been claimed, the winner might be able to claim the NFT and then cancel their bid to get their money back. Executing two transactions in the same block with precise timing is challenging but not impossible. It would require a good understanding of Ethereum transaction mechanics and possibly some luck.
The main Cause of this kind of Vulnerability, is the window of opening of the required checks in which there is a chance that if both are called at the same time the user might get away with this exploit.
When a NFT Auction winner tries to call the Claim Auction function and gets their NFT Transferred to them, their bid struct status is not set to false as seen down below
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);
This Makes this kind of attack a possibility as the cancelBid
function checks to see if the auction bid struct status is still true, as shown below
require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true);
This kind of issue creates a Window For the Malicious user to execute. All things considered, the fact that a huge profit can be gained by this kind of exploit mainly based on Expensive NFTs and the simple fix that it will take to mitigate the risk, makes it a high-priority fix for the protocol.
Manual Review
The Mitigation for this is simple as i have pointed out earlier in my report, their is actually no reason for the check in the claimAuction function to have a =
sign when checking if the time allotted to the auction has ended, it can simply have a simple >
check, Then this issue is completely fixed.
require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true;
There is another fix for this kind of vulnerability, if you add a check to the cancel bid to check if the auction has been claimed before, then it should revert. But I think that the first one is simply the best way to resolve this issue.
MEV
#0 - c4-pre-sort
2023-11-14T10:10:44Z
141345 marked the issue as duplicate of #1904
#1 - c4-pre-sort
2023-11-14T23:31:50Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-01T14:35:46Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T14:35:58Z
alex-ppg marked the issue as duplicate of #1788
#4 - c4-judge
2023-12-08T17:41:54Z
alex-ppg marked the issue as satisfactory