Tom Hirst

Posted on Nov 15, 2021Read on Mirror.xyz

Cut Minting Gas Costs By Up To 70% With One Smart Contract Tweak

In this article you’ll learn how to decrease the mint cost of your NFTs, increasing the chances of making more sales by saving your users real money.

Gas costs are a big factor in NFT projects. Especially with ETH reaching recent all-time highs. Whatever you can do to keep costs down, you should. At the Shiny Object Social Club, we’ve found a straightforward solution that many creators are missing.

When Shiny member, Craig Burgess, shared the extraordinarily low minting gas cost of his project, Real Fake Turnips, our developers, Jonathan Snow and Tom Hirst, wanted to figure out why. The smart contract used had a pattern we’d seen before. So we put things to the test.

We want to share this with the wider community so your next NFT project can benefit. Here’s what we discovered explained simply, along with steps to improve your code.

For reference, here are the before and after contracts we used to test transactions:

Disclaimer: There may be links to additional contracts in this article. We tested and deployed many variations of the above. Please don’t use any of the code within the contract without understanding its use case. In some cases, we valued speed of deployment over code review and wanted to have these on a live network for our tests and for the community to review.

TLDR

Here’s our main finding:

If you avoid using ERC721Enumerable and replace totalSupply() with a Counter to track token IDs, buyers can save up to 70% on minting fees and up to 35% on transfer fees.

https://twitter.com/tomosman/status/1459639356822638595

We encourage you to read the rest of this article to understand the finer details, get our bonus gas saving tips and hear our philosophy on the benefits smart contract efficiency offers to the wider space.

What Is Gas and Why Does It Matter

When you do anything that writes to the blockchain, you pay a transaction fee. These costs are commonly referred to as gas fees.

The full transaction fee to execute a function is calculated by taking the number of gas units used and multiplying it by the price per gas unit.

A useful analogy is to compare executing a smart contract transaction like planning a trip. You might know the destination, and how far away it is, but the trip can cost more or less depending on the gas costs for your specific vehicle.

Executing a smart contract is similar because when the gas price per unit on the protocol is high, the total calculation is more expensive. Meaning, minting an NFT will cost more at certain times than others, but there’s still something you can do about reducing costs relatively. A focus on reducing the gas used by a transaction is like buying a more fuel-efficient vehicle. You reach the same destination, but the cost of the journey will be less.

Gas fees are necessary on blockchains to prevent misuse. If it were free to transact the blockchain would become jammed with nefarious transactions. Genuine transactions would struggle to get through. In short, attaching a fee to transactions prevents the protocol from grinding to a halt through overuse.

While we’ve established the importance of gas fees, we need to touch on the problems they cause for the NFT space. As more people become interested, less will be savvy to the true cost of purchasing an NFT. When a consumer mints a piece of art, for example, they’ll pay the advertised fee from the creator + the transaction fee from the blockchain. This can be unexpected and lead to disappointment, especially in instances where the transaction fee matches or exceeds that of the mint price advertised.

There’s a demand to consider too. Ethereum is at the height of its popularity, with DeFi, altcoins and DAO tokens competing for block space alongside NFT smart contract transactions. When network traffic is high, gas costs more, because nodes can charge more to process transactions. And, those who want to push their transactions to the front of the queue, will pay these fees, oftentimes pricing out newcomers and average users.

The way that transaction fees are calculated and the specific gas costs for different operations, has changed over time. Most users of Ethereum, and some developers to boot, won’t notice these updates. But they’re important to keep track of when you’re focused on smart contract optimisation. The recent EIP-1559 update had a direct impact on gas fees that’s useful to explain here.

Previous to this upgrade, users submitting transactions would set a max price per gas. Any difference between the current “reasonable” gas price and the max price that you set, would go to the miner who processed your transaction as a tip. With EIP-1559, these fees are split explicitly. Buyers now choose the max price they’re willing to pay per gas, as well as a separate priority fee. The priority fee is the tip to the miner and the max price is the absolute highest the user is willing to pay per unit of gas. This gets complicated when you look at how a unit of gas gets priced.

The standout thing to remember here is that a base rate (price) exists; the current price of gas in Gwei. A block is a batch of transactions along with a hash of the previous block data in the chain (hence, “blockchain”). Blocks use automatic gas size adjustment based on network demand. Block capacity isn’t determined by the number of transactions it can process, but gas used by said transactions. If the previous block has a fill rate (gas used from the gas available) above 50%, the base gas price will increase by up to 12.5%. If the previous block has a fill rate of 50% there’s no change. If the fill rate is less than 50%, the base gas price will decrease by up to 12.5%.

For a transaction to succeed, your max price per gas would need to be at least that of the base gas price plus the priority fee you’ve promised to the miner. If not, your transaction might get stuck. Because it’s not priced competitively.

While this article isn’t about how transaction gas works per se, it’s helpful to understand the wider implications of inefficient smart contracts. If a smart contract uses more gas than necessary, does this artificially increase the block fill rate? In effect, increasing the base gas price, in turn, costing the very same inefficient smart contracts even more in transaction fees.

Are we perpetuating gas problems on Ethereum through lack of optimisation?

Gas problems are exacerbated with the increasing price of ETH. At the time of writing, ETH has almost tripled from July 20th 2021 ($1,787) to November 3rd 2021 ($4,590), in turn, making accessibility a bigger challenge than ever.

This presents a problem if you’re looking to launch an NFT project. You can advise people to wait until gas fees are low, but what if it never drops to an “acceptable” level for your target audience? No matter what your mint price, gas fees will render minting your project untenable to many. Good projects are struggling to mint out.

Luckily, there’s a portion of this calculation that’s within your engineering team’s control; to make your contract execution more “fuel-efficient” by using less gas.

Our tests to follow focus on the optimisation of ERC721 based smart contracts. We wanted to see if we could keep the same outcome to the end-user using a gas-savings-focused approach. The results were eye-opening.

The Good And Bad Of Boilerplate Code

Many NFT projects take advantage of the ERC721 standard contracts that are maintained by OpenZeppelin, a library of boilerplate smart contracts with essential features to be built on top of. Most non-fungible token projects require only minor additions after inheriting its core functionality.

A mantra of web3 is to embrace the benefits of collaboration and composability. Open source code allows developers to build on top of existing, tried and tested work. Crypto projects are more open than ever, with the smart contract code available for all to see—and learn from—on Etherscan.

There’s a lot of good to be had here. Teams can get projects out quicker, development costs are lower (meaning cheaper mints for consumers) and security is typically better as open source code is greater battle-tested in the wild than closed source code.

Anyone wanting to spin up an NFT project can take a look at the Bored Ape Yacht Club contract and implement something similar.

So what’s the bad? In a world where StackOverflow makes code available to everyone, it’s easy for developers to become lazy. We’ve all taken snippets and used them in our builds without taking the time to understand them. In Solidity development, where developer resources are sparse, oversights are punished with real money costs.

The immutable nature of smart contracts (they can’t be edited after deployment) means inefficiencies can’t be patched as they can in traditional software environments. The code you write now has to be good for the lifetime of the project.

Gas inefficiency in an ERC721-based project can hurt you—and your users—long-term.

The Discovery

While ideating in the build channel of Shiny’s Discord, Craig Burgess shared a new marketing angle for his project. While Real Fake Turnips got off to a flying start, the overall NFT market slow-down had bled into the project.

Craig had noticed, however, mounting positive feedback from investors noting the low gas fee when minting. In an environment where gas fees felt high in the general market, this assertion stood out.

You used to mint me on the blockchain.

But it wasn’t the first time we’d seen this. Shiny developer, Jonathan Snow, noted that he’d seen an out of the ordinary gas fee while minting the Entrope project, too.

He’d also encountered average gas consumption while building a product of his own and wanted to be able to identify the significant discrepancy. So being sticklers for optimisation, we decided to get our hands dirty.

Could we find a pattern? As developers, we wanted to understand what was happening here. Why do most other smart contracts cost more to mint? Jonathan and Tom quickly paired up to start testing, reviewing findings and producing this write up.

The Thesis

We compared the Real Fake Turnips smart contract with the Entrope smart contract. Then against most other projects that have cost us more to mint personally and Jonathan’s build.

The difference between the higher and lower gas fee contracts was the use of OpenZeppelin’s ERC721Enumerable. This contract is an extension of the ERC721 standard, with a handful of additional features. Diving deeper, we pattern matched the following:

The higher fee minting contracts were:

  • Using ERC721Enumerable or importing it in one of their contracts
  • Calling the inherited totalSupply() function when minting. This was a sign of the inefficiency, but in some cases, contracts implemented the Enumerable Library and did not leverage totalSupply()

The lower fee minting contracts were:

  • Using a basic ERC721 implementation
  • Replacing the use of totalSupply() with a Counters based solution for tracking token quantities and indexes

We had our point of focus, totalSuppy(). The contracts not leveraging it (specifically those not using the Enumerable library at all) were consuming 40-50% less gas when minting a single NFT. To identify whether other developers had documented this issue, Googling ensued. In an article titled, What I Learned From Building Cool Cats, developer xtremetom, detailed the unnecessary use of the totalSupply() function in “borrowed code” NFT projects:

“Every time totalSupply() is called it costs the user, resulting in higher gas prices.”

While this is useful information and may save gas, our solution focuses on replacing the functionality of totalSupply() with a lightweight counter that reduces the gas consumed when interacting with the contract. Our feelings were that the very inclusion of ERC721Enumerable is gas heavy, whether totalSupply() is called or not, due to additional state update operations that occur on every contract interaction.

So what does totalSupply() do? A call to the function returns the current number of ERC721 tokens minted by the contract. This is useful when developers want to determine what the total number of tokens minted are, and what ID to assign to the next token to mint. When a customer calls the contract’s mint function, the token ID is assigned and the totalSupply() function will update automatically to reflect the number of tokens minted.

One way to replace this functionality is to use a counter. Instead of calling totalySuppy() from ERC721Enumerable, one could keep track of the current token ID manually. Open Zeppelin has a Counters utility contract that can help you implement this. This is the solution both Real Fake Turnips and Entrope are using.

When writing Solidity, sometimes what looks like more code, which you’d think would increase gas consumption, can result in less. Just like this case. Importing a contract and inheriting functionality is quick, but it doesn’t mean that there isn’t a more efficient way.

Blindly using the totalSupply() functionality found in ERC721Enumerable might make things easier for developers in the short term. But not taking the time to understand, test and optimise your smart contract will incur people mounting costs in the long term.

These initial observations landed us on this thesis:

ERC721Enumerable costs more in gas to mint than ERC721 + Counters.

Next, we wanted to prove it.

Testing Our Thesis

Both Jonathan and Tom have been working on access pass NFT contracts for personal projects. Jonathan, for his NFT market analytics tool, niceJPEG, Tom, for his educational products. Jonathan took the reins on testing using his development environment for niceJPEG.

For context, the niceJPEG smart contract is very basic. Its goal is to provide tradable NFTs to grant owners of the NFT access to the software. It doesn’t need much functionality beyond that of the ERC721 standard. One could say this is a simple example. However, when you look at many other NFT projects, we feel this is a fair comparison of features needed in the wider space.

For reference and later comparison, niceJPEG mints using ERC721 Enumerable and totalSupply() were costing 142,378 to mint one token, and 602,190 to mint five.

We’d established that Entrope was minting with low gas consumption. Jonathan minted two NFTs from this contract with a gas consumption of 92,894 and 75,794. While this presented a slight range due to first interaction costs, these mints both used at least 40% less gas than the niceJPEG contract.

So what was the difference between the Entrope contract and the niceJPEG test subject? The former uses the ERC721 + Counters solution. The latter uses ERC721Enumerable calling totalSupply().

Onto the tests:

Test 1 - Avoid Calling totalSupply()

The first test Jonathan ran was to deploy the same contract for niceJPEG, but exclude calling totalSupply() explicitly in his own functions, while still importing ERC721Enumerable.

Gas consumption actually went up to 164,505 even though he wasn’t calling totalSupply() when minting, or at all. This is most likely due to the Counter (more below) being stored with a value of 0, costing additional gas. This wasn’t the smoking gun we were looking for. Which led us to a second test.

Test 2 - Exclude totalSupply() Altogether

Next, Jonathan deployed a new version of the niceJPEG contract. This time switching from ERC721 Enumerable to ERC721 + Counters.

Upon minting, gas consumption on transaction 1 (the first mint) was 92,889. The interesting finding was on transaction 2 gas consumption was just 58,689.

When testing a 5-mint loop, the first mint cost 178,553 while the second mint was just 161,453. This means that we could get a mint for 5 NFTs to process with comparable gas to a single mint previously. This is a reduction in over 400,000 gas for a single transaction.

We went on to test 5 and 10 mints within a loop. These tests showed that gas consumption was also lower (but stable) on each subsequent mint function call after the first. This is significant because many NFT projects use a looping mechanism, allowing users to mint more than one token at a time.

Our tests showed that great savings can be made with minor changes.

The Solution

With our thesis proven, let’s look at the solution. Here’s how you can reduce the cost of minting from your ERC721Enumerable-based smart contracts.

Step 1 - Replace ERC721Enumerable With ERC721

You need to strip out ERC721Enumerable entirely to benefit. To do so, replace your import of ERC721Enumerable:

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

And import ERC721 instead:

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

Then, replace the contract that your custom contract is based on. From:

contract MyJPEGs is ERC721Enumerable {}

To:

contract MyJPEGs is ERC721 {}

You’ll lose some other functionality alongside totalSupply() with this change. Namely, tokenOfOwnerByIndex() and tokenByIndex(). However, these are usually unneeded to facilitate a working NFT minting smart contract. And more importantly, a gas efficient one.

Step 2 - Replace Total Supply Reliant Functions With A Counters Solution

Because we lose totalSupply() from ERC721Enumerable, we need to recreate it.

First, import the Counters utility from Open Zeppelin:

import "@openzeppelin/contracts/utils/Counters.sol";

Then, declare your counter:

using Counters for Counters.Counter;
Counters.Counter private _tokenSupply;

Next, switch up your totalSupply() reliant minting function. Here’s what the old function looked like in the niceJPEGs contract (for comparison purposes only, don’t use):

function mintOld() public payable {
  require(saleIsActive, “JPEGS not for sale yet!”);

  uint256 mintIndex = totalSupply() + 1;
  require(mintIndex <= MAX_SUPPLY, “JPEGS sold out”);
  require(msg.value >= MINT_PRICE, “Not enough ETH”);

  _safeMint(msg.sender, mintIndex);
}

And here’s the new one to use:

function mint() public payable {
  require(saleIsActive, “JPEGS not for sale yet!”);

  uint256 mintIndex = _tokenSupply.current() + 1;
  require(mintIndex <= MAX_SUPPLY, “JPEGS sold out”);
  require(msg.value >= MINT_PRICE, “Not enough ETH”);

  _tokenSupply.increment();
  _safeMint(msg.sender, mintIndex);
}

Here’s how the solution might look in a loop where more than one token can be minted in one call:

for (uint256 = 0; i < numberOfTokens; i++) {
  _tokenSupply.increment();
  _safeMint(msg.sender, _tokenSupply.current());
}

You’ll notice the main difference is having to manually increment the Counter, whereas previously, the totalSupply() function would have done that for you.

If you want to retain a totalSupply-like function using this solution, use this:

function tokensMinted() public view returns (uint256) {
  return _tokenSupply.current();
}

That’s it!

Bonus Step 3 - Change Your Contract’s Counter Logic

There’s an additional bonus saving to be had here too. Remember the first mint costing more issue? This happens when tracking the total tokens minted to use in your mint function. Specifically, when this number initialises at 0. Setting a variable to 0 incurs a wasted space penalty due to the gas calculation of SSTORE operations.

You could implement this by leveraging the counter mechanism to create a _nextTokenId variable, as opposed to a _tokenSupply variable tracking the total minted. Then, increment the _nextTokenId to 1 in your contract’s constructor (as opposed to moving from 0 to 1 the first time your mint function is called). There doesn’t have to be a token with an ID of 0, but there will always be 0 tokens minted in the beginning. There’s a potential further 17,000 gas cost savings, for the first minter, using this approach.

Assuming you already have a counter declared named _nextTokenId, here’s an example constructor:

constructor ERC721(“MyJPEGS”, “MJPs” {
  _nextTokenId.increment();
}

And here’s how your associated minting function might look:

function mint() public payable {
  uint256 mintIndex = _nextTokenId.current();
  require(mintIndex <= MAX_SUPPLY, “JPEGS sold out”);
  require(msg.value >= MINT_PRICE, “Not enough ETH”);

  _nextTokenId.increment();
  _safeMint(msg.sender, mintIndex);
}

When using counters, it’s important to understand the strategy your project requires and how you can make that efficient too. While the focus of this article is to make gas savings simple to achieve with a like-for-like totalSupply() replacement, we felt compelled to mention this bonus once discovered through our testing.

Bonus Step 4 - Audit Your Libraries To Check For Unobvious ERC721Enumerable Imports

The offending totalSupply() function can creep into your smart contracts in other ways too. While most of the contracts that we researched imported ERC721Emumberable.sol directly, others had imported separate libraries which in turn imported Enumerable. One example of this is the OpenSea ERC721Tradable library, which implements ERC721Enumerable.

Always make sure you review the libraries you are importing into your contracts. Oftentimes you can find a more efficient way to do something that you’re importing a large library for. It might be the case that you only need a small portion of the functionality included, but you (and anyone interacting with the smart contract) are incurring unexpected and unnecessary gas fees from bundled contract imports that you have no use for.

Savings

Let’s look at the savings potential in more detail starting with some simple math:

At 150 Gwei, a 40,000 reduction in gas on a minting function saves the customer ~0.006 ETH. On a 10,000 piece collection, we’re looking at 60 ETH in transaction fee costs that could be avoided. During high traffic periods, with a popular mint, these numbers could be much higher.

Of course, this scenario is generalised. Nonetheless, we can see how inefficiencies on the code side of NFT projects are making real losses. A single project using a standard that OpenSea recommends could be costing its investors $280,000 at current prices of ETH. Because this OpenSea example contract imports ERC721Enumerable and thus loads totalSupply(). And you can understand developers wanting to adhere to the standards of the biggest marketplace around.

At the rate projects are being released—what feels like 10 per day—now the figures become enormous. If just 4 projects are released using inefficient smart contracts, the space bleeds over a million dollars in ETH.

We need to consider the costs of wasted ETH to the ecosystem at large, too. The higher transaction fees are, the more Ether is burned. Which gives a disproportionate advantage to holders because they can benefit from this deflationary mechanic. While conversely, new entrants to the marketplace are punished due to these increasing fees.

So why does any of this matter and what can those of us who’ve been in the space for a while do about it? We can’t do anything about existing projects. Once a smart contract is deployed, it’s immutable. What we can do is build the most efficient smart contracts we can going forward.

Gas efficiency is less important when trading higher priced assets. Someone buying a Bored Ape for 30 ETH is unlikely to care about the cost difference between a $420 transaction fee and a $300 one. But for someone buying in at lower asset prices, it could make all the difference.

New entrants to emerging markets need to feel welcomed. And with the current state of Ethereum gas fees—when buying an NFT can cost more in gas than the asset—we have an onboarding problem.

The good news is that we can do something about it. We can test, optimise and educate. We can start making changes today by reducing the gas consumption of many NFT minting transactions by up to 70% using the research in this article.

Because all of this information is baked into blocks—perhaps the hardest part of blockchain technologies to understand—gas price becomes the scapegoat for the real issue, smart contract inefficiencies. If you can reduce the gas consumed by your transactions, users of your smart contract can pay a higher gas price yet face the same overall transaction costs. In addition, the fewer units of gas your transactions consume, the less congested the network becomes. Thus reducing fees for everyone using the protocol. We, as developers, need to look at gas usage and not transaction fees, when building smart contracts.

While layer 2 solutions aren’t always ideal and ETH 2.0 may be some time away, it’s comforting to know that, as developers, we can make a direct impact on improving the current conditions for everyone.

Impact Beyond Minting

Hold onto your hats, because this goes deeper still.

These inefficiencies bleed into transfer transactions too. And with NFTs being transferable by default, this puts a huge multiplier on the problem. Not only does minting an NFT from a contract using ERC721 Enumerable cost more gas, transferring it does too. An NFT minted from an inefficient smart contract is inefficient for its lifetime.

We tested two transfers using the ERC721Enumerable based contract with totalSupply() loaded, then two transfers using the efficient alternative based on our solutions. The old contract transactions consumed 96,787 and 84,499 gas. The two transfer transactions using the new contract consumed just 62,418 and 62,430, proving an impact beyond minting.

Unfortunately, the hole doesn’t bottom there. 3rd party contracts that interact with your contract inherit its inefficiencies. The biggest culprit in the space is, currently, OpenSea sales. When you post an NFT for sale, OpenSea’s contracts work with your smart contract’s internal transfer function to facilitate the exchange of one user’s currency for another user’s asset. As with our direct transfer testing, we tested two OpenSea sales through our old and new style contracts. The former consumed 227,438 and 215,138 gas respectively. The latter, just 193,129 and 176,005.

We can explain the contract specific variation between transactions by considering the first interaction mechanic we discussed earlier. Our tests were spread across multiple wallets to ensure real market conditions were replicated as much as possible. This variance aside, the 20,000-40,000 gas-saving range presents consistently.

We’ve shown a material gas saving in both sets of tests. Now imagine the amount of volume passing through OpenSea and you’ll soon start painting a picture of severity. Leveraging niceJPEG sales data, in the three month period from August 1st, 2021 until October 31st, 2021 there were just under 4.8-million successful transactions on OpenSea. This does not account for collections that are using the OpenSea shared contracts, only those using custom smart contracts.

Let’s run through some hypothetical math using this information. If we could save 20,000 gas per marketplace transaction, assuming that a large majority of contracts are inefficient, this represents a saving of 96-billion gas. At a gas price of 100 Gwei (which is cheap in current conditions), this represents a transaction fee reduction of 0.002 ETH. This sounds minimal on the transaction level, but totalled represents 9,600 ETH wasted by contract inefficiencies on transfers alone. At current prices, this equals $45-million USD of wastage in the last 3 months. And this doesn’t account for minting inefficiencies.

We may be in the mania phase of NFTs, but if we expect the market to continue to grow, we have to be better. We can do better.

While the OpenSea legacy marketplace contract, with its own inefficiencies, takes much of the blame for high gas fees, it's not the only problem. The lifetime inefficiencies of many NFT projects’ contracts are costing the space millions.

Having end-users pay gas fees that they don’t have to is damaging to the entire space. If your contract is inefficient at deployment time every action undertaken by it will be inefficient. While a 10,000 mint project can only cause a finite amount of damage at mint time, it can cause an infinite amount of damage during its lifetime through secondary sales.

Conclusion

Our solution shows small changes that you can make to pass on big savings. We hope you consider using it to start optimising your smart contracts today.

More efficient smart contracts mean everyone wins. End users who find gas fees problematic will appreciate all the savings they can get, especially on lower value asset purchases. Projects that offer lower than average gas fees can stand out and attract investors. And the Ethereum blockchain as a whole can make a dent in the “unusable due to gas fees” argument that many people (rightly, at times) present.

The NFT summer of 2021 was net positive for web3. It brought many bright and curious people into the space and kick-started new important narratives around crypto. However, much was rushed. Smart contracts were copied and pasted time and again without due diligence. It’s understandable when most projects were selling out with only a small time investment required.

But the rush feels over now. Of course, layer 2s and future Ethereum upgrades will help the situation at large. But we need to be moving towards sustainability with what we have now. And we can make significant progress through the optimisation of our smart contracts.

This research could save minters millions. If you found it useful at all, please consider collecting an edition of this article or leaving a tip for the authors below.

split://0x860D5603d291E4ac49E62F16eaa149C6b1d547fA

We do stuff like this within the Shiny Object Social Club all the time. If you’re interested in building the future of the web together, we’d love you to join us here.

Recommended Reading