ZkRoninsMeta64.sol
ZkRoninsMeta64.sol is the main Base64 metadata contract in the META64 module and contains all the logic for enabling, storing and retrieving Base64 encrypted token URIs. This will make it easier to change metadata, for here we construct the (majority of the) JSON scheme on-chain.
With the non-Base64 URI methods we simply point towards an URI that's stored someplace else and therefore making it impossible to change the metadata without having to re-upload a new JSON file. With Base64 changes can be made directly on-chain and it would reflect immediately in the URI.
A case study
Let's give an example of the power behind leveraging the Base64 format.
The following code is a simplified version of the return statement in our current ZkRoninsMeta64.sol contract:
return string.concat(
    'data:application/json;base64,',
    Base64.encode(
        bytes(string.concat(
            '{"name":"', namePrefix, tokenId.toString(), " - ", name,
            '","description":"', description,
            '","image":"', image,
            '","animation_url":"', animation_url,
            '","attributes":[', attributes, ']}'
        )
    ))
);Normally, metadata would be returned as a simple URI (https://example.com/2.json). But here we create one that is based on values stored inside of the contract itself (variables like namePrefix, tokenId, name, image, animation_url, attributes and description represent these). 
With this method we could for instance change the value of namePrefix from zkRonin # to Ronin which would instantly change the name format for all the ronins on-chain from zkRonin #10 - Hira Kyoshi to Ronin 10 - Hira Kyoshi.
This simple change would have been costly with non-Base64 metadata as it can only be done by changing all the .json files, re-uploading them and finally re-linking them to the contract. Whereas here we change only one variable with a single transaction on-chain.
This example may seem irrelevant, but it showcases the sturdiness of non-Base64 URI versus the flexibility of Base64 URI metadata.
(copy and paste this into your address bar to see the metadata for a simple ronin)
data:application/json;base64,eyJuYW1lIjoiUm9uaW4gMTAgLSBIaXJhIEt5b3NoaSIsImRlc2NyaXB0aW9uIjoiVGhpcyBpcyBhIHRlc3Qgcm9uaW4uIiwiaW1hZ2UiOiJodHRwczovL3prcm9uaW5zLmNvbS9yb25pbi8wLmpwZyIsImFuaW1hdGlvbl91cmwiOiJodHRwczovL3prcm9uaW5zLmNvbS9yb25pbi8wLmh0bWwiLCJhdHRyaWJ1dGVzIjpbeyJ0cmFpdF90eXBlIjoiQWdlIiwidmFsdWUiOjIwfV19Fields
Name, description, image, animation_url, attributes or individual traits are all Fields we can use to describe and create a ronin. In our code we've made it possible to add and remove Fields, which is where the dynamic nature of the project really comes into play. Because now we can introduce new traits to a character based on whatever mechanism we choose to create. Like e.g. a Slot Counter, Revision Amount or Royalty Bonuses allowing a holder to gain a percentage of made secondary sales (see Royalty Collector for this very early-stage concept).
Breakdown
- Administration - getFieldreturns the value of a field for a specific token ID, slot and fieldName.
- getTraitsreturns a list of all the available traits. These are basically Fields that have been labeled attributes.
- setFieldsets the value for a field for a specific token ID and slot combination. Can only be called by the admin.
- setFieldssets multiple fields for a specific token ID and slot combination. Can only be called by the admin.
- setFieldsManysets multiple fields for multiple token ID and slot combinations or sets multiple fields for a token ID range, where slot is a fixed value (for example: if we want to populate all the first slots for a large chunk of token IDs). Can only be called by the admin.
- addTraitpushes the name of a field we should consider as a trait to the list of traits. Can only be called by the admin and is only possible if the trait doesn't exist.
- addTraitsis the many variant of- addTraitand skips the traits that have already been added. Can only be called by the admin.
- removeTraitis self-explanatory. Can only be called by the admin and is only possible if the trait exists.
- setContractSlotssets the address to the contract having the storage for the token slots, which in our case happens to be the NFT (ERC721) contract. Can only be called by the admin.
- setContractMeta64Xsets the address for the ZkRoninsMeta64X.sol contract. Can only be called by the admin.
 
- Metadata functionality - slotURIonly returns a Base64 URI if the specific token ID and slot has Base64 enabled. This function gathers all necessary Fields and creates a Base64 encoded URI.
- slotURIsreturns multiple slotURIs for a given tokenID. The slots that have disabled Base64 will return as an empty string.
- tokenURIonly returns a Base64 URI if the specific token ID has Base64 enabled on the currently selected slot (see- selectedSlotin ZkRoninsMeta.sol).
- tokenURIsis the many variant for- tokenURI. Base64 disabled slots return an empty string.
- slotFieldsis similar to- slotURIbut returns the key-value pairs ¹ stored in a specific slot for a ronin ².
- slotFieldsManyis the Fields equivalent of- slotURIs; meaning it returns key-value pairs ¹ instead of URIs ².
- tokenFieldsis similar to- tokenURIbut returns the key-value pairs ¹ stored for a ronin ².
- tokenFieldsManyis the Fields equivalent of- tokenURIs; meaning it returns key-value pairs ¹ instead of URIs ².
- setReadyBase64sets whether the Base64 metadata is ready or not. Since we as the contract owners have to populate the contracts with all the metadata, holders shouldn't be able to enable Base64 if it is not deemed ready. Can only be called by the admin.
- setReadyBase64Manyis self-explanatory. Can only be called by the admin.
- setEnableBase64enable or disable Base64 metadata on a per token or per slot basis. If the owner chooses to enable it on a per token basis, then every slot will have Base64 enabled by default. This function is guarded by the upgrade role modifier ³.
- setNamePrefixsets the prefix for the name of all the NFTs in the collection; see the case study above. Can only be called by the admin.
 
NOTE: slotURI is the function where most of the logic lies. Moreover, it's upgradable, which will allow us to create more advanced algorithms for the computation of the Base64 URIs (e.g. fully-autonomous metadata; see ZkRoninsMeta64X.sol for more on this).
¹ Key-value pairs are useful if we don't necessarily want to create an URI, but just need all the information a ronin is composed of.
² The fields getter functions, which only developers might use, will only return correct values if ZkRoninsMeta64X.sol isn't linked to this contract. This can be noticed by checking whether contractMeta64X equals the zero-address (0x0000...0000). If it does link, then we will also have to call the Meta64X's slotFields-function for a complete result.
³ Functions with the upgrade role modifier can only be called by the UpgradeHandler.sol contract in the STORE module. This is done to separate storage and logic for modularity, to improve the contract's upgradability and to circumvent Solidity's immutable nature. See our Code of Conduct for more about this.
Transparency and Security
- No withdraw method This contract cannot recover funds and shouldn't receive any, so be extra attentive not to accidentally send any funds to this contract. 
- Static and dynamic An NFT in our collection will be able to have both non-Base64 and Base64 types of metadata with our Token Slots approach. The current thought process for this is to give owners the possibility to have static metadata if they don't want to join the Base64 movement. Allowing both also makes it possible to give owners a way to copy over their Base64 metadata to the NFT (ERC721) contract's storage, switch back to non-Base64 and treat their metadata as a static URI. 
Last updated
Was this helpful?