ERC721G — A Creative and Utilitarian Approach to Batch Minting
This is the developer-oriented article. For a general overview, find twitter thread and https://erc721g.gangsterallstar.com
For a business-oriented description, scroll to the bottom.
Batch processing in this context is a SSTORE optimization method for smart contracts to achieve results that otherwise would require multiple SSTORE writes in a single SSTORE write.
What this essentially means is that we are able to save gas on otherwise gas-intensive transactions such as multi-minting NFTs heavily by making sure that the result of the transaction correlates with the truth-of-state that a multi-SSTORE write transaction such as multi-minting would be.
To simplify, batch minting saves gas costs on multi-mints.
ERC721G is my own, personal creative take on the batch minting, and phantom methods that has been popularized by ERC721A of Azuki.
The Creative Process
To write ERC721G comes from two inefficient mappings, and 1 conceptual theory.
By optimizing these two points, we are able to create a powerful contract that can do much more than a standard ERC721, while still consuming near-same amounts of gas doing so, and in multiple ways, reduce gas cost for processes that otherwise would require much more SSTORE and processing to achieve.
The Inefficient Mappings
ownerOf stores the owner of the token ID. It uses a uint256 to address mapping which uses 20 bytes. 12 available bytes is wasted.
balanceOf stores the balance of NFTs to an address. It uses a uint256 which is a 32 byte type, but generally since NFTs don’t exceed 50,000, this can be easily stored in 4 to 8 bytes. Thus, 28 available bytes is wasted.
The New Efficient Mappings
To use these available bytes, we used Structs that can make use of these wasted data slots.
In replacement of ownerOf, we have _tokenData which links a uint256 tokenID to an OwnerStruct Struct that utilizes the entire 32 available bytes — 20 for the owner address, 4 for the last transfer, 4 for the stake timestamp, and 4 for total time staked.
In replacement of balanceOf, we have _balanceData which links an address to a BalanceStruct that utilizes 8 of 32 available bytes — 4 for the token balance, and 4 for the amount of tokens minted for that address. Yes — there is still available bytes for further features!
By implementing these new, efficient data structures, we are able to pack additional features into a single contract:
- Tracking the last transfer time
- Stake the NFT
- Track the total amount of time staked
All in one contract, in a single SSTORE! This is incredibly efficient and saves a lot of GAS in order to do these actions. In a realistic multi-contract and multi-mapping implementation, the estimated GAS savings could be up to 95%. That’s from 100 USD in GAS down to 5 USD.
The Conceptual Theory
The Conceptual Theory that we will be using is that — given a function’s arguments, a smart contract behavior is derived. If we are able to derive the smart contract behavior in such a way that the outside interactions (generally, through read functions) is as expected, optimizations can be performed.
This is the core of batch processing — save SSTORE writes but output READs in such a way that mimics the behavior of a smart contract that requires multiple SSTORE writes.
If that sounds too complex, imagine instead of pushing a heavy box to the other side of the room, you use a trolley instead.
The result is the same, just the force required to achieve it was less.
This is, in a way, the conceptual theory.
Of course, there are more specific details, and programming is not as simple as pushing a heavy box, but the achievable result is that minting costs are reduced for multi-mints, albeit at the cost of a more expensive initial transfer. This affects all implementations of batch minting.
The Efficient Implementation
However, SSTORE packing, either through Structs or bytecode, can achieve the above result without trade-offs as well, by implementing efficient usage of available bytes and packing multiple variables in Structs — exactly what we did with ERC721G
By packing all 4 data points (address, last transfer, stake timestamp, total time staked) into a single 32-byte Struct, we are able to save 4x of the SSTORE gas cost of each of these data points.
Batch minting is a method to mint multiple NFTs using a single SSTORE write. This generally is done through lookup tracing and phantom minting.
Lookup tracing is a way to find the source-of-truth through a while-loop or a for-loop. In this batch minting implementation, we use a mapping of mintIndex as the source-of-truth lookup, and do a lookup tracing from the initial token ID backwards to figure out the location of the source-of-truth then read from it.
mintIndex is a mapping of uint256 token ID to address (of minter) which forever stores the initial state of owners through lookup tracing.
As seen from above, there are two states of knowledge — Initialized, and Uninitialized.
Initialized state stands for the state once _tokenData has been populated — which happens on the first transfer of the token. If the token is initialized, always use the initialized state. A SLOAD is done and returned from _tokenData.
Uninitialized state stands for the state before _tokenData has been populated — which happens on the mint, and the token has never been transferred. A lookup trace is done to find the source-of-truth. A SLOAD is done and returned from mintIndex.
With these implementations, and a clever lookup tracing logic, we are able to implement batch minting. Hooray!
Batch Minting + Staking
In addition to batch minting, ERC721G implements a creative innovation that allows batch minting and staking at once. This has minimal gas impacts to batch minting, and efficiently mints and stakes multiple tokens at once for a fraction of the cost.
Following the batch minting logic, we added a tracker for Stake Timestamp as mentioned before. In batch mint and stake, we modify the Stake Timestamp to be the current time (block.timestamp) in mintIndex.
Since mintIndex uses source-of-truth logic, on a lookup trace, the source-of-truth returns the Stake timestamp as well.
This enables one to do batch minting AND batch staking in a SINGLE SSTORE write. Absolutely nuts!
Two Transfer events are also emitted to indicate a mint to the minter, and then a transfer to the staking address, making the transaction events appear as intended of any staking contract.
The last part of staking is achieved through a special ownerOf that accounts for both the Initialized and Uninitialized state, as well as Staked and Unstaked conditions.
This allows the token to be in multiple state-possibilities at once by using the same SSTORE slot — and then proxied through the Stake Timestamp to determine whether the owner is the staking address or the owner in Initialized or Uninitialized states.
This in turn results in a completely compliant staking logic and READ outputs of any smart contract that implements NFT staking.
If anyone has done staking before, then they will know that they will have to store the staker address of the token ID to the smart contract in order to figure out who has the ability to unstake.
For ERC721G, our creative take utilizes the same SSTORE slot to store the staker address. Internally, instead of using ownerOf in the unstake function, which returns the staking address when it determines that the token is staked, we use the internal _trueOwnerOf function which always returns either the Initialized or Uninitialized token owner data.
Because of this, we are able to use a single SSTORE slot for batch minting, batch mint and stake, as well as unstaking. Amazing!
Keeping Track of Total Time Staked
In addition to staking and unstaking, I figured a useful metric that we can measure for an NFT is the total time staked, which is used in some staking implementations such as MoonBirds’ “nesting” logic.
Because our OwnerStruct stores this data in a 4-byte uint32 data type, we are able to pack this in the single SSTORE that we used as well.
Our _setStakeTimestamp, which is our internal staking logic, calculates and adds the total time staked to the tracker immediately on unstake, which results in a unique tracker for each NFT for their total time staked. Cool!
Last but not least, each token comes with their own tracker for the last timestamp of transfer. Project owners can use this logic to determine how long a token has been held and do creative things with it.
The implementation of this is simple — a 4-byte uint32 storage type in the OwnerStruct SSTORE that gets set to the block.timestamp on an internal _transfer method.
ERC721G’s functions are divded into nice modules of internal functions.
Which developers can use to implement, omit, override, and modify to their hearts’ content.
If you don’t need _stake and _unstake functions in your NFT contract, simply comment out the public, built-in stake and unstake functions, and you have a working ERC721 smart contract that serves batch minting purposes.
For modifications, you can either inherit _stake and _unstake to your own stake functions, or override the public built-in stake and unstake functions to implement your own custom logic before or after the staking function.
More details will come out once the contract is released to the public and open-sourced.
And there you have it! A token standard that does:
- Batch Minting
- Batch Mint and Stake
- Single-Contract Staking
- Single-Contract Unstaking
- Keep track of Total Time Staked
- Keep track of Last Transfer Time
All in a single contract. Yay!
I am excited to see how developers will implement, build upon, and modify ERC721G!
A more Business-Oriented Description
For a more business-oriented description, consult this segment.
ERC721G is an innovative and creative take on the possibilities of an ERC721 contract.
It is a creative and innovative take created by 0xInuarashi, lead developer of Gangster All Star, to push the NFT space forwards, and invent new and efficient methods of achieving results with significantly reduced gas costs.
For this, a contract was created that implements the following logic in a single contract:
- Batch Minting — a gas-efficient method of minting
- Batch Mint and Stake — a gas-efficient method of minting and staking, potentially 2 to 3 times more efficient than Batch Minting alone
- Single-Contract Staking — a gas-efficient method of staking an NFT
- Single-Contract Unstaking — a gas-efficient method of unstaking an NFT
- Keep track of Total Time Staked — a useful metric tracker for NFTs that can be implemented for holding and staking rewards down the line
- Keep track of Last Transfer Time — a useful metric tracker for NFTs that can be used to encourage HODLing and reward users who held their NFTs for extensive periods of time
ERC721G implements these methods very efficiently, reducing all the above actions from requiring multiple contracts to implement, and costly gas, to a single contract, and saves up to 95% of the gas costs associated with each of the following functionalities through clever and creative programming.
ERC721G desires to bring additional utility and innovation to the NFT space that encourages creative programming as well as efficient, gas-saving practices for the growth of the space.
Made with love by 0xInuarashi