Skip to main content

Making art that affects the air Part 2: How to Write Your Own Smart Contract

Right now, NFTs are making headlines for all the wrong reasons. But can we turn their reputation on its head? Can we swap “carbon catastrophe” for “art with a heart” and actively encourage change for the better?

That’s the question I set out to answer for my part in the DesignSpark Air Quality Project.

So far I’ve got up to speed with all the jargon, and explored the real carbon cost of blockchain tech, as compared with more familiar things like cheeseburgers or flights – and found the answers weren’t as straightforward as you might expect. (If you missed it, you can catch yourself up here.)

But as all makers know, the best way to really understand something is to make one yourself. So next is to get stuck in coding a smart contract of my own, and to show you what I did.

The idea here is to give you an unbiased look at the inner workings of the technology, so you can follow along and decide for yourself. Whether you publish a project or not, a peek behind the curtain will leave you armed to make a decision: is blockchain the future, or destined for the dustbin of history?

With that, let’s dive in…

HOW TO BUILD A SMART CONTRACT – BASICS

Smart Contract

To create our NFT from scratch, we’re going to need to write a “smart contract”. These are little pieces of code that live on the blockchain and can bring NFTs into being – through a process called “minting”. Smart contracts also manage some of the features of an NFT and keep track of the current owner.

They can be written in a variety of programming languages, including many you might already be familiar with, but for this example I’ll be using Solidity. This language has been developed specifically for writing smart contracts, which makes the process quicker since things are streamlined for this purpose. Being as Solidity is a popular choice for blockchain developers, there are also plenty of tutorials and resources available to help. The learning curve isn’t too steep, especially if you’ve programmed before in some flavour of C or JavaScript, then you’ll find a lot of it feels familiar.

Once you’ve picked your language, to get started on your project, you’ll need a code editor, and a couple of other bits of software to make your life easier. I chose to use…

Visual Studio Code – Popular, free code editor, available for Windows, Mac and Linux.

NPM package manager – A package manager can install, configure and keep track of the versions of different dependencies of your project (dependencies being things like modules for some programming languages or other software tools).

Hardhat – A tool specifically to help with testing and deploying blockchain projects. One helpful thing it can do is create a sort of mini private blockchain on your computer that you can deploy your project to, that way you can interact with and test your smart contract before you put it on any public blockchain.

NFT STANDARDS

What actually makes an NFT an NFT? Just like a cat or dog must display certain characteristics to be considered a particular breed, or how the legal definition of whisky requires it to be at least 40% ABV, there are standards for what constitutes an NFT.

The original and still most popular is ERC-721, which is just a universally agreed upon set of functions and behaviours that a smart contract and the NFTs it creates must have.

Another advantage of writing our smart contract in Solidity is that it makes it easier for us to adhere to this standard: a company called OpenZeppelin has a list of template contracts written in Solidity that meet different standards. We can just import one of their ERC-721 examples as the starting point for our project.

THE ART PART

NFTs are ultimately just code that stores metadata, so they don’t have to be art. Technically, they can be just a number, a ticket, a record, or anything else you can represent in a relatively short string. But a lot of them, especially the ones making the news at the moment, represent pieces of art, so I plan to do something similar in this project (albeit with my own little twist – more on that later).

One criticism of NFTs you might have seen is purchasers talking about their art disappearing after they bought it. How can this happen when the whole point of the blockchain is that it’s ultra secure and transparent? It comes down to the difference between on-chain and off-chain art.

In most cases, the actual artwork that an NFT represents ownership over doesn’t exist on the blockchain at all. Instead, the token’s metadata contains a link that points to where a copy of the artwork can be found.

In the worst cases, this is just a regular URL pointing to a single web server where some company is hosting the artwork. If their business fails or they simply stop hosting the image, then your token will still exist, but the link it contains will point to nothing. Not very decentralised.

In slightly better cases, the metadata will point to IPFS, a peer-to-peer file storage network. The idea is that images on IPFS are stored in a distributed and decentralised way, making it more resilient (i.e. your image won’t disappear because one person stopped hosting it). Although this is a better situation, since the art is still stored somewhere that isn’t the blockchain you’re not immune to the threat of the artwork disappearing and leaving a useless token behind.

An alternative to both of these approaches is to actually host the art on-chain – in other words, have the raw data that constitutes the image actually be part of the NFT’s metadata.

At first glance this seems like an obviously good solution, so why don’t all NFT projects just do this by default? The catch is there’s a limit to how much data you can squeeze into a token, and even before that point, the bigger it gets, the more cost prohibitive it becomes to sell and transfer. A simplified explanation is that because you’re paying a fee to computers on the network to process your transaction, the more data it includes (or the bigger your NFT), the more effort it is for them, so the more expensive it becomes.

This rules out storing things like HD movies this way. But when it comes to artwork, there’s a secret weapon we can use to pack more into less: SVG. This image format lets you build detailed, scalable pictures with few lines of code, and most browsers and NFT marketplaces will display it correctly without you having to do anything.

So this is the approach we’re going to use: SVG art on-chain. We’ll need to find a balance between making it simple enough to be cost effective, but detailed enough to be an interesting piece of art.

HOW TO TAKE IT TO THE NEXT LEVEL

Just a plain old SVG isn’t super exciting, and doesn’t do the best job of highlighting the interesting things people are doing with NFT technology (even with on-chain art). If my goal here is to investigate what’s possible, we’re going to need to push some boundaries.

Did you know NFTs don’t have to be static? Some more exciting projects recently have added elements that can change. This might sound impossible, given that one of the selling points of storing things on the blockchain is that they’re immutable.

The quick answer is this: think of it like a mini application. The source code doesn’t change, but the program can do different things as you interact with it. You can “change” an NFT if the associated smart contract contains a function that lets you, for example, update a variable. So we could write a function in our smart contract called “changeBackgroundColour”, which accepts a positive integer between 1 and 5, and updates the token’s metadata to point to one of 5 IPFS links representing versions of the same image with different background colours. Calling this function would change a token’s metadata, meaning it will also display differently on whichever marketplace or viewer you’re using. The token can’t change in any unpredictable way, but it has a few pre-coded variations that you can select between. (We can also decide when we write our code who is allowed to call this function – just the NFT owner, or anyone.)

This is a neat trick, but it requires some input from the user. Going one better, there’s a way to automate the process: the ChainLink Keeper Network. ChainLink describes this service as “a decentralized network of nodes that are incentivized to perform all registered jobs (or Upkeeps) without competing with each other”, meaning you can pay a small fee to have their nodes call a function in your smart contract at an interval that you specify. This interval could be daily, weekly, longer, or even based on some function you write yourself.

So for this project, how about an NFT that actually changes with air quality? We could write a function that changes the look of our NFT based on some input, and we could get the Keeper to call it at regular intervals to update.

But how would the keeper know the actual air quality data to give to our smart contract?

You might think we could make an API call from the function in our smart contract, but the nature of the blockchain means it’s a little tricker than that.

Remember in the previous article, we talked about how transactions get chunked into blocks and blocks get added to the blockchain? All the machines on the network have a copy of the ledger, and they all work on the calculations to add a block to the chain. Now imagine your contract involved an API call for a piece of data that can fluctuate rapidly, like an exchange rate. All the nodes in the system that are trying to either create or validate the block containing your transaction will make a request to the API at slightly different times, quite possibly getting different results. This means the results of their calculations will also be different – but they can’t be, because confirming that the nodes get the same answer for each of the blocks is how they reach consensus and verify additions to the blockchain.

The solution is to use another service to take that API data and record it on the chain in a transaction, then our contract can use that one agreed upon datapoint to do its thing.

For this service, we’re going to call on the ChainLink Oracle network. For a small fee, the Oracles will go off, call the API, agree a result and report that result on-chain for your smart contract to make use of, while maintaining the deterministic nature of the blockchain.

Put all of these pieces together and what you get is an NFT that can automatically change at regular intervals, responding to read-world data!

THE BIG IDEA

I’m going to make an air quality tamagotchi.

In other words, I’m going to make a little pet SVG Earth that looks happy when the air quality is good, and sad when the air quality is bad. It’s gamifying good air – make a bunch of these, sell them and their new owners will feel motivated to improve the air quality to see the tamagotchi Earth smiling.

But there’s more – I’m going to make sure the proceeds of the sales all go to a reputable carbon offset or carbon removal scheme. And the transparency of the blockchain means you can be sure that happens. Sell them for enough money and they can remove even more carbon than their creation released, making them actively good for the environment.

You might also have heard about NFTs and royalties, so whenever a token is bought and sold, a portion of the sale price goes to a specific wallet. It would be great to use this trick for tamagotchi Earth, so trading your tamagotchis has a continued benefit in the effort of combating climate change by sending more money to carbon offset and removal. But there’s a bit of a caveat.

Royalties as it stands aren’t well implemented at the smart contract level. One reason for this is the fact people often have multiple wallets and move assets between them. It’s not possible to tell if a transaction where an NFT moves between two wallets is a sale or an owner shuffling assets around. A royalty payment coded directly into the contract would charge owners to move their NFT between their wallets, which most owners wouldn't be in favour of. So you could make the payment voluntary instead, but then you risk people choosing not to bother on legitimate sales.

At the moment, royalties are implemented through marketplaces – when you sell through a marketplace, the transaction happens through a marketplace contract that collects a fee from the buyer, extracts a cut for royalties, as well as a cut for the platform, and pays the rest to the seller. It then transfers the NFT to the buyer, acting as a middleman in the transaction. But these royalties don’t transfer between platforms, so it’s possible that the new owner of a piece you sell on OpenSea could decide to resell the token on Rarible instead, in which case you wouldn’t get the royalty payout you would have done if it was resold through OpenSea.

For now, the best approach seems to be to register the project on as many marketplaces as possible, so we can set a royalty amount, and hope people keep in the spirit of the project – after all, it’ll be apparent on the blockchain if they don’t.

THE CODE

Finally! We get to the project itself! Here’s a quick rundown of the code I’ve written for my NFT and what it all does.

THE SMART CONTRACT

(Once the project is complete, you'll be able to find the final version of this code on the blockchain, as well as through my Github.)

We start off really simple, by specifying what version of Solidity we want our contract to compile with.

pragma solidity ^0.8.0;

The next few lines are where we borrow code from elsewhere to get us started – the first one is our ERC-721 compliant template, the next is some helper code that will let us write our SVG metadata in a way our browser can easily read, the next two are provided by ChainLink so we can access their Keeper network and their Oracles, and the last one lets us “own” the contract as well as transfer ownership.

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "base64-sol/base64.sol";
import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";
import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@openzeppelin/contracts/ownership/Ownable.sol";

In solidity, all contracts begin with “contract”, and everything in the curly braces that follows… well… is your contract! Give it a name to start, then name the pre-written contracts you’re borrowing from. In this case, we’ve called ours “OurContractName”.

contract OurContractName is ERC721URIStorage, KeeperCompatibleInterface, ChainlinkClient, Ownable {

Then we set up some variables we’re going to use later:

//Counter to keep track of how many NFTs we generate
 uint256 public tokenCounter;
 //Mapping that will tie the unique background each token has to its token number
 mapping(uint256 => string) public tokenIdToSVGPart1;
 //The particulate matter reading – which we'll get from the API
 uint256 public pmValue;
 //How often we update the pmValue and therefore the image
 uint public immutable interval =60;
 //Variable to keep track of the last time we updated the pmValue
 uint public lastTimeStamp;
 //Event to give us some feedback when we've created an NFT
 event CreatedNFT(uint256 indexed tokenID, string tokenURI);
 //Required line for using Chinklink’s network
 using Chainlink for Chainlink.Request;

Next, we’ll need a bunch of strings to hold parts of the SVG code. Later in the project, we’ll concatenate these along with our external data to form the full SVG that will live in our token’s metadata. (I’ve abbreviated some strings for easy reading.)

string public part1 = '<svg …';
 string public part2 = '"/><stop offset="95%" stop-color="';
 string public part3 = '" /></linearGradient>...';
 string public grin= '</text>...</svg>';
 string public smile= '</text>...</svg>';
 string public neutral= '</text>...</svg>';
 string public annoyed = '</text>...</svg>';
 string public sad= '</text>...</svg>';
 string public dead= '</text>...</svg>';
 string[6] colors = ["#A8F", "#FDC", "#7DB", "#D69", "77B", "#6AD"];

… and some more that we’ll need for the Oracle.

//Oracle details for making request to API
 address private oracle;
 bytes32 private jobId;
 uint256 private fee;

Now for the thoughtfully named “constructor”. In the brackets, give your NFT collection an overall name, as well as a “symbol”. The symbol is a short name for the tokens – a bit like GBP or USD.

constructor() ERC721 ("Our NFT Name", "X"){ 
 //Initialise token counter to zero
 tokenCounter =0;
 //Set last updated time to current time
 lastTimeStamp = block.timestamp;
 //Details for oracle – these will vary depending on the particular Oracle job you’re using. You can see ChainLink’s documentation for more info. 
 setPublicChainlinkToken();
 oracle = XXXXXXXXX;
 jobId = "XXXXXXXXX";
 fee = 0.1 * 10 ** 18;
 }

Next comes the real meat of things: all the functions. First up is the function to mint our NFTs. The only owner modifier means that only the owner of the smart contract can call the function. Most often, this is the person who wrote and deployed the smart contract, but ownership can be transferred. In this style of minting, the contract owner would mint all the NFTs, paying any transaction fees, then sell them afterward. Another option would be to make it so that anyone can call the function and mint an NFT for themselves, but you require that the “value” of the transaction is above some amount, i.e. the price you want to sell the NFT for. In this case, the fee is paid to the contract, which essentially acts as a wallet, so you need another function that allows you to withdraw the fee (and you probably want to make this function an “onlyOwner”!)

function create() public onlyOwner {
 _safeMint(msg.sender, tokenCounter);

We need to give our newly minted NFT some metadata, so next, we’ll create the SVG, base64 encode it and format it so that it will display properly. To make this function neater and more readable, we’ll break these actions into steps and create functions to do each part.

uint256 tokenId = tokenCounter;
 string memory initialSVG = createInitialSVG(tokenId);
 string memory imageURI = createImageURI(initialSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenId, tokenURI);
//Emit an event so we have some feedback that our NFT has been created
 emit CreatedNFT(tokenId, tokenURI);
 //Once we've created a token, we increment the counter
 tokenCounter = tokenCounter +1;
 }

In this next function, we concatenate the parts of the SVG to build our initial image – we’re giving our tamagotchi Earth a neutral expression to begin, before we update it once we grab some air quality data from the API. We also pick a pseudorandom background, by making a gradient from two of the colours in the list we created at the top of the code.

function createInitialSVG(uint256 tokenId) public returns (string memory initialSVG){
 //SVG will go part1, color, part2, color, part3, text, face
 string memory SVGPart1 = string(abi.encodePacked(part1, colors[tokenId%tokenId], part2, colors[(tokenId+1)%tokenId], part3));
 tokenIdToSVGPart1[tokenId] = SVGPart1;
 initialSVG = string(abi.encodePacked(SVGPart1, uint2str(pmValue), neutral));
 return initialSVG;
 }

Then we want to format it properly, to let the browser or viewer know that what follows is xml and base64 encoded, so it displays correctly.

function createImageURI(string memory fullSVG) public returns(string memory imageURI){
 string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked(fullSVG))));
 string memory baseURL= "data:image/svg+xml;base64,";
 return imageURI = string(abi.encodePacked(baseURL, svgBase64Encoded));
 }

Finally, we take the pieces and stitch them together into the JSON object that’s actually going to form the metadata for our token.

function formatTokenURI(string memory imageURI) public pure returns (string memory){
 string memory baseURL = "data:application/json;base64,";
 return string(abi.encodePacked(baseURL,
 Base64.encode(
 bytes(abi.encodePacked(
 '{"name": "Our NFT", ',
 '"description": "A set of NFTs that look different and change with air quality", ',
 '"attributes": "", ',
 '"image": "', imageURI, '"}'
 )
 )
 )
 ));
 }

After that come the functions to deal with Oracles and Keepers. To interact with the Keepers (which automate a process in our contract), we need a “checkUpkeep” function and a “performUpkeep” function. The checkUpkeep function is what decides the interval at which our automatic action is performed. Whenever a new block is added to the blockchain, the Keepers will simulate calling the checkUpkeep to check whether it returns true, and if it does, the Keeper will call the performUpkeep function. In performUpkeep, we’re going to call another function – one that will use the Oracle network to grab a value from an API, then update the token metadata to reflect that.

function checkUpkeep(bytes calldata checkData) external override returns (bool upkeepNeeded, bytes memory performData){
 upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
 performData = checkData;
 }
 function performUpkeep(bytes calldata performData) external override { 
 //Set the last updated time to the current time
 lastTimeStamp = block.timestamp; 
 requestData();
 performData;
 }

The next two functions deal with the request to the Oracle to get the API data, and the response from the Oracle. The request is reasonably straightforward – you just need the URL for your GET request and to know what you expect the response to look like. Then the Oracle calls back with the result, passing it to the fulfill function.

function requestData() public returns (bytes32 requestId)
 {
 Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
 // replace URL will the target URL of the GET request
 request.add("get", "URL); 
 // replace “X” with the path to the data that you want, assuming the request returns a JSON response where values are nested 
 request.add("path", "X"); 
 //Solidity can’t really handle decimals, so multiply the result by 1000000000000000000 to remove them
 int timesAmount = 10**18;
 request.addInt("times", timesAmount); 
 // Send the request
 return sendChainlinkRequestTo(oracle, request, fee);
 }
 function fulfill(bytes32 _requestId, uint256 _pmValue) public recordChainlinkFulfillment(_requestId)
 {
 pmValue = _pmValue;
 //loop through all tokenIDs and update each one
 for(uint i=0; i<tokenCounter; i++){
 updateTokenURI(i);
 }
 }

Now we also need a function to do the actual updating of the token metadata after we’ve called the API and got the data back. For my tokens, I’m giving tamagotchi Earth one of 6 expressions, depending on how good or bad the air quality is. This is achieved with a simple “if” statement – we check which bracket the air quality value falls into, then concatenate the correct pieces of the SVG to build the appropriate image. Then, we format that SVG correctly using some of the functions from above and update the token metadata with this new image.

function updateTokenURI(uint256 tokenID) public {
 if(pmValue<16){
 //grinning face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), grin));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 else if(pmValue<31){
 //smiling face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), smile));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 else if(pmValue<56){
 //neutral face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), neutral));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 else if(pmValue<81){
 //annoyed face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), annoyed));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 else if(pmValue<111){
 //sad face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), sad));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 else {
 //dead face
 string memory fullSVG = string(abi.encodePacked(tokenIdToSVGPart1[tokenID], uint2str(pmValue), dead));
 string memory imageURI = createImageURI(fullSVG);
 string memory tokenURI = formatTokenURI(imageURI);
 _setTokenURI(tokenID, tokenURI);
 }
 }

The very last thing in the contract is just a helper function that converts integer values into strings. We need this so we can convert the air quality value from the API into a string and concatenate it in our SVG. You can find the original here: https://stackoverflow.com/questions/47129173/how-to-convert-uint-to-string-in-solidity/65707309#65707309

function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
 if (_i == 0) {
 return "0";
 }
 uint j = _i;
 uint len;
 while (j != 0) {
 len++;
 j /= 10;
 }
 bytes memory bstr = new bytes(len);
 uint k = len;
 while (_i != 0) {
 k = k-1;
 uint8 temp = (48 + uint8(_i - _i / 10 * 10));
 bytes1 b1 = bytes1(temp);
 bstr[k] = b1;
 _i /= 10;
 }
 return string(bstr);
 }
}

Aside from what we have in the actual contract, there’s a little additional setup we need for the Keepers and Oracles. They don’t do the job for free, so we need to make sure our contract is funded with Link, the currency of the Chainlink network, so we can pay the Oracle for fulfilling our API request. We also have to register our upkeep job with the Keeper network, so they know that we want them to look out for our contract. The Keepers need funding in Link too, through a separate address.

We can keep the cost for each individual upkeep and API call small, so if we limit the number of them then we can minimise the cost. The plan is to fund the contract and Keeper with enough Link that they can keep doing their job for years to come.

It’s always possible to add more funds – and we can make it so that anyone can do that, not just the contract creator or owner. But if we know roughly how much our transactions cost and how many we’re making, we can future proof quite effectively.

THE DEPLOY SCRIPTS

Now we’ve written the smart contract, how do we get it on the blockchain? With a deploy script and a bit of patience – because we probably want to put it elsewhere first!

I’d recommend deploying in three stages: locally first, then to a testnet, finally to mainnet.

Locally means using Hardhat so your computer essentially fakes your own private little blockchain to deploy to, so you can test that everything works like you expect.

Next, testnets are public blockchain networks, but they don’t cost any real money – they let you see how your NFT will actually appear on marketplaces like OpenSea and explore it with services like Etherscan. You can get testnet Link for free by following the directions in the Chainlink documentation.

Once your project is working perfectly on a testnet, then it’s time to deploy to mainnet and fund your contract with real money.

The majority of my deploy script came from this great tutorial by Patrick Collins: https://youtu.be/9oERTH9Bkw0

His scripts are especially useful because they include lines to fund the contract with Link directly.

WHICH CHAIN?

The code I’ve written here will work for “EVM blockchains”, or Ethereum compatible blockchains – which means they’ll work on the Ethereum network, but also on a group of other blockchains based on similar tech. There are hundreds, but a popular example is Polygon/Matic.

I want to try and strike a balance between making my NFT as green as possible in its creation, but also making it easy to purchase, trade and interact with. This means it might be better to deploy to a popular blockchain that’s supported by the largest marketplaces. Polygon/Matic might be a good option: it’s not quite as popular as Ethereum, but it’s still widely used, and it uses proof of stake rather than the more energy intensive proof of work. But I’ll decide that for sure in part three…

PART THREE

Join me in the third and final instalment of this project, when I finally deploy my contract to a mainnet, and decide how I’m going to sell my NFTs.

Hannah is a former science journalist, now a special effects technician. When she isn't busy using giant robot arms to carve spaceship wings out of foam, or crafting circuitboards for stage shows, she makes videos for YouTube about her whimsical inventions. These also featured in her column for New Scientist magazine, which later spawned the How to Be a Maker tutorial series. Hannah is a passionate promoter of the maker movement, and an advocate for tools that make tech more accessible.