IERC721Dna - Metadata Encoding Standard
The IERC721Dna.sol contract defines a standard for efficiently encoding on-chain NFT metadata. This enables powerful on-chain mechanics combining NFTs such as Crafting, Breeding or other decentralized incentives. The standard is inspired by ideas first pioneered by cryptokitties.co which encoded "DNA" into their kitty NFTs for the purpose of gamified breeding. However, Owl Protocol's NFT trait encoding mechanic is fundamentally different as it is designed with standardisation and composability in mind.
The goal of the IERC721Dna.sol
standard is to be as flexible as possible. As such, the dna
of an NFT is returned as an arbitrary bytes
array which can be used to encode any data.
Interface
The IERC721Dna.sol interface itself is quite minimal defining only 4 functions:
interface IERC721Dna {
function mintWithDna(address to, bytes memory dna) external returns (uint256);
function safeMintWithDna(address to, bytes memory dna) external returns (uint256);
function getDna(uint256 tokenId) external returns (bytes memory);
function updateDna(uint256 tokenId, bytes memory dna) external;
}
mintWithDna
andsafeMintWithDna
are essentially the same. Notable is the lack of a specifiedtokenId
as our contract usesAutoId
mechanic as thetokenId
is NOT used to encode the dna of the NFT.getDna
gets thedna
data of an NFT. Notable here is the fact that unlike other solutions, thedna
of an NFT is returnedupdateDna
sets thedna
of an NFT. It is recommended that this function limited with permissions to the owner of an NFT or to external proxy contract for additional composability.
While only the external interface is defined here, the simplest way to implement this interface is by introducing a mapping(uint256 tokenId => bytes dna)
into the contract.
Gas Costs
The addition of on-chain metadata encoding adds NO OVERHEAD for regualr EIP-721 transfers. However, this does introduce a slight overhead for minting new tokens, which is proportional to the amount of DNA written. If stored efficiently however, this can be minimal and only cost 1 additional SSTORE
operation (if the dna
< 31 bytes layout_in_storage.html#bytes-and-string). For getDna
and updateDna
operations, the cost will scale similarly.
DNA Standard
Owl Protocol's IERC721Dna.sol interface does not necessarily specify how to encode the dna but we highly recommend using the Owl DNA encoding standard to maximize efficiency and composability.
What to encode
Before, you use Owl Protocol DNA standard, you should think about what data is relevant to store on-chain. We recommend a hybrid model. Not all data should be stored on-chain. Only data with execution value, meaning that it is relevant to the logic of other smart contracts (eg. DAO, staking, crafting), should be used on-chain.
Image Layers
Images should not be encoded on-chain. Storing an image on-chain can be prohibitively expensive or imposible. Instead pointers to images should be stored in the form of numbers. For example, if an NFT can have 4 background images (eg. circle, triangle, triangle, pentagon), an number
trait from 0-3
should be stored on-chain. The collection.json
encoding spec can then define how each number points to a different image and can be used to create a truly generative NFT (see later section). The collection.json
and associated image resources can then be uploaded to IPFS and referenced by the contraryURI()
function extending the contract-level-metadata standard. See later sections for more info on how to use generative image layers using "Image Traits".
Nested DNA Encoding
We define the following recommended encoding standard for IERC721Dna.sol NFTs.
bytes memory fullDna = abi.encode(bytes inherentDna, bytes[] childDna)
The fullDna
of an NFT is defined as the encoding of its inherentDna
which is stored in IERC721Dna.sol (and can be updated) and its childDna[]
which is defined as an array of child dna encoding for NFTs that are attached to this NFT. This encoding enables extending the DNA encoding standard to support additional data that can be encoded in the NFT but not expressely stored in the contract itself.
For example, our IERC721TopDown.sol interface defines a composable nft similar to EIP-998. The combined interface IERC721TopDownDna.sol, enables users to componse NFTs in a parent-child relationship, and have the parent NFT's dna be defined recursively as the encoding of its inherentDna
and of its attached childDna[]
.
Traits & Attributes
While the on-chain encoding is useful for smart contracts, we also need a way to define how the on-chain data is visualized. We therefore create a collection.json
file that will serve as an encoding map, for both the inherentDna
and the childDna[]
. This will be used to encode/decode attributes of an NFT to human-readable metadata such as text or images that can be rendered in a marketplace.
We define the following terms:
fullDna
: The encoding of an NFT's attributes and its associated child NFTsinherentDna
: The encoding of a set of attributes of an NFTchildDna[]
: The encoding of the set child attached NFTs. Each element of the array is encoded asfullDna
itself.collection.json
: The definition of howinherentDna
of an NFT is decoded to attributes, and how child NFT'sinherentDna
is decoded.trait
: The definition of the various options an attribute can take. (eg.size: small, medium, large
)attribute
: The specific value of a trait decoded usinginherentDna
and thecollection.json
(eg.decode(0) = small
)
Below a simple example collection.json
defining a collection with 2 traits, and one optional child attachment.
{
"name": "Shapes",
"generatedImageType": "png",
"traits": {
"faction": {
"name": "faction",
"type": "enum",
"options": [
"earth",
"wind",
"fire",
"water"
],
"abi": "uint8"
},
"strength": {
"name": "strength",
"type": "number",
"abi": "uint8"
}
},
"children": {
"fg": {
"name": "Shapes NFT Child",
"traits": {
"strokeWidth": {
"name": "strokeWidth",
"type": "number",
"abi": "uint8"
},
}
}
}
}
The two key fields are the traits
and children
properties which defines traits for the inherentDna
and the childDna
respectively. The children
field itself also defines a collection in the same format, which can be extended infinitely.
Each trait is defined by its name
, type
(enum, number...), and its abi
, which defines how the trait is encoded (default uint8
but can be uint16
, uint32
).
The order of the keys in the traits
field matters as if there are 2 traits, the order will define how the traits are decoded:
(uint8 faction, uint8 strength) = abi.decode(inherentDna, (uint8, uint8));
Similarly, the order of the keys in the children
field matters as if there are 2 children, the order will define how the children are decoded.
(bytes memory inherentDna, bytes[] memory childDna) = abi.decode(fullDna, (bytes, bytes[]));
bytes child0 = childDna[0];
bytes child1 = childDna[1];
Various traits are already supported by @owlprotocol/nft-sdk in addition to enum
and number
Number
Defines a number trait. By default, 0-255 though this can be lower or higher (provided abi
is changed to uint16
). Dna is decoded as an integer (eg. decode(0) = 0
)
{
"name": "strength",
"type": "number",
"abi": "uint8"
}
Enum
Defines text that can be chosen from a set of options (in other words a category). By default, up to 255 options are supported though this can be lower or higher (provided abi
is changed to uint16
). Each value specified in the options
field, is decoded to its index position (eg. decode(0) = options[0]
)
{
"name": "faction",
"type": "enum",
"options": [
"earth",
"wind",
"fire",
"water"
],
"abi": "uint8"
}
Image
An image trait can be used to define a layer that can be used to create a truly generative collection. By default, an image trait can support up to 255 options but this can be lower or higher (provided abi
is changed to uint16
).
A collection can support multiple image traits (each with their own options), that will then be merged as layers using the merge-images library. To use generative image merging specify "generatedImageType": "png"
for PNG merging or "generatedImageType": "svg"
for SVG merging.
Image layers can be of two types. PNG or SVG (aka vector files). PNG images are usually easier to use by artists but SVG open up greater possibilties by enabling the parametrization of the image generation using other attributes (eg. number
attribute to scale size or color
attribute to set color). In any case ALL image traits and their possible options must be of the same type.
PNG Image
{
"name": "imageBg",
"type": "image",
"image_type": "png",
"options": [
{
"value": "circle",
"image_url": "http://example.com/bgCircle.png"
},
{
"value": "square",
"image_url": "http://example.com/bgSquare.png"
}
]
}
SVG Image
{
"name": "imageBg",
"type": "image",
"image_type": "svg",
"options": [
{
"value": "circle",
"image_url": "http://example.com/bgCircle.svg"
},
{
"value": "square",
"image_url": "http://example.com/bgSquare.svg"
}
]
}
Color
Defines an RGB color. Can be used by an svg
image layer for rendering.
8bit color
An NFT with a color trait with abi = uint8
MUST also defined a colormap trait that the color trait points to (see below). Due to only storing 8bits, the color is similar to a number
trait, being able to only take 255 values. The colormap enables conversion from the 8bit color trait to an RGB value using the selected colormap.
16bit color Not implemented
24bit color Not implemented.
Colormap
Defines a set of RGB colors, in the form of an array. A colormap trait can store up to 255 different colormaps. Each colormap having up to 255 colors.
{
"name": "colormap",
"type": "colormap",
"options": [
{
"value": "white" //white gradient
},
{
"value": "black" //black gradient
},
{
"value": "custom" //custom colors
"colors": [[0, 0, 0], [0, 1, 1], [0, 2, 2]] //...
}
]
},
A set of standard colormap options from colormap are also supported where you don't need to specify the color options but simply set value
as one of the names below.