NextGen - rahul'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: 49/243

Findings: 1

Award: $141.88

Gas:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Awards

141.8832 USDC - $141.88

Labels

bug
G (Gas Optimization)
grade-a
selected for report
sufficient quality report
edited-by-warden
G-16

External Links

Summary

OptimizationGas Saved
[G-1] Derive min / max collection bounds dynamically54,522
[G-2] Remove redundant flag for creation of collection34,044
[G-3] Remove duplicate reference to core contract28,517
[G-4] Remove redundant flag for setting up collection26,581
[G-5] Remove duplicate reference to randomizer contract22,761
[G-6] Remove redundant external call during airdrop3066
[G-7] Optimise creation of collection976

Findings

[G-1] 54,522 gas can be saved by deriving min / max collection bounds dynamically

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenCore:setCollectionData()19909015438944701Yes
NextGenCore:burn()8427083948322Yes
NextGenCore:setFinalSupply()54949495905359Yes
NextGenCore:mint()5293825252424140Yes

Token min / max index define the valid range of tokens within a collection. There is no need to store them in state variables as these can be derived using collectionID and collectionTotalSupply and their values can be read using the getter functions wherever required.

Recommendation

STEP 1: Update the getter functions

    function viewTokensIndexMin(...) {
-       return(collectionAdditionalData[_collectionID].reservedMinTokensIndex);
+       return _collectionID * 10000000000;
    }

📄 Source: smart-contracts/NextGenCore.sol#L383-L385

    function viewTokensIndexMax(...) {
-       return(collectionAdditionalData[_collectionID].reservedMaxTokensIndex);
+       return _collectionID * 10000000000 + collectionAdditionalData[_collectionID].collectionTotalSupply - 1;
	}

📄 Source: smart-contracts/NextGenCore.sol#L389-L391

STEP 2: Remove declaration

    struct collectionAdditonalDataStructure {
        address collectionArtistAddress;
        uint256 maxCollectionPurchases;
        uint256 collectionCirculationSupply;
        uint256 collectionTotalSupply;
-       uint256 reservedMinTokensIndex;
-       uint256 reservedMaxTokensIndex;
        uint setFinalSupplyTimeAfterMint;
        address randomizerContract;
        IRandomizer randomizer;
    }

📄 Source: smart-contracts/NextGenCore.sol#L44-L54

STEP 3: Remove assignment:

   function setCollectionData(...) {
       __SNIP__
        if (collectionAdditionalData[_collectionID].collectionTotalSupply == 0) {
            collectionAdditionalData[_collectionID].collectionCirculationSupply = 0;
            collectionAdditionalData[_collectionID].collectionTotalSupply = _collectionTotalSupply;
            collectionAdditionalData[_collectionID].setFinalSupplyTimeAfterMint = _setFinalSupplyTimeAfterMint;
-           collectionAdditionalData[_collectionID].reservedMinTokensIndex = (_collectionID * 10000000000);
-           collectionAdditionalData[_collectionID].reservedMaxTokensIndex = (_collectionID * 10000000000) + _collectionTotalSupply - 1;
            wereDataAdded[_collectionID] = true;
        } 
      __SNIP__
    }

📄 Source: smart-contracts/NextGenCore.sol#L147-L166

STEP 4: Read value via getter:

    function burn(uint256 _collectionID, uint256 _tokenId) public {
        __SNIP__
-        require ((_tokenId >= collectionAdditionalData[_collectionID].reservedMinTokensIndex) && (_tokenId <= collectionAdditionalData[_collectionID].reservedMaxTokensIndex), "id err");
+        require ((_tokenId >= this.viewTokensIndexMin(_collectionID)) && (_tokenId <=  this.viewTokensIndexMax(_collectionID)), "id err");
        __SNIP__
    }

📄 Source: smart-contracts/NextGenCore.sol#L204-L209

STEP 5: Since max value is calculated dynamically, no need to update it on supply change

    function setFinalSupply(...) {
     __SNIP__
        collectionAdditionalData[_collectionID].collectionTotalSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply;
-       collectionAdditionalData[_collectionID].reservedMaxTokensIndex = (_collectionID * 10000000000) + collectionAdditionalData[_collectionID].collectionTotalSupply - 1;
    }

📄 Source: smart-contracts/NextGenCore.sol#L307-L311

STEP 6: Fetch via value via getter

    function getTokenName(...)  {
-       uint256 tok = tokenId - collectionAdditionalData[tokenIdsToCollectionIds[tokenId]].reservedMinTokensIndex;
+       uint256 tok = tokenId - this.viewTokensIndexMin(tokenIdsToCollectionIds[tokenId]);
        __SNIP__
    }

📄 Source: smart-contracts/NextGenCore.sol#L361-L364

POC
Test Result
POC test file
const {
  loadFixture,
  time,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { ethers } = require("hardhat");
const fixturesDeployment = require("../scripts/fixturesDeployment.js");

let signers;
let contracts;
const MAX_TOTAL_SUPPLY = 10_000_000_000;

describe("Verify removal of min/max params from storage", async function () {
  before(async function () {
    ({ signers, contracts } = await loadFixture(fixturesDeployment));
  });

  it("SHOULD deploy contracts", async function () {
    expect(await contracts.hhAdmin.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhCore.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhDelegation.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhMinter.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandomizer.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandoms.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
  });

  it("SHOULD create a collection", async function () {
    await contracts.hhCore.createCollection(
      "TestCollection",
      "Artist 1",
      "For testing",
      "www.test.com",
      "CCO",
      "https://ipfs.io/ipfs/hash/",
      "",
      ["desc"]
    );
  });

  it("SHOULD successfully set min / max params for a collection", async function () {
    const SUPPLY = 1000;

    await contracts.hhCore.setCollectionData(
      1,
      signers.owner.address,
      50,
      SUPPLY,
      100
    );

    expect(await contracts.hhCore.viewTokensIndexMin(1)).eq(
      1 * MAX_TOTAL_SUPPLY
    );

    expect(await contracts.hhCore.viewTokensIndexMax(1)).eq(
      1 * MAX_TOTAL_SUPPLY + (SUPPLY - 1)
    );
  });

  it("SHOULD mint token", async function () {
    await contracts.hhCore.addMinterContract(contracts.hhMinter);
    await contracts.hhCore.addRandomizer(1, contracts.hhRandomizer);
    await contracts.hhMinter.setCollectionCosts(
      1, // _collectionID
      0, // _collectionMintCost
      0, // _collectionEndMintCost
      0, // _rate
      0, // _timePeriod
      1, // _salesOptions
      "0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B" // delAddress
    );
    await contracts.hhMinter.setCollectionPhases(
      1, // _collectionID
      1696931278, // _allowlistStartTime
      1696931278, // _allowlistEndTime
      1696931278, // _publicStartTime
      1700019791, // _publicEndTime
      "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870" // _merkleRoot
    );
    await contracts.hhMinter.mint(
      1, // _collectionID
      2, // _numberOfTokens
      0, // _maxAllowance
      '{"tdh": "100"}', // _tokenData
      signers.owner.address, // _mintTo
      ["0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870"], // _merkleRoot
      signers.addr1.address, // _delegator
      2 //_varg0
    );
  });

  it("SHOULD burn valid token", async function () {
    let min = await contracts.hhCore.viewTokensIndexMin(1);
    await expect(contracts.hhCore.burn(1, min)).not.to.be.reverted;
    expect(await contracts.hhCore.balanceOf(signers.owner.address)).eq(1);
  });

  it("SHOULD successfully set final supply", async function () {
    await time.increaseTo(1700019900);
    await expect(contracts.hhCore.setFinalSupply(1)).not.to.be.reverted;
  });

    it("SHOULD successfully get token name", async function () {
      let min = await contracts.hhCore.viewTokensIndexMin(1);
      // @audit getTokenName was changed to public for the purpose of unit testing.
      expect(await contracts.hhCore.getTokenName(min + 1n)).to.be.eq(
        "TestCollection #1"
      );
    });
});

[G-2] 34,044 gas can be saved by removing redundant flag for creation of collection

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenCore:createCollection()25255523025322302Yes
NextGenCore:setCollectionData()19907819903147Yes
NextGenCore:updateCollectionInfo()519935194647Yes
NextGenCore:changeMetadataView()627556270847Yes
NextGenCore:freezeCollection()570735702647Yes

**

DeploymentBeforeAfterGas Saved
NextGenCore5501771549021711554
Recommendation

The NextGenCore.sol contract uses a storage variable called isCollectionCreated to check if a collection has been created.

Alternatively, the existence of a collection can also be verified by checking if the collectionID is less than newCollectionIndex, since all existing collections will have a value that is less than newCollectionIndex. This optimization saves an expensive storage write during contract creation:

STEP 1: Remove the storage variable declaration, and write from createCollection().

__SNIP__
- mapping (uint256 => bool) private isCollectionCreated;
__SNIP__

    function createCollection(...) {
        collectionInfo[newCollectionIndex].collectionName = _collectionName;
        collectionInfo[newCollectionIndex].collectionArtist = _collectionArtist;
        collectionInfo[newCollectionIndex].collectionDescription = _collectionDescription;
        collectionInfo[newCollectionIndex].collectionWebsite = _collectionWebsite;
        collectionInfo[newCollectionIndex].collectionLicense = _collectionLicense;
        collectionInfo[newCollectionIndex].collectionBaseURI = _collectionBaseURI;
        collectionInfo[newCollectionIndex].collectionLibrary = _collectionLibrary;
        collectionInfo[newCollectionIndex].collectionScript = _collectionScript;
-       isCollectionCreated[newCollectionIndex] = true;
        newCollectionIndex = newCollectionIndex + 1;
    }
__SNIP__

📄 Source: smart-contracts/NextGenCore.sol#L62

📄 Source: smart-contracts/NextGenCore.sol#L139

STEP 2: Refactor the require statements to verify using newCollectionIndex instead in setCollectionData(), updateCollectionInfo(), changeMetadataView(), and freezeCollection(). It also verifies that collectionID greater than zero since newCollectionIndex starts with 1.

- require((isCollectionCreated[_collectionID] == true) && (collectionFreeze[_collectionID] == false) && (_collectionTotalSupply <= 10000000000), "err/freezed");
+ require((_collectionID > 0 && _collectionID < newCollectionIndex) && (collectionFreeze[_collectionID] == false) && (_collectionTotalSupply <= 10000000000), "err/freezed");

📄 Source: smart-contracts/NextGenCore.sol#L148

- require((isCollectionCreated[_collectionID] == true) && (collectionFreeze[_collectionID] == false), "Not allowed");
+ require((_collectionID > 0 && _collectionID < newCollectionIndex) && (collectionFreeze[_collectionID] == false), "Not allowed");

📄 Source: smart-contracts/NextGenCore.sol#L239

- require((isCollectionCreated[_collectionID] == true) && (collectionFreeze[_collectionID] == false), "Not allowed");
+ require(( _collectionID > 0 && _collectionID < newCollectionIndex) && (collectionFreeze[_collectionID] == false), "Not allowed");

📄 Source: smart-contracts/NextGenCore.sol#L267

- require(isCollectionCreated[_collectionID] == true, "No Col");
+ require((_collectionID > 0 && _collectionID < newCollectionIndex), "No Col");

📄 Source: smart-contracts/NextGenCore.sol#L293

POC
Test Result
POC test file
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("verify removal of `isCollectionCreated` storage variable", async function () {
  before(async function () {
    ({ signers, contracts } = await loadFixture(fixturesDeployment));
  });

  it("SHOULD deploy contracts", async function () {
    expect(await contracts.hhAdmin.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhCore.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhDelegation.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhMinter.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandomizer.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandoms.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
  });

  it("SHOULD revert when setting data on non existent collection", async function () {
    await expect(
      contracts.hhCore.setCollectionData(1, signers.owner.address, 50, 100, 100)
    ).to.be.revertedWith("err/freezed");
  });

  it("SHOULD revert when updating non existent collection", async function () {
    await expect(
      contracts.hhCore.updateCollectionInfo(
        1,
        "Test Collection 1",
        "Artist 1",
        "For testing",
        "www.test.com",
        "CCO",
        "https://ipfs.io/ipfs/hash/",
        "",
        999,
        ["desc"]
      )
    ).to.be.revertedWith("Not allowed");
  });

  it("SHOULD revert when changing meta data of a non existent collection", async function () {
    await expect(
      contracts.hhCore.changeMetadataView(1, true)
    ).to.be.revertedWith("Not allowed");
  });

  it("SHOULD revert when freezing non existent collection", async function () {
    await expect(contracts.hhCore.freezeCollection(1)).to.be.revertedWith(
      "No Col"
    );
  });

  it("SHOULD create a collection", async function () {
    await contracts.hhCore.createCollection(
      "Test Collection 1",
      "Artist 1",
      "For testing",
      "www.test.com",
      "CCO",
      "https://ipfs.io/ipfs/hash/",
      "",
      ["desc"]
    );
  });

  it("SHOULD succeed when setting data on an existing collection", async function () {
    await expect(
      contracts.hhCore.setCollectionData(1, signers.owner.address, 50, 100, 100)
    ).not.to.be.revertedWith("err/freezed");
  });

  it("SHOULD succeed when updating an existing collection", async function () {
    await expect(
      contracts.hhCore.updateCollectionInfo(
        1,
        "Test Collection 1",
        "Artist 1",
        "For testing",
        "www.test.com",
        "CCO",
        "https://ipfs.io/ipfs/hash/",
        "",
        999,
        ["desc"]
      )
    ).not.to.be.revertedWith("Not allowed");
  });

  it("SHOULD succeed when changing meta data of an existing collection", async function () {
    await expect(
      contracts.hhCore.changeMetadataView(1, true)
    ).not.to.be.revertedWith("Not allowed");
  });

  it("SHOULD succeed when freezing an existing collection", async function () {
    await expect(contracts.hhCore.freezeCollection(1)).not.to.be.revertedWith(
      "No Col"
    );
  });
});

[G-3] 28,517 gas can be saved by removing duplicate reference to core contract

MethodBeforeAfterGas SavedTest Passes?
NextGenMinterContract:airDropTokens()7449407429402000Yes
NextGenCore:mint()4044744024742000Yes

**

DeploymentBeforeAfterGas Saved
NextGenRandomizerNXT63407460955724517
Recommendation

This variable is redundant because the gencoreContract variable is already used to store the address of the Gencore contract. The gencore variable is only used in the calculateTokenHash function, which could be easily updated to use the gencoreContract variable instead.

STEP 1: Remove deceleration

contract NextGenRandomizerNXT {

    IXRandoms public randoms;
    INextGenAdmins private adminsContract;
    INextGenCore public gencoreContract;
    // @audit redundant storage variable
-    address gencore;  
}

STEP 2: Remove assignment from constructor

    constructor(address _randoms, address _admin, address _gencore) {
        randoms = IXRandoms(_randoms);
        adminsContract = INextGenAdmins(_admin);
-       gencore = _gencore;
        gencoreContract = INextGenCore(_gencore);
    }

STEP 3: Remove assignment from updateCoreContract

    function updateCoreContract(address _gencore) public FunctionAdminRequired(this.updateCoreContract.selector) { 
-       gencore = _gencore;
        gencoreContract = INextGenCore(_gencore);
    }

STEP 4: Update calculateTokenHash

    // function that calculates the random hash and returns it to the gencore contract
    function calculateTokenHash(uint256 _collectionID, uint256 _mintIndex, uint256 _saltfun_o) public {
-       require(msg.sender == gencore);
+       require(msg.sender == address(gencoreContract))
        bytes32 hash = keccak256(abi.encodePacked(_mintIndex, blockhash(block.number - 1), randoms.randomNumber(), randoms.randomWord()));
        gencoreContract.setTokenHash(_collectionID, _mintIndex, hash);
    }
POC

This can be verified using existing test suite.


[G-4] 26,581 gas can be saved by removing redundant flag for setting up collection

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenCore:setCollectionData()19245917026922190Yes

**

DeploymentBeforeAfterGas Saved
NextGenCore550177154973804391
Recommendation

The NextGenCore.sol contract uses a storage variable called wereDataAdded to check if additional data has been added to a collection.

This flag is set to true only if collectionTotalSupply is not set for a collection. And so, this value can be derived by checking from collectionTotalSupply.

STEP 1: Remove declaration


-  // checks if data on a collection were added
-  mapping (uint256 => bool) private wereDataAdded;

__SNIP__
    function setCollectionData(...) {
		__SNIP__
		// @audit As expected, this can be set only once. Since setting supply cannot be
		//		  set to zero: `_collectionTotalSupply` cannot be zero as
		//        expression-1 will underflow.
        if (collectionAdditionalData[_collectionID].collectionTotalSupply == 0) {
			__SNIP__
			// @audit expression-1
			collectionAdditionalData[_collectionID].reservedMaxTokensIndex = (_collectionID * 10000000000) + _collectionTotalSupply - 1;
-           wereDataAdded[_collectionID] = true;
        } 
	    __SNIP__
    }

📄 Source: smart-contracts/NextGenCore.sol#L64-L65

📄 Source: smart-contracts/NextGenCore.sol#L157

STEP 2: The getter function can be refactored to use collectionTotalSupply as proxy to check if collection data has been added:

    function retrievewereDataAdded(uint256 _collectionID) external view returns(bool){
-      return wereDataAdded[_collectionID];
+      return collectionAdditionalData[_collectionID].collectionTotalSupply != 0;
	}

📄 Source: smart-contracts/NextGenCore.sol#L378

STEP 3 (optional): This function appears to check if collection data has been initialized correctly, based on how it is used in the codebase. For example:

    function airDropTokens(...)  {
        require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data");
        __SNIP__
    }  

📄 Source: smart-contracts/MinterContract.sol#L181-L192

To make the function's purpose more clear, it could be renamed to something like hasCollectionDataBeenAdded.

This change would also have the side effect of highlighting the fact that the codebase relies on the collectionTotalSupply to verify if required collection data is present. This gives the team an opportunity to review whether this behavior is desired, or if additional constraints should be added.

POC
Test Result
POC test file
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("verify removal of `wereDataAdded` storage variable", async function () {
  before(async function () {
    ({ signers, contracts } = await loadFixture(fixturesDeployment));
  });

  it("SHOULD deploy contracts", async function () {
    expect(await contracts.hhAdmin.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhCore.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhDelegation.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhMinter.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandomizer.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandoms.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
  });

  it("SHOULD create a collection", async function () {
    await contracts.hhCore.createCollection(
      "Test Collection 1",
      "Artist 1",
      "For testing",
      "www.test.com",
      "CCO",
      "https://ipfs.io/ipfs/hash/",
      "",
      ["desc"]
    );
  });

  it("SHOULD return `false` when `retrievewereDataAdded` is called", async function () {
    await expect(await contracts.hhCore.retrievewereDataAdded(1)).to.be.false;
  });

  it("SHOULD set collection data", async function () {
    await expect(
      contracts.hhCore.setCollectionData(1, signers.owner.address, 50, 100, 100)
    ).not.to.be.reverted;
  });

  it("SHOULD return `true` when `retrievewereDataAdded` is called", async function () {
    expect(await contracts.hhCore.retrievewereDataAdded(1)).to.be.true;
  });
});

[G-5] 22,761 gas can be saved by removing duplicate reference to randomizer contract

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenCore:addRandomizer()706965353517161Yes
NextGenMinterContract::mint()4044744024742000Yes
NextGenMinterContract::airDropTokens()7449407429402000Yes
NextGenMinterContract::burnToMint()2764742748741600Yes
Recommendation

It is sufficient to only store the address of the randomizer in collectionAdditonalDataStructure. The randomizer contract instance can be referenced by providing this address to IRandomizer interface. The gas savings also cascade to critical operations such as mint , burnToMint, and airDropTokens.

STEP 1: Remove duplicate declaration from struct

    struct collectionAdditonalDataStructure {
        address collectionArtistAddress;
        uint256 maxCollectionPurchases;
        uint256 collectionCirculationSupply;
        uint256 collectionTotalSupply;
        uint256 reservedMinTokensIndex;
        uint256 reservedMaxTokensIndex;
        uint setFinalSupplyTimeAfterMint;
        address randomizerContract;
-       IRandomizer randomizer;
    }

📄 Source: smart-contracts/NextGenCore.sol#L44-L54

Step 2: Remove the duplicate assignment

function addRandomizer(...) {
		__SNIP__        
	collectionAdditionalData[_collectionID].randomizerContract = _randomizerContract;
-   collectionAdditionalData[_collectionID].randomizer = IRandomizer(_randomizerContract);
}

📄 Source: smart-contracts/NextGenCore.sol#L170-L174

Step 3: Get randomizer instance using IRandomizer interface

    function _mintProcessing(...) internal {
      __SNIP__
-    collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o);
+    IRandomizer(collectionAdditionalData[_collectionID].randomizerContract).calculateTokenHash(_collectionID, _mintIndex, _saltfun_o);
	 __SNIP__
    }

📄 Source: smart-contracts/NextGenCore.sol#L229

Optionally, for ease of understanding struct property could be renamed from randomizerContract to randomizerContractAddress.

POC
Test result
POC test file

POC can be verified using the existing test suite by adding the following test to cover burnToMint:

    context("Verify `burnToMint`", async function () {
      it("SHOULD burn existing token and mint a new one", async function () {
        contracts.hhCore.tokenOfOwnerByIndex();
        await contracts.hhMinter.initializeBurn(1, 3, true);
        await expect(
          contracts.hhMinter
            .connect(signers.addr1)
            .burnToMint(1, 10000000000, 3, 2, {
              value: await contracts.hhMinter.getPrice(3),
            })
        ).to.not.be.reverted;

        expect(await contracts.hhCore.ownerOf(30000000002)).to.be.eq(
          signers.addr1.address
        );
      });
    });

[G-6] 3066 gas can saved by removing redundant external call during airdrop

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenMinterContract:airDropTokens()112610111230353066Yes
Recommendation
    function airDropTokens(...) {
-       require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data");
        uint256 collectionTokenMintIndex;
        for (uint256 y=0; y< _recipients.length; y++) {
            collectionTokenMintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID) + _numberOfTokens[y] - 1;
            require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_collectionID), "No supply");
            }
        }
    }

📄 Source: smart-contracts/MinterContract.sol#L182

The removed line of code checks to see if the collection data has been added. If it has not, the function reverts with the error message "Add data". As discussed in [G-4], retrievewereDataAdded returns true only once total supply is added. The function later checks to see if the total tokens to be airdropped is within total supply. If they are not, the function will revert with the error message "No supply". Therefore, the first check for whether the collection data has been added is not necessary.

POC
Test Result
POC test file
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("verify removal of redundant external call from airdropTokens", async function () {
  before(async function () {
    ({ signers, contracts } = await loadFixture(fixturesDeployment));
  });

  it("SHOULD deploy contracts", async function () {
    expect(await contracts.hhAdmin.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhCore.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhDelegation.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhMinter.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandomizer.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
    expect(await contracts.hhRandoms.getAddress()).to.not.equal(
      ethers.ZeroAddress
    );
  });

  it("SHOULD create a collection and setup collection", async function () {
    await contracts.hhCore.createCollection(
      "Test Collection 1",
      "Artist 1",
      "For testing",
      "www.test.com",
      "CCO",
      "https://ipfs.io/ipfs/hash/",
      "",
      ["desc"]
    );

    await contracts.hhCore.addMinterContract(contracts.hhMinter);
    await contracts.hhCore.addRandomizer(1, contracts.hhRandomizer);
  });

  it("SHOULD revert when attempting to airdrop when collection is NOT added", async function () {
    await expect(
      contracts.hhMinter.airDropTokens(
        [signers.addr1.address],
        ["data"],
        [1],
        1,
        [5]
      )
    ).to.be.revertedWith("No supply");
  });
  it("SHOULD successfully airdrop tokens when collection data is added", async function () {
    await contracts.hhCore.setCollectionData(
      1,
      signers.owner.address,
      50,
      500,
      100
    );
    await contracts.hhMinter.airDropTokens(
      [signers.addr1.address],
      ["data"],
      [1],
      1,
      [5]
    );
    expect(await contracts.hhCore.balanceOf(signers.addr1.address)).eq(5);
  });
});

[G-7] 976 gas can be saved by optimizing creation of collection

Impact
MethodBeforeAfterGas SavedTest Passes?
NextGenCore:createCollection()252555251579976Yes
Recommendation

Refactoring the creation of a collection, a critical entry point, into simpler code can save gas:

    function createCollection(...) {
-        collectionInfo[newCollectionIndex].collectionName = _collectionName;
-        collectionInfo[newCollectionIndex].collectionArtist = _collectionArtist;
-        collectionInfo[newCollectionIndex].collectionDescription = _collectionDescription;
-        collectionInfo[newCollectionIndex].collectionWebsite = _collectionWebsite;
-        collectionInfo[newCollectionIndex].collectionLicense = _collectionLicense;
-        collectionInfo[newCollectionIndex].collectionBaseURI = _collectionBaseURI;
-        collectionInfo[newCollectionIndex].collectionLibrary = _collectionLibrary;
-        collectionInfo[newCollectionIndex].collectionScript = _collectionScript;
-        isCollectionCreated[newCollectionIndex] = true;
-        newCollectionIndex = newCollectionIndex + 1;
 
+       isCollectionCreated[newCollectionIndex] = true;
+       collectionInfo[newCollectionIndex++] = collectionInfoStructure(
+           _collectionName,
+           _collectionArtist,
+           _collectionDescription,
+           _collectionWebsite,
+           _collectionLicense,
+           _collectionBaseURI,
+           _collectionLibrary,
+           _collectionScript
+       );
	}

📄 Source: smart-contracts/NextGenCore.sol#L130-L141

POC

This can be verified using existing test suite.

#0 - 141345

2023-11-26T05:58:55Z

191 rahul l r nc 3 1 3

G 1 n G 2 n G 3 l G 4 n G 5 l G 6 l G 7 r

#1 - c4-pre-sort

2023-11-26T06:00:14Z

141345 marked the issue as sufficient quality report

#2 - alex-ppg

2023-12-02T17:17:27Z

G-4 might be invalid due to set final supply which can in certain circumstances result in 0 for a collection that "exists". Otherwise solid contender for best report!

#3 - c4-judge

2023-12-02T17:17:31Z

alex-ppg marked the issue as grade-a

#4 - c4-judge

2023-12-09T00:35:39Z

alex-ppg marked the issue as selected for report

#5 - c3phas

2023-12-10T12:36:29Z

Now, this is an interesting report. Nice work

#6 - thebrittfactor

2024-01-03T21:10:30Z

Just a note that C4 is excluding the invalid entries from the official report.

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