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: 242/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#L58 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105 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#L135 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#L116
As described in the Proof of Concept
section below, a cunning user is able to always win the auction and claim the auctioned token for free.
The file AuctionDemo.sol
contains the functions: participateToAuction
, claimAuction
, cancelBid
and cancelAllBids
:
participateToAuction
function is intended to be used when the auction is OPENclaimAuction
function is intended to be used when the auction is CLOSEDcancelBid
function is intended to be used when the auction is OPENcancelAllBids
function is intended to be used when the auction is OPENHowever, based on the implementations of these functions, exactly at the moment block.timestamp == minter.getAuctionEndTime(_tokenid)
, the auction is both OPEN and CLOSED at the same time. The snippets from these functions allowing an auction to be OPEN and CLOSED at the same time are given below:
participateToAuction
function: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L58C59-L58C112claimAuction
function: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105C17-L105C70cancelBid
function: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L125C17-L125C70cancelAllBids
function: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L135C17-L135C70The root of the issue is that non-strict inequalities are used: >=
and <=
instead of strict inequalities: >
and <
when block.timestamp
and minter.getAuctionEndTime(_tokenid)
are compared.
The fact that at block.timestamp == minter.getAuctionEndTime(_tokenid)
the auction is both OPEN and CLOSED can be exploited and the following scenario is possible:
Alice calls the participateToAuction
function and makes the highest bid up to the moment for the token being auctioned. The transaction becomes part of a block for which block.timestamp == minter.getAuctionEndTime(_tokenid)
is true
.
Alice calls the claimAuction
function in a transaction that is part of a block for which block.timestamp == minter.getAuctionEndTime(_tokenid)
is true
. By calling the claimAuction
function, Alice wins the auction (she has the highest bid) and gets the token. Because the return value of the low-level call
function is not checked in the following 2 places:
there is a possibility that the call wasn't successful, but there is no revert. In case these call
function invocations fail, bidders and the original owner of the token being auctioned will not receive ETH
and some ETH
amount will remain inside the auctionDemo
contract.
cancelBid
function in a transaction that is part of a block for which block.timestamp == minter.getAuctionEndTime(_tokenid)
is true
. As a result of the cancelBid
function invocation, Alice can claim the ETH
amount from her winning bid back (this might happen if the auctionDemo
contract has enough ETH
, which can be possible if there are failing call
function invocations in the claimAuction
function beforehand, which is possible, since the return values of the call
function invocations in claimAuction
are not checked).To recap, following this strategy:
Alice always wins the token auction, because she makes the winning bid at the last possible moment (block.timestamp == minter.getAuctionEndTime(_tokenid)
). Since the auction is both OPEN and CLOSED at that moment, she is able to claim the token being auctioned right after making her winning bid.
Alice takes the ETH
amount from her winning bid back by cancelling her winning bid at the moment block.timestamp == minter.getAuctionEndTime(_tokenid)
, right after claiming the auctioned token.
So, Alice managed to win the auction and get the auctioned token for free.
Manual review.
To mitigate the possibility of a cunning user always winning the auction, we should make sure that in no moment in time is it possible the auction to be both OPEN and CLOSED. This can be achieved by modifying the claimAuction
function in the way shown below:
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}(""); >>>>> require(success, "ETH transfer failed"); <<<<< 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}(""); >>>>> require(success, "ETH transfer failed"); <<<<< emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
The essential change is on the line starting with >>>
and ending with <<<
and the change consists of replacing block.timestamp >= minter.getAuctionEndTime(_tokenid)
with block.timestamp > minter.getAuctionEndTime(_tokenid)
. This way a cunning user cannot make the winning bid and win the auction in the same block. This fix also ensures that the auction winner cannot take his winning bid back, since the cancelBid
function cannot be successfully called after the claimAuction
function, because cancelBid
requires the auction to be OPEN and claimAuction
requires the auction to be CLOSED and the fix ensures there is no moment in time when the auction is both OPEN and CLOSED. So, the cancelBid
function can be called successfully only before the claimAuction
function.
For completeness, I've also added checks for the success of the ETH
transfer in the mitigated code snippet above, although that's something already known from the bot report. The lines of the checks added start with >>>>>
and end with <<<<<
.
Timing
#0 - c4-pre-sort
2023-11-14T10:58:23Z
141345 marked the issue as duplicate of #1904
#1 - c4-pre-sort
2023-11-14T10:58:55Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-11-14T10:59:04Z
141345 marked the issue as duplicate of #1370
#3 - c4-pre-sort
2023-11-14T14:21:10Z
141345 marked the issue as duplicate of #962
#4 - c4-judge
2023-12-01T15:04:13Z
alex-ppg marked the issue as not a duplicate
#5 - c4-judge
2023-12-01T15:04:22Z
alex-ppg marked the issue as duplicate of #1788
#6 - c4-judge
2023-12-08T17:54:42Z
alex-ppg marked the issue as satisfactory