What are ERC20 tokens?
ERC20's are fungible tokens. For non-native English speakers like myself, "fungible" may not be as familiar as the concepts "replaceable" or, "mutually interchangeable". So, they are mutuall interchangeable tokens and that's all basically.
If we compare ERC20 to ERC721 non-fungible tokens (NFTs), we can say that ERC20 tokens are like dollars and ERC721 tokens are like collectible baseball cards. ERC20 tokens are used to represent value, while ERC721 tokens are used to represent ownership of a unique asset.
If we go to ethereum.org, we see that tokens can represent virtually anything in Ethereum:
- reputation points in an online platform
- skills of a character in a game
- lottery tickets
- financial assets like a share in a company
- a fiat currency like USD
- an ounce of gold
and more...
Now, ERC20s are just smart contracts. A type of program that can run on the Ethereum Virtual Machina (EVM), most of the time, these smart contracts is written with the EVM specific language "Solidity". So, like, these tokens are just pieces of code and that's all. Why they're called ERC20 is because of historical and domain specific reasons: In Ethereum, back in the day, there were no standard for creating tokens so people created many, different tokens with different properties. They were not interchangeable. Thus a standard was proposed: ERC20. With this standard, tokens should have some core properties. Like, they should have a name and a symbol, they should have a function that shows the total supply, they should be able to transferred from one account to another etc. I'll go over them when we create our own token.
The ERC20 standard
ERC20 contains several functions that a compliant token must be able to implement.
TotalSupply: provides information about the total token supply
BalanceOf: provides account balance of the owner's account
Transfer: executes transfers of a specified number of tokens to a specified address
TransferFrom: executes transfers of a specified number of tokens from a specified address
Approve: allow a spender to withdraw a set number of tokens from a specified account
Allowance: returns a set number of tokens from a spender to the owner1
They also have 2 events:
Transfer: triggers when tokens are transferred
Approval: triggers when an allowance is set
Now what this means is that, any piece of Solidity code that complies with these specifications are ERC20 tokens. That was the part that I didn't understand at first. There are no ready-made ERC20 tokens or they are not anything special at all, they are just pieces of code that are called smart contracts in the Ethereum ecosystem. So, if you want to create your own token, you can just write a piece of code that complies with the ERC20 standard and you have your own token. That's it.
How to create an ERC20 token
Now, I will show 2 ways of creating an ERC20 token. The first way will be more cumbersame and manual, while the second one will be much easier. So, if you don't want to learn how do they work under the hood, you can just skip to the second part.
The manual way
Here, we're going full coding mode so be prepared. To disect how ERC20 tokens work, I'm going to recreate the famous Shiba Inu contract.
That means that I'll be using their code as a reference. You can find it here: https://etherscan.io/address/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce#code
Start by opening a new file on the text editor of your choice. Taking that you're total beginner, you can also use Remix IDE that's specifically for Ethereum development. A quick Google search will show you how to use it.
I open it via VsCode and name my contract file ManuelToken.sol (.sol being the extension for Solidity language). I start the code as such:
NB: I'm omitting the interface and the SafeMath implementation for the sake of simplicity.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ManualToken{
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
}
Well, what's going on above code? All Solidity smart contracts start with a license identifier that's commented out. In this case, MIT means that it's free to use, manipulate and whatsoever. Then, you have to define the Solidity compiler version.
If you're directly coming from Javascript, this is new to you. You never needed to compile your code before. Solidity needs to be compiled to EVM bytecode before it can be deployed to the Ethereum blockchain. So, you need to specify the compiler version.
Then you see mappings, they don't natively exist in JS too. I was very, very confused by how they work. Are they like objects, or arrays, or like both of them? Actually, they're quite easy to understand. My problem was I was overestimating the complexity of blockchain development. I didn't yet know blockchain is like a huge database and we're just doing backend development. So, if you already are doing some NodeJS stuff, these will come to you like a breeze.
So, yeah, mappings. Mappings basically are data structures that store key-value pairs. But wait, aren't objects are the same too? Nah, mappings are non iterable, fast, and they take only one key and value. In our example, we are creating a private variable called _balances.
Ah, wait. What is that private keyword? Well, Solidity is a typed language, again, something new to our JS friends. If you develop with Typescript, you're good to go. If not, this part might take some time to get used to. In Javascipt, when we define a variable by using either let or const (and var in the old days), we don't need to specify the type of that variable. We can just write let myArr= []
and it works just fine. Also, I can put anything inside of that array. I can populate it with objects, numbers, strings, other arrays and all. So, let myArr=[1,2,"one", {myKey:myValue}, [55,"I'm a different array yo"]]
is totally acceptable and valid. Although, the tradeoff you get from such flezibility in Javascript, as we know, is that it's prone to bugs. Anyway, I digress.
In SOlidity, private variables are variables that can only be accessed within the scope of the contract in which they are declared. Private variables are not visible or accessible to other contracts or to external accounts. Solidity docs adds a note here:
Everything that is inside a contract is visible to all observers external to the blockchain. Making something private only prevents other contracts from reading or modifying the information, but it will still be visible to the whole world outside of the blockchain.
Okay, we get what's a mapping and what are private variables. But what's up with the syntax, man? Like, again, in JS we don't do it in that way do we? If I wanted to write this line in JS mapping(address => uint256) private _balances;
I'd probably write something like this let _balances = {address: uint256}
. I say something as mappings are not native to JS. So in Solidity when we create a variable, we start with the type of the variable. so it is a mapping, and a mapping named _balances that takes an address and points that addres to a uint256.
Oh, don't even get me started with uints eh? They are unsigned integers. So, they can only be positive. The number after the uint is the number of bits that the integer will take. So, uint8 is an integer that takes 8 bits, uint16 is an integer that takes 16 bits and so on. The maximum number that can be stored in a uint256 is 2^256-1. That's a lot of numbers. So, you can store a lot of money in a uint256. Like, a lot.
It takes an address I said, addresses are another type in Solidity language. They represent, well, addresses. They are 20 bytes long and they are unique. So, you can use them to identify users. They are also used to identify smart contracts. So, if you want to send money to a smart contract, you need to send it to the address of that smart contract.
Okay, so we have a mapping that takes an address and points it to a uint256. What's the point of that? Well, it's a mapping that stores the balance of each address. So, if I want to know how much money I have, I can just call the _balances mapping
and pass my address as a key. So, if I have 1000 tokens, I can just call _balances[myAddress]
and it will return 1000. If I have 0 tokens, it will return 0. Yes, that's how we call mappings.
The second mapping in our code is a bit more complicated. It points to another mapping. mapping(address => mapping(address => uint256)) private _allowances;
So basically when I pass an address to _allowances mapping, I'll be returned to another mapping that takes an address and points it to a uint256. So, if I want to know how much money I can spend from another address, I can call _allowances[myAddress][anotherAddress]
and it will return the amount of money I can spend from that address.
Phew, then we have a basic uint256 private _totalSupply;
. That's the total supply of the tokens.
Following them, we'll have some functions coming up
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) public returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}
function allowance(
address owner,
address spender
) public view returns (uint256) {
return _allowances[owner][spender];
}
The first two functions are easy to understand, the only thing that's different from JS is again you have to define what's the function private or public or whatever, and what's it returning. Again, if you're coming from TS, you know this stuff.
Transfer function takes the address of the recipient and the amount of value that'll be transferred, and returns a boolean. It calls the _transfer function we'll create later on. Here, msg.sender
is the address of the person who called the function. So, if I call transfer
function, msg.sender
will be my address. So, I'm basically saying, transfer the amount of money from my address to the recipient address.
The last function is allowance. It takes the owner address and the spender address and returns the amount of money that the spender can spend from the owner.
Now let's go forward with some new set of functions.
function approve(address spender, uint256 value) public returns (bool) {
_approve(msg.sender, spender, value);
return true;
}
function transferFrom(
address sender,
address recipient,
uint256 amount
) public returns (bool) {
_transfer(sender, recipient, amount);
_approve(
sender,
msg.sender,
_allowances[sender][msg.sender].sub(amount)
);
return true;
}
function increaseAllowance(
address spender,
uint256 addedValue
) public returns (bool) {
_approve(
msg.sender,
spender,
_allowances[msg.sender][spender].add(addedValue)
);
return true;
}
function decreaseAllowance(
address spender,
uint256 subtractedValue
) public returns (bool) {
_approve(
msg.sender,
spender,
_allowances[msg.sender][spender].sub(subtractedValue)
);
return true;
}
Now with the above functions, you see that we call again the _transfer function, and in addition to that, we're calling an _approve function we'll also create in a minute. In simple words, they respectively approve a transaction, transfer value from one place to another, and increase the allowance of a certain entity that has the right to spend value in lieu of someone else.
I'm going over them fast because in order to understand them, we need to see those underscore-starting functions of _transfer and _approve. SO, you know what, let's do that. Let's go and delve into those underscory functions.
function _transfer(
address sender,
address recipient,
uint256 amount
) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_balances[sender] = _balances[sender].sub(amount);
_balances[recipient] = _balances[recipient].add(amount);
emit Transfer(sender, recipient, amount);
}
function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
function _burn(address account, uint256 value) internal {
require(account != address(0), "ERC20: burn from the zero address");
_totalSupply = _totalSupply.sub(value);
_balances[account] = _balances[account].sub(value);
emit Transfer(account, address(0), value);
}
function _approve(address owner, address spender, uint256 value) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}
function _burnFrom(address account, uint256 amount) internal {
_burn(account, amount);
_approve(
account,
msg.sender,
_allowances[account][msg.sender].sub(amount)
);
}
The first function, _transfer takes 3 parameters as you can see: an address of a sender, an address of a recipient, and an amount in the type of uin256 (which can be a biiig biiig number). The require statements make sure that the addresses are valid, as if I'm not mistaken, address 0 is a special kind of address in Solidity, so first they make sure that the addresses are not 0. Then, they subtract the amount from the sender's balance, and add it to the recipient's balance. Then they emit a Transfer event. Now, those sub
and add
are coming from a whole different contract called safemath, which I said I'm going to omit for now. But, if you're interested, you can check it out in the original contract address I've shared above.
_mint function, well, minting is just a fancy word for creating new tokens. So, it takes an address and an amount, and it adds the amount to the total supply, and adds it to the balance of the address. Then it emits a Transfer event.
_burn function is the opposite of minting. It takes an address and an amount, and it subtracts the amount from the total supply, and subtracts it from the balance of the address. Then it emits a Transfer event.
_approve function takes an owner address, a spender address, and an amount, and it sets the allowance of the spender to the amount. Then it emits an Approval event.
_burnFrom function is the opposite of approve. It takes an address and an amount, and it burns the amount from the address, and it also decreases the allowance of the spender by the amount.
Okay, that was a lot of code an explanation. Now, as I promised, I'll show you how to make things extremely simple. So, let's go back to the original contract, and let's see how we can make it simple.
The simple way
There's this thing called OpenZeppelin, which is a library of smart contracts that are already written and tested. So, instead of writing all the code above, we can just import the OpenZeppelin library, and use the contracts that are already written. So, let's do that.
I install openzeppelin via npm, and I import the ERC20 contract from the library.
// contracts/GLDToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract GLDToken is ERC20 {
constructor(uint256 initialSupply) ERC20("Gold", "GLD") {
_mint(msg.sender, initialSupply);
}
}
That's it. I'm not joking. I took it from their documentation and that's literally how much code it requires to create a new token. How does it work though? What happened all those fancy functions and mappings and uints we've just written following the Shiba Inu contract? Well, they are there. At least, openzeppelin specifications are there, at the ERC20.sol contract we're importing.
We can actually go to their github and see the code for ourselves too. The super thing is that when interacting with this new contract we've created, we can call all those fancy, complicated functions coming from openzeppelin's ERC20 contract. We can transfer tokens, give allowances and all. You can actually check the contract out here => https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
That's it. Next time, if you'd like, I can go over how to create ERC721 NFT contracts.
Thanks for reading. I hope you enjoyed it. If you have any questions, please let me know. I'll be happy to answer them.
Top comments (0)