Tutorial

Fully On-Chain SVG NFTs — Part 4: Efficient String Concatenation with (Dynamic) Buffers

A deep dive into gas-efficient SVG generation using dynamic buffers for fully on-chain NFTs.

Written By

Eto Vass

Published On


Fully On-Chain SVG NFTs — Part 4: Efficient String Concatenation with (Dynamic) Buffers

Recap/Intro

In the first 3 articles for fully on-chain SVG NFTs, we covered all of the basics that would allow you to create such NFTs. Starting from part 4, we will cover more advanced concepts that are beyond the scope of the basics but are important to enhance your knowledge.

Problem Statement

When developing fully on-chain SVG NFTs, you generate SVG content with Solidity code. They are also called EVM-Generated NFTs.

SVG generation is a core part of each EVM-generated NFT. Thus, it has to be efficient and consume as little gas as possible.

GAS

Gas is an essential element of the Ethereum network. It is important for transactions and read operations (tokenURI, for example). Every EVM node has a limit for read gas.

As a rule of thumb:

  • public/free nodes have a read gas limit of 30M
  • commercial nodes like Alchemy, Infura etc, have different higher limits, it is safe to assume that 300M is the lower limit for them. Marketplaces like Opensea use these nodes, thus they will be able to display your on-chain SVG NFT if they consume under 300M read gas

Although 300M is a practical limit, we recommend trying to fit in 30M gas limit if you want to be a real on-chain maxi. 30M makes SVG NFTs much more interesting from a technical standpoint due to the gas limit.

String Concatenation

When constructing SVG content with Solidity, string concatenation is one of the most important operations. Thus, it has to be efficient. In this article we will test several ways of concatenating strings:

1. string.concat()

1string memory svg = "";
2svg = string.concat(svg, '<circle cx="10" cy="10" r="10" fill="red"/>');

2. abi.encodePacked()

1string memory svg = "";
2svg = string(abi.encodePacked(svg, '<circle cx="10" cy="10" r="10" fill="red"/>'));

3. (Dynamic) Buffers

(Dynamic) Buffers is a method for concatenating strings, using a pre-allocated buffer that can be fixed or can grow. This approach is much more gas efficient that string.concat() and abi.encodePacked(), because the other two methods technically create new memory allocation on each concatenation, thus could be very inefficient.

Multiple implementations are available for Dynamic Buffers. We will test several implementations:

3.1. Dynamic Buffer that was initially created by David Huber (https://x.com/cxkoda) and Simon Fremaux (https://x.com/dievardump), which was further enhanced by 0xthedude (https://x.com/0xthedude) in scripty.sol. Implementation is available here: https://github.com/intartnft/scripty.sol/blob/main/contracts/scripty/utils/DynamicBuffer.sol

3.2 Solady (https://x.com/optimizoor) Dynamic Buffer https://github.com/Vectorized/solady/blob/main/src/utils/DynamicBufferLib.sol

3.3 no_side (https://x.com/no_side666) Dynamic Buffer https://etherscan.io/address/0xE3ccab3bC1A943Edd01d3a4B0F7C2B3D74C2b7B0#code#F31#L1

no_side recently released a platform for fully on-chain pixel art. Don’t miss out on checking it out; it is an impressive work, Here.

Putting It All Together

In directory tutorial-4-buffers I put all of the code for this tutorial. It is mainly the file src/BasicSVGRenderer2.sol, which renders certain number of circle into SVG (through tokenId param), so we can measure how much gas each of the methods consume.

tut-4-1

tut-4-2

We also modified test files in test/ directory and created a script called run-tests.sh

This script will run BasicSVGRenderer contract with 10 50 100 200 300 500 1000 1200 1500 2000 3000 5000 10000 15000 as value for tokenId (i.e number of circles) and for each of the SVG rendering methods, to compare the GAS consumption.

Results

Running the script will create a results.csv file, which measures the gas consumption for each method for a specific number of circles.

tut-4-3

From this results it is very visible that string.concat() and abi.encodePacked() are very similar in gas consumption. But also very inefficient.

Thus, if your project involves many string concatenations, it is highly recommended that you use some of the (Dynamic) Buffers implementation.

It is visible that no_side implementation is the most gas-efficient for smaller outputs of up to around 50K. For bigger outputs, the_dude outperforms it, since no_side implementation allocates double the amount of memory, but it is really dynamic — i.e., you don’t need to know the output in advance. The biggest disadvantage of the_dude implementation is that you need to estimate the size of the buffer you will need since it must be pre-allocated. Sokady and no_side implementations allow dynamic resizing.

Links

Git Repo for this tutorial:

More On-Chain Projects

Jalt2
created by: AltJames
Cypher

Cypher

created by: Hideki Tsukamoto
Nouns

Nouns

created by: Nouns

Panopticon

created by: teto
NFTIME
created by: @nftxyz.art
smartbags.
created by: Nahiko & dievardump
Golly Ghosts
created by: Michael Hirsch
Rad Impressions
created by: Frank Force