NextGen - inzinko'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: 228/243

Findings: 1

Award: $0.00

🌟 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
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105-L106 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L126

Vulnerability details

Impact

In the claimAuction and cancelBid Function, their is a single opportunity that at the right timing a malicious winner that won the auction, can prepare both the two function transactions, before the auction end time, then send the transactions to the mempool, which will get exactly mined at the right time the windows open with this kind of vulnerability, but with the claimAuction executing their functions, first and then the cancelBid following. If all this process happens, then the user can both claim The NFT and also get paid in a refund for the same NFT they just claimed. A user could potentially execute claimAuction right after the auction ends (thus transferring the NFT to themselves and sending the highest bid to the owner) and then execute cancelBid (thus refunding their highest bid amount) if both transactions are mined in the same block

Proof of Concept

Let's Explore how this Vulnerability can happen and what it will take to actually make it happen

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

Checks: It requires that the auction has ended, the auction hasn't been previously claimed, and the auction is still active require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true). Claim Process: If the conditions are met, it marks the auction as claimed, transfers the NFT to the highest bidder, and sends the highest bid amount to the owner of the contract.

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

Checks The function requires that the auction hasn't ended yet block.timestamp <= minter.getAuctionEndTime(_tokenid) and that the sender is the bidder of the specific bid they are trying to cancel, and the bid is active. Cancellation Process: If the conditions are met, it sets the bid status to false (canceled) and refunds the bid amount to the bidder.

Potential Exploit Scenario Timing: executing claimAuction just after the auction ends and then immediately executing cancelBid while still within the same block, assuming the timestamp hasn't updated yet.

Outcome: If both transactions are included in the same block and if cancelBid doesn't check whether the auction has already been claimed, the winner might be able to claim the NFT and then cancel their bid to get their money back. Executing two transactions in the same block with precise timing is challenging but not impossible. It would require a good understanding of Ethereum transaction mechanics and possibly some luck.

The main Cause of this kind of Vulnerability, is the window of opening of the required checks in which there is a chance that if both are called at the same time the user might get away with this exploit.

When a NFT Auction winner tries to call the Claim Auction function and gets their NFT Transferred to them, their bid struct status is not set to false as seen down below

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

This Makes this kind of attack a possibility as the cancelBid function checks to see if the auction bid struct status is still true, as shown below

require(auctionInfoData[_tokenid][index].bidder == msg.sender && auctionInfoData[_tokenid][index].status == true);

This kind of issue creates a Window For the Malicious user to execute. All things considered, the fact that a huge profit can be gained by this kind of exploit mainly based on Expensive NFTs and the simple fix that it will take to mitigate the risk, makes it a high-priority fix for the protocol.

Tools Used

Manual Review

The Mitigation for this is simple as i have pointed out earlier in my report, their is actually no reason for the check in the claimAuction function to have a = sign when checking if the time allotted to the auction has ended, it can simply have a simple > check, Then this issue is completely fixed.

require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
        auctionClaim[_tokenid] = true;

There is another fix for this kind of vulnerability, if you add a check to the cancel bid to check if the auction has been claimed before, then it should revert. But I think that the first one is simply the best way to resolve this issue.

Assessed type

MEV

#0 - c4-pre-sort

2023-11-14T10:10:44Z

141345 marked the issue as duplicate of #1904

#1 - c4-pre-sort

2023-11-14T23:31:50Z

141345 marked the issue as duplicate of #962

#2 - c4-judge

2023-12-01T14:35:46Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2023-12-01T14:35:58Z

alex-ppg marked the issue as duplicate of #1788

#4 - c4-judge

2023-12-08T17:41:54Z

alex-ppg marked the issue as satisfactory

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