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
.
-
name
is the contract name, set to "Eth Buddies" at deployment. -
symbol
is the contract symbol, set to "ETHBUDDIES" at deployment. -
metadataGenerator
is address of the contract that generates the metadata and image for each eth buddy. -
contractMetadataGenerator
is address of the contract that generates thecontractURI
.
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:
-
Convert the
tokenId
to the attributes that make up each buddy -
Create a
StringBuilder
to avoid the gas issues caused by naive string concatenation - Efficiently encode svg path commands by reducing the possible values of the path points
- Use a fixed point representation for the points on the curve to preserve the fidelity of buddies
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.