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”
​
This is a solidity file, so give the file a .sol extension. The name is not important
​
Copy the code from above, or better yet, type it out yourself.
​
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.
​
To test the functions, scroll down on the left menu, then click on them. They will return the values you expect them to.
​
What if we want to make changes? Delete the contract with by clicking the trash icon.
​
Now change the code, recompile with command S, then click deploy. Test the functions again.
​
If a function requires an argument, it will be supplied next to the button.
​
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:
​
​
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!
​
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.
​
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.
​
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.
​
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.
​
​
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
​
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.
​
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
​
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.
​
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.
​
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.
​
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.
​
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!
​
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.
​
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
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.”
​
​
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.
​
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.
​
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.
​
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.
​
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.
​
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?
​
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.
​
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.
​
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.
​
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
​
​
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.
​
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?
â