Onchain Block Invaders (OBI) is a fully on-chain NFT collection on ethereum network. The most remarkable thing about this collection is that all of the metadata belonging to the NFTs are stored and generated on-chain as well as having modular contracts that gives holders the chance to have interchangeable skins and color palettes for their NFTs.
One of the most interesting things about this collection is that thanks to its fully on-chain metadata and modular contracts holders can switch between skin and color contracts enabling them to have a completely different looking NFT while preserving their right to switch. While this switch process requires a small gas fee as it is an on-chain transaction, platforms that already have cached images can display all the current and alternative looks of an OBI by querying the assets (skins and color palettes) a holder has.
Modular contracts lets us to render our OBIs with a totally different configuration in the future which is pretty exciting as it opens a door for endless possibilities. For now there are 3 main contracts which are as follows:
1- Mint Contract:
What color palettes and skins an OBI have can be registered here. Every OBI can be combined with 32 color palettes and 32 skins which makes up to 1024 combinations. Minting new assets for OBIs in the future will not lead to minting of new NFTs but rather unlocks new looks for existing OBIs
2- Skin Contract
In charge of generating and storing skins on-chain
3- Color Contract
This contract will store and generate new color palettes and SVG effects that can be applied to each skin that one holds
Contracts reduces gas by generating images through strings as they can be tightly packed while uploaded to the chain. The team claims that they have used a utils library for strings which have made their job much more easier. If you are dealing with complex string operations I highly advice you to check out the Github repo of the library:
You can further read about the gas reducement on generating images on-chain from the following Twitter thread:
The entire collection is about 17kb. The fee of storing a 256 bit word is 20k gas. In the time of writing this article average gas price is 10 gwei. This means that the cost of 1kb is as follows:
20000 * 10 * 0,00000001 * 1000 = 2 USD
as 1 gwei equals 0,00000001 ethers and 1 ether equals 1000 USD. Thus we can say that the total cost is around 34$
As the team highly values building relations inside the community they have created so-called gifting system in which holders will be able to mint skins or colors to other holders that can be tied to seasonal events.
There will be a nostalgic retro game in which you can earn giveaways, WL spots, skins and palettes by interacting with your assets (skins and color palettes).
Most of the IERC721 function implementations are similar to ones of Openzeppelin except some util functions which I will mention soon and the ownership state of tokens are held in a dynamic array with the identifier owners in which index of the array represents the token id whereas the value is a struct called _contractStruct in which there are 7 fields as follows:
Honestly at first I had a difficult time understanding what all those field for which I guess is because of the bad naming. idx1 represents the skin index this OBI uses and idx2 is the color palette index used by the OBI. cnt1 and cnt2 are the number of skins and colors the OBI have currently in order. bitmap1 and bitmap2 are the data structures from which the mint contract is able to check if the OBI has the newer skins and colors (to see if the OBI can mint the skin or color )as well as storing the skins and colors minted historically. And the account field is obviously for storing the holder address.
The utils functions are for checking if the OBIs have the newer skins and color palettes and if not minting those new assets for the OBI.
function isBitSet( uint32 _packedBits,uint8 _bitPos) internal pure returns (bool){
uint32 flag = (_packedBits >> _bitPos) & uint32(1);
return (flag == 1 ? true : false);
}
function setBit( uint32 _packedBits,uint8 _bitPos) internal pure returns (uint32){
return _packedBits | uint32(1) << _bitPos;
}
function countSetBits(uint32 _num) internal pure returns (uint32)
{
uint32 count = 0;
while (_num > 0) {
count = count + (_num & 1); // num&1 => it gives either 0 or 1
_num = _num >> 1; // bitwise rightshift
}
return count;
}
Here one of the most confusing things is to understand how the skin and color infos (whether they were minter or not) are stored. The team’s decision is to use bits for each color or skin for optimization. As I mentioned before the bitmap variables are uint32s which exactly hold 32 bits (do you remember we mentioned you could have up to 32 skins and colors). Each bit represents a different skin or color while the functions above are operations that manipulate those bits. To elaborate, isBitSet
function checks if the bit with index _bitPos
is non-zero (the holder haven’t minted the skin or color with that index) while the setBit
function returns a new uint32 which switches a bit of _packedBits
of given index.
First of all I want to mention how the modular contract system I stated before works allowing totally different rendering chances in future skins
mapping(uint256 =>address) private _tokenIndexToAddress;
this mapping stores the rendering contract addresses from each skin index an OBI has (To be objective again the identifier here is not clear about what its purpose is). So whenever one decides to switch the skin his/her OBI uses (like the morphOBI
function) he/she will trigger the following statement
_owners[tokenID].idx1 = skinNr;
which may cause the OBI to be rendered with a different contract as the tokenURI
function returns the following:
IMotherShip motherShip = IMotherShip (_tokenIndexToAddress[_owners[_tokenId].idx1]);
return motherShip.launchPad(_tokenId,_owners[_tokenId].idx1,_owners[_tokenId].idx2,_owners[_tokenId].cnt1,_owners[_tokenId].cnt2);
The launchPad
function of MotherShip
interface renders the OBI depending on the given parameters. However the key point to understand here is that by running the previous code we actually change the MotherShip contract we are interacting with.
Overall, I believe the team made a pretty innovative job in a such period where most of the NFT projects are the imitation of each other. Both the concept and the way the code is written is unique and opens some exciting doors for the NFT space. Hope new projects that uses similar techniques will emerge as a life without surprises would be like crypto without Ethereum :)