NextGen - 0xMAKEOUTHILL's results

Advanced smart contracts for launching generative art projects on Ethereum.

General Information

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

NextGen

Findings Distribution

Researcher Performance

Rank: 131/243

Findings: 2

Award: $5.49

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 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

Awards

0 USDC - $0.00

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L103-L121 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L123-L144

Vulnerability details

Impact

  1. An auction winner can get the won NFT for free.

  2. A bidder can get his refund twice, stealing funds from users.

Proof of Concept

Let's take a look at the 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 whole idea is that, a winner can claim his won NFT when block.timestamp >= minter.getAuctionEndTime(_tokenid) - mind that it's >=, and he has the the highest bid, so the function has a for loop going through all bids that were present for this tokenID.

Now let's look at the cancelBid function:

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); }

It checks if block.timestamp <= minter.getAuctionEndTime(_tokenid) - mind that it's <=. Then finds the msg.sender's bid and refunds it, also setting his status to false so he can't call it again and get another refund, but that actually does not hold.

As you can see cancelling a bid and claiming a bid can happen in the same block.timestamp, because of the = sign present in both <= & >=.

Scenario on how a bidder can get refunded twice:

  1. It's all about timing, a bidder can time when an auction winner has called claimAuction, and bundle his cancelBid in the same block, but after claimAuction. Getting the refund twice is possible because in claimAuction whenever the winner calls it, it loops through all participants, and if they are not the highest bidder(the winner), they get their bid refunded, but the status is not set to false as you can see in claimAuction's code snippet above, so the bidder can then call cancelBid, and get his refund one more time because this check will pass require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true);.

That way a bidder can steal non-refunded amount of ETH in the contract. Also this attack can be made even easier if an auction winner participates in the bid with a second account so he knows exactly when the claimAuction will be called and again take advantage of other user's refunds.

If there is enough amount of ETH in the contract to cover the auction winner's bid, he can even get the NFT for free.

Tools Used

Manual audit

In claimAuction make the following changes:

--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);

Assessed type

Timing

#0 - c4-pre-sort

2023-11-20T13:18:59Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-01T15:25:15Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-01T15:25:24Z

alex-ppg marked the issue as duplicate of #1788

#3 - c4-judge

2023-12-08T18:05:34Z

alex-ppg marked the issue as satisfactory

Awards

5.4864 USDC - $5.49

Labels

bug
2 (Med Risk)
partial-50
duplicate-175

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57-L60 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120

Vulnerability details

Impact

A bidder can bid for an auction the same time an auction is being finalized(claimed) and depending on which is executed first, it's possible for a bidder to lose his funds.

Proof of Concept

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); }

Participating in an auction is available when msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true.

The important detail here is that: block.timestamp <= minter.getAuctionEndTime(_tokenid) - keep in mind it's <=.

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 {} } }

Claiming an auction can be done when block.timestamp >= minter.getAuctionEndTime(_tokenid) - again keep in mind it's <=, which means participating and claiming an auction can be done in the same block.timestamp which can be a problem depending on which transaction is executed first, because if the claimAuction is executed before participateToAuction and in the same block.timestamp, bidder's bid will be lost because there are no checks to see in participateToAuction if an auction has been claimed, it only checks if they are in the same block.timestamp. So participateToAuction WILL NOT revert and user will lose their bid, since there is no way to refund it.

Tools Used

Manual Audit

In claimAuction make the following changes:

-- 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);

Assessed type

Timing

#0 - c4-pre-sort

2023-11-14T09:53:07Z

141345 marked the issue as duplicate of #1935

#1 - c4-pre-sort

2023-11-14T14:21:44Z

141345 marked the issue as duplicate of #962

#2 - c4-judge

2023-12-02T15:32:23Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2023-12-02T15:32:36Z

alex-ppg marked the issue as primary issue

#4 - c4-judge

2023-12-04T13:03:52Z

alex-ppg marked issue #175 as primary and marked this issue as a duplicate of 175

#5 - c4-judge

2023-12-08T18:47:21Z

alex-ppg marked the issue as satisfactory

#6 - c4-judge

2023-12-08T21:49:07Z

alex-ppg marked the issue as partial-50

#7 - c4-judge

2023-12-08T22:37:25Z

alex-ppg marked the issue as satisfactory

#8 - c4-judge

2023-12-08T22:37:36Z

alex-ppg marked the issue as partial-50

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter