NextGen Smart Contract Breakdown

Nattawat Songsom
Valix Consulting
Published in
30 min readNov 27, 2023

Unraveling the code by identifying its purpose

Disclaimer:

This article is based on the non-production version of the platform. The platform is still in the auditing process on Code4rena. Please note that the actual functionality and features may differ from what is described in this article once the platform is released to production.

Additionally, we are not publishing or discussing any findings publicly before the release of the audit report. This article solely focuses on breaking down the functionality of the smart contract.

NextGen is a series of smart contracts that make it easy for artists to create and mint generative art NFTs.

In this article, we will analyze how NextGen is implemented by breaking down its smart contract.

Here is the outline of this article:

  • What are generative art NFTs
  • How NextGen works at a high level
  • How NextGen code is organized
  • NextGen functionalities
  • Selling NFT break-down

What are generative art NFTs?

Generative art NFTs are a type of digital art that uses algorithms or autonomous systems to create unique pieces of art. They are often characterized by the repetition of patterns, shapes, colors, and motifs, as well as the use of randomness in their composition.

Generative art, like Autoglyphs, uses a unique algorithm to render images directly within the smart contract itself.

For example, this is a generative art NFT metadata which is a character art pattern encoded in hexadecimal data.

.|.-.+........|.-.-.+........|.-..|.-.-.+........|.-.-.+......|.
|.....--......|.....|--.....||....||-+.....|.....|-+...........|
..-|.+-...+...|..-|..+-..+...|...-|.+-.......|..-|..+...+...|...
-.|+..........|..|..-..+..+..|..-.|+.....+....|..|..-..-..+..|..
...........|||..|............||||||............|--|||...........
+.+..-.+........|.+.+......|.|.-..|.|.-.+.+....|.-.-.+......+...
.--............--+....++....||....||++....++.....--....--.......
.-...+...|...-|..+...+...+...|...-|..-...+.......|...-|.+-...+..
.........|..-.|+.|+.......|..|..-.|+.|+.-..+..|..|..-.|+.-..-..+
.......|||...|..............||||||||..............|---|||.......
..+........-.-.+..-.+.+....|.|.-..|.|.-...............-.-.+....-
....|.....--....-+.....+....||....||+....-+...........--...|-+..
....|...-...+|..+-..+....|...|...-|..+|...-..+....|...-...-|..+-
....|..-.|-.|+.-..+..+....|..|..-.|+.|+.-..+..+....|..|..-.|..-.
||||...||..................||||||||||..................||---||||
......-.+.+..-.-......+....|.|.-..|.|.+.+..-.+.............-.-..
-...||-....-+....++..........|....|-+...--+....+..........||....
..-|..++|..+-...+...+....|...|...-|..+|..+-...+...+....|....|...
-.|..+..+.-..+..+.........|..|..-.|..|..+.-+.-..+..+.........|..
..........................||||||||||||..........................
+|.-.+....+.+....+....|......|.-..|.-.+..|.-.+..-.+....+.+....|.
.-+....+.....+.........||....|-...|-...||+...--+...-+....++.....
.--...+...+....+....|...|....|...-|..+-|..+|..+-....-...+...+...
...+..+....+.........|..|....|..-.|..-.|+.|..+.-+.-....-..+....+
.....................||||||||||||||||||||||.....................
..+....+....|....|......|....|.-..|.-..|.-.+..|.+.+|.-.+..-....-
...+....|....|....||....|....|-...|-...|-+..||+...|+...|-+...-+.
.....|....|...||...|....|...-|...-|...-|...-|..+||..+|...+|..+--
.|....|..|.|..|....|....|..-.|..-.|..-.|..-.|..-.|+.|.|+.|..+.|.
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
....|....|....|....|.-..|.-..|.-..|.-..|.-..|.-..|.-..|.-..|.-..
-...|-...|-...|-...|-...|-...|-...|-...|-...|-...|-...|-...|-...
...-|...-|...-|...-|...-|...-|...-|...-|...-|...-|...-|...-|...-
..-.|..-.|..-.|..-.|..-.|..-.|..-.|..-.|..-.|....|....|....|....
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.|.+..|.+|.|.+|.-..|.-..|.-..|.-..|.-..|....|....|..|.|..|....|.
--+..|+...|+..||+..|-...|-...|-...|-...|....|...||...|....|.....
.+-...+-|...+|...+||..+-|...-|...-|....|....||....|....|....+...
-....-..+.-.|+.+.|..+.-.|..-.|..-.|....|......|....|....+....+..
.....................||||||||||||||||||||||.....................
+....+..-....-.+-.+..|.+|.-..|.-..|....|..|.........+....+..+...
...+...+...-....-+..|+..|-+..|-...|....|...|....+....+...+...--.
.....++....+-...+--...+||...-|...-|....||.........+.....+....+-.
.|....+.+....+.-..+.-.|..+.-.|..-.|......|....+....+.+....+.-.|+
..........................||||||||||||..........................
..|.........+..+..-.+-.+..|..|.-..|..|.........+..+..-.+..+..|.-
...|....|....+...+...-+..|+..|-...|...|....+...+...-+..|++..|-..
....||..........+....+--...+-|....|..........++....+-....-||...-
..-.-.............+.-..+.+.|.|..-.|.|....+......-.-..+.+.-......
||||---||..................||||||||||..................||...||||
.-..|.-..|..|....+..+..-.+|.+|.-..|..|....+..+..-.+|.-|.-..|....
-+..|-...-...|....+..-...|+..|-...|...|....+..-+..|+...-...|....
..+-|...--...........+-....+||....||....+.....+-....--.....|....
-....+.-.-...............-.|.|..-.|.|....+.+.-..+.-.-........+..
.......|||---|..............||||||||..............|...|||.......
+..-..-.+|.-..|..|..+..-.+|.+|.-..|..|.......+|.+|.-..|.........
..+...-+.|-...|.......+...-..|-...|...+...+...+..|-...|...+...-.
.......--....--.....++....++||....||....++....+--............--.
...+......+.-.-.|....+.+.-.|.|..-.|.|......+.+.|........+.-..+.+
...........|||--|............||||||............|..|||...........
..|..+..-..-..|..|....+.....+|.-..|..+..+..-..|..|..........+|.-
...|...+...+..|-..|.......-+.|-...|...+..-+..|-..|...+...-+.|-..
|...........+-|.....|.....+-||....||.....--|.....|......--.....|
.|......+.-.-.|........+.-.-.|..-.|........+.-.-.|........+.-.|.

This data can then be interpreted and displayed on a screen using written instructions within the smart contract to provide a guide for rendering these patterns.

The rendered version of Autoglyphs NFT:

source: https://opensea.io/assets/ethereum/0xd4e4078ca3495de5b1d4db434bebc5a986197782/1

On the other hand, NextGen provides a framework for artists to define the rules that govern their artwork’s creation by using p5.js, Babylon.js, three.js, tone.js, and more, as well as the ability to use randomness to create unique pieces of art.

How NextGen works at a high level

NextGen is a platform that enables artists to create and mint generative art NFTs. It provides a comprehensive framework for artists to define the rules that govern their artwork’s creation, as well as the ability to use randomness to create unique pieces of art. Furthermore, it provides multiple minting models, including selling, auction, MintPass, and airdropping.

How NextGen code is organized

The NextGen smart contract architecture consists of four main components:

source: https://seize-io.gitbook.io/nextgen/nextgen-smart-contracts/architecture
  1. Core: The Core contract serves as the foundation of the system, handling the minting of ERC721 tokens. It adheres to the ERC721 standard and incorporates additional setter and getter functions. The Core contract stores essential collection data, including the collection name, artist’s name, library, script, and total supply. Additionally, it seamlessly integrates with other NextGen contracts to facilitate flexible, adaptable, and scalable functionality.
  2. Minter: The Minter contract is specifically designed to mint ERC721 tokens for a collection on the Core contract. It operates based on predetermined requirements established before the minting process commences. The Minter contract maintains comprehensive information regarding upcoming drops, encompassing details such as starting and ending times of various phases, Merkle roots, sales models, funds, and the primary and secondary addresses of artists.
  3. Admin: The Admin contract assumes responsibility for managing administrative privileges. It empowers authorized admins to add or remove global or function-based admins, granting them the ability to execute specific functions within both the Core and Minter contracts.
  4. Randomizer: The Randomizer contract plays a crucial role in the minting process, generating a unique random hash for each token. Once generated, the hash is transmitted to the Core contract for storage and subsequent utilization in producing the generative art token. NextGen currently offers three distinct Randomizer contracts:
  • A Randomizer contract that uses Chainlink VRF.
  • A Randomizer contract that uses ARRNG.
  • A custom-built Randomizer contract implementation.

Additionally, NextGen has native integration of NFTDelegation.com contract. This allows collectors to safely mint from hot wallets without exposing their NFT vaults.

NextGen functionalities

  1. Allowlist and Phase-based Minting: NextGen’s default minting process utilizes allowlists and phases. This strategy provides flexibility as:
  • Customizable Phases: Collections can have an arbitrary number of phases, each with its allowlist and starting/ending times.
  • Optional Public Sale: After phases conclude, collections can have an optional public sale period, allowing broader participation.
  • Public Minting without Allowlists: Public mints without any Allowlist phases are also possible, providing an alternative minting option.

2. Several minting models: NextGen offers six distinct minting models that can be combined within a single collection drop by utilizing phases. These models cater to diverse pricing and distribution strategies:

  • Airdrops: NFTs are directly airdropped to a pre-selected list of collectors, fostering loyalty and appreciation.
  • Fixed Price Sale: The minting cost remains fixed throughout the minting phase, providing predictability and stability.
  • Exponential Descending Sale: The minting cost decreases exponentially at each period, gradually lowering the entry barrier and generating excitement.
  • Linear Descending Sale: The minting cost decreases linearly at each period, providing a controlled and steady price reduction as shown in the image.
source: https://seize-io.gitbook.io/nextgen/for-creators/sales-models
  • Periodic Sale: Minting is limited to one token per period, encouraging strategic decision-making and scarcity. The minting cost can either remain stable or increase over time, allowing for long-lived or experimental mints. Unminted tokens from previous periods are carried over for future minting opportunities.
  • Burn-to-Mint: This model offers two variations:
    - Burning a NextGen token transfers tokenData to the newly minted token, creating a sense of continuity and value transfer.
    - Burning a non-NextGen token acts as a MintPass, allowing the newly minted token to inherit tokenData from the burned token.
  • Mint-to-Auction: Minting is limited to one token per period, and the newly minted token is immediately sent to an auction, fostering a dynamic pricing mechanism.

Selling NFT break-down

At a high level, NextGen provides several options for setting NFT prices and selling periods.

For NFT price, we can choose between

  1. Fixed price
  2. Exponential descending price
  3. Linear descending price
  4. Ascending price per minting

and for NFT selling periods, we can use 2 kinds of sale periods including

  1. Allowlist sale phase
  2. Public sale phase

Let’s take a look at the selling NFT process.

  1. Artists provide collection information: Artists initiate the process by supplying an admin with essential details about their collection, including the collection name, artist name, description, website, artist’s ETH address, license, base URI, library used, generative art script, and total supply. Note that, this is an off-chain process.
  2. Admins create the collection: Admins use the artists’ information to create ERC721 collections on the NextGen platform. Once a collection is established, a range of token IDs is automatically assigned to it. Note that, multiple ERC721 collections are under a single smart contract address.
  3. Admins set primary and secondary splits: Admins can set how much money from sales goes to the artist and how much goes to the NextGen team for each collection. This crucial step determines the fair allocation of revenue between artists and the NextGen platform.
  4. Artists define purchasing conditions: Artists collaborate closely with admins to determine the most suitable purchasing conditions for their collections. They can decide on the total supply and the maximum purchase per address at this step.
  5. Artists sign and propose revenue distribution: Artists digitally sign their collections by sending their signatures on-chain. Additionally, they propose separate addresses for primary sales proceeds and secondary royalty proceeds, providing clarity on revenue distribution. Note that, the minting revenue allocated for the artist will be distributed to their proposed addresses for primary sales.
  6. Artists define the minting process: Artists collaborate closely with admins to determine the most suitable minting process for their collections. They can select from various options, including allowlists, public sales, or a combination of both. Furthermore, they can choose from NextGen’s diverse range of NFT price models: Fixed Price Sales, Exponential Descending Sales, and Linear Descending Sales.
  7. Admins oversee the minting process: Admins set up crucial components like the token hash randomizer. This specialized tool generates a unique identifier, or hash, for each minted token. This hash is then sent to the Core contract, where it is securely stored and utilized by the generative script to produce the final artwork.
  8. Users mint NFTs: With the minting process in place, users can seamlessly pay the specified price and mint the NFT. This triggers the Randomizer contract to generate a unique random hash for each minted token. These hashes are securely stored on-chain on the Core contract, providing the necessary input for the generative script to produce the final artwork.
  9. Admins finalize the minting process: After careful consideration of the artists’ preferences and ensuring alignment with the collection’s objectives, admins finalize the minting process using the artist’s chosen minting process.
  10. Admins send the money from sales to the artist: Admins can use the smart contract to send the money from sales directly to the artist’s address.

Let’s walk through each step line by line.

1. Artists provide collection information: Artists initiate the process by supplying an admin with essential details about their collection, including the collection name, artist name, description, website, artist’s ETH address, license, base URI, library used, generative art script, and total supply. Note that, this is an off-chain process.

This is an off-chain step. Therefore, we move on to analyze the next step.

2. Admins create the collection: Admins use the artists’ information to create ERC721 collections on the NextGen platform. Once a collection is established, a range of token IDs is automatically assigned to it. Note that, multiple ERC721 collections are under a single smart contract address.

This step requires

  1. Initializing the NextGenAdmins contract
  2. Initializing the NextGenCore contract
  3. Admin calling the createCollection function
  4. Admin calling the updateCollectionInfo function (optional)

Let’s examine each step in detail

  1. Initializing the NextGenAdmins contract

First, the deployer deploys the NextGenAdmins contract. The constructor function is called in the process.

// sets global admins
mapping(address => bool) public adminPermissions;

constructor() {
adminPermissions[msg.sender] = true;
}

Then, the deployer is granted the global admin access level.

If he wants to add/remove global admins, he could call the registerAdmin function

// function to register a global admin

function registerAdmin(address _admin, bool _status) public onlyOwner {
adminPermissions[_admin] = _status;
}

Then, he could optionally grant the function admin access level to another account by calling the registerFunctionAdmin function or registerBatchFunctionAdmin function

// certain functions can only be called by an admin
modifier AdminRequired {
require((adminPermissions[msg.sender] == true) || (_msgSender()== owner()), "Not allowed");
_;
}

// function to register function admin

function registerFunctionAdmin(address _address, bytes4 _selector, bool _status) public AdminRequired {
functionAdmin[_address][_selector] = _status;
}

// function to register batch functions admin

function registerBatchFunctionAdmin(address _address, bytes4[] memory _selector, bool _status) public AdminRequired {
for (uint256 i=0; i<_selector.length; i++) {
functionAdmin[_address][_selector[i]] = _status;
}
}

Additionally,

That is it! Now we move on to the next step.

2. Initializing the NextGenCore contract

First, the deployer deploys the contract, and the constructor function is called in the process.

constructor(
string memory name,
string memory symbol,
address _adminsContract
) ERC721(name, symbol) {
adminsContract = INextGenAdmins(_adminsContract);
newCollectionIndex = newCollectionIndex + 1;
_setDefaultRoyalty(0x1B1289E34Fe05019511d7b436a5138F361904df0, 690);
}

Note that, the admin contract address is registered at this step.

adminsContract = INextGenAdmins(_adminsContract);

If admins need to update the address, they can use the updateAdminContract function.

// certain functions can only be called by a global or function admin

modifier FunctionAdminRequired(bytes4 _selector) {
require(
adminsContract.retrieveFunctionAdmin(msg.sender, _selector) ==
true ||
adminsContract.retrieveGlobalAdmin(msg.sender) == true,
"Not allowed"
);
_;
}

function updateAdminContract(
address _newadminsContract
) public FunctionAdminRequired(this.updateAdminContract.selector) {
require(
INextGenAdmins(_newadminsContract).isAdminContract() == true,
"Contract is not Admin"
);
adminsContract = INextGenAdmins(_newadminsContract);
}

Note that, updating the admin contract address is granted to 2 access levels including

  • global admins
  • updateAdminContract function admin

3. Admin calling the createCollection function

Now, admins can use the artists’ information to create ERC721 collections on the NextGen platform. Let’s walk through the code line by line.

// declare variables
uint256 public newCollectionIndex;

struct collectionInfoStructure {
string collectionName;
string collectionArtist;
string collectionDescription;
string collectionWebsite;
string collectionLicense;
string collectionBaseURI;
string collectionLibrary;
string[] collectionScript;
}

// mapping of collectionInfo struct
mapping(uint256 => collectionInfoStructure) private collectionInfo;

// checks if a collection was created
mapping(uint256 => bool) private isCollectionCreated;

// function to create a Collection

function createCollection(
string memory _collectionName,
string memory _collectionArtist,
string memory _collectionDescription,
string memory _collectionWebsite,
string memory _collectionLicense,
string memory _collectionBaseURI,
string memory _collectionLibrary,
string[] memory _collectionScript
) public FunctionAdminRequired(this.createCollection.selector) {
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;
}

First, this function is restricted to 2 access levels including:

  1. the global admin
  2. the createCollection function admin
// certain functions can only be called by a global or function admin

modifier FunctionAdminRequired(bytes4 _selector) {
require(
adminsContract.retrieveFunctionAdmin(msg.sender, _selector) ==
true ||
adminsContract.retrieveGlobalAdmin(msg.sender) == true,
"Not allowed"
);
_;
}

FunctionAdminRequired(this.createCollection.selector)

Then, this function collects NFT collection data including

  • the collection name
  • artist name
  • description
  • website
  • license
  • base URI
  • library used
  • generative art script
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;

Finally, the collection ID is registered and the collection ID counter is updated.

isCollectionCreated[newCollectionIndex] = true;
newCollectionIndex = newCollectionIndex + 1;

Note that, the first function called can be interpreted as

  • isCollectionCreated[1] = true
  • newCollectionIndex = 2

If the admin wants to update NFT collection data, he could move on to the next step.

4. Admin calling the updateCollectionInfo function (optional)

The full code is

    function updateCollectionInfo(
uint256 _collectionID,
string memory _newCollectionName,
string memory _newCollectionArtist,
string memory _newCollectionDescription,
string memory _newCollectionWebsite,
string memory _newCollectionLicense,
string memory _newCollectionBaseURI,
string memory _newCollectionLibrary,
uint256 _index,
string[] memory _newCollectionScript
)
public
CollectionAdminRequired(
_collectionID,
this.updateCollectionInfo.selector
)
{
require(
(isCollectionCreated[_collectionID] == true) &&
(collectionFreeze[_collectionID] == false),
"Not allowed"
);
if (_index == 1000) {
collectionInfo[_collectionID].collectionName = _newCollectionName;
collectionInfo[_collectionID]
.collectionArtist = _newCollectionArtist;
collectionInfo[_collectionID]
.collectionDescription = _newCollectionDescription;
collectionInfo[_collectionID]
.collectionWebsite = _newCollectionWebsite;
collectionInfo[_collectionID]
.collectionLicense = _newCollectionLicense;
collectionInfo[_collectionID]
.collectionLibrary = _newCollectionLibrary;
collectionInfo[_collectionID]
.collectionScript = _newCollectionScript;
} else if (_index == 999) {
collectionInfo[_collectionID]
.collectionBaseURI = _newCollectionBaseURI;
} else {
collectionInfo[_collectionID].collectionScript[
_index
] = _newCollectionScript[0];
}
}

Let’s walk through the code line by line

// certain functions can only be called by a collection, global or function admin

modifier CollectionAdminRequired(uint256 _collectionID, bytes4 _selector) {
require(
adminsContract.retrieveCollectionAdmin(msg.sender, _collectionID) ==
true ||
adminsContract.retrieveFunctionAdmin(msg.sender, _selector) ==
true ||
adminsContract.retrieveGlobalAdmin(msg.sender) == true,
"Not allowed"
);
_;
}

This function requires the “Collection Admin” access level.

This access level can be either

  • the admin for this specific collection
  • the admin for this updateCollectionInfo function
  • the global admin
require(
(isCollectionCreated[_collectionID] == true) &&
(collectionFreeze[_collectionID] == false),
"Not allowed"
);

This function requires the collection to be created first and must be frozen.

We discuss this freezing/locking mechanism later.

if (_index == 1000) {
collectionInfo[_collectionID].collectionName = _newCollectionName;
collectionInfo[_collectionID]
.collectionArtist = _newCollectionArtist;
collectionInfo[_collectionID]
.collectionDescription = _newCollectionDescription;
collectionInfo[_collectionID]
.collectionWebsite = _newCollectionWebsite;
collectionInfo[_collectionID]
.collectionLicense = _newCollectionLicense;
collectionInfo[_collectionID]
.collectionLibrary = _newCollectionLibrary;
collectionInfo[_collectionID]
.collectionScript = _newCollectionScript;
} else if (_index == 999) {
collectionInfo[_collectionID]
.collectionBaseURI = _newCollectionBaseURI;
} else {
collectionInfo[_collectionID].collectionScript[
_index
] = _newCollectionScript[0];
}

This function supports full update and partial update of the collection data.

else {
collectionInfo[_collectionID].collectionScript[
_index
] = _newCollectionScript[0];
}

The partial update reduces gas costs by updating the specific part of the script array.

That is a wrap for “Admins creating the collection”. Let’s move on to the next step.

3. Admins set primary and secondary splits: Admins can set how much money from sales goes to the artist and how much goes to the NextGen team for each collection. This crucial step determines the fair allocation of revenue between artists and the NextGen platform.

This requires the admin to interact with the MinterContract.

Let’s review its constructor function first.

constructor(address _gencore, address _del, address _adminsContract) {
gencore = INextGenCore(_gencore);
dmc = IDelegationManagementContract(_del);
adminsContract = INextGenAdmins(_adminsContract);
}

The constructor function points the contract to different addresses.

Now, we can review the main function of this step.

The setPrimaryAndSecondarySplits function.

// function to set primary splits

function setPrimaryAndSecondarySplits(
uint256 _collectionID,
uint256 _artistPrSplit,
uint256 _teamPrSplit,
uint256 _artistSecSplit,
uint256 _teamSecSplit
) public FunctionAdminRequired(this.setPrimaryAndSecondarySplits.selector) {
require(_artistPrSplit + _teamPrSplit == 100, "splits need to be 100%");
require(
_artistSecSplit + _teamSecSplit == 100,
"splits need to be 100%"
);
collectionRoyaltiesPrimarySplits[_collectionID]
.artistPercentage = _artistPrSplit;
collectionRoyaltiesPrimarySplits[_collectionID]
.teamPercentage = _teamPrSplit;
collectionRoyaltiesSecondarySplits[_collectionID]
.artistPercentage = _artistSecSplit;
collectionRoyaltiesSecondarySplits[_collectionID]
.teamPercentage = _teamSecSplit;
}

Let’s review it line by line.

FunctionAdminRequired(this.setPrimaryAndSecondarySplits.selector)

This function requires either the function admin or the global admin to call it.

require(_artistPrSplit + _teamPrSplit == 100, "splits need to be 100%");
require(
_artistSecSplit + _teamSecSplit == 100,
"splits need to be 100%"
);

This function validates the sum of the revenue allocation between the artist and the NextGen team to be 100%.

Note that

  1. This code supports only integer percent without fractions.
  2. The primary split is for the revenue from the primary market.
  3. The secondary split is for the revenue from the secondary market.
collectionRoyaltiesPrimarySplits[_collectionID]
.artistPercentage = _artistPrSplit;
collectionRoyaltiesPrimarySplits[_collectionID]
.teamPercentage = _teamPrSplit;
collectionRoyaltiesSecondarySplits[_collectionID]
.artistPercentage = _artistSecSplit;
collectionRoyaltiesSecondarySplits[_collectionID]
.teamPercentage = _teamSecSplit;

Finally, the function updates the related variables.

Let’s move on to the next step.

4. Artists define purchasing conditions: Artists collaborate closely with admins to determine the most suitable purchasing conditions for their collections. They can decide on the total supply and the maximum purchase per address at this step.

Note that, this is an off-chain process. The artist must delegate the admin to call the smart contract for him.

The full code for this step is

// function to add/modify the additional data of a collection
// once a collection is created and total supply is set it cannot be changed
// only _collectionArtistAddress , _maxCollectionPurchases can change after total supply is set
function setCollectionData(
uint256 _collectionID,
address _collectionArtistAddress,
uint256 _maxCollectionPurchases,
uint256 _collectionTotalSupply,
uint _setFinalSupplyTimeAfterMint
)
public
CollectionAdminRequired(_collectionID, this.setCollectionData.selector)
{
require(
(isCollectionCreated[_collectionID] == true) &&
(collectionFreeze[_collectionID] == false) &&
(_collectionTotalSupply <= 10000000000),
"err/freezed"
);
if (
collectionAdditionalData[_collectionID].collectionTotalSupply == 0
) {
collectionAdditionalData[_collectionID]
.collectionArtistAddress = _collectionArtistAddress;
collectionAdditionalData[_collectionID]
.maxCollectionPurchases = _maxCollectionPurchases;
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;
} else if (artistSigned[_collectionID] == false) {
collectionAdditionalData[_collectionID]
.collectionArtistAddress = _collectionArtistAddress;
collectionAdditionalData[_collectionID]
.maxCollectionPurchases = _maxCollectionPurchases;
collectionAdditionalData[_collectionID]
.setFinalSupplyTimeAfterMint = _setFinalSupplyTimeAfterMint;
} else {
collectionAdditionalData[_collectionID]
.maxCollectionPurchases = _maxCollectionPurchases;
collectionAdditionalData[_collectionID]
.setFinalSupplyTimeAfterMint = _setFinalSupplyTimeAfterMint;
}
}

Let’s break it down.

CollectionAdminRequired(_collectionID, this.setCollectionData.selector)

This function requires the caller to be either

  • the specific collection admin
  • the setCollectionData function admin
  • the global admin
require(
(isCollectionCreated[_collectionID] == true) &&
(collectionFreeze[_collectionID] == false) &&
(_collectionTotalSupply <= 10000000000),
"err/freezed"
);

Then, this function requires the collection to be created and not frozen.

Furthermore, the desired total supply of the NFT collection must not exceed 10¹⁰.

if (
collectionAdditionalData[_collectionID].collectionTotalSupply == 0
) {
collectionAdditionalData[_collectionID]
.collectionArtistAddress = _collectionArtistAddress;
collectionAdditionalData[_collectionID]
.maxCollectionPurchases = _maxCollectionPurchases;
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;
}

If the total supply is still zero, the function will update the state variables for

  • total supply
  • max purchase per address for the public sale
  • the gap period for the admin to finalize the final total supply after the minting process ends

Furthermore, the artist’s ETH address is also updated.

else if (artistSigned[_collectionID] == false) {
collectionAdditionalData[_collectionID]
.collectionArtistAddress = _collectionArtistAddress;
collectionAdditionalData[_collectionID]
.maxCollectionPurchases = _maxCollectionPurchases;
collectionAdditionalData[_collectionID]
.setFinalSupplyTimeAfterMint = _setFinalSupplyTimeAfterMint;
}

On the other hand, if the total supply is not zero and the artist has not signed the collection yet (we will discuss this artist signing mechanism later), then the admin could still update

  • artist’s ETH address
  • max purchase per address for the public sale
  • the gap period for the admin to finalize the final total supply after the minting process ends

But if the total supply is not zero and the artist has signed the collection, then the admin can only update

  • max purchase per address for the public sale
  • the gap period for the admin to finalize the final total supply after the minting process ends

Let’s move on to the next step.

5. Artists sign and propose revenue distribution: Artists digitally sign their collections by sending their signatures on-chain. Additionally, they propose separate addresses for primary sales proceeds and secondary royalty proceeds, providing clarity on revenue distribution. Note that, the minting revenue allocated for the artist will be distributed to their proposed addresses for primary sales.

Let’s break it down.

Artists digitally sign their collections by sending their signatures on-chain

This step requires the artist to call the artistSignature function.

// function for artist signature
function artistSignature(
uint256 _collectionID,
string memory _signature
) public {

require(
msg.sender ==
collectionAdditionalData[_collectionID].collectionArtistAddress,
"Only artist"
);
require(artistSigned[_collectionID] == false, "Already Signed");
artistsSignatures[_collectionID] = _signature;
artistSigned[_collectionID] = true;
}

Note that,

  1. This function requires the caller to be the artist account of the specific collection.
  2. The signature must not already be set.

Additionally, they propose separate addresses for primary sales proceeds and secondary royalty proceeds, providing clarity on revenue distribution.

Let’s take a look at the code of this proposing process first

// function to propose primary addresses and percentages for each address

function proposePrimaryAddressesAndPercentages(uint256 _collectionID, address _primaryAdd1, address _primaryAdd2, address _primaryAdd3, uint256 _add1Percentage, uint256 _add2Percentage, uint256 _add3Percentage) public ArtistOrAdminRequired(_collectionID, this.proposePrimaryAddressesAndPercentages.selector) {
require (collectionArtistPrimaryAddresses[_collectionID].status == false, "Already approved");
require (_add1Percentage + _add2Percentage + _add3Percentage == collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage, "Check %");
collectionArtistPrimaryAddresses[_collectionID].primaryAdd1 = _primaryAdd1;
collectionArtistPrimaryAddresses[_collectionID].primaryAdd2 = _primaryAdd2;
collectionArtistPrimaryAddresses[_collectionID].primaryAdd3 = _primaryAdd3;
collectionArtistPrimaryAddresses[_collectionID].add1Percentage = _add1Percentage;
collectionArtistPrimaryAddresses[_collectionID].add2Percentage = _add2Percentage;
collectionArtistPrimaryAddresses[_collectionID].add3Percentage = _add3Percentage;
collectionArtistPrimaryAddresses[_collectionID].status = false;
}

The first part is, that this function requires the caller to be either

  • the artist
  • the proposePrimaryAddressesAndPercentages function admin
  • the global admin
// certain functions can only be called by an admin or the artist
modifier ArtistOrAdminRequired(uint256 _collectionID, bytes4 _selector) {
require(msg.sender == gencore.retrieveArtistAddress(_collectionID) || adminsContract.retrieveFunctionAdmin(msg.sender, _selector) == true || adminsContract.retrieveGlobalAdmin(msg.sender) == true, "Not allowed");
_;
}

Then, this function requires the proposal not to be accepted already

require (collectionArtistPrimaryAddresses[_collectionID].status == false, "Already approved");

Furthermore, the sum value of each split must equal the total allocation of the artist

require (_add1Percentage + _add2Percentage + _add3Percentage == collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage, "Check %");

Finally, it updates the artist’s proposal addresses for distributing the primary market sale revenue.

collectionArtistPrimaryAddresses[_collectionID].primaryAdd1 = _primaryAdd1;
collectionArtistPrimaryAddresses[_collectionID].primaryAdd2 = _primaryAdd2;
collectionArtistPrimaryAddresses[_collectionID].primaryAdd3 = _primaryAdd3;
collectionArtistPrimaryAddresses[_collectionID].add1Percentage = _add1Percentage;
collectionArtistPrimaryAddresses[_collectionID].add2Percentage = _add2Percentage;
collectionArtistPrimaryAddresses[_collectionID].add3Percentage = _add3Percentage;
collectionArtistPrimaryAddresses[_collectionID].status = false;

The same logic is applied to the secondary market sale revenue

// function to propose secondary addresses and percentages for each address

function proposeSecondaryAddressesAndPercentages(uint256 _collectionID, address _secondaryAdd1, address _secondaryAdd2, address _secondaryAdd3, uint256 _add1Percentage, uint256 _add2Percentage, uint256 _add3Percentage) public ArtistOrAdminRequired(_collectionID, this.proposeSecondaryAddressesAndPercentages.selector) {
require (collectionArtistSecondaryAddresses[_collectionID].status == false, "Already approved");
require (_add1Percentage + _add2Percentage + _add3Percentage == collectionRoyaltiesSecondarySplits[_collectionID].artistPercentage, "Check %");
collectionArtistSecondaryAddresses[_collectionID].secondaryAdd1 = _secondaryAdd1;
collectionArtistSecondaryAddresses[_collectionID].secondaryAdd2 = _secondaryAdd2;
collectionArtistSecondaryAddresses[_collectionID].secondaryAdd3 = _secondaryAdd3;
collectionArtistSecondaryAddresses[_collectionID].add1Percentage = _add1Percentage;
collectionArtistSecondaryAddresses[_collectionID].add2Percentage = _add2Percentage;
collectionArtistSecondaryAddresses[_collectionID].add3Percentage = _add3Percentage;
collectionArtistSecondaryAddresses[_collectionID].status = false;
}

After the artist proposes, the admin can choose to accept the proposal by calling the acceptAddressesAndPercentages function

// function to accept primary addresses and percentages

function acceptAddressesAndPercentages(uint256 _collectionID, bool _statusPrimary, bool _statusSecondary) public FunctionAdminRequired(this.acceptAddressesAndPercentages.selector) {
collectionArtistPrimaryAddresses[_collectionID].status = _statusPrimary;
collectionArtistSecondaryAddresses[_collectionID].status = _statusSecondary;
}

Now, the artist has set their address for distributing the revenue.

Let’s move on the the next step.

6. Artists define the minting process: Artists collaborate closely with admins to determine the most suitable minting process for their collections. They can select from various options, including allowlists, public sales, or a combination of both. Furthermore, they can choose from NextGen’s diverse range of NFT price models: Fixed Price Sales, Exponential Descending Sales, and Linear Descending Sales.

Let’s break it down.

Artists collaborate closely with admins to determine the most suitable minting process for their collections. They can select from various options, including allowlists, public sales, or a combination of both.

First, the artist chooses his preferred price model and sale phases.

They can select from various options, including allowlists, public sales, or a combination of both.

For sale phases, he can set both

  • allowlist sale phase
  • public sale phase

Furthermore, they can choose from NextGen’s diverse range of NFT price models: Fixed Price Sales, Exponential Descending Sales, and Linear Descending Sales.

For the price model, he can choose between

  • Fixed price
  • Descending price: this can be exponential descending or linear descending
  • Ascending price: the price will be increased per minting

Let’s take a look at the code for setting the sale phases

// function to add a collection's start/end times and merkleroot

function setCollectionPhases(uint256 _collectionID, uint _allowlistStartTime, uint _allowlistEndTime, uint _publicStartTime, uint _publicEndTime, bytes32 _merkleRoot) public CollectionAdminRequired(_collectionID, this.setCollectionPhases.selector) {
require(setMintingCosts[_collectionID] == true, "Set Minting Costs");
collectionPhases[_collectionID].allowlistStartTime = _allowlistStartTime;
collectionPhases[_collectionID].allowlistEndTime = _allowlistEndTime;
collectionPhases[_collectionID].merkleRoot = _merkleRoot;
collectionPhases[_collectionID].publicStartTime = _publicStartTime;
collectionPhases[_collectionID].publicEndTime = _publicEndTime;
}

This function requires the caller to be either

  • the collection admin
  • the CollectionAdminRequired function admin
  • the global admin
CollectionAdminRequired(_collectionID, this.setCollectionPhases.selector)

Then, this function sets the period for the allowlist sale phase and the public sale phase.

collectionPhases[_collectionID].allowlistStartTime = _allowlistStartTime;
collectionPhases[_collectionID].allowlistEndTime = _allowlistEndTime;
collectionPhases[_collectionID].merkleRoot = _merkleRoot;
collectionPhases[_collectionID].publicStartTime = _publicStartTime;
collectionPhases[_collectionID].publicEndTime = _publicEndTime;

Note that, a collection can have an arbitrary number of allowlist phases (starting and ending times), each with its allowlist (this refers to the merkleRoot variable).

However, to have an arbitrary number of allowlist phases the admin must wait until the previous allowlist phase ends and call the setCollectionPhases again.

Also, both the allowlist sale phase and public sale phase are optional.

Now, let’s take a look at the code for setting the price

// function to add a collection's minting costs

function setCollectionCosts(uint256 _collectionID, uint256 _collectionMintCost, uint256 _collectionEndMintCost, uint256 _rate, uint256 _timePeriod, uint8 _salesOption, address _delAddress) public CollectionAdminRequired(_collectionID, this.setCollectionCosts.selector) {
require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data");
collectionPhases[_collectionID].collectionMintCost = _collectionMintCost;
collectionPhases[_collectionID].collectionEndMintCost = _collectionEndMintCost;
collectionPhases[_collectionID].rate = _rate;
collectionPhases[_collectionID].timePeriod = _timePeriod;
collectionPhases[_collectionID].salesOption = _salesOption;
collectionPhases[_collectionID].delAddress = _delAddress;
setMintingCosts[_collectionID] = true;
}

First, this setCollectionCosts function requires the caller to be either

  • the collection admin
  • the CollectionAdminRequired function admin
  • the global admin
CollectionAdminRequired(_collectionID, this.setCollectionCosts.selector)

Then, this setCollectionCosts function requires the setCollectionData to be called properly, we already discussed how this setCollectionData function works.

require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data");

Since this setCollectionCosts function supports both 3 pricing models, let’s start with how the code for the first one works, the “Fixed price” model.

First, the fixed price can be set to the variable called collectionMintCost.

collectionPhases[_collectionID].collectionMintCost = _collectionMintCost;

This fixed-price model has no use for the variables called collectionEndMintCost, rate, and timePeriod. The admin can set these values to any value, it won’t be read in the getPrice function anyway.

collectionPhases[_collectionID].collectionEndMintCost = _collectionEndMintCost;
collectionPhases[_collectionID].rate = _rate;
collectionPhases[_collectionID].timePeriod = _timePeriod;

The important part is, for the fixed-price calculation to work correctly. The admin must set the variable salesOption to the value 1.

collectionPhases[_collectionID].salesOption = _salesOption;

Finally, the flag for setting the price will be triggered to the value true.

setMintingCosts[_collectionID] = true;

Note that, besides setting the price this setCollectionCosts function is also used for setting the delegation address that the customers desire for getting the NFT in the minting process. We will discuss how this delegation process works later.

That is a wrap for the fixed-price model code.

For the descending price model, the admin must choose between exponential/linear descending.

If the exponential descending price is chosen, the admin must call the setCollectionCosts with these arguments.

_collectionMintCost = starting price
_collectionEndMintCost = ending price
_rate = 0
_timePeriod = time period for each time the price is decreased
_salesOption = 2

For example, the price can be shown in the image below

source: https://seize-io.gitbook.io/nextgen/for-creators/sales-models

In this example, the NFT price initially starts at 4 ETH and gradually decreases over time until it reaches the resting price of 1 ETH 30 minutes after the sale begins. This price remains constant until the end of the minting process.

Here’s a breakdown of the price changes during each time period:

  • 1st time period (0 to 10 minutes): The price decreases from 4 ETH to 2 ETH.
  • 2nd time period (10 to 20 minutes): The price decreases from 2 ETH to 1.3333… ETH.
  • 3rd time period (20 to 30 minutes): The price decreases from 1.3333… ETH to 1 ETH.

After reaching the resting price of 1 ETH at the end of the 3rd time period, the price remains unchanged until the end of the minting process.

That is a wrap for the exponential descending pricing model.

On the other hand, the arguments for the linear descending pricing model can be set as

_collectionMintCost = starting price
_collectionEndMintCost = ending price
_rate = the rate for decreasing the price (must be > 0)
_timePeriod = time period for each time the price is decreased
_salesOption = 2

The price will be decreased in a linear manner as shown in the image below.

source: https://seize-io.gitbook.io/nextgen/for-creators/sales-models

Finally, for the ascending price model, the arguments can be set as

_collectionMintCost = starting price
_collectionEndMintCost = ending price
_rate = the rate for increasing the price (if zero, then the price is stable)
_timePeriod = any value, it will not be used in this model anyway
_salesOption = 3

The price will increase per minting as shown in the image below

https://seize-io.gitbook.io/nextgen/for-creators/sales-models

The ascending price model limits the number of NFTs that can be minted per period. For example, only one NFT can be minted per minute, hour, or day. The minting price can either remain constant or increase over time using a bonding curve.

For instance, let’s assume the minting period is set as 10 minutes and the minting sale starts on July 24, 2023, at 2:00 PM. The first minting occurs at 2:03 PM. Users can mint again after the time period ends, which is at 2:10 PM. If they attempt to mint before 2:10 PM, their transaction will be canceled.

Any unminted NFTs from previous periods are carried over to future periods for minting. In the example above, if no NFTs were minted during the first period, a user could mint two NFTs during the second period. However, since the maximum number of NFTs that can be minted per period is one, the user would need to execute two separate transactions to mint the two NFTs.

This is a wrap for the step “Artists define the minting process”.

Let’s move on to the next step.

7. Admins oversee the minting process: Admins set up crucial components like the token hash randomizer. This specialized tool generates a unique identifier, or hash, for each minted token. This hash is then sent to the Core contract, where it is securely stored and utilized by the generative script to produce the final artwork.

To prepare the minting components, the admin needs

  • set the randomizer of the NextGenCore contract
  • set the minter contract address to the NextGenCore contract

Let’s review each step’s code

  1. Setting the randomizer of the NextGenCore contract

The full code is

// Add Randomizer contract on collection

function addRandomizer(
uint256 _collectionID,
address _randomizerContract
) public FunctionAdminRequired(this.addRandomizer.selector) {
require(
IRandomizer(_randomizerContract).isRandomizerContract() == true,
"Contract is not Randomizer"
);
collectionAdditionalData[_collectionID]
.randomizerContract = _randomizerContract;
collectionAdditionalData[_collectionID].randomizer = IRandomizer(
_randomizerContract
);
}

Let’s break it down

FunctionAdminRequired(this.addRandomizer.selector)

This addRandomizer function requires the caller to be either

  • the addRandomizer function admin
  • the global admin
require(
IRandomizer(_randomizerContract).isRandomizerContract() == true,
"Contract is not Randomizer"
);

Then this function requires the desired randomizer contract to implement the isRandomizerContract function and returns the value true when called.

Note that, the expected randomizer is either

  • RandomzierNXT contract: for NextGen custom-built randomizer aimed to reduce the oracle’s fee
  • RandomzierRNG contract: for the Arrng service
  • RandomizerVRF contract: for the ChainLink service

Finally, this addRandomzier function updates the related variables.

collectionAdditionalData[_collectionID]
.randomizerContract = _randomizerContract;
collectionAdditionalData[_collectionID].randomizer = IRandomizer(
_randomizerContract
);

That is a wrap for the step “Admins oversee the minting process”.

Let’s move on to the next step.

8. Users mint NFTs: With the minting process in place, users can seamlessly pay the specified price and mint the NFT. This triggers the Randomizer contract to generate a unique random hash for each minted token. These hashes are securely stored on-chain on the Core contract, providing the necessary input for the generative script to produce the final artwork.

Before taking a look at the full code. Note that, the admin should check if the artist’s signature is set before the users begin to mint NFTs. This is an off-chain process since it is not included in the smart contract.

Ok. the full code of minting is

// mint function

function mint(
uint256 _collectionID,
uint256 _numberOfTokens,
uint256 _maxAllowance,
string memory _tokenData,
address _mintTo,
bytes32[] calldata merkleProof,
address _delegator,
uint256 _saltfun_o
) public payable {
require(setMintingCosts[_collectionID] == true, "Set Minting Costs");
uint256 col = _collectionID;
address mintingAddress;
uint256 phase;
string memory tokData = _tokenData;
if (
block.timestamp >= collectionPhases[col].allowlistStartTime &&
block.timestamp <= collectionPhases[col].allowlistEndTime
) {
phase = 1;
bytes32 node;
if (_delegator != 0x0000000000000000000000000000000000000000) {
bool isAllowedToMint;
isAllowedToMint =
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
0x8888888888888888888888888888888888888888,
msg.sender,
1
) ||
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
0x8888888888888888888888888888888888888888,
msg.sender,
2
);
if (isAllowedToMint == false) {
isAllowedToMint =
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
collectionPhases[col].delAddress,
msg.sender,
1
) ||
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
collectionPhases[col].delAddress,
msg.sender,
2
);
}
require(isAllowedToMint == true, "No delegation");
node = keccak256(
abi.encodePacked(_delegator, _maxAllowance, tokData)
);
require(
_maxAllowance >=
gencore.retrieveTokensMintedALPerAddress(
col,
_delegator
) +
_numberOfTokens,
"AL limit"
);
mintingAddress = _delegator;
} else {
node = keccak256(
abi.encodePacked(msg.sender, _maxAllowance, tokData)
);
require(
_maxAllowance >=
gencore.retrieveTokensMintedALPerAddress(
col,
msg.sender
) +
_numberOfTokens,
"AL limit"
);
mintingAddress = msg.sender;
}
require(
MerkleProof.verifyCalldata(
merkleProof,
collectionPhases[col].merkleRoot,
node
),
"invalid proof"
);
} else if (
block.timestamp >= collectionPhases[col].publicStartTime &&
block.timestamp <= collectionPhases[col].publicEndTime
) {
phase = 2;
require(
_numberOfTokens <= gencore.viewMaxAllowance(col),
"Change no of tokens"
);
require(
gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) +
_numberOfTokens <=
gencore.viewMaxAllowance(col),
"Max"
);
mintingAddress = msg.sender;
tokData = '"public"';
} else {
revert("No minting");
}
uint256 collectionTokenMintIndex;
collectionTokenMintIndex =
gencore.viewTokensIndexMin(col) +
gencore.viewCirSupply(col) +
_numberOfTokens -
1;
require(
collectionTokenMintIndex <= gencore.viewTokensIndexMax(col),
"No supply"
);
require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH");
for (uint256 i = 0; i < _numberOfTokens; i++) {
uint256 mintIndex = gencore.viewTokensIndexMin(col) +
gencore.viewCirSupply(col);
gencore.mint(
mintIndex,
mintingAddress,
_mintTo,
tokData,
_saltfun_o,
col,
phase
);
}
collectionTotalAmount[col] = collectionTotalAmount[col] + msg.value;
// control mechanism for sale option 3
if (collectionPhases[col].salesOption == 3) {
uint timeOfLastMint;
if (lastMintDate[col] == 0) {
// for public sale set the allowlist the same time as publicsale
timeOfLastMint =
collectionPhases[col].allowlistStartTime -
collectionPhases[col].timePeriod;
} else {
timeOfLastMint = lastMintDate[col];
}
// uint calculates if period has passed in order to allow minting
uint tDiff = (block.timestamp - timeOfLastMint) /
collectionPhases[col].timePeriod;
// users are able to mint after a day passes
require(tDiff >= 1 && _numberOfTokens == 1, "1 mint/period");
lastMintDate[col] =
collectionPhases[col].allowlistStartTime +
(collectionPhases[col].timePeriod *
(gencore.viewCirSupply(col) - 1));
}
}

This mint function supports every sale phase and pricing model.

Let’s break it down line by line.

public payable

First, this function can be called by anyone and can be sent with ETH. We will discuss the sent ETH later.

The next part of the code handles the sale phases including

  • allowlist sale phase
  • public sale phase

For the allowlist phase, let’s group the related code first.

This code handles

  1. minting delegation
  2. validating the minter

Let’s break down the code for handling minting delegation

if (_delegator != 0x0000000000000000000000000000000000000000) {
bool isAllowedToMint;
isAllowedToMint =
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
0x8888888888888888888888888888888888888888,
msg.sender,
1
) ||
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
0x8888888888888888888888888888888888888888,
msg.sender,
2
);

If the buyer desires to use delegation, the mint function will check if the desired delegator delegates this msg.sender for either.

  • All NFT collection (0x8888888888888888888888888888888888888888) for all usecases (use case value = 1)
  • All NFT collection (0x8888888888888888888888888888888888888888) for minting (use case value = 2)
if (isAllowedToMint == false) {
isAllowedToMint =
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
collectionPhases[col].delAddress,
msg.sender,
1
) ||
dmc.retrieveGlobalStatusOfDelegation(
_delegator,
collectionPhases[col].delAddress,
msg.sender,
2
);
}

If the check fails, then the mint function will check for the delegation for this specific NFT collection (collectionPhases[col].delAddress) rather than for all collections (0x8888888888888888888888888888888888888888).

require(isAllowedToMint == true, "No delegation");

If the desired delegation is valid, then the call is reverted.

node = keccak256(
abi.encodePacked(_delegator, _maxAllowance, tokData)
);
require(
_maxAllowance >=
gencore.retrieveTokensMintedALPerAddress(
col,
_delegator
) +
_numberOfTokens,
"AL limit"
); //this line
mintingAddress = _delegator;

Then, the mint function will prevent the desired delegator from minting over their limit in the allowlist phase.

The same “preventing from minting over their limit” logic is applied for non-delegation minting.

else {
node = keccak256(
abi.encodePacked(msg.sender, _maxAllowance, tokData)
);
require(
_maxAllowance >=
gencore.retrieveTokensMintedALPerAddress(
col,
msg.sender
) +
_numberOfTokens,
"AL limit"
); // this line
mintingAddress = msg.sender;
}

Then, the mint function will validate the minting address to be in the allowlist accounts.

require(
MerkleProof.verifyCalldata(
merkleProof,
collectionPhases[col].merkleRoot,
node
),
"invalid proof"
);

That is a wrap for the allowlist sale phase. Let’s move on to the public sale phase.

else if (
block.timestamp >= collectionPhases[col].publicStartTime &&
block.timestamp <= collectionPhases[col].publicEndTime
) {
phase = 2;
require(
_numberOfTokens <= gencore.viewMaxAllowance(col),
"Change no of tokens"
);
require(
gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) +
_numberOfTokens <=
gencore.viewMaxAllowance(col),
"Max"
); // this line
mintingAddress = msg.sender;
tokData = '"public"';
}

For the public sale phase, the mint function will prevent the minters from exceeding their public minting amount limit.

After handling the sale phases, the next part is preventing the minting amount from exceeding the total supply.

uint256 collectionTokenMintIndex;
collectionTokenMintIndex =
gencore.viewTokensIndexMin(col) +
gencore.viewCirSupply(col) +
_numberOfTokens -
1;
require(
collectionTokenMintIndex <= gencore.viewTokensIndexMax(col),
"No supply"
);

Then, the mint function validates the sent ETH to exceed the minting price.

require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH");

The NFT minting occurs and the collected ETH is accounted.

for (uint256 i = 0; i < _numberOfTokens; i++) {
uint256 mintIndex = gencore.viewTokensIndexMin(col) +
gencore.viewCirSupply(col);
gencore.mint(
mintIndex,
mintingAddress,
_mintTo,
tokData,
_saltfun_o,
col,
phase
);
}
collectionTotalAmount[col] = collectionTotalAmount[col] + msg.value;

Finally, for the ascending pricing model, the minting amount per period rule is applied. We have already discussed this “the minting amount per period” rule earlier.

// control mechanism for sale option 3
if (collectionPhases[col].salesOption == 3) {
uint timeOfLastMint;
if (lastMintDate[col] == 0) {
// for public sale set the allowlist the same time as publicsale
timeOfLastMint =
collectionPhases[col].allowlistStartTime -
collectionPhases[col].timePeriod;
} else {
timeOfLastMint = lastMintDate[col];
}
// uint calculates if period has passed in order to allow minting
uint tDiff = (block.timestamp - timeOfLastMint) /
collectionPhases[col].timePeriod;
// users are able to mint after a day passes
require(tDiff >= 1 && _numberOfTokens == 1, "1 mint/period");
lastMintDate[col] =
collectionPhases[col].allowlistStartTime +
(collectionPhases[col].timePeriod *
(gencore.viewCirSupply(col) - 1));
}

That is a wrap for the step “Users mint NFTs”.

Let’s move on to the next step.

Admins finalize the minting process: After careful consideration of the artists’ preferences and ensuring alignment with the collection’s objectives, admins finalize the minting process using the artist’s chosen minting process.

The full code of this step is

// set final supply

function setFinalSupply(
uint256 _collectionID
) public FunctionAdminRequired(this.setFinalSupply.selector) {
require(
block.timestamp >
IMinterContract(minterContract).getEndTime(_collectionID) +
collectionAdditionalData[_collectionID]
.setFinalSupplyTimeAfterMint,
"Time has not passed"
);
collectionAdditionalData[_collectionID]
.collectionTotalSupply = collectionAdditionalData[_collectionID]
.collectionCirculationSupply;
collectionAdditionalData[_collectionID].reservedMaxTokensIndex =
(_collectionID * 10000000000) +
collectionAdditionalData[_collectionID].collectionTotalSupply -
1;
}

Let’s break it down.

FunctionAdminRequired(this.setFinalSupply.selector)

This function requires the caller to be either

  • the setFinalSupply function admin
  • the global admin
require(
block.timestamp >
IMinterContract(minterContract).getEndTime(_collectionID) +
collectionAdditionalData[_collectionID]
.setFinalSupplyTimeAfterMint,
"Time has not passed"
);

Then, this setFinalSupply function validates the call to be after the minting process ends.

collectionAdditionalData[_collectionID]
.collectionTotalSupply = collectionAdditionalData[_collectionID]
.collectionCirculationSupply;
collectionAdditionalData[_collectionID].reservedMaxTokensIndex =
(_collectionID * 10000000000) +
collectionAdditionalData[_collectionID].collectionTotalSupply -
1;

Finally, it updates the total supply to be equal to the total minted NFT count.

That is a wrap the step “Admins finalize the minting process”. Let’s move on to the final step.

10. Admins send the money from sales to the artist: Admins can use the smart contract to send the money from sales directly to the artist’s address.

The full code for this step is

// function to pay the artist

function payArtist(
uint256 _collectionID,
address _team1,
address _team2,
uint256 _teamperc1,
uint256 _teamperc2
) public FunctionAdminRequired(this.payArtist.selector) {
require(
collectionArtistPrimaryAddresses[_collectionID].status == true,
"Accept Royalties"
);
require(
collectionTotalAmount[_collectionID] > 0,
"Collection Balance must be grater than 0"
);
require(
collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage +
_teamperc1 +
_teamperc2 ==
100,
"Change percentages"
);
uint256 royalties = collectionTotalAmount[_collectionID];
collectionTotalAmount[_collectionID] = 0;
address tm1 = _team1;
address tm2 = _team2;
uint256 colId = _collectionID;
uint256 artistRoyalties1;
uint256 artistRoyalties2;
uint256 artistRoyalties3;
uint256 teamRoyalties1;
uint256 teamRoyalties2;
artistRoyalties1 =
(royalties *
collectionArtistPrimaryAddresses[colId].add1Percentage) /
100;
artistRoyalties2 =
(royalties *
collectionArtistPrimaryAddresses[colId].add2Percentage) /
100;
artistRoyalties3 =
(royalties *
collectionArtistPrimaryAddresses[colId].add3Percentage) /
100;
teamRoyalties1 = (royalties * _teamperc1) / 100;
teamRoyalties2 = (royalties * _teamperc2) / 100;
(bool success1, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd1
).call{value: artistRoyalties1}("");
(bool success2, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd2
).call{value: artistRoyalties2}("");
(bool success3, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd3
).call{value: artistRoyalties3}("");
(bool success4, ) = payable(tm1).call{value: teamRoyalties1}("");
(bool success5, ) = payable(tm2).call{value: teamRoyalties2}("");
emit PayArtist(
collectionArtistPrimaryAddresses[colId].primaryAdd1,
success1,
artistRoyalties1
);
emit PayArtist(
collectionArtistPrimaryAddresses[colId].primaryAdd2,
success2,
artistRoyalties2
);
emit PayArtist(
collectionArtistPrimaryAddresses[colId].primaryAdd3,
success3,
artistRoyalties3
);
emit PayTeam(tm1, success4, teamRoyalties1);
emit PayTeam(tm2, success5, teamRoyalties2);
}

Let’s break it down line by line.

FunctionAdminRequired(this.payArtist.selector)

This function requires the caller to be either

  • the payArtist function admin
  • or the global function admin
require(
collectionArtistPrimaryAddresses[_collectionID].status == true,
"Accept Royalties"
);

Then, this function requires the admin to accept the artist's proposed address for splitting revenue from primary market sales.

require(
collectionTotalAmount[_collectionID] > 0,
"Collection Balance must be grater than 0"
);

This line requires the collected ETH amount to be greater than zero.

require(
collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage +
_teamperc1 +
_teamperc2 ==
100,
"Change percentages"
);

Then, this payArtist function validates allocation between the NextGen developer team and the artist.

require(
collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage +
_teamperc1 +
_teamperc2 ==
100,
"Change percentages"
);

Finally, this payArtist function updates to withdrawn ETH and sends the ETH to artist addresses and NextGen developer addresses

collectionTotalAmount[_collectionID] = 0;
address tm1 = _team1;
address tm2 = _team2;
uint256 colId = _collectionID;
uint256 artistRoyalties1;
uint256 artistRoyalties2;
uint256 artistRoyalties3;
uint256 teamRoyalties1;
uint256 teamRoyalties2;
artistRoyalties1 =
(royalties *
collectionArtistPrimaryAddresses[colId].add1Percentage) /
100;
artistRoyalties2 =
(royalties *
collectionArtistPrimaryAddresses[colId].add2Percentage) /
100;
artistRoyalties3 =
(royalties *
collectionArtistPrimaryAddresses[colId].add3Percentage) /
100;
teamRoyalties1 = (royalties * _teamperc1) / 100;
teamRoyalties2 = (royalties * _teamperc2) / 100;
(bool success1, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd1
).call{value: artistRoyalties1}("");
(bool success2, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd2
).call{value: artistRoyalties2}("");
(bool success3, ) = payable(
collectionArtistPrimaryAddresses[colId].primaryAdd3
).call{value: artistRoyalties3}("");
(bool success4, ) = payable(tm1).call{value: teamRoyalties1}("");
(bool success5, ) = payable(tm2).call{value: teamRoyalties2}("");

That’s a wrap for NextGen’s selling NFT breakdown. In addition to the features we’ve discussed, the platform also provides several other functionalities:

  • Air dropping NFT
  • NFT MintPass
  • NFT auction
  • Failsafe mechanism

Although we won’t dive into these topics in this article, they represent additional valuable features that NextGen offers to its users.

Also please note that:

  1. This article is based on the non-production version of the platform. The platform is still in the auditing process on Code4rena. The actual functionality and features may differ from what is described in this article once the platform is released to production.
  2. we are not publishing or discussing any findings publicly before the release of the audit report. This article solely focuses on breaking down the functionality of the smart contract.

Author Detail

Nattawat Songsom, Smart Contract Auditor and Consultant.

About Valix Consulting

Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise, industry knowledge, and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please contact us via Twitter, Facebook, or info@valix.io.

--

--