How to Implement 100% On-Chain NFT Contracts

mbvissers.eth
Quick Programming
Published in
4 min readFeb 11, 2022

--

NFTs without IPFS and fully on-chain? Yes Please!

Photo by Hitesh Choudhary on Unsplash

A big problem that the crypto and NFT space are trying to fix is centralization. Blockchain is a big part of that. And without the blockchain, we wouldn’t have DAOs that hold millions of dollars or projects like CryptoPunks. But a problem that still resides from most projects is: “What if IPFS goes down?”.

IPFS is very decentralized already, so it is not something to worry about. But some collectors might prefer pure on-chain data of their tokens. Metadata and art. Thankfully we can do this in Solidity. It’s compatible with OpenSea as well.

How can we store images on-chain?

You can store anything on-chain, the problem is how to store it. You most likely want to base64 encode your data and prefix/suffix it with some more information so that a computer knows what it is looking at. This is what works for OpenSea, where I’ve tested this.

A smart contract (or library) can be used to encode the data to base64 on-chain. I used this library. It is made by someone on the Loopring team.

Now, In your smart contract for your NFTs, you want to be able to encode SVG data into base64 and prefix it with the right information.

This function will be used in another function later on in our safeMint function, but it essentially encodes the SVG ‘string’ to base64 and returns it encoded for Solidity to deal with further.

SVGs aren’t strings, they're images, right?

No, they can be seen as images, but they’re just written as code, very similar to HTML. I’ve even written an article on how to write SVG as well. To get the code, you can just open any SVG file in a text editor and copy the code.

How will the image look? Raw, and encoded?

Since I’ve already tested this before for a potential client, I also found a simple SVG file to test with. It looks like this in plain SVG:

And, base64 encoded it looks like this:

data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNTAwJyBoZWlnaHQ9JzUwMCcgdmlld0JveD0nMCAwIDI4NSAzNTAnIGZpbGw9J25vbmUnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZmlsbD0nYmxhY2snIGQ9J00xNTAsMCxMNzUsMjAwLEwyMjUsMjAwLFonPjwvcGF0aD48L3N2Zz4=

You can copy this encoded string and paste it into your browser's address bar to see the actual parsed SVG (at least with Chrome). That’s exactly what we’re doing, we make it readable for the browser, except we need to make it accessible for browser, AND blockchain which is why we encode it further with abi.encodePacked() .

Notice the first (bold) part of the string. It’s saying that the data is an image from type svg+xml encoded in base64 .

The browser reads it without any issues

Encoding the TokenURI

A TokenURI is (often) a link to a JSON file with specific properties, you can find the standard here. But, like the image, we can base64 encode the JSON file as well and make it readable in the browser with the right prefix. Instead of the SVG prefix we use data:application/json;base64 to let the browser know that it’s a base64 encoded JSON file.

We can create the whole file in Solidity as well. We encode it, encode it some more, and encode it some more. Although it looks funny, and everything but neat, this code works for OpenSea.

As parameters, I take the different properties that a tokenURI has and I use encodePacked as a way to paste the strings all together (since JSON can be used as a string).

Note: The _properties value is a string that can be an array, like "[1, 2, 3]" .

We encode that final JSON to base64 using the library we imported, and we use encodePacked again to prefix it to make it useable for the browser as if it were a regular URL.

ContractURI?

Aside from the TokenURI, which gives information about each individual token, we also have the ContractURI. It gives information about the collection as a whole, and OpenSea does (somewhat) support this as well. You won’t have to add anything manually in OpenSea apart from the contract address.

I encoded it with the following code. It’s similar to the TokenURI but with fewer options. I just pass the whole ContractURI JSON as a single string.

Note: OpenSea doesn’t update the collection information from the blockchain, so set this once before you add it to OpenSea!

Minting new tokens

Minting new tokens does require some more information now that everything is on-chain. Instead of simply checking the price, or minting a new token we also need to set all information (Or set it later using the setTokenURI() function).

So we pass a lot more information along with the safeMint function like the _description , _name , _source (SVG), _properties which we then all encode using the functions we went through earlier.

Conclusion

We went through a ton of stuff. The complete contract I have written to test this with can be found here. I recommend you actually read the article completely to understand how everything works and why some choices were made.

Although creating an on-chain collection might seem cool, be wary that it is everything but cheap for those who will mint the tokens. You have to submit a ton more data to the blockchain, and that will guzzle gas like you wouldn’t believe.

Thank you so much for reading and have an excellent day.

Support me by supporting Medium and becoming a member. It helps me out a lot, it won’t cost you extra, and you can read as many Medium articles as you like!

Follow me on Twitter and gm.xyz to keep up with me.

Check out the project I’m the dev of here.

Check out my latest NFT collection on Polygon.

--

--

mbvissers.eth
Quick Programming

I occasionally write about programming. Follow me on Twitter @0xmbvissers