Playground Meta Launch Metadata On-Chain Randomization

Playground Meta
9 min readApr 6, 2022
A collection of Meta Komodo building the Playgroundverse

What is Metadata?

Let’s start with Metadata, the essence of each and every NFT.

Metadata is crucial to NFTs because, without its metadata, NFTs will not have an image, name, attributes, and so on.

Metadata is commonly uploaded onto what we call as an InterPlanetary File System (IPFS), with common format such as: ipfs://QmZtkTHdp9Qy7WKw63RC9vexBYMM8DsM244AYPxGXm738/10.

Now, why do we use IPFS? IPFS is used to solve the problem for when an NFT’s metadata (such as image & attributes) can be easily changed by the devs if it is simply hosted in a private server with conventional naming (i.e: https://theplaygroundmeta.com/metadata/1.json / https://theplaygroundmeta.com/metadata/1.png).

With conventional naming, one can easily change the content of, let’s say, 1.json / 1.png into another image or attribute. Now, would you want to mint an Ape NFT, for it to suddenly and magically change to a Bug NFT the next morning? Of course not. Therefore, it is much more secure with IPFS.

Current Minting Metadata Common Practice

Now, onto the current common practice for minting metadata.

Usually, when you mint an NFT of a project, you will normally receive an NFT with unrevealed metadata, where you will have to wait until reveal day for your final metadata and image to appear. This NFT’s metadata is stored in an IPFS with the format of the base URI and token ID (ipfs://{baseURI}/{tokenID}).

In our opinion, your NFT’s metadata (image and rarity level that you get) is not entirely 100% random. Devs actually know exactly which token ID will get which image and which rarity level, be it legendary or common.

This practice allows some projects’ devs to be able to mint some of the rare NFT for themselves (like an insider trading in forex / stocks market, if you may), which may benefit them.

As we are firm believers in randomized chance (which is one of the fun things about NFTs), we aimed to challenge this method.

Our Approach

That mission led us to our own unique approach.
This is not actually new, a lot of other big projects have implemented such on-chain randomization too, such as Roborovski, which even implemented building the attributes itself on-chain, really state-of-the-art amazing work.

We set a totally randomization algorithm in our smart contract, so that when someone mint our NFT, the token’s metadata will not use the commonly used token ID as the pointer to the metadata. Instead, the particular NFT that is minted will have a random metadata pointer picked from a metadata pool of 5550 pointers.

Due to each NFT not using its own token ID as its metadata, for instance, token ID 5 may receive a random metadata in the IPFS that is not in the 5th index. Instead, it may receive the 1000th or 500th index.

To avoid confusion of the commonly used #{tokenID} series (i.e #0001, #2300, #0555) and not getting the index of the metadata number, we changed our naming system by naming our NFTs with 3 letters alphabet series, starting from #AAA to #YIN.

Our Metadata Naming

First off, with total supply of 5,555, we are reserving the last 5 to be auctioned off on Opensea to raise funds for a charity where 100% of the sales from these last 5 NFTs will be fully donated to this particular charity.

So that leaves us with the rest 5,550 total supply.

From this number, we divided 5,550 supply into 25 groups, which is identified by the 1st letter (A-Y).

Each group then will have 222 Leaf Index (5550 divided by 25 groups), identified by the 2nd & 3rd letter (AA-IN).

To illustrate, the metadata uploaded to IPFS will be as follows:

1st index = #AAA
26th index = #AAZ
27th index= #ABA
222nd index = #AIN
223rd index = #BAA

Randomization Algorithm Method

We first create a struct (read here about Solidity Struct) to store the metadata options. Our struct will consists of two uint64 variables which we call mdValue & mdKey.
mdValue is the current leaf index value in that group.
mdKey is the current group key.

What’s mdKey used for? at the start, mdKey is the original index of the mapping, and when the mdValue of certain group index reach max value, we will change the mdKey with the last available mdKey, reducing the max group counter, thus creating a randomized effect.
This method is inspired by Ping Chen’s Great Article on Card Drawing.

struct metaData {
uint64 mdValue;
uint64 mdKey;
}
// This struct will be packed and stored as just one uint256 with still much bit space needed

Secondly, we create 25 mappings of each group (A-Y) in the smart contract (read here to understand mapping). The mapping consists of uint256 key with metaData struct value (read here to understand Solidity Value Types).

mapping(uint256 => metaData) public mdPool;
for(uint256 i; i < 25; i++) {
mdPool[i] = metaData(1, i);
}

So, with 25 mapping(uint256 => metaData), we populate each mapping with the value of struct metaData(1, key).

Now, why do we need to populate the value? Why not just leave a 0? That’s because we do not want our members to pay the gas for the first-time storing value in the mapping (changing a value from 0 to non-zero), which costs 20,000 gas).

Let’s call this variable mdPool (mapping(uint256 => metaData))

Secondly, we set a maximum incremental number of each group to 222 (222 x 25 groups = 5,550).

Let’s call this variable mdMax = 222

We also needed to set a counter of the number of groups still available (a group is marked as unavailable when the incremented value in it reach the maximum of 222).

Let’s call this variable mdCounter = 25

Each time a mint function is called, we pick a random number ranging from 0 to max group still available.

r = uint(blockhash(block.number — 1)) % mdCounter;// This will produce a random number from 0 to max group available
// If mdCounter is default=25, random number range 0-24 (modulo will not results 25 & mapping start from 0 index)

After we got a random number for the group, we will then check the value of the group, for example if we got r = 5, then we will take the value of mdKey in mdPool[5].
Originally, mdKey in mdPool[5] will be 5, but let’s say mdPool[5].mdValue has reached its max value (222), then we replace the mdKey of mdPool[5] to the last group available (for instance group 24), we will replace mdPool[5].mdKey = mdPool[24].mdKey.
The next time a random number 5 comes out, it will check for the value from the mdPool[5].mdKey (which is now has been replaced with 24).

// Grab the key
uint256 mapKey = mdPool[r].mdKey;
// Bitwise operation to put 5 metadata into uint256
uint256 metadata;
metadata |= uint256(mapKey)<<(m*32);
metadata |= uint256(mdPool[mapKey].mdValue)<<((m*32)+16);

Let’s make an illustration that the value of mdPool[5] is 1.
Then we will store the metadata as [Group][Leaf Index], i.e: [Group = 5],[Leaf Index = 1], so the metadata will be (Group 5 = Alphabet E, Leaf Index 1 = AA), therefore #EAA.

Then we incremented the value of the chosen group by 1, to avoid double of course, so the next time a random number 5 comes out, the metadata will be [Group = 5][Leaf Index = 2], which is #EAB, and so on.

We will store 5 metadata into 1 uint256 variable (save a lot of cost), by using bitwise operation, storing each metadata into 32 bit.
This method is greatly inspired by Billy Rennekamp’s Article and Maksym’s Article, so much thanks for the both of you.

What happens when a group value reach maximum value(222)? Then, we simply lower the mdCounter, and mark that particular group as unavailable, setting a marker to the last available group.

// Increment value at the given key
if(mdPool[mapKey].mdValue < 222) {
mdPool[mapKey].mdValue = mdPool[mapKey].mdValue + 1;
} else {
// If group reach max value, replace current key with latest available key
mdPool[r].mdKey = uint64(mdPool[mdCounter-1].mdKey);
// Reduce counter for total group
mdCounter--;
}

For instance, when mdPool[5] reaches its max value (222), we would reduce mdCounter (mdCounter — , becomes 24), then we set metaData.key to the last available key. Therefore, the next time a random number is 5, it will take the value from the last group available, which is mdCounter-1 (because mapping start at 0 index).
Because we have reduced the mdCounter to 24, it will be impossible to get a random number 24, the random number will range from 0–23.

mdPool illustration originally
mdPool when index 1 reach max value 222, replace key with the last available key

Gas Cost

Inspired by Azuki’s ERC721A method (hats off to the team!), by not storing each token’s owner in each mapping slot, thus minting 1 and multiple cost is basically almost the same.

https://www.azuki.com/erc721a
https://www.azuki.com/erc721a

We implement this method in storing each token’s metadata, However there is a limitation to this, we store several metadata pointer in 1 uint256 variable.
In our case, we put 5 token’s metadata into 1 variable uint256 by using bitwise operation.

What we needed to store for each metadata is number of the group & leaf index.

As both numbers will not exceed more than 255 (group value is 0–24, leaf value is 1–222), we can safely store each number in an 8 bit. Thus, each metadata will require 16 bits (8 bit for group, 8 bit for leaf).

So, technically we could fit 15 Metadata (255 divided 16) into 1 uint256.

But we decided to set a maximum quantity per mint to be 5, so we fit each metadata into 32 bits (16 bit each for group & leaf), for no reason, we just have too much unused space, so why not use more of it :D

Each slot of uint256 in the mapping will contain up to 5 metadata of the next token

Yes we are very aware that there is a little gas increase compared to the original ERC721A. Is it a good trade-off? It’s up to you to measure, is the chance of playing at a gacha / slot machine that is not “rigged” worth it? However, we will keep trying our hardest to reduce the gas fee as much as possible, and we are very open to hearing incredible & creative innovation that we missed and not think of.

Final Code

Create a struct for each metadata group and populating the mapping with non-zero value
Mint function implement picking a random number from the available metadata pool, and storing a maximum of 5 metadata in 1 uint256 using bitwise operation
Reading & parsing the metadata token URI of each NFT

Deployed Smart Contract in Testnet

https://rinkeby.etherscan.io/address/0x4e19a42845ff35ada0041aaa0080ca1d4f303c67

You can play around with this deployed smart contract in the Rinkeby network to test out our metadata randomization algorithm.

Conclusion

This NFT realm is one space that is constantly evolving every minute. In that way, it is invigorating to see and experiment just how far we can play with the boundaries.

We are continuously learning and are very much open to new innovations and learning curves.

Lastly, let’s connect in this vast multi ver— we mean, metaverse (a little Doctor Strange reference there for you, Marvel fans :D). Please do share with us if you have any feedback or question, we are more than happy to hear back from you.

Cheers,
Edwinius
Playground Meta Dev

--

--

Playground Meta
0 Followers

A collection of 5555 Meta Komodo in Web3