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: 218/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
function claimAuction( uint256 _tokenid ) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { require( block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true );
function cancelBid(uint256 _tokenid, uint256 index) public { require( block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended" );
At block.timestamp == minter.getAuctionEndTime(_tokenid)
, it is possible to call both of these functions. This oversight creates an opportunity for a severe theft of funds.
claimAuction
. claimAuction
refunds his first bid, which triggers Adversary's fallback, which calls cancelBid
, which successfully refunds Adversary's bid again. The same happens for each of adversary's bids, except the winning one.Example of Adversary's fallback:
uint public index; bool cancel = true; fallback() external payable { if (index < 9) { if (cancel) { cancel = false; auctionDemo(msg.sender).cancelBid(tokenId, index++); } else { cancel = true; } } else { owner.call{value: address(this).balance}(""); } }
Full Foundry PoC (works with and without require(success)
)
Depending on whether the mitigation for the M-01 finding from the bot race is implemented, there are two ways of how the Step 4 will play out:
require(success)
is not implemented:
cancelBid
for the respective bid.require(success)
is implemented:
require(success)
after each call will make sure that every ether transfer has to be successful, or the whole transaction reverts. This means that the owner and all other bidders must get their ether back. If there is only one auction, and the Adversary tries to execute the original attack, the txn will revert, as at least one of the refunds will fail (due to insufficient balance of AuctionDemo).
Thus, the attack will not work unless there are other funds in the contract, but there usually will be, because the AuctionDemo contract may host multiple auctions at once.
To sum up:
if require(success)
is used, the Adversary will need at least one other auction going in parallel for the attack to be successful;
if require(success)
is not used, the attack won't have such requirement and will work out without other auctions.
The Adversary can also decide if he wants to pay for the NFT, or just get his winning bid back. Similarly to the previous strategy, he may add a call to his fallback, that would cancel his winning bid. This will essentially make the owner keep the NFT, and refund every bidder of the auction.
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);
If the winning bid is cancelled, the condition will always be false, as the highest bid will have status == false
The contract also allows to make bids at t == minter.getAuctionEndTime(_tokenid)
. So the doubling strategy can be upgraded to draining all other auctions' funds.
Scenario:
Steps 3-6 are repeated, each time with a bigger bid.
However, this particular scenario becomes impossible if the array length is cached (G-20 from the bot report).
Foundry
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ - require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
Timing
#0 - c4-pre-sort
2023-11-15T08:54:20Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:40:28Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:16:20Z
alex-ppg marked the issue as satisfactory