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: 79/243
Findings: 3
Award: $30.73
🌟 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#L104-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L130
A timing vulnerability exists within the auctionDemo
contract, where the claimAuction
and cancelBid
functions can be executed in the same block when block.timestamp == minter.getAuctionEndTime(_tokenid)
. This concurrency flaw could be exploited by a malicious auction winner (highest bidder) to claim the NFT via claimAuction
and simultaneously cancel their bid using cancelBid
using a onERC721Received
hook. This results in the auction winner receiving the NFT for free and to lost funds for other bidders.
A malicious actor could write a smart contract which bids on an auction. When the auction time ends (block.timestamp == minter.getAuctionEndTime(_tokenid)
), the malicious actor calls the bidding smart contract which then calls the claimAuction
function of the auctionDemo contract, which transfers the ownership of the NFT to the smart contract using safeTransferFrom
. This triggers the onERC721Received
function inside the malicious contract which then calls the cancelBid
function of the auctionDemo contract, which returns due to missing checks the bid amount to the bidder (smart contract of the malicious actor). As no success checks for transferring the ETH to the owner (payment for the NFT) or the ETH of lower bids back to bidders is present in the claimAuction
no reverts happen on insufficient funds which leaves the malicious actor getting the NFT for free and the contract owner or other bidders with lost funds.
A step by step explanation:
Step 1; malicious actor makes bids using a smart contract, with an onERC721Received
function that calls cancelBid
if it receives an NFT from the auction.
Step 2; if malicious actor has the highest bid at the end of the auction time, malicious actor calls the bidding smart contract, which in turn calls the claimAuction
function of the auctionDemo contract.
contract Exploit { ... // malicious actor calls this function function exploit() external { // user already checks beforehand if this contract made the highest bid. If not, this call throws an error auctionContract.claimAuction(tokenId); } ... }
Step 3; auctionDemo contract transfers the NFT ownership to the bidding smart contract
// auction winner (address with highest bid) or admins can call this function. function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ // requires current block.timestamp to be greater or EQUAL to auction end time, auction to be not yet claimed and auction status to be true require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); // mark auction as claimed auctionClaim[_tokenid] = true; // get highest bid -> in this case the bid from the contract which called this function uint256 highestBid = returnHighestBid(_tokenid); // get current owner of NFT address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); // get highest bidder -> in this case the contract which called this function address highestBidder = returnHighestBidder(_tokenid); // iterate over all bids for this NFT for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { // if current bid is the highest bid if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { // then transfer the NFT ownership to the highest bidder -> contract by malicious actor in this case // this triggeres the onERC721Received IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); // the rest of the function runs in Step 6 // NFT owner is payed for NFT // note that success of transfer is not required so function does not revert if insufficient funds are available. (bool success, ) = payable(owner()).call{value: highestBid}(""); // emit ClaimAuction event emit ClaimAuction(owner(), _tokenid, success, highestBid); // else if bid not the highest bid and not canceled } else if (auctionInfoData[_tokenid][i].status == true) { // return funds to bidder // note that success of transfer is not required so function does not revert if insufficient funds are available. (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); // emit Refund event emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
Step 4; onERC721Received
of the bidding smart contract is called which in turn calls cancelBid
of the auctionDemo contract (still in the same time block which is equal to auctionTimeEnd).
contract Exploit { ... // ERC721 standard function which is called after a successful transfer. function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) { // During the same transaction, cancelBid is called, which refunds the bid amount auctionContract.cancelBid(tokenId, bidIndex); // bidIndex to be determined as per auction logic return this.onERC721Received.selector; } ... }
Step 5; auctionDemo contract cancels bid of smart contract and sends the bid amount back to the bidding smart contract.
// no restrictions on who can call this function function cancelBid(uint256 _tokenid, uint256 index) public { // requires current block.timestamp to be smaller or EQUAL to auction end time require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); // require caller to be the owner of bid at provided index and bid to not yet be canceled require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); // mark bid as canceled auctionInfoData[_tokenid][index].status = false; // transfers the bid amount from this contract to the caller // note that success of transfer is not required so function does not revert if insufficient funds are available. (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); // emit CancelBid event emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
Step 6; auctionDemo completes the claimAuction
function call and pays the NFT seller (highest bid) and transfers the unsuccessful bids back to the bidders. If the auctionDemo contract does not have sufficient funds to return all funds no revert happens because no successful transfer is for function success required. (see code in Step3)
Step 7; malicious actor transfers funds and NFT to himself.
Auction winner (highest bidder) getting the NFT for free and the bidders from other auctions pay the price for it.
Manual Review
Make cancelBid
and claimAuction
not be callable at the same time by changing the required block.timestamp to > and < instead of ≥ and ≤.
Timing
#0 - c4-pre-sort
2023-11-15T07:20:31Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-04T21:41:44Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T17:59:13Z
alex-ppg marked the issue as satisfactory
#3 - alex-ppg
2023-12-08T18:00:40Z
This is the same submission as #1240 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/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L130
A timing vulnerability exists within the auctionDemo
contract, where the claimAuction
and cancelBid
functions can be executed in the same block when block.timestamp
is equal to minter.getAuctionEndTime(_tokenid)
. This concurrency flaw could be exploited by a malicious auction winner (highest bidder) with another smaller non-winning bid or any other bidder when winner or admin calls the claimAuction
at block.timestamp
equal to minter.getAuctionEndTime(_tokenid)
by invoking cancelBid
in the same transaction using a onReceive
function. This results in the bidder receiving twice the amount of their bid back.
A malicious actor could write a smart contract which bids on auctions and has a receive function which calls the cancelBid
function on receiving funds. For this to work, the malicious actor needs a bid which is not the winning bid (one can have multiple bids). When the claimAuction
function is invoked at block.timestamp
equal to minter.getAuctionEndTime(_tokenid)
by the auction winner (which can be the malicious actor with another bid) or an admin, the funds of non-winning bids are returned to the bidding contract. The receive function of the malicious contract then calls the cancelBid
in turn and receives the funds the second time.
A step by step explanation:
Step 1; malicious actor makes bids of which at least one is not the winning bid using a smart contract with a receive function that calls the cancelBid
function on receiving funds from an auction.
Step 2; claimAuction function of the auctionDemo contract is called at block.timestamp == auctionEndTime.
// auction winner (address with highest bid) or admins can call this function. function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ // requires current block.timestamp to be greater or EQUAL to auction end time, auction to be not yet claimed and auction status to be true require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); // mark auction as claimed auctionClaim[_tokenid] = true; // get highest bid uint256 highestBid = returnHighestBid(_tokenid); // get current owner of NFT address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); // get highest bidder address highestBidder = returnHighestBidder(_tokenid); // iterate over all bids for this NFT for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { // if current bid is the highest bid if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { // then transfer the NFT ownership to the highest bidder IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); // NFT owner is payed for NFT // note that success of transfer is not required so function does not revert if insufficient funds are available. (bool success, ) = payable(owner()).call{value: highestBid}(""); // emit ClaimAuction event emit ClaimAuction(owner(), _tokenid, success, highestBid); // else if bid not the highest bid and not canceled } else if (auctionInfoData[_tokenid][i].status == true) { // return funds to bidder // note that success of transfer is not required so function does not revert if insufficient funds are available. // if current bid is the bid of the bidding smart contract of our malicious actor this triggeres an onReceive function of the smart contract (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); // emit Refund event emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
Step 4; The onReceive
function of the bidding smart contract of the malicious actor is triggered, which in turn calls cancelBid
of the auctionDemo contract
contract Exploit { ... // Fallback function which is called when the contract receives Ether fallback() external payable { // Call the cancelBid function on the auction contract // MISSING: check that this is only called on the first return of funds and not the second auctionContract.cancelBid(tokenid, bidIndex); } ... }
Step 5; auctionDemo contract cancels bid of smart contract and sends the bid amount back to the bidding smart contract.
// no restrictions on who can call this function function cancelBid(uint256 _tokenid, uint256 index) public { // requires current block.timestamp to be smaller or EQUAL to auction end time require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); // require caller to be the owner of bid at provided index and bid to not yet be canceled require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true); // mark bid as canceled auctionInfoData[_tokenid][index].status = false; // transfers the bid amount from this contract to the caller // note that success of transfer is not required so function does not revert if insufficient funds are available. (bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}(""); // emit CancelBid event emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid); }
Step 6; auctionDemo completes the claimAuction
function call and pays the NFT seller (highest bid) and transfers the unsuccessful bids back to the rest bidders. If the auctionDemo contract does not have sufficient funds to return all funds, no revert happens because no successful transfer is for function success required. (see code in Step 2)
Step 7; malicious actor transfers funds of smart contract to himself.
Bidder left with double the funds put into the auction and lost funds for the auction holder and/or other bidders. As bidder can have multiple bids, he could drain all the funds of the auctionDemo contract
except from the funds of this auction, as this would lead to a revert.
Manual Review
Make cancelBid
and claimAuction
not be callable at the same time by changing the required block.timestamp to > and < instead of ≥ and ≤.
Timing
#0 - c4-pre-sort
2023-11-15T07:19:55Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-01T15:12:38Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T15:13:12Z
alex-ppg marked the issue as duplicate of #962
#3 - c4-judge
2023-12-04T21:41:46Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:58:57Z
alex-ppg marked the issue as satisfactory
🌟 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#L124-L130 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57-L61 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120
A malicious actor can manipulate an auction by creating a huge bid at the very beginning of an auction making it impossible for others to bid in the auction, as the bid function only allows bidding more than the current highest bid. At the end of the auction (block.timestamp == minter.getAuctionEndTime(_tokenid)
) the malicious actor cancels their high bid, makes a small bid of eg 1 wei and claims auction in the same transaction. This results in the NFT being bought for 1 wei even if its worth is much greater.
Right at the start of an auction, the malicious actor makes such a high bid that makes it unattractive or unfeasible for others to bid in the auction. At the end of the auction (block.timestamp == minter.getAuctionEndTime(_tokenid)
) the malicious actor cancels their bid, makes a bid of e.g. 1 wei and claims the auction in the same transaction which is possible because of their end time check (respectively being greater or equal to and smaller or equal to).
Step 1; Malicious actor makes a really high bid and as the participateToAuction function only allows bids which are greater than the current bid, prevent other users from participating in the auction:
function participateToAuction(uint256 _tokenid) public payable { require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid); }
Step 2; At block.timestamp == minter.getAuctionEndTime(_tokenid)
the malicious actor calls the cancelBid
function removing the unfeasible high bid, makes a new smaller bid of e.g. 1 wei and claims the auction by calling claimAuction
:
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); } 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 {} } }
Makes it possible for malicious actors to buy NFTs for 1 wei and prevent other users from participating in the auction.
Manual Review
Making bids even possible if they are smaller than the current highest bid.
Invalid Validation
#0 - c4-pre-sort
2023-11-15T07:19:36Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:12:24Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:14:57Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:50:49Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:17:46Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T17:27:55Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T17:58:48Z
alex-ppg marked the issue as partial-50
🌟 Selected for report: bird-flu
Also found by: 00decree, 0xAadi, AS, Audinarey, DeFiHackLabs, Eigenvectors, Fitro, Hama, Kaysoft, Krace, REKCAH, SovaSlava, The_Kakers, Viktor_Cortess, cartlex_, degensec, devival, evmboi32, funkornaut, jacopod, openwide, peanuts, rotcivegaf, smiling_heretic, xAriextz, xiao
25.2356 USDC - $25.24
The recipient of the highest bid in an auction should be the owner of the token, as stated out in the main invariants in the C4 description of the protocol. Instead, the funds are sent to the owner of the auctionDemo contract.
One of the main invariants in the C4 description mentions that the recipient of the funds from the highest bidder in an auction should be the owner of the token: The highest bidder will receive the token after an auction finishes, the owner of the token will receive the funds and all other participants will get refunded. This main invariant is broken as the funds are sent to the wrong address. Here, we can see that the funds are instead sent to the owner of the auctionDemo contract by calling the owner() function:
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) { ... } else {} } }
The wrong address will receive the auction funds and a main invariant is broken.
Manual Review
Change the recipient from owner() to ownerOfToken.
Token-Transfer
#0 - c4-pre-sort
2023-11-19T13:48:11Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:26:22Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2023-12-09T00:22:20Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 Selected for report: HChang26
Also found by: 0x3b, 0xMAKEOUTHILL, 0xSwahili, 0xarno, ABA, DeFiHackLabs, Eigenvectors, Haipls, Kow, MrPotatoMagic, Neon2835, Nyx, Zac, alexfilippov314, ayden, c3phas, immeas, innertia, lsaudit, merlin, mojito_auditor, oakcobalt, ohm, oualidpro, peanuts, phoenixV110, sces60107, t0x1c, tnquanghuy0512, ubl4nk, volodya, xAriextz
5.4864 USDC - $5.49
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57-L61 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L124-L143
A new highest bid can be made after the auction has been claimed if the claim happens first, but both actions happen in the same block (block.timestamp == minter.getAuctionEndTime(_tokenid)
). This results in the newest bid being locked inside the auctionDemo
contract as after auction end time the only way to receive funds back is by calling claimAuction
which can not be called anymore, as it was already claimed.
The auctionDemo
contract is susceptible to a timing loophole, where bids can be placed at the same block timestamp when the auction ends and is claimed. The claimAuction
function does not lock out subsequent bids within the same block, leading to a scenario where a bid submitted after claimAuction
is accepted and locked in the contract with no means of retrieval post-auction claim.
Timeline for this would be (all at the same block.timestamp
):
claimAuction
.// function can be called by auction winner or admin function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ // require block.timestamp >= auction end time and auction not yet claimed -> in this case block.timestamp == auction end time require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); // mark auction as claimed auctionClaim[_tokenid] = true; // return all non-winning bids, transfers ownership of NFT to auction winner and pays previous NFT owner the winning bid ... }
// callable by everyone function participateToAuction(uint256 _tokenid) public payable { // require new bid to be bigger then previous bid and block.timestamp to be <= auction end time and auction status to be true in minter contract (not set to false by claiming auction) -> on this case block.timestamp == auction end time require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); // create new bid struct auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); // store new bid auctionInfoData[_tokenid].push(newBid); }
Now the funds of this bid are locked forever as claimAuction
can not be called anymore without error as auctionClaim[_tokenid] == true
and the two other functions to cancel bids (cancelBid
and cancelAllBids
) can only be called when block.timestamp
is smaller equal auction end time.
Loss of funds for newest highest bidder when bidding at exactly the end of the auction, but auction was already claimed. Also, a main invariant given by the protocol in the C4 description is broken, as not all participants will receive the refund. The main invariant: The highest bidder will receive the token after an auction finishes, the owner of the token will receive the funds and all other participants will get refunded.
Manual Review
Making it impossible to make bids when auction was already claimed.
Invalid Validation
#0 - c4-pre-sort
2023-11-15T07:19:03Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:33:14Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:35:14Z
alex-ppg marked the issue as duplicate of #1926
#3 - c4-judge
2023-12-08T18:49:57Z
alex-ppg marked the issue as partial-50