ZkRoninsMeta.sol

NOTE: some functions of this contract are upgradable.

ZkRoninsMeta.sol is part of the NFT (ERC721) contract and holds all the logic for the storage and retrieval of metadata. Separating this is perhaps unconventional, but it gave us a clearer overview to what is possible and what would be worth exploring considering metadata.

Our approach is a mixture of three "standards", all leveraging one another to make sure resources are used cost-efficiently. These standards have already been somewhat touched on in URI, but will now be explained in greater detail, specifically from a coder's point of view.

Three standards

  1. Saving one baseURI for all This is the most used method to store metadata. The reason why it became so popular is because it's cheap, fast and simple to implement. It only requires one URI to be stored for all token IDs. This URI points towards a directory containing all the separate JSON files for each token ID. Now every time a request for a certain tokenURI is made, the link gets constructed by appending [tokenId].json to the baseURI. Example: https://allzkroninsmetadata.com/66.json, where the baseURI is https://allzkroninsmetadata.com/ and 66 the appended tokenId.

  2. URI Storage This method requires more gas, but gives more freedom to make changes to the individual tokens. This especially becomes useful if a mistake is discovered in one of the NFTs. With the previous approach we would've been forced to re-upload all NFTs, or worse, leave the problem unsolved. So, by saving the complete URI in storage we are able to change a specific token ID, without impacting the rest. Also it allows us to make this collection more flexible, which we'll talk more about below.

  3. On-chain Base64 URI generation (or: JSON URI) This method is probably the most expensive (for us as the contract owners), but the most interesting of the three. It not only allows for individual customization, but also makes it possible to let NFTs change based on whatever is programmed. For instance, we could create a leveling system, where you could farm experience and level your NFT, which would directly reflect in the metadata. Or: create time-based utility, introduce equipment, player stats, utilize randomizers and so much more. The only caveat to this method are the costs involved for us as the contract owners, as we have to store all the metadata for every token ID on-chain in order to construct the Base64 URI whenever the tokenURI gets requested. The construction is somewhat comparable to the first method, but instead of only concatenating the baseURI with the tokenId, we gather all stored data (e.g. name, description, traits etc.) together and with it generate the eventual Base64 encrypted URI. Take a look at this example (you may need to copy-paste this into your address bar for it to work): data:application/json;base64,eyJuYW1lIjoiUm9uaW4gIzAgLSBUZXNzYSBUYW5ha2EiLCJkZXNjcmlwdGlvbiI6IlRoaXMgaXMgYSB0ZXN0IEpTT04gZmlsZS4ifQ==

Implementation

It's already uncommon to implement all three approaches in an NFT project, but it is the approach that gives us and the holders more lanes to explore.

  1. The baseURI will be the 'most static' version of metadata and will mainly be treated as the fallback URI. This means that whenever there is no custom URI stored for an NFT - whether this is the usual format or Base64 - the baseURI + [tokenId].json gets used.

  2. Our URI Storage method deviates a bit from the standard as ours will be able to have more than one URI linked to every NFT. We call these Token Slots. Our version could for that reason more so be considered a Multi-URI Storage rather than the standard URI Storage. This enables features like e.g. revisions, skin "selection" and in overall being able to request non-destructive changes to your NFT.

  3. The Base64 URIs will be computed via a separate contract, which we'll link to this one when we're ready to convert to the most dynamic version of our collection. We'll likely treat this as an optional upgrade, where the owner gets to choose whether they want to participate in this or keep their NFT in a non-Base64 format. The option to revert back will also be there on a per token slot basis.

Breakdown

  • ERC Standard inheritance and overrides

    • Override: the _baseURI function returns baseURI.

    • Override: tokenURI method has custom logic.

    • Setting and getting of baseURI.

    • Setting and getting of the contractURI - not really an acknowledged standard, but it's what some marketplaces (like OpenSea) use for the collection's metadata.

  • Metadata functionality

    • tokenURI returns either a 'normal' link or an on-chain generated Base64 link ¹. This uses the slot the owner of the requested ID currently has selected.

    • tokenURIs is the many variant for tokenURI. This could save on the amount of calls one has to do when multiple tokenURIs are requested ².

    • slotURI returns either a 'normal' link or an on-chain generated Base64 link for a specific token ID and slot ¹.

    • slotURIs is the many variant for slotURI. This again saves on the amount of calls one has to do when multiple slotURIs are requested ².

    • setSlotUri sets the URI for a specific ID and token slot. This function is guarded by the upgrade role modifier ³.

    • setAllowBase64 sets whether Base64 metadata is allowed or not. This will be called when we move towards a more dynamic collection. Can only be called by the admin.

    • setContractBase64 sets the address for the ZkRoninsMeta64.sol contract. Can only be called by the admin.

  • Slot functionality

    • acquiredSlots returns how many slots a token ID has acquired. This value only represents the amount bought for the token ID, not the total slots available for the NFT. That logic will be in a separate contract.

    • selectedSlot returns which slot is selected for a token ID. This value gets used when tokenURI gets called and allows the token owner to change which metadata gets shown on other platforms (marketplaces, wallets, apps, etc.).

    • selectedSlots is the many variant for selectedSlot. Again, saves on the amount of calls one has to make if more slots has to be requested.

    • acquireSlot ups the amount of slots for a token ID by the quantity given. This function is guarded by the upgrade role modifier ³.

    • yieldSlot is the opposite of acquireSlot. Again, as well guarded by the upgrade role modifier ³.

    • selectSlot selects which slot will be the default when tokenURI gets called (see selectedSlot).

NOTE: the logic for acquiring and yielding slots will be separate from this contract, see ³.


¹ Which type of URI gets returned depends on the value of allowBase64 and whether the owner has Base64 enabled.

² These variants only return non-Base64 URIs and can thus return incorrect or old metadata if a holder has Base64 URI enabled. If you use these functions, only do it in conjunction with the many variants in the ZkRoninsMeta64.sol contract. For a more thorough explanation we suggest reading the description at the top of the contract and the comments in the functions themselves.

³ 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

  • Unlocked metadata In countless NFT projects - unbeknownst to most holders - the possibility for the contract owner to change the project's metadata is there (unless ownership has been revoked, but this rarely happens). It's only due to the direction we are taking in this project that this fact becomes more apparent. We argued on whether we should introduce the option for holders to lock their metadata, but this could lead to more potential issues than the "boost in ownership" it would provide. Not just in our case, but for NFTs in general. Imagine: an owner becomes inactive and for whatever reason the metadata link dies, resulting in an unfixable NFT in the collection. A fun article to read about this particular concern can be found here (the irony if this link dies).

  • Switching Base64 on/off We being able to allow or deny Base64 by using the (global) allowBase64 variable implies that your metadata could revert back to non-Base64 URIs without us having asked for permission. This could raise concerns. The reason why we haven't made this a one-way activation is for wanting to have the option to revert back in case the Base64 metadata proves to be or become unstable. We expect it to work - also since our collection isn't as big as others - but having this prevention mechanism makes us feel less worried about breaking the collection and more at ease.

Last updated