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: 143/243
Findings: 2
Award: $2.77
🌟 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
Any auction could be manipulated and disrupted by placing a very low initial bid. Subsequently, the attacker places a really high bid and/or consistently front-run bids, dissuading other bidders from participating. Just before the auction concludes, all but the first attacker's bids are canceled and refunded, enabling the attacker to win the auction with the initially low bid, incurring minimal costs, primarily limited to gas expenses.
The current implementation of cancelAllBids()
and cancelBid()
allows for the cancellation of any bid, even if it happens to be the winning bid:
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); ...
function cancelAllBids(uint256 _tokenid) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); for (uint256 i=0; i<auctionInfoData[_tokenid].length; i++) { if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true) { ...
Modify your fixtures hardhat/scripts/fixturesDeployment.js
to add a 4th addr:
const { ethers } = require("hardhat"); // Setup test environment: const fixturesDeployment = async () => { const signersList = await ethers.getSigners(); const owner = signersList[0]; const addr1 = signersList[1]; const addr2 = signersList[2]; const addr3 = signersList[3]; const addr4 = signersList[4]; const signers = { owner: owner, addr1: addr1, addr2: addr2, addr3: addr3, addr4: addr4, }; ...
Use this modified nextGen.test.js that:
vs code, hardhat
Modify the cancelAllBids()
and cancelBid()
to check that the highest bid is not canceled:
function cancelBid(uint256 _tokenid, uint256 index) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); require(auctionInfoData[_tokenid][index].bid < returnHighestBid(_tokenid), "Can not cancel HighestBid");
function cancelAllBids(uint256 _tokenid) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); uint256 highestBid = returnHighestBid(_tokenid); for (uint256 i=0; i<auctionInfoData[_tokenid].length; i++) { if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true && auctionInfoData[_tokenid][index].bid < highestBid) { auctionInfoData[_tokenid][i].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit CancelBid(msg.sender, _tokenid, i, success, auctionInfoData[_tokenid][i].bid); } else {} } }
Additionally, I would like to suggest incorporating a minBidIncreasePercentage
check to ensure that a new bid is a certain percentage larger than the last high bid. This measure is intended to discourage sniping by 1 wei or front-runners.
DoS
#0 - c4-pre-sort
2023-11-18T11:48:47Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:12:38Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:15:37Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:50:18Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:22:46Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T17:27:58Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:11:12Z
alex-ppg marked the issue as partial-50
#7 - c4-judge
2023-12-09T00:20:29Z
alex-ppg changed the severity to 3 (High Risk)
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0xAsen, 0xDetermination, 0xJuda, 0xWaitress, 0xhunter, 0xlemon, 0xpiken, Al-Qa-qa, Arabadzhiev, CSL, CaeraDenoir, DarkTower, DeFiHackLabs, Greed, Haipls, MaNcHaSsS, NentoR, NoamYakov, PENGUN, Ruhum, Soul22, SovaSlava, Talfao, Toshii, TuringConsulting, VAD37, Vagner, Valix, Viktor_Cortess, ZdravkoHr, audityourcontracts, btk, codynhat, flacko, funkornaut, glcanvas, gumgumzum, immeas, innertia, ke1caM, lanrebayode77, lsaudit, mrudenko, niki, nmirchev8, openwide, oualidpro, r0ck3tz, rvierdiiev, trachev, yojeff
2.7688 USDC - $2.77
The current state of the AuctionDemo handles bids and claiming an auction in a way that allows a griefing party to permanently lock all non-canceled bids using a griefing attack by forcing the claiming transaction out of gas. This attack could be as inexpensive as the attacker desires, given the absence of a minimum bid check in the current implementation of the auction.
The claimAuction()
function iterates through non-canceled bids, refunds bidders, and transfers the auctioned NFT and the winning bid to the owner. However, a malicious contract can disrupt the process when funds are transferred to it.
The current 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 {} } }
The vulnerability arises when a griefing contract gets refunded on a placed bid. This happens because the griefing contract receive()
intentionally runs out of gas when its being refunded using an infinite loop.
To reproduce this, add the next contract and interface at hardhat/smart-contracts/Griefer.sol
and hardhat/smart-contracts/IAuction.sol
pragma solidity ^0.8.2; interface IAuction { /** * @dev Participate in the auction by placing a bid on the specified token. * @param _tokenid The ID of the token in the auction. */ function participateToAuction(uint256 _tokenid) external payable; }
pragma solidity ^0.8.2; import "./IAuction.sol"; contract Griefer { // Function to call participateToAuction and send msg.value function callParticipateToAuction( address _auctionContractAddress, uint256 index ) public payable { IAuction auction = IAuction(_auctionContractAddress); auction.participateToAuction{value: msg.value}(index); } // Fallback function that runs ot of gas receive() external payable { uint i = 0; while (true) { } } }
Modify your fixtures hardhat/scripts/fixturesDeployment.js
to add the griefer contract
const { ethers } = require("hardhat"); // Setup test environment: const fixturesDeployment = async () => { const signersList = await ethers.getSigners(); const owner = signersList[0]; const addr1 = signersList[1]; const addr2 = signersList[2]; const addr3 = signersList[3]; const addr4 = signersList[4]; ... const nextGriefer = await ethers.getContractFactory("Griefer"); const hhGriefer = await nextGriefer.deploy(); const contracts = { hhAdmin: hhAdmin, hhCore: hhCore, hhDelegation: hhDelegation, hhMinter: hhMinter, hhAuction: hhAuction, hhRandomizer: hhRandomizer, hhRandoms: hhRandoms, hhGriefer: hhGriefer, }; const signers = { owner: owner, addr1: addr1, addr2: addr2, addr3: addr3, addr4: addr4, }; ...
Finally you can run the test with the next modified nextGen.test.js, which:
vs code, hardhat
Allowing the retrieval of any non-winning bids at all times and simplifying the claimAuction()
function to handle only the winning bid.
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 cancelAllBids(uint256 _tokenid) public { // require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); for (uint256 i=0; i<auctionInfoData[_tokenid].length; i++) { if (auctionInfoData[_tokenid][i].bidder == msg.sender && auctionInfoData[_tokenid][i].status == true) { auctionInfoData[_tokenid][i].status = false; (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit CancelBid(msg.sender, _tokenid, i, success, auctionInfoData[_tokenid][i].bid); } else {} } }
// claim Token After Auction 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); } } }
It should be noted that this only resolves the issue in the scope of the griefer attack; the claiming functions in the current state still allow another kind of DoS attack, which has been dealt with in a separate issue.
DoS
#0 - c4-pre-sort
2023-11-15T09:12:08Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:42Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:56:25Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:56:39Z
alex-ppg marked the issue as duplicate of #1782
#4 - c4-judge
2023-12-08T21:02:57Z
alex-ppg marked the issue as satisfactory