top of page
hero.png

Free Solidity Tutorial

Who is this for?

No email required! This is for experienced programmers who want to get to the point quickly and immediately practice the information they just gained in a highly optimized topic order. We emphasize the unexpected and unusual aspects of the Solidity language while glossing over things we can reasonably assume to be obvious to a competent developer. Aside from an occasional humorous remark, we’ve made the tutorials as short as possible (but not shorter). Although this is a solidity beginner tutorial, it is intended for experienced coders.

Get ready to experience the learning efficiency of RareSkills!

Why this tutorial is superior (besides being free)

Tutorials that were created a long time ago use older versions of solidity.

As of Solidity 0.8.19 (released February 22, 2023), our tutorial is up to date.

Another significant difference is that we teach the foundry development environment from the get-go. Previously, Truffle, then Hardhat were the industry standard. As of 2023, Foundry is the dominant development framework.

The Solidity language is not difficult to learn. It looks a lot like javascript (or Dart, for those of you who know that language). This tutorial assumes you already know how to code in a popular language and leverages that knowledge to learn Solidity quickly. You should already know what functions, integers, strings, arrays, and so on are. If you are learning to code for the first time, this tutorial is not appropriate for you. It’s for experienced coders to get up to speed in Solidity quickly.

An important part of the tutorial is you will spend more time coding than reading materials or watching videos. We will provide you just enough information to get you started, then provide some practice problems designed to enforce what you just learned. The practice problems are a very important part of learning the language. If you are just curious, it’s okay to just read the content. But if you want to actually learn Solidity, you need to do the practice problems. When you finish, check out or suggested beginner solidity projects to enforce what you've learned. 

Don’t be fooled by the fact that this class is free. This is not low-effort content. This tutorial was designed by Jeffrey Scholz, who is a two time best seller for the only expert-level Solidity courses on Udemy. The practice problems were created by a team of 3 additional experienced solidity engineers. We urge you to compare our syllabus to other paid courses to see for yourself.

We are not shy about the quality of our work.

The free content at RareSkills is superior to the paid content elsewhere.

Why are we making it free when others charge hundreds of dollars for content like this?

Because we hope later on, at least a few of you will take our Advanced Solidity Bootcamp, which is paid.

There is a distinction between knowing a language, and knowing a domain. Knowing Python doesn’t make you a data scientist, knowing Javascript doesn’t make you a frontend developer, and knowing Kotlin does not make you an Android developer. Similarly, knowing Solidity does not make you an Ethereum smart contract developer.

However, Solidity is a prerequisite for developing smart contracts. We teach smart contract development in our Advanced Solidity Bootcamp. That course, obviously, assumes you know Solidity.

This tutorial, both the material and the practice problems, is 100% free, does not require a login, a credit card, or any information from you. We hope you benefit from this valuable resource and maybe attend our training programs and join our amazing community down the road!

License

We are providing free of charge information that others charge money for, sometimes hundreds of dollars. To prevent abuse, please understand the terms of our copyright.

Although this information is free of charge, it is not free to redistribute, modify, or copy. Redistribution of any kind is not authorized. The source code in the projected is licensed under the Business Source License. Redistributing, reproducing, or creating derivative works is strictly prohibited.

If you would like to share this with others, please simply provide a hyperlink to this page.

Let's Start

Development Environment

 

To get started, we will learn solidity as a language. We won’t begin with deploying contracts on the blockchain, that will just make things more complicated.

Head over to remix.ethereum.org

You are strongly encouraged to use Remix to follow along with the examples in this course.

Let’s create a hello world.

After you go to remix.ethereum.org, right click contracts and left click “New File”

solidity remix ide create new solidity file

 

This is a solidity file, so give the file a .sol extension. The name is not important

solidity remix ide .sol file extension

 

Copy the code from above, or better yet, type it out yourself.

paste solidity code into remix ide

 

To compile the code, hit Command S on mac (ctrl S on Windows). If you see a red bubble above the solidity symbol, you have a syntax error. if you see orange, you only have warnings which you can ignore for now.

Now deploy the functions. Click the Ethereum symbol on the left, then click deploy.

how to deploy a smart contract in the remix ide

 

To test the functions, scroll down on the left menu, then click on them. They will return the values you expect them to.

test solidity smart contract in the remix ide

 

What if we want to make changes? Delete the contract with by clicking the trash icon.

use the trash icon to delete the smart contract in the remix ide

 

Now change the code, recompile with command S, then click deploy. Test the functions again.

recompile solidity smart contract in the remix ide

 

If a function requires an argument, it will be supplied next to the button.

test a solidity function that takes arguments in the remix ide

 

You are now ready to experiment with Solidity smart contracts!

Fixed Size Datatypes: Solidity is a typed language.

 

Solidity is a typed language.

Unlike javascript or python where you can assign a bool or a string or a number to a variable, each variable can only have one type, and it must be explicitly declared as such.

This applies to functions too. You must explicitly specify the argument type and the return type.

Let’s discuss the most commonly used types now:

  • The unsigned integer, or uint256

  • The boolean variable or bool

  • The address type, which stores Ethereum wallet addresses or smart contract addresses

 

Solidity has arrays, strings, structs, and other types, but they require a little different treatment, so we’ll discuss them later.

Let’s look at three different functions that return each of these types.

 

In these examples, we assigned the value to a variable and then returned it. We can of course directly return the value like so.

 

It’s very important that the function signature matches the return type. The following code will produce an error

Address

An address is represented as a hex string that has 40 characters in it, and always starts with 0x. A valid hex string contains the characters [0-9] or [a-f] inclusive.

warning: be careful when typing addresses manually. Solidity will covert 0x1 into an address with the value 0x0000000000000000000000000000000000000001. If you have an address with less than 40 hex characters, it will pad it with leading zeros.

 

If you create an address with more than 40 characters, it won’t compile.

Note that the 40 characters does not include the leading 0x.

uint256

Let’s revisit uint256 what exactly does that mean?

The u means unsigned. It cannot represent negative numbers. The 256 means it can store numbers up to 256 bits large, or 2^256-1.

Let’s plug that into python to see how big that number is.

 

That’s a very large number, big enough for pretty much everything you’ll need to do on the blockchain.

This will compile in Solidity

But if you make the number bigger, the code won’t compile.

As you can imagine, a uint128 stores unsigned numbers that are up to 2^128 - 1 in size.

Most of the time, you should only use uint256. The times you would use a smaller type like a uint64 or uint128 is a more advanced subject. Just stick to uint256 for now.

The Boolean type

This one is pretty obvious, it’s just like other languages. A bool variable holds either a true or a false. That’s it.

Setup your Environment

If you have not yet set up cURL on your system you can visit the following link to do so:

cURL setup

You should see output that looks like the following

What exactly is “forge” and “foundry” here?

You can think of it like gulp or webpack for javascript or maven for java or tox for python. Foundry is a development framework to make testing, development, and deployment easier. Without a doubt, it is the most popular framework in 2023, and absolutely worth knowing as a Solidity developer.

One thing that is really cool about it is that you can write unit tests in Solidity, so that makes testing easier. Previous tools used javascript, which forced context switching between languages and made casting types a little bit tricky.

VS Code Extension

If you haven’t already downloaded the following extension, you should!

screenshot of the solidity vscode extension

Arithmetic

 

Arithmetic in Solidity behaves exactly the same as in other languages, so we won’t belabor the point here.

You can add numbers this way

Exponents are the same as in other c like langauges.

And so is the modulus

Subtracting, multiplying, and dividing are obvious, so I won’t insult your intelligence by teaching you how to do them.

Solidity does not have floats

If you try to divide 5 by 2, you won’t get 2.5. You’ll get 2. Remember, unit256 is an unsigned Integer. So any division you do is integer division.

But what if you really want to know what 10% of 200 is? That seems very reasonable for, say, calculating interest.

 

The solution to this is to convert x * 0.1 into x * 1 / 10. This is valid and will produce the correct answer.

 

If your interest was some amount like 7.5%, then you would need to do the following

 

If you wanted to know the percentage population of a city relative to a nation, you cannot do the following.

This requires a more advanced solution we will describe later.

Note: Why doesn’t solidity support floats? Floats are not always deterministic, and blockchains must be deterministic otherwise nodes won’t agree on the outcomes of transactions. For example, if you divide 2/3, some computers will return 0.6666, and others 0.66667. This disagreement could casue the blockchain network to split up! Therefore, solidity does not allow floats.

Solidity does not underflow or overflow, it stops the execution

What happens if you try to do the following?

What happens if x is 2 and y is 5? You won’t get negative 3. Actually, what happens is the execution will halt with a revert.

Solidity doesn’t throw exceptions, but you can think of a revert as the equivalent of an uncaught exception or a panic in other languages.

It used to be the case solidity would allow overflows and underflows, but this lead to enough smart contracts breaking or getting hacked that the language built overflow and underflow protection into the language. This feature was added after Solidity version 0.8.0.

You’ve probably noticed by now a lot of solidity files have a line

This means that the source code is compiled with version 0.8.0 or later. If you see a version earlier than that, then you cannot assume overflow protection is built into the code.

If you want to allow underflow and overflow, you need to use an unchecked block

You can use an unchecked block to allow underflow and overflow. This is not recommended unless you have a very good reason to do so. An unchecked block can be used like this:

Note that anything inside the unchecked block will not revert even if it overflows or underflows. This is a very advanced feature that you should not use unless you know what you are doing.

Practice Problems

Add

Divide

Exponent

Unchecked

If Statements

 

If statements behave exactly the same as other languages

 

The argument inside the if statement must be a boolean. Note that our code above is equivalent to the following.

 

Unlike dynamic languages such as Python or javascript, you cannot do the following

 

Solidity also supports the “else if” construction, but we will assume you are already familiar with what that looks like.

 

Solidity does not have a switch statement like Java and C do.

Practice Problems

IfStatement

For Loops

 

Just like if statements, there is nothing surprising about for loops. Here is the code to add up all the numbers from 1 to 99

 

Solidity also supports the += operator if you prefer to do it that way.

 

Solidity also has while loops and do while loops but these are so rarely used that it isn’t worth mentioning them at this point.

 

A very natural usecase for for loops is iterating over an array. But we haven’t introduced arrays yet, so we’ll explain it at that point. 

 

Like other languages, you can do an early return from a function inside a for loop. This code will loop from 2 to the number until it finds a prime factor.

Practice Problems

IsPrime

Fibonacci

Introduction to arrays and strings

 

In this section we will introduce the array data structure and the string data structure. These behave differently from the solidity datatypes we discussed earlier, so we will discuss them here.

Syntax for declaring arrays

Let’s look a function that takes an array and returns an array. There is quite a bit to unpack here!

First, it should be clear that the syntax for declaring an array of numbers is uint256[]. We’ll get to “calldata” and “memory” in a moment.

 

If you wanted an array of addresses or booleans, it would be the following:

 

So what is this calldata and memory bit? First off, if you don’t include them, the code won’t compile. Here are two examples of code that doesn’t compile.

 

So what is calldata and memory?

If you are familiar with C or C++, this concept will be intuitive. Memory in solidity is like the heap in C, C++, or Rust. Arrays can have unlimited size, so storing them on the execution stack (don’t worry if you don’t know what that is), could lead to a stackoverflow error (not to be confused with the famous forum!).

Calldata is something unique to solidity. It is the actual “transaction data” that is sent when someone transmits a transaction to the blockchain.

Calldata means “refer to the data in the Ethereum transaction itself.” This is a fairly advanced concept, so don’t worry if you don’t fully understand it for now.

When in doubt: the function arguments for arrays and strings should be calldata and the function arguments for the return type should be memory.

There are some exceptions to using “calldata” in a function argument, but the return type for an array should always be memory, never calldata, or the code won’t compile. To avoid bombarding you with information, we will talk about the exceptions to calldata later.

Here is how to use arrays of numbers with Remix.

how to enter an array of numbers into the remix ide

Arrays are zero indexed like every other language

No surprises here.

 

Note that the return type is uint256, because we are returning a number, not an array.

Note that if the array was empty, the transaction will revert.

To get the length of an array, use .length

This is the same as javascript.

 

This is also how you can loop over an array.

Arrays can be declared to have a fixed length

In the previous examples, the square brackets had nothing inside of them during declaration. If you want to force an array to have a fixed size, you can put the size inside of the square brackets.

 

If the function is passed an array of any size other than 5, it will revert.

Strings

Strings behave very similar to arrays. In fact, they are arrays under the hood (but with some differences). Here is a function that returns the string you passed it.

 

And here is hello world finally.

 

Concatenating strings

Funnily enough, solidity did not support string concatenation until February 2022 when Solidity 0.8.12 was released. If you want to do string concatenation in solidity, make sure the pragma at the top of the file is at least 0.8.12

 

There is a reason support for concatenation was added so late, smart contracts usually deal with numbers, not strings.

Strings cannot be indexed

In languages like javascript or python, you can index a string like you would an array and get a character back. Solidity cannot do this. The following code won’t compile

Strings do not support length

Solidity does not support getting the length of a string. This is because unicode characters can make the length ambiguous, and solidity represents strings as a byte array, not a sequence of characters.

What we’ve left out

  • Arrays in solidity support operations like pop(), but this has side-effects which are more advanced, so we will teach this later.

  • Declaring arrays and strings inside a function, as opposed to in the argument or return value, has a different syntax

 

Practice Problems

FizzBuzz

SumArray

FilterOddNumbers

IsSorted

Mean

Nested Arrays

 

Nested arrays are rarely used in practice, but we include them here for the sake of completeness.

Nested arrays, as the name suggests, refer to arrays that are contained within another array.

 

In this example, the function is receiving a rectangular grid.

 

Here it is running in remix.

enter a nested array of numbers in a solidity function in the remix ide

 

You can also get a 1D array from a 2D array

 

To declare arrays of a fixed size, use the following syntax

 

What may be confusing is that when you access a specific item in an array, the order may feel backwards from other languages, but it makes sense if you think about it.

 

Just like 1D arrays, if you access an out-of-bound area, the transaction will revert.

Note that nested arrays are extremely rare in practice. If you feel like skipping this section, feel free to.

Problems

NestedArray

TicTacToe

Storage Variables

 

Up until this point, all of our functions have just returned values that purely depend on the function arguments. They do not depend on anything other than the immediate input. That’s why they are called pure functions. They are not aware of the blockchain state or anything that has happened in the past.

This would be quite problematic if we were keeping track of something, like how much money is owed to a certain person, or how many points they have in a game.

Now we introduce the storage variable.

 

These look like “class variables” in other languages, but don’t really behave like them. You can think of them as variables that behave like a miniature database.

Let’s look at an example

 

We have a lot to unpack here!

Variables declared outside of functions are storage variables. They keep their value after the transaction ends.

Note that getX() has the modifier view instead of pure. That’s because it views the blockchain state, I.e. what is stored in the variable x. If you change view to pure in this example, the code will not compile. You can also think of view as read-only. Also note that the return value of getX has the same type as x, both are uint256.

Second, note that setX does not have a view or a pure modifier. That’s because it is a state changing function. Functions that change storage variables, or make some other lasting change to the blockchain cannot have the view or pure modifier, this is because they are not read only and thus cannot be labelled as view, and certainly not pure.

To enforce the point, note that the following code is invalid

 

Note that the variable x itself has the modifier internal. This means other smart contracts cannot see the value.

Just because a variable is internal does not mean it is hidden. It’s still stored on the blockchain and anyone can parse the blockchain to get the value!

This is where things get confusing.

The following code is also valid, but it’s considered bad practice.

 

In this case, we removed the internal modifier to x, and it still compiles. This is considered bad practice because you aren’t being explicit about your intentions for the visibility of X.

The following code is also valid

 

When a variable is declared public, it means other smart contracts can read the value but not modify it.

This is confusing because public functions can modify variables, but public variables cannot be modified unless there is a function to change their value.

Summary

  • Storage variable are declared outside of functions

  • Public functions that do not have a view or pure modifier can change storage variables

  • Pure functions cannot access storage variables

Arrays in storage

 

You may have noticed in our section about arrays we curiously left off

  • writing to indexes in the array

  • appending to an array

  • popping from an array

 

This is because you very rarely do that to arrays that are supplied as function arguments.

However, when arrays are in storage, this operation is more common.

Here’s some example code

 

I recommend you copy and paste this code into remix so you can gain an intuition for what is happening.

Call setArray with [1,2,3,4,5,6]

Now call getLength() it returns 6, which is the length of the array.

Now call addToArray with argument 10. Call getLength() again. Now it returns 7.

Call removeFromArray() followed by getLength(). It now returns 6 as you expect.

Because myArray is public, Remix shows it as visible as a function. But it will not return the entire array. It will ask for an index and return the value at that index. The myArray function behaves like this

It is worth noting that because myArray is public, Remix shows it as visible as a function. This means that the compiler will automatically generate a function called myArray() that can be called to read the values stored in myArray.

solidity does not return an array, it can only return the index you ask for. Screenshot of the remix ide.

 

But it will not return the entire array. It will ask for an index and return the value at that index. The myArray function behaves like this

 

However, the function getEntireArray() returns the entire array.

Note that pop() does not return the value.

Removing an item

Solidity does not have a way to remove an item in the middle of a list and reduce the length by one. The following code is valid, but it does not change the length of the list.

 

If you want to remove an item and also reduce the length, you must do a “pop and swap”.

It removes the element at the index argument and swaps it with the last element in the array

 

Solidity cannot delete from the middle of the list and preserve the array’s original order.

Strings

Strings behave similarly to arrays, except when they are public they return the entire string, because strings cannot be indexed (confusing, isn’t it?). There is no pop or length operation for strings.

Practice Problems

ListOfNumbers

InsertInArray

Stack

Mappings

 

Mapping, Hashmap, associative array, maps, whatever you want to call it, solidity has it.

We’ll call it “mapping” because that’s the keyword Solidity uses. Let’s see an example.

 

This does what you think it does. Because myMapping is public, solidity wraps it with a getter function you can direclty access the values with. However, if you want to access the map through a function, you can follow the pattern in getValue.

Here is the first surprising thing

If you access an mapping with a key that has not been set, you will NOT get a revert. The mapping will just return the “zero value” of the datatype for the value

In the following example, the mapping will return false if you supply a number that hasn’t been set.

 

I encourage you to paste this code into remix, then plug in numbers for the key to see the zero values that come back.

Mappings

 

By the way, ERC20 tokens use mappings to store how many tokens someone has! They map an address to how many tokens someone owns.

 

This implementation has a flaw that anyone can invoke the public functions and send tokens between addresses willy-nilly, but we’ll fix that later.

Counterintuitively, ERC20 tokens are not stored in cryptocurrency wallets, they are simply a uint256 associated with your address in a smart contract. We think of as “ERC20 tokens” are simply a smart contract.

Here is the smart contract for USDC, an ERC20 token: https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48

And here is the token for ApeCoin, the currency of the Bored Ape Yacht Club ecosystem

https://etherscan.io/token/0x4d224452801aced8b2f0aebe155379bb5d594381

Surprise 1: Mappings can only be declared as storage, you cannot declare them inside a function

This may seem like a very odd restriction, but this has to do with how the Ethereum Virtual machine works. Blockchains in general don’t like hashmaps because of their unpredictable runtime. The following code is invalid.

Surprise 2: mappings cannot be iterated over

There is no way to iterate over the keys of a mapping. Every key is technically valid, it just defaults to zero.

Surprise 3: mappings cannot be returned

The following code is invalid. Maps are not a valid return type for solidity functions.

 

Problems

SpecialNumbers

Nested Mappings

 

In most languages, a hashmap can contain another hashmap, and solidity does this too. However, because mappings are not valid return types, you must supply all the keys the maps require.

Let’s look at an example

 

Nested maps are quite common in smart contracts, unlike nested arrays. For example, you can do bookkeeping like this

 

Note that the order matters here. In this construction, one lender can have several borrowers. If we had set borrower to be the first key, it would imply a borrower might have mutiple debtors.

The same restrictions that apply to regular mappings apply to nested mappings. You cannot iterate over the keys, declare them inside a function, or return them from a function.

Public Nested Mappings Don’t Work

Here’s yet another strange quirk of solidity. Solidity automatically creates getting functions for variables when you declare them as public. However, it the public getter functions allow you to supply the necessary arguments.

Yes. You read that right.

The solution is to make nested mappings private and wrap them in a public function that gets their value. Time to practice this!

Practice Problems

NestedMapping

TripleNestedMapping

msg.sender and address(this)

Remember our previous example of a bad ERC20 token?

Here it is again

 

The issue is that we have no idea who is calling the function.

Luckily, solidity has a mechanism to identify who is calling the smart contract: msg.sender. Msg.sender returns the address of who is invoking the smart contract function.

Try out the following code in remix

 

It will return the test address you are using in remix.

Now change the test address by hitting the “ACCOUNT” dropdown. Then try the function again. The address returned will be different.

msg.sender

 

By combining msg.sender with an if statement, you can give certain addresses special privileges.

Let’s say we want the default address in remix to be the special address.

 

The code above lets people view their balances (because balances is a public variable), but only the banker can change it.

By being a little bit clever, we can actually allow people to transfer their balance to someone else without the banker doing it for them. Consider the following example.

 

The function transfer can be called by anyone. However, it can only debit (deduct) balances from msg.sender. As an exercise for the reader, I encourage you to think about why it is impossible to steal someone else’s balance using transfer.

A natural question is, what happens if someone tries to send more amount than they have balance for? If you are using Solidity 0.8.0 or higher, nothing happens. The transaction reverts because you cannot subtract an unsigned number such that it becomes negative.

tx.origin

There is another mechanism to get the sender, tx.origin. Although it behaves similarly to msg.sender, you should not use it. To avoid bombarding you with too much information right now, we won’t explain the security issues around tx.origin yet. But the important point is, do not use tx.origin except in very specific circumstances.

address(this)

A smart contract can know its own address with the following code


Try it out in Remix and see the address matches
 

a screenshot in remix of a smart contract returning its own address

 

Problems

WhoCalledMe

Constructor

 

Going back to our rolling ERC20 example, we did something a little weird, we set the banker variable directly in the contract.

 

That’s okay, but what if someone wants to deploy the contract and set themselves to be the banker?

Smart contracts have a special function that is called at deployment time called the constructor. This is pretty similar to other object oriented programming languages. Here is what it looks like

 

Note that it’s “constructor()” and not “function constructor()” and we don’t specify public because constructors can’t be modified with things like pure, view, public, and so forth.

If you wanted the banker to be configured by the person deploying the contract, then you could use it as a function argument.

 

By the way, you’ll see this pattern variable = _variable a lot in constructors. Solidity doesn’t require you to do that, but it’s considered conventional.

When deploying a contract on Remix that has constructor arguments, you’ll have to put the arguments into the box that appears next to “deploy.”

solidity constructor arguments in remix

Unlike other functions, calldata cannot be used for arrays and strings, you must use memory

Again, for reasons we cannot get into right now, calldata cannot be used in constructor arguments. I know, it seems like a very weird and random restriction, but it will make sense later, after you understand how Ethereum works under the hood.

Here is how you would set a string during construction time

 

You may be tempted as a response to just use memory everywhere and not bothering to use calldata. But it is worth trying to remember this for now, because calldata results in cheaper transactions (I.e. lower gas fees for the user).

Also, in case you were wondering, constructors cannot return values.

Problems

Deployer

Require

 

There’s just one more essential solidity key word, and then we are ready to create our own ERC20 token.

Although we can use an if statement to check if inputs to a function are valid, or the correct msg.sender is calling the function, the elegant way is to use the require statement. The require statement forces the transaction to revert if some condition is not met.

 

Try the above code out in remix.

Note that leaving out the error message is valid, but considered bad practice, because it makes understanding the failure harder.

 

You can use this construction to ensure msg.sender is who they are supposed to be. But you can practice that in the following problems.

Practice Problems

NotEnough

Owner

ERC20 Tokens

 

We are now ready to make an ERC20 token!

ERC20 tokens typically have a name and a symbol. For example, ApeCoin has the name “ApeCoin” but the symbol “APE.” The name of the token generally doesn’t change, so we’ll set it in the constructor and not provide any functions to change it later. We’ll make these variables public so that anyone can check the name and symbol of the contract.

 

Next, we need to store everyone’s balances.

 

We say “balanceOf” because that is part of the ERC20 specification. ERC20 as a specification means that people can call the function “balanceOf” on your contract, supply an address, and get how many tokens that address owns.

Everyone’s balance is zero right now, so we need a way to bring tokens into existence. We’ll allow a special address, the person who deployed the contract, to create tokens at will.

 

It is general practice that the function mint() takes the to and amount as the parameter argument. It allows for the contract deployer to mint tokens to other accounts. For the sake of simplicity the function mint() only allows the deployer of the mint tokens into his account.

To keep track of how many tokens there are in existence, the ERC20 specification requires a public function or variable called totalSupply that tells us how many tokens have been created.

 

If you’ve used ERC20 tokens in your wallet, no doubt you’ve seen instances where you have a fraction of the coin. How does that happen when unsigned integers have no decimals?

The largest number a uint256 can represent is

115792089237316195423570985008687907853269984665640564039457584007913129639935

Let’s reduce the number a bit to make it more clear

10000000000000000000000000000000000000000000000000000000000000000000000000000

To be able to describe “decimals”, we say the 18 zeros to the right are the fractional part of the coin.

10000000000000000000000000000000000000000000000000000000000.000000000000000000

Thus, if our ERC20 has 18 decimals, we can have at most

10000000000000000000000000000000000000000000000000000000000

Full coins, with the zeros to the right being decimals. That’s 10 octodecillion coins, or for those unfamiliar with such uselessly large numbers, that’s 1 quadrillion x 1 quadrillion x 1 quadrillion x 1 trillion.

Which should be enough for most applications, even countries that go into hyperinflation.

The “units” of the currency are still integers, but the units are now very small values.

18 decimal places is pretty standard, but some coins use 6 decimal places.

The decimal of the coin should not change, it’s just a function that returns how many decimals the coin has.

 

If you are paying attention, I did throw a curve ball at you here. The number type is uint8, not uint256. uint8 can only represent numbers up to 255. However, a uint256 has 77 zeros (if you feel like counting the zeros of the numbers above, you can verify this). Thus, it isn’t possible to have more than 77 decimal places if you want to have one whole coin. So the standard dictates we use a uint8 since the number of decimals can never be very large.

Transfer

Now let’s add our transfer function back.

 

Aha, we snuck in an extra line of code there: require(to != address(0), “cannot send to address(0))

Why is this? Well, nobody “owns” the zero address, so tokens sent there are un-spendable. By convention, sending a token to the zero address should reduce the totalSupply so we want to have a separate function for that.

Now we introduce a concept of allowance.

Allowance

Allowance enables an address to spend someone else’s tokens, up to a limit that they specify.

Why would you allow someone to spend tokens for you? This is a very long story, but to summarize, think about how you would “know” someone transferred you ERC20 tokens. All that happens is a function gets executed and a mapping changed values. You didn’t “receive” the tokens, they just became associated with your address.

Now, as an entity outside the blockchain, you can inspect it for events that make you richer.

However, smart contracts cannot do that.

The established pattern for smart contracts to be recipients of transfers is to allow the smart contract to have a certain allowance, then tell that smart contract to withdraw the balance from your account.

When you want to transfer tokens to a smart contract, the typical method is to first approve the smart contract to withdraw a certain amount of tokens from your account. Then, you instruct the smart contract to withdraw the approved amount of tokens from your account. This is a common pattern used in smart contracts to enable token transfers to the contract.

Let’s add the tracker for allowance, and a way to give allowance to another user.

 

In the line allowance[msg.sender][spender] = amount;, spender refers to the address of the account that is being granted the allowance by the msg.sender. The msg.sender is giving permission to the spender to spend a certain amount of tokens from their account.

Therefore, msg.sender is the owner of the tokens and spender is someone who has been approved by the owner to spend a certain amount of tokens on their behalf.

Ah, but we don’t have a way to actually use the allowance given, it just sits there! That’s what transferFrom is for.

transferFrom

 

Let’s unpack what we just did here.

First, it is possible for the owner of the coin to call transferFrom. In that case, allowance is meaningless, so we don’t bother checking the allowance mapping, and update the balances accordingly.

Otherwise, we check to see the spender has been given enough allowance, then subtract the amount they are spending. If we didn’t subtract their spending, we would have unlimited spending power.

There is one more cleanup to do. If we read the original specification for EIP 20 it says that approve, transfer, and transferFrom must return true after they succeed. So let’s add that.

 

At the risk of throwing too much information at you, there is a cleanup to this code we can do. Note that transferFrom and transfer have duplicate code in them. What can we do about that? We could factor out the balance update code into a separate function, but we need to make sure that function isn’t public or someone can steal coins!

 

Much cleaner!

Practice Problems

  • Modify the above code so that doesn’t allow more than 1 million tokens to enter circulation, even if the owner tries to mint more

Tuples

 

We’re going to go off on a slight tangent here to introduce the tuple data type, because it is a prerequisite for upcoming sections.

If you’ve used tuples in a language like Python or Rust, no surprises here. It’s an array of fixed size, but the types inside of it can be a mixture.

Here’s an example of a function that returns a tuple

 

Note that tuples are implied. The keyword “tuple” never appears in solidity.

Tuples can also be “unpacked” to get the variables inside, like in the following example.

 

As with other languages, tuples do not need to be of length 2. It can be 3, 4 or even longer.

Practice Problems

Tupledore

Application Binary Interface Encoding (abi encoding)

 

We’re going to have to go on what seems like another random tangent before we can introduce our next piece of information.

But I want you to understand what the following things are

abi.encode

abi.decode

abi.encodeWithSignature

To motivate them, let’s create another smart contract, open the “debug” dropdown, and get a certain piece of information.

debug smart contract transaction in the remix ide

 

When we copy that, we get

0x92d62db5

What exactly is this? This is the function signature of “meaningOfLifeAndAllExistence()”. We’ll learn later how this is derived.

Whenever you “call” a smart contract, you are actually sending an ethereum transaction with some data attached so the smart contract knows which function to execute.

Let’s look at the information from another vantage point.

remix ide showing the return value of a solidity function

 

We’ve changed the return type to “bytes memory” (don’t worry that we haven’t seen that before), and returned a variable called msg.data (also don’t worry that we haven’t seen that before).

The important thing to note is that we get an identical byte sequence back!

So what is happening?

When you call a function in a smart contract, you aren’t actually doing a “function call” per se, you are sending data to the contract with some information about which function should be executed.

Makes sense right? When you fire up your browser wallet and trade ERC20 tokens, there’s no way you can “call a function” on an ERC20 contract remotely. Function calls only happen inside the same execution context. Describing transactions as functions however, is convenient. But we need to look behind the curtain to see exactly what is happening to really understand Solidity.

When you “call a smart contract” you are sending data to the contract with instructions for how to execute.

There are many data encodings, json, xml, protobufs, etc. Solidity and ethereum use the ABI encoding.

We won’t get into the specification of ABI here. But what you need to know is that it always looks like a sequence of bytes.

Functions are identified as a sequence of four bytes. Our original byte sequence (0x92d62db5) had four bytes in it: 92, d6, 2d, b5.

Remember, a byte is 8 bits, and 8 bits can be a value up to 255 (2^8 - 1). A byte, represented in hex, can go from 0x00 to 0xff. Convert 0xff to decimal, and hopefully this makes it clear.

When a function takes no arguments, sending four bytes that represent the function instructs the smart contract to execute that function.

But what would the data look like if it took an argument?

solidity msg.data containing a function argument

 

We get

0xf8689fd30000000000000000000000000000000000000000000000000000000000000007

returned to us. The f8689fd3 portion means call function “takeOneArg” and the 7 with a lot of leading zeros means pass the number 7.

It would be horribly confusing if we had to do this by hand.

Thankfully we don’t.

Watch this.

solidity function using abi.encodeWithSignature

 

We don’t need to concern ourselves with the specification of ABI encoding now, let’s just get comfortable using it.

 

Consider the following example.

how to conveniently use a solidity function in the remix ide that takes two arguments. The dropdown is highlighted.

 

Note that we are using “abi.encode” and “abi.decode”. The “withSignature” bit is when functions are involved, but that is not the case here.

In this example, the variables x and y are abi encoded into

0x0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000f

The decimal numbers got converted to hex, which is why “5” is still “5” but 15 became “f”.

If we know in advance that this is a pair of uint256, we can “decode” it back to a pair using the function screenshotted above.

The tuple that appears as the second argument in abi.decode is instructions for how to decode the data. If you provide the wrong datatypes or the wrong length of tuple here, you’ll either get the wrong result, or the code will revert.

Problems

Encode

Decoder

Calling other contracts

 

Everything we’ve been doing up to this point is calling smart contracts directly. But it’s also possible, and in fact, desirable, for smart contracts to be able to communicate with each other.

Let’s give a minimum example of this.

 

Here’s how you can see it in action

remix ide showing the use of two smart contracts

Because we reviewed tuples, abi encoding, and bytes memory already the only surprising ting here should be call and the fact that askTheMeaningOfLife() is not a view function.

Why isn’t askTheMeaningOfLife() a view function? If you try compiling it with the view modifier, it won’t compile.

View functions are read only. When you call the function of an arbitrary smart contract, you can’t know if it is read-only or not. Therefore, solidity doesn’t let you specify a function as view if it calls another smart contract.

Also, although we can see that meaningOfLifeAndAllExistence in AnotherContract returns a uint256, we can’t know that in the general case. It could return a string.

Functions always return abi encoded bytes. How does remix know to format strings as strings and numbers as numbers? Behind the scenes, it is doing the abi.decode operation that we are doing here.

What is the bool ok portion of the tuple? Function calls to other smart contracts can fail, for example if the function reverts. To know if the external call reverted, a boolean is returned. In this implementation, the calling function, askTheMeaningOfLifeAndAllExistence, reverts also, but that is not necessarily a general requirement.

Here’s something interesting. What happens if you call a non-existent smart contract?

Try askTheMeaningOfLife(address source) with 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db as the argument.

Don’t be lazy, try it out in the code above!

It reverts, but that’s not because the address isn’t there, but because you tried to decode empty data. If we comment out the decoding part, then the function no longer reverts when we call a non-existent address.

When you open the transaction dropdown in remix, you see the revert explanation here.

Calling other contracts

 

What if the other contract takes arguments? Here is the code to do it.

 

Be careful to not have spaces in "add(uint256,uint256)"

Ready to put this into practice?

Practice Problems

CrossContract

Token Exchange mini project

 

This is challenging

Believe it or not, you now have enough prerequisite knowledge to build a very simple token exchange smart contract! Here is your mission.

Build two ERC20 contracts: RareCoin and SkillsCoin (you can change the name if you like). Anyone can mint SkillsCoin, but the only way to obtain RareCoin is to send SkillsCoin to the RareCoin contract. You’ll need to remove the restriction that only the owner can mint SkillsCoin.

Here is the workflow

  • mint() SkillsCoin to yourself

  • SkillsCoin.approve(address rareCoinAddress, uint256 yourBalanceOfSkillsCoin) RareCoin to take coins from you.

  • RareCoin.trade() This will cause RareCoin to SkillsCoin.transferFrom(address you, address RareCoin, uint256 yourBalanceOfSkillsCoin) Remember, RareCoin can known its own address with address(this)

  • RareCoin.balanceOf(address you) should return the amount of coin you originally minted for SkillsCoin.

 

Remember ERC20 tokens(aka contract) can own other ERC20 tokens. So when you call RareCoin.trade(), it should call SkillsCoin.transferFrom and transfer your SkillsCoin to itself, I.e. address(this).

If you have the SkillsCoin address stored, it would look something like this

 

Deploy these contracts in Remix and test that they work.

If you are new to Solidity, set aside a couple days for this. A lot of engineers get weirded out by the fact that balances are stored in smart contracts, not wallets, so it takes a bit of time to get used to this. Also, trust me, you will get confused by the cross contract calls.

Payable Functions

 

Up until this point, we’ve been using tokens to represent value, but what about Ether? Let’s introduce how a smart contract can interact with Ether.

 

After you deploy this contract on Remix, you’ll note that the button for interacting with payMe turns Red. This means you can specify in the field above how much value (Ether) to send when you call the function.

sending ethereum to a payable function in a smart contract in the remix ide

 

You can send Ether in units of Wei, Gwei, Finney, or Ether.

One Wei is 1/10^18 Ether, One gwei is one billionth of an Ether, and one Finney is one tenth.

Let’s make this simple and just send one Ether.

When we click “howMuchEtherIHave” we actually get back

1000000000000000000

This doesn’t mean we created a ton of Ether out of thin air. Remember floats are not a thing on the blockchain, so Ether uses the same strategy for decimals that ERC20 tokens do. A unit of Ether is actually one Wei, and what we traditionally think of as one Ether is 10^18 Wei.

By the way, the .balance construction works on arbitrary addresses. A smart contract can determine how rich you are with the following function

 

10000000000000000000

Unless functions have the payable modifier, they will revert if they receive Ether.

Why have this construction? If someone wants to send us Ether, why not accept it?

This has been a subject of debate, but the general idea is that a function should constrained in such a way to have extremely well defined behavior. Anything outside of that should be restricted. The more constrained the behavior, the easier it is to reason about the smart contract’s functionality.

By the way, solidity provides a very convenient keyword for dealing with all the zeros involved with Ether. Both of these functions do the same thing, but one is more readable.

 

It is also valid to make a constructor payable, if you want your smart contract to begin life with privilege and a headstart. But you still need to explicitly send ether at construction time.

Just because a function is payable does not mean that the person calling the function has to send Ether.

Sending Ether

It’s clear how to send Ether if you initiate the transaction from Remix, but what if another smart contract wants to send Ether?

You will use the **call function we described earlier, but with an extra “meta argument.” It may look strange at first, but you’ll get used to it.]

 

Let’s break down what we are looking at here.

  • call has a funny looking json-like object between call and the arguments. This is how ether is sent with a call. The “value” key determines the amount sent. This is zero by default

  • We have a tuple with an empty second argument; (bool ok,). This means we are ignoring the return value of takeMoney(). If we don’t care about the return value, we use this construction.

  • We still care if the transfer fails. So we have the require(ok) construction in place.

 

Some experiments

  • Deploy both contracts, but supply Ether to SendMoney at construction time. View myBalance on both contracts before and after calling sendMoney

  • Remove the “payable” modifier on takeMoney and see what happens (it should revert)

  • Use ether when calling sendMoney and note how the balance receiveEther gets increased

Payable functions cannot be view or pure

Changing the Ether balance of a smart contract is a “state change” on the blockchain. It’s a permanent alteration that endures even after the transaction is finished, similar to updating a storage variable. Therefore, payable functions cannot be view or pure. The compiler won’t accept that.

Practice Problems

PriceIsRight

Receive

It was a bit annoying to have to abi encode a function just to send Ether. Luckily, Solidity has a nice way to handle this.

 

Note a couple new things:

  • receive is a function, but it doesn’t have the function keyword. This is because it is a “special” function like the constructor, so to emphasize that, the “function” keyword is not included.

  • We used a modifier external rather than public

 

Up until now, we’ve been using the modifier public whenever we wanted the function to be callable outside the contract. External means it can only be called outside the contract. For simplicity, we’ll be using public but we’ll get into the distinction between external and public more later. Solidity however, only allows the receive function to be external.

It must also be payable. Try deleting the payable keyword and compiling the contract. That won’t succeed.

Now, how can another function send ether to it?

 

Now plug this into remix to test it out.

This is a lot cleaner. No abi encoding or anything.

This construction is also how we send money to wallets. Here is a contract that allows only one address to withdraw ether.

 

It might seem strange that we are doing a “function call” to a wallet, which has no functions.

The convention is a bit confusing. Just remember this.

All call invocations transfer ether. But zero is a valid amount of ether to transfer.

In Ethereum, all function calls can transfer ether, even if the value parameter is not explicitly set. However, zero is a valid amount of ether to transfer, and can be useful in situations where no ether needs to be transferred.

Block.timestamp and Block.number

 

We’ve been able to do some cool stuff up to this point, but we haven’t been able to track the passage of time.

You can get the unix timestamp on the block with the block.timestamp. Let’s try it

 

Try it out in Remix.

The number that comes back is the number of seconds since January 1, 1970 UTC, the traditional unix time. Remember, this is seconds not miliseconds as your linux desktop or other programming languages might respond with.

Ethereum progresses with blocks, and whichever timestamp you get back is what the validator put into the block when they produced it. Since blocks are produced every 12 seconds, the block.timestamp will roughly increment by that amount. You shouldn’t trust block.timestamp on the order of second intervals. There’s too much variation. Over the course of minutes however, it is quite reliable.

If you want to ensure someone doesn’t call a function more than once per day, you can use the following construction

 

Solidity has a much nicer way to represent time instead of multiplying seconds like that.

 

In fact, seconds, minutes, hours, days, and weeks are all valid units of time, which are just handy shortcuts for multiplying out the number of seconds you need. In case you were wondering, seconds doesn’t change the value, but it does provide readability if you intend to use seconds as a measure.

block.number

You can also know what block number you are on with this variable. Hopefully it’s obvious what it does. Some people mistakenly multiply the average blocktime by the block.number to measure the passage of time. Don’t do that.

Don’t use block.number to track time, only to enforce ordering of transactions.

When do you need to enforce transaction order? Not often. So if you aren’t sure, use block.timestamp.

Etherscan shows the current blocknumber, if you want to have an idea of how large it currently is.

screenshot of etherscan.io showing the current block number

 

The code above will tell you which block the transaction happened on. In this case, it will update dynamically.

If you want to enforce that a function is called after another one, that is, in a later block, you can use the following construction.

 

Technically, our “ERC20” token is not fully ERC20 compliant. It’s missing an important feature: events.

General rule of thumb: If a function causes a state change, it should be logged.

Why log things? Isn’t it the case that the blockchain already immutably stores every transaction?

This is true, events are not strictly necessary. However, they make auditing past events a lot easier. Rather than combing through a bunch of transactions, the user can filter by the log that they care about and quickly find events (transactions) that might be of interest.

This is how your cryptocurrency wallet can quickly discover your ERC20 balance. It would be pretty annoying to have to look through every transaction that ever occurred on an ERC20 token to discover if you own any. But logs are stored in such a way that this retrieval is efficient.

Events cannot be seen by other smart contracts. They are optimized for being queried offchain.

Let’s look at an example.

 

An event can have up to 3 indexed types, but there isn’t a strict limit on the number of unindexed parameters.

If you have a database background, you can think of “indexes” exactly the same way you would about a database index.

By the way, argument names after the datatype is optional. We could have written the event above as

 

with no I’ll effects, except that perhaps it is a bit less readable.

When should a variable be indexed or not? If you might be interested in finding that value quickly, like “has an address been involved with this token contract” then you should index it. You probably are not interested in the question “has anyone ever transfered exactly 1,370,904 tokens in this contract, so don’t index the amount. Here is our ERC20 token with the events added. Note that these events are required by the specification.

Pay close attention to where the events have been added, especially the mint function! The convention of address(0) being the source means the tokens came into existence out of nothing, rather than from another address. Recommended reading: https://www.rareskills.io/post/ethereum-events

 

Practice Problems

Emitter

Emitting Events

Inheritance

 

Implementing an ERC20 contract from scratch each time would no doubt get tiring. Solidity behaves like an object oriented language and allows for inheritance. Here is a minimal example.

 

Deploy to Remix, but from the dropdown, choose Child to deploy, not Parent.

remix ide screenshot demonstrating inheritance in solidity

 

Even though Child is empty, we see the function in the Child

testing a smart contract in the remix ide that was created using inheritance

When a “contract” is “another contract”, it inherits all it’s functionality.

Like other object oriented programming languages, functions can be overriden. Here is the construction for changing the value.

a screenshot of the remix ide demonstrating virtual functions in solidity

Note that only virtual functions can be overriden. If you try to override a function that isn’t virtual, the code won’t compile.

Also, when a function overrides, it must match exactly, both in name, arguments, and return type.

 

Solidity supports multiple inheritance

 

In case you are wondering, if the two parents had a function with the same name, the child must override it or the behavior will be ambiguous. If you end up in this situation, you probably did something wrong in your software design. So let’s not go down that road.

Private vs Internal

There are two ways to make a function not accessible from the outside world: giving them a private or internal modifier. The distinction is simple.

Private functions (and variables) cannot be “seen” by the child contracts.

Internal functions and variables can.

The super keyword

The super keyword means “call the parent’s function.” Here’s how it can be useful

 

If we didn’t include the super keyword here, foo() would call itself and go into infinite recursion. Try removing super and running the code in Remix. The transaction will revert because of the infinite recursion (Ethereum doesn’t let code run forever, it forcibly terminates them. The exact mechanism is an intermediate topic for later discussion).

Super means “call the parent’s foo, not mine.” This let’s us get all the functionality of foo without having to copy and paste the code.

Calling the parent’s constructor

Solidity won’t let you inherit from a parent contract without initializing it’s constructor. Consider this situation.

 

The fix is to call the parent constructor at the point of inheritance

Let’s summarize what we’ve learned

  • Only virtual functions can be overriden

  • Functions that override a parent’s function must have an override modifier

  • The overriding function must match exactly, in name, arguments, and return type

  • Instead of copying and pasting the parent function’s code, you can use the super keyword

  • You can inherit from multiple contracts

  • You must explicitly call a parent’s constructor when doing inheritance.

Create an ERC20 token with little effort

Inheritance, combined with the import statement, makes it easy for us to leverage libraries created by other people. Deploy this contract in Remix, and you’ll see all the ERC20 functions have been implemented for you.

a screenshot of the remix ide showing an empty contract that inherits from the OpenZeppelin erc20 smart contract

A point of clarification

There is a world of difference between a smart contract as a solidity object and a smart contract deployed on the blockchain.

You cannot inherit contract deployed on the blockchain.

They are binary blobs living outside of you. Because of the ambiguous terminology, some solidity developers have worried that functions and variables can be inherited and overridden by a malicious contract. This cannot occur. Even though we refer to deployed code as a “contract” and solidity code as a “contract” they are not the same thing.

Interfaces

 

I’m sure you thought the way we were calling other contracts with .call and abi.encodeWithSignature was a bit clumsy.

I wanted you to go through that exercise so that you know what is happening under the hood.

Now it’s time to introduce the ergonomic way Solidity enables cross contract calls.

 

V1 and V2 are very similar, but they have a key difference under the hood. We will get to that in a second.

The important fact here is that V2 is much cleaner than V1!

The interface nicely wraps up the abi encoding and decoding for us so that we don’t have to think about it. The interface defines the return type, which defines how the abi decoding will work, and the function signature and arguments define the abi encoding.

Now we can abstract that all away and call another contracts function as if it was just another function call.

Pretty cool right?

Note that the modifier for the interface function is external not public. You cannot declare interfaces to be public, only external. You don’t declare the internal functions because outside contracts won’t know anything about it.

I’ve been simplifying things by making everything public up to this point, except where absolutely necessary. But you’ve grown as a Solidity dev since the twenty+ chapters we’ve been though. Therefore, I will now introduce a best practice you should adhere to.

Unless a function needs to be called from inside a smart contract, it should be external, not public.

Interfaces don’t allow you to declare functions as public because it’s irrelevant on the outside if the function you are interacting with can be called from within the other smart contract.

Now let’s look at the other key difference.

getSum was a regular public function in V1, but it was a view function in V2.

Wait, wasn’t it the case that we couldn’t do that, because we don’t know if the other function will modify the state?

Solidity has a special kind of call, a staticcall that behaves like a regular call but forces the transaction to revert if a state change happens.

This is valid solidity.

 

I know you are dreading the ABI encoding stuff, but I want to show you what happens behind the scenes when an interface declares a function to be view. It means the underlying action will happen with a staticcall.

The reader is encouraged to do state changing things inside of add to see the transaction revert.

If you have a Java background, this whole bit with interfaces is probably a rather obvious. But it’s important to remember that, behind the scenes, a cross-contract call, with abi encoding, is happening. You aren’t “compiling” another smart contract into your own, like how Java objects would be combined together.

Modifiers

 

The onlyOwner construction is so common that we’ll dedicate a section to it.

Consider the following

 

Let’s look at the star of the show here:

 

It simply means “execute the code before the underscore, then execute the function.”

This is a handy way to “modify” the function behavior, hence the name “modifiers.”

Note that even though HoldFunds inherited from Ownable, it didn’t override any functions. Inheritance in Solidity is more often a mechanism for including behavior than for defining some kind of polymorphism (don’t worry if you don’t know what that is).

So in this case, if you want your smart contract to have nice handy functions inside of it, you can import another contract that provides the functionality you need.

It isn’t strictly necessary. You can put all your code into one big contract. But that code would be less readable.

Modifiers can be used for things other than checking ownership, but ownership checks are the most common use case.

Don’t modify state inside modifiers. Although solidity allows you to do this, it makes the code harder to reason about. This is considered bad practice.

Immutable variables

 

If you aren’t ever going to change a variable, it’s best to explicit about your intentions. Solidity has a keyword for this.

 

If a variable is set in the constructor and never updated, it should be immutable

If you try to write to an immutable variable, the code will not compile.

 

Practice Problems

Immutable

Constants

 

Immutable variables can be set once in the constructor, but what if you have a number that you never want to change?

Like other languages, Solidity has the constant keyword. This signifies that the value is fixed an never changes.

Let’s say you have an ERC20 token that should never have more than 22 million minted.

This would be the clean way to do it

 

Note that 22000000 was written as 22_000_000. They mean the same thing, but the latter is more readable. Underscores in numbers are simply ignored.

Deploying an ERC721 to OpenSea

 

An ERC721 is very similar to an ERC20. It has an notion of transfering, balance, and allowance. The only difference is that each token has a unique ID, and there is only one of them.

We’ve been throwing a lot of information at you with calling contracts and sending ether. We’ll build an ERC721 to reinforce everything we learned earlier.

Now that we know how to transfer Ether, you have enough knowledge to sell NFTs for Ether, like a traditional NFT sale.

This tutorial is in video form.

https://www.youtube.com/watch?v=LIoFbudNVZs

Launch and verify an NFT with Foundry

In the video tutorial above, you put an NFT on Opensea using the Remix environment.

This is fine, but Remix is not ideal for production use.

In this chapter, we will show you how to

  1. Set up Foundry with the NFT

  2. Deploy it to the Sepolia testnet and verify it on Etherscan

 

If you’ve been doing the exercises, you should already have foundry installed, so let’s set it up.

 

Create a new folder; call it foundry-nft, cd to it, and run “forge init” in the empty folder.

Copy and Paste the code

Here’s the code for creating the NFT. Rename the Counter.sol file in the “src” directory to FoundryNFT.sol and paste this code.

Installing Openzeppelin

We import and inherit the Openzepplin ERC721 contract, so to install it use the following:

Using remappings

Coming from a Remix background, you’ll notice that the OpenZeppelin import path here differs. Remix doesn’t store libraries in the same location Foundry does. However, Foundry provides remappings to allow file lookups on import to be redirected to a different location.

To see all the available remappings, we run forge remappings.

We get this:

 

We can see the OpenZepplin remapping, so we don’t need to specify the full path to the OpenZeppelin contracts. We can use openzeppelin-contracts/contracts/token/ERC721/ERC721.sol instead of specifying the “lib/” directory first.

Not using remappings

If we were not using forge remappings, we would have to specify the full path to our files or directories.

For example, for the ERC721.sol file, we would do something like this to import it.

 

To confirm this is still valid, run forge build and see if it compiles.

 

And it does

 

Now undo the changes back to how it was.

Generating the remappings.txt file in one step

This can be done with the following operation

 

 

Change the file names

 

We can further configure where the remappings point by creating a remappings.txt file in the project root directory and adding this line @openzeppelin=lib/openzeppelin-contracts/contracts to the file.

After doing this, we can use import "@openzeppelin/token/ERC721/ERC721.sol"; to import the ERC721 token instead of explicitly writing the full file path.

This remapping can be configured in the remappings.txt file if it points to the right path.

Getting an Etherscan key

When we deploy our NFT, we need an Etherscan API to verify the contract. This will let us easily connect to Etherscan to verify the smart contract using forge without going to the Etherscan website and following the process.

Head to Etherscan, log in, and create an API key.

Etherscan Home Screen
Etherscan API Key

 

We have created U3D9IS6Z5E872VFS7M7AWR1SBA8786ZZ3Y as our API key. We’ll be using this API key later.

 

 

Creating a throwaway wallet

Never use the private keys to a hardware wallet or any wallet that holds significant value.

 

To deploy the NFT contract to the testnet, we need a private key with test ether to sign the transaction.

 

For the sake of simplicity, we have created a throwaway wallet and funded it with some sepolia ether.

 

Here’s the private key of the wallet; 787ea4ec95ab4f4e66c4c4c387cd0b5fbbec84a9293db485fa5f86f490c157d4.

(This shouldn’t be used as it is considered comprised already.)

 

 

Put the Etherscan key and private key of the wallet in your environment variables

Now that we have both our API key and private key, the next step is to create a .env file in the project’s root directory and add this to it.

Make sure your .env file is included in your .gitignore so you don’t accidentally publish your private keys!

Run this script

Run this script to deploy and verify the NFT contract on sepolia.

 

View on Etherscan

 

We have successfully deployed our NFT contract on the sepolia testnet.

Units of Ethereum: wei, gwei, and ether

 

The following two lines of code are equivalent

 

If you feel like counting, that’s 10^18. Remember, there aren’t floats in Solidity, so “1 Ethereum” is actually 10^18 units of its smallest unit.

The smallest unit of Ether is 1 wei. 10^18 wei is 1 Ether.

 

Another commonly used unit is gwei. One gwei is 1 billion wei, or 1 billionth of an Ether.

 

Remix doesn’t let you specify a fraction of an Ether when sending value, so you’ll have to calculate the amount from the fraction and convert it to wei or gwei.

 

By the way, even though floats are not supported in Solidity, you can specify fractions of an Ether. The solidity compiler is smart enough to understand that a fraction of an Ether isn’t a fraction itself. The following lines of code are equilvalent

 

By the way, Ether has other names for units, like Finney and Sazbo, but these units are so rarely used, it’s better to just Google them when you need to know their value. The following values must be memorized however:

  • wei is the smallest unit of Ether.

  • 10^18 wei is 1 Ether

  • One gwei is one billion wei, or one billionth of an Ether.

Structs

Structs in Solidity behave similar to C. They group different variables within a single composite data type, which can be extremely useful in organizing data and creating more complex data structures.

Here’s how you declare a struct in Solidity.

 

myFoo is a public variable of struct Foo, it stores both uint256 a and uint256 b. As you can see if we deploy it in remix, myFoo returns:

myfoo.png

To pass a struct in Remix to a function that takes a struct as an argument (we’ll talk more about this in a moment), encode it as follows:

encode

The function in question takes the example Foo above, which consists of two uint256 variables. It might be a bit confusing to format it like an array, but that’s how it works.

To create a new instance of Foo in Solidity, simply wrap the values in struct Foo.

  • Foo( a , b )

To access or assign each individual variable in the struct myFoo , use the dot notation.​

  • myFoo.a

  • myFoo.b

Why do we use structs? Let’s say we have a deposit contract that keeps track of the depositor’s name and balance.

In the contract above, the depositor’s name and balance is stored into two separate mapping data structure.

The address variable in the mapping is repeated twice for both the name and balance of the same msg.sender, and hence it is not efficient.

So here’s where structs come in handy, we can register both the name and balance under a struct variable, and store that variable in one key value pair mapping like this.

See how useful it is? It makes your code cleaner and more efficient.

How to use Structs

Simple right? Here’s the demonstration.

If you want to pass struct Foo as an argument or as a return value, here are some rules you have to follow:

  • Structs passed as arguments must be declared as a memory and

  • Structs as return types must also be declared as a memory. Here’s how it looks.

It’s important to note that structs in Solidity cannot contain a member of its own type. For example, this is not allowed

Arrays and mappings

Structs can be used as the value type in arrays and mappings. For example, you could create a dynamic array of the Foo instance like this:

arrayFoo is an array composed of Foo instances.

To demonstrate:

You could also create a mapping where the keys are addresses and the values are Foo instances:

Up to this point, it should be obvious what’s going on here. We have a mapping of address ⇒ struct Foo; mappingFoo.

To assign a Foo instance to an address mapping, here’s how we do it

and to modify it

Real Life Example

A more practical use case would be in a ticket system. We have a BuyTickets contract that sells one ticket at a price of 0.01 ether. An address can’t purchase more than 10 Tickets and we have a function that displays an address’ ticket information.

We could use NFTs for tickets, but if users aren’t going to transfer tickets to each other, then that would be overkill.

Exercises

StudentDB

Strings

 

Strings are dynamically-sized UTF-8 encoded bytes is also a dynamically-sized byte array. Both of which are interchangeable by just using string() to convert strings to bytes and bytes() to convert bytes to strings respectively. This greatly helps us in doing operations on string like we can with other programming languages. However since strings are UTF-8 encoded if such character requires more than one byte then it increases the difficulty in string manipulation.

Because strings are arrays, they need the calldata or memory modifier when passed to functions, and the memory modifier when returned.

To check a string’s length:

 

This does NOT mean how many characters are in the string, but how long the byte array is. Unicode characters take up more than one byte.

To access a string’s character:

 

Keep in mind this will only work if the entire string is ascii. If we pass in unicode characters that take up more than one byte, for example “你好” the code will crash.

Getting a character from a string is a bit harder than just indexing it like javascript or python because we have to initialise a string array of length 1 and then insert the character we want to get to the new string array. This is what the code above is doing.

Solidity support unicode strings:

 

Something slightly misleading is solidity is that we use “strings” to represent hex data if the hex modifier is used. The following shoes casting the hexadecimal encoding of “helloworld” to helloworld.

 

Concatenating strings are made easy at solidity 0.8.12 with the addition of string.concat(). Below that version, and string.concat() is not available.

Unit Testing in Solidity

It's about time you learned how to write unit tests! We've already written a tutorial on unit testing, so no need to repeat it on this page

Read it here: Solidity Unit Testing (please link "Solidity Unit Testing" here: https://www.rareskills.io/post/foundry-testing-solidity)

Homework:

  • Unit test your NFT. Make sure that when you mint, the ownerOf the NFT is the address that minted it. Also check that "balanceOf" for that address becomes 1.

  • Check that the balance of the contract went up by the price of the NFT

  • When the owner calls withdraw, check that their Ether balance went up by the expected amount

bottom of page