ERC20 standard has become one of the most prevalent methods of creating tokens today. It has been a popular way to do crowdfunding of various crypto projects. There are now tens of thousands of different ERC20 tokens that have been created and are functioning as per ERC20 standards.
ERC20 is an Ethereum Improvement Proposal introduced by Fabian Vogelsteller on 2015. It allows creating a new token that abides by the same rules as Bitcoin and Ethereum using Ethereum’s smart contract functionality.
The standard has several base functions already baked in. It is possible to use a contract with these functions immediately without having to modify much of its code. It is also possible to extend this contract and add your own functions that are relevant to your project.
The methods supported in the ERC20 are below. The syntax is written in Solidity.
Creating a Basic ERC20 Contract
The most basic contract you can create is the skeleton contract which you can import from OpenZeppelin. You can specify the name, ticker and the supply. The simplest way to handle supply is to mint a finite supply and move it to the contract deployer. From that point you can distribute your tokens to other addresses from one giant wallet.
It’s worth noting that the decimals is completely up to you, but you can just use Ethereum’s standard decimals() which defaults to 18. In example below we have 20,000,000 tokens with 18 decimals. So if you call totalSupply() don’t be surprised when you see a very large number 20000000000000000000000000.
pragma solidity 0.8.6;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";
contract P2PM is ERC20 {
address public admin;
constructor() ERC20("ppm", "PPM") {
_mint(msg.sender, 20000000 * 10 ** decimals());
admin = msg.sender;
}
}
ERC20 Standard Functions
ERC20 contains several functions that a compliant token must be able to implement.
contract ERC20 { function totalSupply() constant returns (uint theTotalSupply); function balanceOf(address _owner) constant returns (uint balance); function transfer(address _to, uint _value) returns (bool success); function transferFrom(address _from, address _to, uint _value) returns (bool success); function approve(address _spender, uint _value) returns (bool success); function allowance(address _owner, address _spender) constant returns (uint remaining); event Transfer(address indexed _from, address indexed _to, uint _value); event Approval(address indexed _owner, address indexed _spender, uint _value); }
Each of the functions is public and can be invoked by anyone, but certain functions like transfer or approve need to be signed by specific user for them to work.
- 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 owner
totalSupply()
Although the supply could easily be fixed, as it is with Bitcoin, this function allows an instance of the contract to calculate and return the total amount of the token that exists in circulation.
balanceOf()
This function allows a smart contract to store and return the balance of the provided address. The function accepts an address as a parameter, so it should be known that the balance of any address is public.
approve()
When calling this function, the owner of the contract authorizes, or approves, the given address to withdraw instances of the token from the owner’s address.
To understand this function consider you going to a hotel and requesting to stay there for the night. The hotel typically puts a hold on your credit card for a certain amount just to make sure that you have enough funds in your account. If you do then you are welcome to stay and then pay in the morning when you check out. Similarly, when working with decentralized exchanges, for example, they might request that you approve them for a certain amount if you want to sell tokens. They will not physically move any tokens, but if there is a buyer then they will make use of your funds as long as they are still authorized to do so.
transfer()
This function lets the owner of the contract send a given amount of the token to another address just like a conventional cryptocurrency transaction. A test in a tool like truffle could look like this.
it('can transfer tokens between accounts', async() => {
let amount = web3.utils.toWei('1000', 'ether');
await p2pm.transfer(accounts[1], amount, { from: accounts[0] });
let balance = await p2pm.balanceOf(accounts[1]);
balance = web3.utils.fromWei(balance, 'ether');
assert.equal(balance, '1000', 'balance should be 1k tokens for recipient');
})
transferFrom()
This function allows a smart contract to automate the transfer process and send a given amount of the token on behalf of the owner.
Seeing this might raise a few eyebrows. One may question why we need both transfer()
and transferFrom()
functions.
Consider transferring money to pay a bill. It’s extremely common to send money manually by taking the time to write a check and mail it to pay the bill off. This is like using transfer()
: you’re doing the money transfer process yourself, without the help of another party.
In another situation, you could set up automatic bill pay with your bank. This is like using transferFrom()
: your bank’s machines send money to pay off the bill on your behalf, automatically. With this function, a contract can send a certain amount of the token to another address on your behalf, without your intervention.
allowance()
This function returns the amount that an address is allowed to spend on behalf of another address. For example, if we approved an address to spend 10000 ppm then this is the amount that the function will return.
Token Name
When creating the token you need to specify the token name and its’ ticker. These are not required but they are useful when you want to identify your token. In case of name collisions the token can be identified by contract address.
contract P2PM is ERC20 { address public admin; constructor() ERC20("dust", "P2PM") { _mint(msg.sender, 20000000 * 10 ** decimals()); admin = msg.sender; } }
There are public functions symbol() and name() that will return whatever the contract was created as. A truffle test example is the following:
it('has a symbol: P2PM', async() => {
let symbol = await p2pm.symbol();
let name = await p2pm.name();
assert.equal(symbol, 'P2PM', 'symbol is P2PM');
assert.equal(name, 'dust', 'name is dust');
})
Final Thoughts
ERC20 tokens are a great way to create your own token without having to deal with lower level implementation of the protocol, blockchain and security. It will sit on top of Ethereum which is an already established ecosystem thats widely used in DeFi and dApps in general. Clearly, we still want to be careful when we design our smart contracts because they could have vulnerabilities too, but the core functionality in ERC20 standard has been proven to be secure.
As you implement more smart contracts there are different variables that will become important. Things like gas will start playing a more important role because each function execution will be associated with a gas * gas_price which is a fee added to someone’s tab. But… this is still negligible compared to the value we get from using the underlying technology.