NextGen - ak1'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: 230/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)
partial-50
edited-by-warden
duplicate-1323

External Links

Lines of code

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

Vulnerability details

Impact

Winner / highest bidder can drain the contract funds.

Highest bidder can steal the NFT with minimal loss

Proof of Concept

AuctionDemo would be used to place bid on a particular NFT. whenever a bid is placed, the function participateToAuction is called and bid is placed.

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

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

This function will get the last highest bided value using the function returnHighestBid and checks with current bid amount. The auction should be active (checks the end time).

Once the auction is over, either the winner or admin can call the function claimAuction and claim the NFT. The amount will be sent to owner.

If the bidder wants to cancel the bid, they can call the function cancelBid or cancelAllBids to cancel and the bided amount is sent to them.

The issue here is the checks that is used to validate whether the bid is active or not ( within the auction end time)

Let see the codes.

    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}(""); -------------------------------------->>> refund first time
                emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid);
            } else {}
        }
    }

    // cancel a single 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}(""); -------------------->>> refund second time
        emit CancelBid(msg.sender, _tokenid, index, success, auctionInfoData[_tokenid][index].bid);
    }

Both functions have a common check require(block.timestamp >= minter.getAuctionEndTime(_tokenid). Please note the equal sign.

In the same block , the highest bidder can call the claimAuction and cancelBid. Note the block.timestamp would be same.

First, highest bidder can call the function claimAuction when block.timestamp == minter.getAuctionEndTime(_tokenid). Note, this function , refund the amount to the bidder if the bid amount is not highest bid (refer the else block). This is the place where the bidder will get the amount first time.

Also, the function cancelBid will be called by this highest bidder. when we see the check require(block.timestamp <= minter.getAuctionEndTime(_tokenid) ---->> note the equal sign. So the, check will allow to cancel one time and refund the amount once again.

To steak the NFT with minimal loss, they will call the functions claimAuction and cancelBid sequentially in the same block where block.timestamp would be same when calling these functions.

Tools Used

Manual review.

We suggest to update the auctionInfoData[_tokenid][index].status = false; inside the claim function as shown below.

    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);
                auctionInfoData[_tokenid][index].status = false; ----------------------------->>>++++
            } 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);
                auctionInfoData[_tokenid][index].status = false; ---------------------------------->> ++++
            } else {}
        }
    }

Assessed type

ETH-Transfer

#0 - c4-pre-sort

2023-11-15T07:55:04Z

141345 marked the issue as duplicate of #1172

#1 - c4-judge

2023-12-06T21:28:08Z

alex-ppg marked the issue as duplicate of #1323

#2 - c4-judge

2023-12-08T18:07:21Z

alex-ppg marked the issue as partial-50

#3 - aktech297

2023-12-10T03:31:09Z

Hi @alex-ppg

Thanks for judging the issues very carefully.

We think that we have provided the adequate information similar to the parent issue (#1323)


First, highest bidder can call the function claimAuction when block.timestamp == minter.getAuctionEndTime(_tokenid). Note, this function , refund the amount to the bidder if the bid amount is not highest bid (refer the else block). This is the place where the bidder will get the amount first time.

Also, the function cancelBid will be called by this highest bidder.
when we see the check  require(block.timestamp <= minter.getAuctionEndTime(_tokenid) ---->> note the equal sign.
So the, check will allow to cancel one time and refund the amount once again.

To steak the NFT with minimal loss, they will call the functions claimAuction and cancelBid sequentially in the same block where block.timestamp would be same when calling these functions.

We believe this issue also qualify for full credit.

Kindly check

Thanks!

#4 - alex-ppg

2023-12-10T12:51:59Z

Hey @aktech297, thanks for your feedback request! This submission was awarded partial credit of 50% because the recommended mitigation is incorrect and would still permit the vulnerability.

The root cause of the vulnerability is the equality check. For example, if we applied the mitigation you specified a user could still execute a re-entrancy attack and fully exploit the same amount. As your recommended mitigation does not solve the vulnerability and does not address the root cause, I consider this submission to be of partial credit.

#5 - aktech297

2023-12-10T16:12:00Z

Hi @alex-ppg

Thanks for your response.

The mitigation suggested here is sufficient and no need to worry about the reentrancy. we see that same mitigation is suggested in #1323.

as we suggested, setting the auctionInfoData[_tokenid][index].status = false; in claimAuction is sufficient to prevent this attack.

If they re-enter, following if condition will not allow the transaction and the funds will be safe.

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

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

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

Thanks!

#6 - alex-ppg

2023-12-12T11:48:56Z

Hey @aktech297, thanks for providing a follow-up response. The recommended mitigation sets the status variable to false after the external call, not before it. This means that the code would not conform to the CEI pattern and be vulnerable to a re-entrancy attack as the status would still be true when the call re-enters the cancelBid function. As such, the partial credit will remain.

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