# Dive into Solidity & EVM: Building an ERC20 contract! ![image](https://hackmd.io/_uploads/ryRA6oXQkg.png) Hey Voyagers! Today we are going to spend some time to build one of the most important solidity smart contracts - **ERC20**. Assuming you already have some coding experience such as Go, Javascript or C++ but you have never written a solidity smart contract before. Learning & building an ERC20 token contract could be a good start to understand how solidity & EVM work! So here's what we gonna do in this tutorial: 1. Learn about what is ERC20 2. Building a simplified ERC20 smart contract in solidity called ERC20.sol 3. Get it deployed and play around with it You gonna need [Remix](https://remix.ethereum.org/), a friendly solidity web IDE for the last part. After all these, we hope you can have a decent understanding of how to write a simple solidity contract and make it work! You will also have your first-ever ERC20 token contract onchain. Feeling excited? Let's go! ## What's ERC20? In a nutshell, ERC20 is a standard for fungible tokens on Ethereum (so does any other EVM compatible blockchains). Tokens like $PEPE, $SHIB, $FLOKI are all based on ERC20 standards. An ERC20 token contract implements the basic features for a fungible token Such as: - Define token information (name, symbol, decimals, token supply) - Record each account's balance - Transfers token from one account to another - Approve others to transfer someone's token for them - Mint new tokens - Burn tokens ### IERC20 Interface Now, from code level, as long as your contract implements all the functions of the IERC20 interface (yes, interfaces just like other languages), your contract is an ERC20 token contract. So the idea of defining a standard similar to ERC20 as an interface has several benefits: 1. It modularizes and standardizes specific implementations by writing the standard into code. 2. It leaves plenty of room for collaborative customization on top of the ERC20 standard. 3. Protocols that want to be compatible with the ERC20 standard can directly align with the content of the standard interface without worrying about the actual contract implementation. 4. It enhances security, readability, and maintainability. Below is a standard IERC20 interface: ```solidity pragma solidity ^0.8.20; interface IERC20 { // Get the total supply of the token function totalSupply() external view returns (uint256); // Get the token balance of a certain address function balanceOf(address account) external view returns (uint256); // Transfer tokens from the function caller address to another address function transfer(address to, uint256 value) external returns (bool); // Get the approved amount of one address to another function allowance(address owner, address spender) external view returns (uint256); // Approved a certain amount of token to the spender account function approve(address spender, uint256 value) external returns (bool); // Transfer tokens from one address to another address function transferFrom(address from, address to, uint256 value) external returns (bool); // Emitted when 'value' token transferred from 'from' address to 'to' address event Transfer(address indexed from, address indexed to, uint256 value); // Emitted when 'value' token approved for 'spender' address by 'owner' address event Approval(address indexed owner, address indexed spender, uint256 value); } ``` ### View Only Functions As you can see, the IERC20 interface provides a certain set of functions to be implemented. Some functions like **totalSupply(), allowance(), balanceOf()** are implemented to read token information, and calling the function won't update the state variables on-chain (which needs to consume resources and thus pay fees), that's the reason why they contain the **view** keyword Taking **balanceof()** function as an example, the function is designed to query the token balance of the certain address, it won't change the account's balance, so adding the **view** keyword is necessary. ### Event It is also noticeable that there are two events, one called **Transfer** and the other called **Approval** that need to be defined and emitted. Events in Solidity are transaction logs stored on the EVM, which have two characteristics: - Responsive: Applications (e.g., ethers.js) can subscribe to and listen to these events through the RPC interface and respond at the frontend. - Economical: It is much cheaper to store data in events, costing ~2,000 gas each. In comparison, storing data with new variables is ten times more expensive. For these two reasons, it is always beneficial to use events to represent important data that is generated constantly. In the case of token, transfer & approval activities are crucial data that needs to be presented and analyses. That's also reason why major blockchain data providers such as Etherscan (blockchain explorer) and Dune use Events to track those on-chain activities. ## Build ERC20 token contract from scratch The first thing you need to do when writing a smart contract is to define a contract name. Let's name it as simple as ERC20. Remember we need to inherit the IERC20 interface, this can be done by adding "is ERC20" after the contract name. ```solidity interface IERC20{ ... } contract ERC20 is IERC20{ } ``` ### Define State Varaibales To implement those functions, let's first define some important variables. ```solidity interface IERC20{ ... } contract ERC20 is IERC20{ uint public _totalSupply; mapping(address => uint) public _balanceOf; mapping(address => mapping(address => uint)) public _allowance; string public name = 'My First ERC20'; string public symbol = 'FIRST'; unit8 public decimals = '18'; } ``` - We use the **string** type to name the token and give it a symbol. - We use the **uint (unsinged integer, it can stored up to 256-bit integers or data units)** type to define the total supply of the token - We use the key-value data structure **mapping** to represent an **address (address type, a unique data structure to represent addresses in solidity)** and its token balance. - Allowance, a mapping of a mapping, map the allowance's spender and the owner, then map the spender with the amount. - Decimals are integers to represent how much "0" can come after "1" when trying to get user representations. You need to be careful when defining decimals, because the amount you should present to users and the amount in solidity is different. This is because there is no float in solidity, so the float is represented by big integers. For example, if the decimal is 2, then a balance of 555 tokens in solidity should be presented to users as (555/10^2) = 5.55 tokens. By defining these variables and adding the **public** keyword, the 'getter' function with the same name will be generated, thus implementing the **totalSupply(), allowance(), balanceOf()** functions in the IERC20 interface. ### How are these state variables stored in solidity? In Solidity, storage slots are the locations used to store variables in the smart contract on blockchain. Each contract has a unique storage space where the state variables are kept, and organized using storage slots for efficient access. #### How are storage slots assigned to each state variable? **Sequential Assignment:** Each state variable in a contract is assigned a storage slot in a sequential manner. The first variable occupies slot 0, the second variable occupies slot 1, and so on. For example: ```solidity uint256 a; // Storage slot 0 uint256 b; // Storage slot 1 uint256 c; // Storage slot 2 ``` **Storage of Mappings:** For mappings, Solidity does not allocate a specific storage slot for the mapping itself. Instead, when storing data in a mapping, the Solidity compiler generates a storage address based on the key of the mapping. The storage address for a mapping is calculated using the following formula: ``` slot = keccak256(abi.encode(key, baseSlot)) ``` Here, baseSlot is the slot for the mapping as defined in the contract (e.g., if it is the first state variable of the contract, baseSlot would be 0), and key is the key used to store data in the mapping. ### Implement The transfer() function Now we have all the state variables we need for a token. Let's implement the first function in the IERC20 interface - transfer(). The general logic of transfer() is super simple. You call the function to send your tokens to another address. In a nutshell: - Your token balance is decreased - The recipient's token balance is increased You can also modify the transfer feature to realize some interesting features such as taxation, dividends, and lottery. Taking taxation as an example, for each transfer, 5% of the transferred amount can be sent to a certain address (in most cases an EOA address served as a fee vault). A lot of meme coins use this method to let people reconsider before they sell and collect fees to later either buyback or airdrop for those diamond hands. ```solidity function transfer(address recipient, uint amount) public override returns (bool) { //Increase msg.sender's token balance balanceOf[msg.sender] -= amount; //Decrease msg.sender's token balance balanceOf[recipient] += amount; //Emit Transfer event emit Transfer(msg.sender, recipient, amount); return true; } ``` #### msg.sender vs tx.origin You can see that we use **msg.sender** to represent the function caller. There is another way to represent the caller in some cases - x.orgin** So what's the difference between these two? - msg.sender: The address that is calling the contract - tx.origin: The initiator of the whole transaction You may think, the transaction initiator is also the contract caller right? Why do we need 2 methods to present the same address? Well, in reality, because there can be a contract call chain, which the transaction initiator calls contract B, and then contract B calls contract C, these 2 methods could return different addresses. Below is a graph of the contract call chain to help you understand it better. ![image](https://hackmd.io/_uploads/B1qg2jQ71x.png) If you understand this, you will also understand why in most cases, you should use **msg.sender**. 1. When there is a contract call chain, **msg.sender** can present the actual caller while **tx.origin** is pointed to the transaction initiator. It pushes different contracts to have their own responsibilities. 2. Using **tx.origin** can be dangerous, because a contract in the contract call chain can transfer the transaction initiator's token without permission or approval! 3. **msg.sender** also helps to prevent reentrancy attacks while enhancing the simplicity and clarity of the contract. ### Implement the approve() & transferFrom() functions So now we have the transfer function. We can send tokens from one address to another, and that's all a token needs to be able to do right? Well, in practice, if you want to interact with all those on-chain applications (including selling your tokens on Uniswap), you need to allow smart contracts to transfer your tokens on your behalf. That's the beauty of smart contracts, with this function, you can implement a lot of features such as token staking, token buy/sell through decentralized exchanges, lottery, etc. And the **approve()** function is what we need for this. #### Why though? - **approve()** function allows users to empower smart contracts to move users' tokens smaller than a certain amount ("allowance") on users' behalf. If there is no **approve()** function, users need to transfer their tokens directly to the smart contract every time they need to. - Unlike **ETH** transfer the recipient actually received $ETH, ERC20 token transfer is essentially the state changes that happen within the ERC20 token contract. So the **approve()** function helps to notify the smart contract when they are allowed to move users' assets. #### So how do we implement it? Basically, you approve a certain amount of the spender address, and after that spender can use the **transferFrom()** function to transfer your tokens on your behalf. Remember we created an **allowance** mapping? To implement the approve() function, we simply add a new record to the mapping. ```solidity function approve(address spender, uint amount) public override returns (bool) { //Udpate the allowance mapping allowance[msg.sender][spender] = amount; //Emit Approval event emit Approval(msg.sender, spender, amount); return true; } ``` And for the **transferFrom()** function, it is quite similar to the **transfer()** function. The major difference is that in order to make it work, the **msg.sender** should have enough allowance from the **sender**. ```solidity function transferFrom( address sender, address recipient, uint amount ) public override returns (bool) { //Deduct the allowance amount allowance[sender][msg.sender] -= amount; //Transfer tokens balanceOf[sender] -= amount; balanceOf[recipient] += amount; //Emit Transfer event emit Transfer(sender, recipient, amount); return true; } ``` Now with approve() and transferFrom(), we have a whole approval workflow for every token: 1. User A first approves a certain amount of tokens to smart contract B using approve(). 2. User A then calls smart contract B's function. The function calls the transferFrom() function to transfer tokens from user A to where it should be. This is what happens when you try to interact with most of the Ethereum smart contracts. Sounds promising right? It is highly functional and protects user funds to some extent. Well, as we all know, nothing is perfect, so does the approval workflow. ### Approval workflow is great, but not perfect Let's discuss why. #### It is safe, but not safe enough Using approve() means you need to trust the spenders. If the spender is a person's address, you need to make sure they don't directly steal your token. If the spender is a contract address, you need to make sure the contract's code does not steal your token. Which in many cases, and for most users, is hard to do. In fact, losing funds due to giving approval to scammers is a major category in crypto security events. #### Bad UX, especially if you want to sell tokens Approval workflow means if I want to interact with a contract, I need to have 2 transactions. 1 for approval, 1 for actual interaction. Now imagine, you are in a hurry to sell your tokens through Uniswap because the token price keeps dropping. And you realize that you need to wait for both transactions to be confirmed to sell your tokens. Which on the Ethereum mainnet, can take up to 25 seconds in total. Trust me, it doesn't feel good. #### Increase/Decrease allowance is not working like it is supposed to When we try to update the allowance, what actually happens is: - You delete the old allowance - You add the new allowance So if used at the right time, the spender could actually utilize both the old allowance and the new allowance. Now imagine: - Alice approves 100 tokens to Bob - Alice wants to decrease the allowance to 50 - Bob finds out and transfers 100 tokens before Alice confirms the update transaction - Alice updates the allowance - Bob transfers 50 tokens out In this case, Alice only wanted Bob to be able to transfer 50 tokens, but Bob ended up transferring 150 tokens, which is even greater than the original 100 allowances! This is definitely not what we expect the approve() function to be! #### Are we solving these issues? As EVM progresses, an increasing number of people are becoming aware of these issues and are seeking solutions. Some of these solutions include comprehensive updates to EVM's account system, such as [EIP4337](https://www.erc3447.io/) and [EIP7702](https://viem.sh/experimental/eip7702). If you want to delve deeper, feel free to explore those links! ### Using mint() & burn() to Manage Token Supply As we mentioned earlier, on top of the IERC20 interface, you can add any functions you want. So let's try to add burn/mint functions to manage the token supply. First, let's implement the mint feature which: - Mint new tokens to the caller - Increase the token's total supply ```solidity function mint(uint amount) external { balanceOf[msg.sender] += amount; totalSupply += amount; emit Transfer(address(0), msg.sender, amount); } ``` For the mint function, we emitted a **Transfer** event since there is no **Mint** event in the IERC20 interface, we use the **address(0) - 0x0000...00** to replace the sender address. Next, let's implement the burn function which: - Burn tokens of the caller - Decreases the tokens' total supply ```solidity function burn(uint amount) external { balanceOf[msg.sender] -= amount; totalSupply -= amount; emit Transfer(msg.sender, address(0), amount); } ``` Similarly, we emitted a **Transfer** event and used the **address(0) - 0x0000...00** to replace the recipient address. ## Deploy the contract and interact with it! Now we have the fully function ERC20 smart contract **ERC20.sol**, let's deploy it on-chain and interact with it! We gonna need [Remix](https://remix.ethereum.org/) to do so. For this section, I recorded a 5-minute video so you can follow step by step with me. [Watch the video](https://www.youtube.com/watch?v=GrPAf0wWqNE) ![image](https://hackmd.io/_uploads/ByOWCjQmke.png)