Solidity 101: Learn to read a Mint Contract
For non-coders. We cover common keywords and use the Azuki contract to learn to read other NFT contracts on your own
TLDR:
Solidity is the most popular language for Smart Contract development, it compiles down to machine readable code known as EVM bytecode.
Basic keywords include bool, uint, string, address, mapping, msg, require, function. A definition of all of these can be found below.
Smart Contracts usually follow the format of: imports, name and implements, state variables, constructor function, remaining functions.
Last week we spoke about “Navigating Smart Contracts on Etherscan”. We covered concepts like what a block explorer is, how to view the code, and how to read and write variables to a smart contract using Etherscan’s interface.
I’d originally planned to split that article into two parts and go deeper into some of the more popular contracts like BAYC, Moonbirds and Art Blocks this week. But I quickly realised that before I could do any of that I needed to dive into the basics of Solidity first. I’ve gone back and removed “Part 1” from the original article and instead in coming weeks I will dive separately into specific famous NFT contracts.
My goal today is not to teach people how to code or how to deploy their own contracts in Ethereum, my goal is simply to teach you enough to be able to read a minting contract and understand at a high-level what is going on. I assume that you don’t want to learn to code but do want to understand enough that you can follow the code. This does require getting a bit technical but my posts will always be intended for an audience of builders who are non-coders.
After reading this you should have a basic idea of what an NFT minting contract looks like and be able to follow other minting contracts you find on Etherscan.
Solidity Language
First of all, Solidity is the main language used to develop smart contracts on Ethereum. It was proposed in 2014 by Gavin Wood and formally created by Christian Reitwiessner and his team during the creation of Ethereum, and released in 2015.
Smart Contracts are the code that make development on blockchain possible. All the tokens, NFTs, DeFi protocols and everything else minted and created on-chain are created with smart contracts. So Solidity is really important! Understanding Solidity means understanding the language that makes the magic on Ethereum happen.
Solidity is an object-oriented, high-level language that compiles down to EVM (Ethereum Virtual Machine) bytecode. In layman’s terms its a human readable language that gets broken down further into machine readable code that any Ethereum compatible blockchain can interpret. This is why so many other blockchains also use Solidity for smart contracts, like Polygon and Avalanche, because they were built using the EVM.
The Solidity language is just one of several languages which can be compiled into EVM bytecode, another language that can too is called Serpent. However, Solidity has proliferated and dominated the space as the main coding language used on the EVM and accounts for the vast majority of contracts on-chain. For reference, Solidity resembles C++ and javascript, while Serpent resembles Python.
There are blockchains that don’t use Solidity like Solana and Polkadot, both of which use the Rust language, but our focus is on Ethereum where most of the smart contract development is happening.
Common Keywords
Coding languages have keywords that get used to define different types of variables, functions and modifiers. For example, words and numbers are pretty different and therefore are stored in different types of variables. We’ll list some of the most common ones below:
bool is one of the most basic keywords and simply represents a true or false statement. For example bool success = true would define a variable called “success” and set it to be “true”, which could be useful for comparisons to see if an operation has succeeded or not.
uint256, uint128, uint64, uint32 all represent numbers. “uint” stands for unsigned integer, which just means a non-negative number, and the suffix 256 down to 32 refer to the number of bytes they take up. The more bytes the more numbers the variable can represent. Developers will try to use less bytes where possible because storing less data on-chain makes contracts more gas efficient.
string is used to save text. For example the name of the BAYC NFT can be saved as string private name = “BAYC”. Here we introduce another keyword private that makes the variable or function inaccessible from the outside, as opposed to public that means other contracts and users can interact with them.
address refers to an ETH address eg. 0xed5af388653567af2f388e6224dc7c4b3241c544. You’ve probably seen them everywhere and already have one with your own wallet. Naturally the code needs to interact with these so there’s a keyword in Solidity.
mapping gets used to save a mapping between one variable type to another. An obvious example of this would be mapping(uint256 => address) that could save NFTs within a collection to the address that own them. For example NFT number 10 could be mapped to an address “0x123…”, meaning “0x123…” owns NFT 10.
msg.sender and msg.value are also incredibly important. Whenever a function is called by a user or smart contract the code can check msg.sender to get the address that called it and msg.value to check the amount of ETH they sent. Functions marked as payable can receive ETH from the msg.value variable.
require is used with a condition to check if some operation is possible or not. For example require(msg.value >= priceOfNFT, “Not Enough Money”) could be used in a mint function to ensure that the ETH sent by the user minting is greater or equal to the price of the NFT. If it is not then the function will exit and return an error saying “Not Enough Money”. Similarly the if keyword can be used to check conditionals too but it won’t return an error, it will carry on executing the code.
variable = value, whenever you see the “=” sign it is being used to save a value into a variable. For example numberOfNFTsMinted = 0 saves the number 0 into a variable for number of NFTs minted. Variables created outside a function get saved on-chain while those created within them do not. Saving data into on-chain variables is the single most expensive operation in terms of gas, gas efficient contracts will write as little as possible on-chain. This cost is on purpose to keep the blockchain from growing too large too fast.
function name(parameters) {body}. This is how a function is defined and although a little complicated at the start you’ll quickly get used to. Functions define the code that can get run at any specific moment within their body. Users and other smart contracts will call functions to perform calculations, read or write variables, and call other functions themselves. For example totalSupply() public {return numberOfNFTsMinted; } is a function called “total supply” that is public so anyone can call it and just returns the variable called numberOfNFTsMinted.
There’s a lot to take in this section and there’s obviously a lot more keywords out there but with these basic ones you will know enough to get started into reading real contracts. Make sure to revisit these whenever stuck, or if you want to dive deeper you can check the Solidity documentation.
Mint Contract structure, using the Azuki Contract
Now we have some common keywords, we can start looking at a real contract to understand the basic structure. We’ll use the Azuki contract to show the format that pretty much all NFT minting smart contracts follow:
https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code
This contract uses a lot of nuanced and complex coding patterns, but don’t let that confuse you, the basic structure is the the same as almost every other NFT contract.
The first thing you’ll see in the link to the Azuki smart contract above is that there are many files, 13 to be exact. You can imagine that although there are 13 files they are all bundled up into a single smart contract on-chain.
The files get referenced in by the import statements at the start, which simply tells the code to include these other files so the new code can run those previously written contracts’ functions. Re-using battle-hardened code is standard in software development as it makes it faster to write and avoids bugs. Note that we also define the version of Solidity that our new contract code is written in.
Contract definition is next. This is where the contract is given a name and we define the other contracts it inherits from. Inheritance basically means that it can call certain functions and set certain variables from those imported files.
For example with Azuki we can see it inherits from 3 other contracts, one of which is the ERC721A that’s a practical implementation for the ERC721 standard written by Azuki themselves. The ERC721A contract is also visible on the same page in Etherscan but we’ll leave understanding ERC standards for another day.
Next we usually have variables used for the contract to keep track of internal state, which will be saved on-chain. For example you can see that there are some constant variables that get set once and never change as they are immutable, for example the maxPerAddressDuringMint. There’s a struct that’s just a grouping of variables. And you can also see the “allow list” that maps addresses to uint256s ie. addresses that map to a non-zero number will be on the mint’s allow list.
Constructor function will comes next, which is the initial function that gets called when the smart contract is originally saved onto the blockchain. It takes a set of parameters and initialises any variables necessary. You can see that Azuki’s constructor takes 4 uint256 parameters. These parameters either get saved directly into this contract’s variables or into variables it has access to through inheriting the ERC721A contract, where for example it sets the name of the NFT itself to “Azuki”!
Afterwards all the remaining functions get implemented in the contract. An example of one of the simpler ones in this contract is the “withdrawMoney” function shown below that follows the basic structure of “function name(variables) {body of function}”, and it makes use of a few interesting keywords.
The onlyOwner part is a “modifier” previously defined by inheriting from Ownable, that means only the owner of this contract can call this function, as if anyone else does it will fail. Which is important as if anyone could withdraw money from the contract it would pretty bad!
msg.sender.call runs a function on the user who interacted with withdrawMoney itself, (ie. the owner). This makes that address receive the amount of ETH passed in, which has been specified as the balance of address(this) (ie. the ETH in this contract). Therefore it sends to the owner the balance of ETH held in the contract!
The output of the call is then saved as a bool, in other words a true or false statement about whether the function succeeded or not. It does a final require check to fail if the transaction didn’t go through and give a legible error message to the user who tried to run the function saying “Transfer failed.”.
And that’s a wrap for today!
We got pretty technical today just like last week. You now know some of the more common keywords for Solidity contracts and how the basic structure of a contract looks. In coming weeks we’ll look at the ERC1155 and ERC721 standards and dive deeper into some other famous contracts!
Keep building builders!