How does EthBuddies work?

The Token Contract

Let's start with the Token contract, it is an Erc721Royalty and Ownable. We notice that the constructor takes four arguments name, symbol, metadataGenerator, and contractMetadataGenerator.

We will discuss the interfaces for the metadata generators when we discuss metadata generation in detail.

The next notable part of Token is the safeMint method that requires 1 ETH to run and mints a new buddy as long as it hasn't been minted before.

The proceeds from minting are withdrawable by the owner using the withdraw method.

Metadata Generators

We can see that there are two interfaces IMetadataGenerator and IContractMetadataGenerator. We created the two interfaces to separate token ownership state from the metadata generation in case we want to improve or discover a bug in either of the generators.

interface IContractMetadataGenerator {
  function generateContractMetadata() external view returns (string memory);

IContractMetadataGenerator generates the contract metadata and is less interesting as current contract that implements the interface simply returns a string from storage.

interface IMetadataGenerator {
  function generateMetadata(uint256 tokenId) external view returns (string memory);

IMetadataGenerator on the otherhand is much more interesting, it accepts a tokenId and returns the corresponding tokenURI string. In our case this string is a base64 encoded json object with the svg of the buddy included in it. This means that the buddies don't require a running server.

Generating the svgs

The magic of eth buddies starts in the method generateSvg. This method creates an svg by decoding our space optimized svg paths that are stored on chain. There are a few tricks that we had to use to get this to work:

  1. Convert the tokenId to the attributes that make up each buddy
  2. Create a StringBuilder to avoid the gas issues caused by naive string concatenation
  3. Efficiently encode svg path commands by reducing the possible values of the path points
  4. Use a fixed point representation for the points on the curve to preserve the fidelity of buddies
We will go over each one in detail.

Splitting the token id

We split the token id into a three digit representation and use each digit to determine what attributes the buddy has. Then we can look up and load the attributes from storage.

The StringBuilder

The string builder uses a bytes buffer and an offset counter to efficiently build a string without incurring the gas costs of repeated calls to ethabi.encodePacked. We essentially avoid the O(n^2) complexity of naively building a string in solidity. ethabi.encodePacked creates a string with the length equal to the sum of the length of the inputs, this means that we need to reallocate and memcpy every call, rather than amortizing over calls as string implementations commonly do.

Path decoding

We will now look at PathLib. XML paths are encoded as a sequence of u8 or (u16, u16). We further require that the most significant bit of each component of each point is zero. We can thus use the msb to determine if we are reading a command msb=1 or a point msb=0. So if we see {128, 129, 130} we know that we have a command. We simply iterate over our encoded path and build a string with proper white space and commas.

Fixed point scheme

We can use a fixed point scheme to allow us to keep the image size relatively normal (1000x1000). So we remap [0, u15::max] to [0.1, u15::max / 10].

Generating the json

All we have left to do is construct the metadata object and base64 encode.