In this blog post, we will learn How to Create a custom ERC-20 token on the Ethereum Blockchain using Solidity and deploy it to a public test net. But first, we will learn what tokens are, and we will have a brief introduction to the ERC-20 standard itself and discover the functions and state variables that make up a valid ERC-20 Token. Next, we will get our hands dirty and create an ERC-20 Token Smart Contract with Solidity. We will write multiple test scripts to ensure that our token works as expected with minimum bugs. We will also learn how to interact with a deployed version of our token using the Truffle console.
This is part of a three part series tutorial.
Part I – Create a Custom ERC20 Token With Solidity
Part II – Create a Crowd sale Token Contract With Solidity
Part III – Create a Frontend to interact with our Smart Contracts
You can find the complete source code of both smart contracts on the github page
If you are excited about this, then let’s get it!
What is a Token?
A token commonly describes a custom-made coin, used for a specific purpose and privately issued/operated. Usually, these tokens are only useful in a specific environment or ecosystem, defined by an entity (a personal token, laundry token, game token, etc.)
In the Blockchain world, a token could represent an asset, a right, and the most obvious one, a currency. And the fact that the blockchain is decentralized means that tokens can have multiple use cases simultaneously. A token can be used to cast a vote in an election and also be used to represent ownership of an asset.
List of Possible Use Cases of Tokens in The Blockchain World
- Currency
- Asset
- Access
- Voting Rights
- Collectible
- Identity
- Utility
Fungible and Non-Fungible Tokens
According to Wikipedia: “In economics, fungibility is the property of a good or a commodity whose individual units are essentially interchangeable.”
An example of a Fungible Token is gold, because regardless of who owns a specific amount of gold, its intrinsic value will not change, and gold owners can exchange their gold tokens with each other without having to worry about its value fluctuating from one owner to the other. These types of tokens will most likely be great replacements for things like reward points (retail), or miles points (transportation). Another example that you are probably waiting for me to mention is Bitcoin, of course. But, let’s move on.
A Non-Fungible Token or NFT is a token that is not interchangeable, whether tangible or intangible. For instance, a Token representing ownership of a particular Fela Kuti painting does not hold the same value as a Picasso. But both could be part of the same “art ownership token.” Each NFT is uniquely identifiable through serial numbers and by other means of identification.
Fun fact: The first blockchain Token was Bitcoin; not Ethereum. When Bitcoin was released, many projects were built on top of Bitcoin as Token platforms, way before Ethereum was ever introduced. The difference is that Ethereum was the first project that introduced a Standard for tokens, which led to a massive explosion of token projects.
Ethereum Tokens
Before we go any deeper, it is crucial to understand the difference between tokens and ether on the Ethereum blockchain. The Ethereum protocol itself only knows about ether, but nothing regarding tokens built on top of it. Ether transactions are a built-in logic inside the Ethereum protocol itself, but token transactions and ownerships are not. In other words, ether balances are handled by the protocol, whereas Token balances are managed by Smart Contracts.
The ERC-20 Token Standard
The Ethereum Foundation (https://ethereum.org/en/developers/docs/standards/tokens/erc-20/#top) says: “The ERC-20 introduces a standard for Fungible Tokens, in other words, they have a property that makes each Token be the same (in type and value) of another Token. For example, an ERC-20 Token acts just like the ETH, meaning that 1 Token is and will always be equal to all the other Tokens.”
The standard in itself consists of several mandatory functions and events, along with optional attributes and functions that could be added by the developers.
ERC-20 Interface Specification in Solidity
// Grabbed from: https://github.com/ethereum/EIPs/issues/20
contract ERC20 {
// Required Funtions
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);
// Required Events
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
ERC-20 Required Functions and Events
- totalSupply(): returns the total units of existing tokens
- balanceOf(): returns the balance of a given Ethereum address
- transfer(): given an address and amount, it transfers the amount of token to the address, from the address that executed the transfer function of the smart contract
- transferFrom(): given a sender, recipient, and amount, it transfers the amount from one account to the other. This can only happen once another function has successfully executed: the approve() function.
- approve(): given a recipient and amount, it authorizes the recipient to execute transfers up to the amount specified, from the account that executed the approve() function.
- allowance(): given an owner and a spender, it returns the remaining amount that the spender is approved to spend on behalf of the owner.
- Transfer(): Event that is triggered after a successful transfer
- Approval(): Event that is emitted after the approve() function is successfully called.
ERC-20 Optional functions
function name() constant returns (string); // returns the name of the Tokenfunction symbol()constant returns (string); // returns the symbol of the Tokenfunction decimals()constant returns (unint); // return the number of decimals used to divide token amounts.
To keep track of accounts allowances and balances, Solidity makes use of a specific data structure called mapping. A typical mapping will look like this:
mapping(address => unit256) balances;mapping(address => mapping(address => uint256)) allowed;
On an OOP level, like JavaScript, the mapping will look like this:
const balances = {
0xBe9E16eA05C23F4A8BE88E53eEF2C35Fc31AdBf4: 1000, // Address mapped to token balance0x5156649c4B389D1F8923af92Be797e91CF50CFb1: 500,
...
}const allowed = {
0xBe9E16eA05C23F4A8BE88E53eEF2C35Fc31AdBf4: {
0x4Ced46D3020D9c32f4C0a2b354955D8cc46Cf94f: 500,
0x4760BD5ECb970d1BE53252F4b634B80D2097b777: 1500,
...
}, // Address mapped to a map of allowed balance0x5156649c4B389D1F8923af92Be797e91CF50CFb1:{
0x4Ced46D3020D9c32f4C0a2b354955D8cc46Cf94f: 57,
0x4760BD5ECb970d1BE53252F4b634B80D2097b777: 120,
...
}, // Address mapped to a map of allowed balance
...
}
// To access the balance of the first account
const balance1 = balances["0xBe...dBf4"] // balance1 = 1000// To access the amount account0x4C...f94f has approved account0xBe...dBf4 to spend on it behalf
const allowance1 = allowed["0xBe...dBf4"]["0x4C...f94f"] //allowance1 = 500
OpenZeppelin ERC20 Implementation
The OpenZeppelin StandardToken is an ERC20-compatible implementation. This implementation comes with additional security precautions as it was designed and developed by experts in the industry. This implementation is just one of the many ERC-compatible standards that are developed and maintained by OpenZeppelin.
You probably got bored with all the literature. I am too. So it is time to get our hands dirty. I know you are itching to get started in learning how to write and deploy a custom ERC20 token on Ethereum with Solidity. Let’s get it!!!
CREATE AN ERC20 TOKEN WITH SOLIDITY
In this post, we will learn how to use Truffle to compile, test and deploy our token, but we will code our ERC20 smart contract in Solidity.
Prerequisites
Before we get started, you must have Node.js and npm installed. For this tutorial, I will be using:
// Node.js version
$ node -v
v16.1.0
// npm version
$ npm -v
v7.11.2
The next dependency we will need is Truffle, which is the most popular Ethereum Smart Contract development, testing, and deployment framework. You can refer to this blog post to learn more about the Truffle framework.
To install Truffle, run the following command:
$ npm install -g truffle
// The version we will be using for this tutorial
+ truffle@5.4.18
installed 1 package in 37.508s
To make sure Truffle was successfully installed, run the command:
$ truffle version
//The output should look like this
Truffle v5.4.18 (core: 5.4.18)
Solidity - 0.8.9 (solc-js)
Node v16.1.0
Web3.js v1.5.3
The next dependency we will need is Ganache, a local blockchain, which ensures development is fast and free (No real transaction fees during development). To learn how to install Ganache, follow this tutorial. But once you complete that tutorial, you should get to a screen that looks like this:
The next dependency we will need is MetaMask, which is a browser extension that allows us to connect to an Ethereum node from a client. Go to https://metamask.io/ and download the extension for your browser and follow the instructions on how to install it.
Now that all prerequisites have been met, we are now going to start writing our ERC20 Token. Let’s go!
Coding The ERC20 Token Smart Contract
Project Set up
First, let’s create a project directory that we will call ERC20-Token; we will cd into it, and initialize a package.json file that will track all our development dependencies within our project:
// Create the project directory
$ mkdir ERC20-Token
// Change Directory to ERC20-Token
$ cd ERC20-Token
// Initialize a package.json file, inside ERC20-token directory
ERC20-Token $ npm init -y
Our package.json file will look like this:
{
"name": "erc20-token",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
The package.json file is created, along with a node_module directory where our dependencies will live.
Next, let’s create a .gitignore file and add files and directories we do not want our project to track.
// Create the .gitignore fileERC20-Token $ touch .gitignore
Inside that file, add the following code
node_modules/
.env
package-lock.json
.gitignore
To initialize a brand new Truffle project, run the command:
// Initialize truffle, inside ERC20-token directory
ERC20-Token $ truffle init
The output will look like this:
Starting init...
================
> Copying project files to <PATH_TO_MY_PROJECT_DIRECTORY>/ERC20-Token
Init successful, sweet!
Try our scaffold commands to get started:
$ truffle create contract YourContractName # scaffold a contract
$ truffle create test YourTestName # scaffold a test
http://trufflesuite.com/docs
Up to this point, your project directory should look like this:
- The contracts directory is where we will write all our Smart Contracts code.
- The migrations directory is where we will write all our deployment scripts, following a specific convention.
- The test directory is where we will write all our Smart Contract tests.
- The truffle-config.js file is where we will set up network configuration, builds and artifacts directory, and more.
Configuring Truffle Deployment Script
Let’s open the truffle-config.js file.
The first field I want us to pay attention to is the compilers, especially the solidity version:
// Configure your compilers
compilers: {
solc: {
version: "0.8.9", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
}
},
It is important to remember that this version should always be greater than or equal to the version of Solidity we declare inside our smart contracts.
The second field worth looking at is the networks:
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
}
If we look back at our Ganache Dashboard, we see that we have network information at the top of the screen.
- The current block: 0
- Gas Price: 20000000000 (20 gwei)
- Gas Limit: 6721975
- Network id: 5777
- RPC Server / host: 127.0.0.1:7545
- Port: 7545
We can use this in our truffle-config.js file to connect to this specific ganache local instance.
First, we will add a new entry inside the network field of ./truffle-config.js called ganache, and we will configure it like so:
[...omitted code...]
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
ganache: {
host: "127.0.0.1", // Ganache RPC Server (default: none)
port: 7545, // Current Ganache Instance port (default: none)
network_id: 5777, // Current Ganache Instance network ID (default: none)
},
[...omitted code...]
Coding the NzouaToken Smart Contract
Inside our contracts sub-directory, we will create a new file called NzouaToken.sol
ERC20-Token $ touch contracts/NzouaToken.sol
Let’s open the file and paste the following code:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaToken {
constructor() {}
}
As seen above, the first line defined the version of Solidity used to code the Smart Contract. The second line defines the smart contract itself, using the keyword contract. Contracts in Solidity behave as Classes in other object-oriented programming languages.
N.B: It is crucial to ensure that the version of Solidity declared inside the truffle-config.js file is greater than or equal to the version declared inside our Solidity smart contract.
We want to make sure everything is still working up to this point; So we will run the compile command:
ERC20-Token $ truffle compile
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to <PATH_TO_YOUR_PROJECT_DIRECTORY>/ERC20-Token/build/contracts
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Everything should still compile successfully.
State Variables and Functions Inside The Smart Contract
In this section, we are going to add more meat to our smart contract.
In a previous section, we touched on the concepts of required and optional functions and variables in Solidity. We are now going to define those parameters inside our Smart Contract.
The first one is totalSupply, which is a required function for any ERC20 token to work correctly. Inside our smart contract, we will declare a new variable called totalSupply and initialize it inside our constructor, like so:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaToken {
uint256 public totalSupply;
constructor() {
totalSupply = 1000000; // 1 million Nzouat Tokens
}
}
It is important to note that because we declared the totalSupply as a public variable, Solidity will give us access to this:
function totalSupply() constant returns (unint256 totalSupply)
This function, when called, will return the totalSupply as per the ERC20 standard/specification. As developers, we do not have to write any function that returns this value. It is given to us for free by Solidity. Pretty awesome.
Let’s move on to another topic; we will get back to our smart contract and add more functions and variables later. But for now, we want to make sure that our deployment will work and that we can interact with the contract on the console and do some preliminary tests.
Inside our migrations sub-directory, let’s create a new migration script file called 2_deploy_contracts.js, like so:
ERC20-Token $ touch migrations/2_deploy_contracts.js
And then paste the following code:
const NzouaToken = artifacts.require("NzouaToken");
module.exports = async function (deployer, network, accounts) {
await deployer.deploy(NzouaToken);
};
Let’s analyze this code for a little bit. Truffle uses artifacts.required() to generate an abstraction of our smart contract code from the ./contracts directory and save it inside a variable called NzouaToken. Then, the module.exports function passes the deployer as the first parameter. The deployer is then in charge of staging deployments to the network.
To create an instance of our deployed Smart contract, let’s add the following code inside the deployment script:
[...omitted code...]
await deployer.deploy(NzouaToken);
const Instance = await NzouaToken.deployed();
[...omitted code...]
Now that we have created our migration script, let’s make sure it works by running our first migration script and deploy to our local ganache instance, like so:
ERC20-Token $ truffle migrate --network ganache
N.B: If you get an unexpected error during the migration, use the –reset flag to reset the state of the blockchain.
If everything went well, we should see the following output:
Compiling your contracts...
===========================
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to <PATH_TO_YOUR_PROJECT>/ERC20-Token/build/contracts
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0xc1b57fc6f4790e16c6404541dab76e6bce72a72eae06f47e5277828af6efe3e9
> Blocks: 0 Seconds: 0
> contract address: 0x2A4d80F7C31B02A29cE29D35DD68feF3429BD613
> block number: 1
> block timestamp: 1636550731
> account: 0x5D16d433aFDB957ceF88231da5CDcf12b083E094
> balance: 99.99502316
> gas used: 248842 (0x3cc0a)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00497684 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00497684 ETH
2_deploy_contracts.js
=====================
Deploying 'NzouaToken'
----------------------
> transaction hash: 0xf71b78c87435898920ee0e0b0079cf6080bbaf62829d6ffe300d2c978cd2cbbe
> Blocks: 0 Seconds: 0
> contract address: 0x53bFe6F47d6A38F0Fb4E75919CE8942eB2b04A1F
> block number: 3
> block timestamp: 1636550732
> account: 0x5D16d433aFDB957ceF88231da5CDcf12b083E094
> balance: 99.99192808
> gas used: 112241 (0x1b671)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00224482 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00224482 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.00722166 ETH
The output shows that the migration script successfully deployed two smart contracts to the local ganache network. Each deployment generated a transaction Hash and each smart contract was assigned a contract address at deployment. The output also tells us that account 0x5D16d433aFDB957ceF88231da5CDcf12b083E094 is the one that deployed both contracts and they both cost a total of0.00722166 ETH to deploy.
Looking back at our ganache dashboard we can see that the balance of account0x5D16d433aFDB957ceF88231da5CDcf12b083E094 is now lower and the account also registered 4 transactions. To see more information about these transactions, click on the Transactions tab on your Ganache dashboard.
Interacting With Our Smart Contract From The Truffle Console
Now that our smart contract has been deployed to Ganache, we can interact with it from the console.
To initialize the truffle console, run the command:
ERC20-Token $ truffle console
truffle(ganache)>
Inside the console, let’s get an instance of our contract, using async/await like so:
truffle(ganache)> const token = await NzouaToken.deployed()
When we hit enter, the console returns undefined. Do not panic.
To read the address that was assigned to our token, let’s type the following from the truffle console.
truffle(ganache)> const tokenAddress = token.address
tokenAddress //'0x53bFe6F47d6A38F0Fb4E75919CE8942eB2b04A1F'
To get the totalSupply, let’s type
truffle(ganache)> let totalSupply = await token.totalSupply()
truffle(ganache)> totalSupply
BN {
negative: 0,
words: [ 1000000, <1 empty item> ],
length: 1,
red: null
}
To convert our totalSupply to a human-readable format, let’s write
truffle(ganache)> totalSupply = totalSupply.toNumber()
1000000
To exit the Truffle console, type .exit on the terminal.
Now that we can write a basic smart contract and deploy it on a local blockchain, we will write our first test to make sure our smart contract is executing as expected. Tests are important in blockchain development because we want to make sure our code works as expected and is bug-free. Otherwise, we might end up paying for unnecessary transaction fees and our smart contract will not be too useful.
Writing Our Smart Contract Test
Inside the test sub-directory, let’s run the following command that will create a new script file called NzouaToken.test.js
ERC20-Token $ touch test/NzouaToken.test.js
Truffle comes bundled with the Mocha framework and the Chai assertion library; so there is no need to install additional dependency for the time being.
Inside test/NzouaToken.test.js, let’s past the following code:
var NzouaToken = artifacts.require('./NzouaToken');
contract('NzouaToken', async (accounts) => {
it('Sets the total supply on deployment', async () => {
const token = await NzouaToken.deployed();
let totalSupply = await token.totalSupply();
totalSupply = totalSupply.toNumber();
assert.equal(totalSupply, 1000000, 'Total supply set correctly to 1,000,000')
})
})
As we can see in the code above, the first thing we did was import our smart contract using the artifacts.require(). Then we declared a contract and named it NzouaToken. Next, we make use of the async/await functions of JavaScript to run our tests.
The first test is to make sure that the total supply is set upon deployment. To do so, we grab a deployed version of the smart contract that we store in a variable called token. Then we call the totalSupply() function given to us for free by the Truffle framework (because the variable totalSupply in our smart contract was declared as a public variable). that function returns the total supply and we store that in a variable called totalSupply. We then convert it into a human-readable form, using the .toNumber() function. Finally, we make use of the Chai assertion library to compare the totalSupply with the expected total supply.
To test our script, run the following command:
ERC20-Token $ truffle test --network ganache
// The output should look like this
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: NzouaToken
✓ Sets the total supply on deployment (41ms)
1 passing (85ms)
Our test should successfully pass.
Let’s modify this lineassert.equal(totalSupply, 1000000, ‘Total supply set correctly to 1,000,000’) to this:
assert.equal(totalSupply, 100, 'Total supply...') // Changed the expected supply to 100
Let’s run our test once again and see what happens:
ERC20-Token $ truffle test --network ganache
// The output should be similar to this:
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: NzouaToken
1) Sets the total supply on deployment
> No events were emitted
0 passing (124ms)
1 failing
1) Contract: NzouaToken
Sets the total supply on deployment:
Total supply set correctly to 1,000,000
+ expected - actual
+ 100 - 1000000
at Context.<anonymous> (test/NzouaToken.test.js:8:16)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
As expected, our test should fail because both the totalSupply (1000000) and the total supply expected (100) don’t match.
Congratulation! We just successfully wrote our first smart contract test. In the following sections, we will expand on our smart contract and add more functions and state variables.
Improving the Constructor() function
As of now, we are still hard-coding the total supply inside our smart contract. A better way of going about this would be to pass it as an argument to the constructor of the smart contract (we will call it _initialSupply) to initialize the total supply when we deploy the contract to the blockchain.
To do so, let’s update our smart contract ./contracts/NzouaToken.sol like so:
// Define the version of Solidity to use for this Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
// Define your Smart Contract with the "Contract" keyword and an empty constructor
contract NzouaToken {
uint256 public totalSupply;
constructor(uint256 _initialSupply) {
totalSupply =_initialSupply;
}
}
To pass the initial supply as an argument when we deploy our contract, let’s update our migration script ./migrations/NzouaToken.js and pass the initial supply as a parameter to the deployer.deploy() function like so:
const NzouaToken = artifacts.require("NzouaToken");
module.exports = async function (deployer, network, accounts) {
await deployer.deploy(NzouaToken, 1000000); // Pass initial supply as argument of the deployer.
const Instance = await NzouaToken.deployed();
// console.log(Instance)
};
If we run our test once again, it should pass
ERC20-Token $ truffle test --network ganache
// The output should be similar to this
Compiling your contracts...
===========================
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--66988-5q05lYtLIb7R
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: NzouaToken
✓ Sets the total supply on deployment (53ms)
1 passing (99ms)
The next thing we would like to do is keep track of account balances inside our smart contract to determine how many tokens are owned by an account.
Keeping Track of Account Balances
Inside the EIP-20 Token Standard located here, we can see that there is a required function as part of the specification called balanceOf(), which takes an address, and returns its balance.
function balanceOf(address _owner) public view returns (uint256 balance)
We will make use of this function inside our smart contract to track accounts balance
To track balances inside our smart contract ./contracts/NzouaToken.sol in an efficient way we will make use of a mapping variable called balanceOf() like so:
[...omitted code...]
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
[...omitted code...]
Whenever we deploy our smart contract, we want to allocate the initial supply of the smart contract to a particular address: the owner. But first, let’s write a test that will ensure that the total supply gets allocated to the correct address.
Inside our ./test/NzouaToken.test.js let’s add the following lines of code:
[...omitted code...]contract('NzouaToken', async (accounts) => {
it('Sets the total supply on deployment', async () => {[...omitted code...]
}),
it('Allocates the total supply to Contract Owner', async () => {
const token = await NzouaToken.deployed();
let ownerBalance = await token.balanceOf(accounts[0]);
ownerBalance = ownerBalance.toNumber();
assert.equal(ownerBalance, 1000000, 'Total supply allocated to account ' + accounts[0]);
})
})[...omitted code...]
The next we would like to do is keep track of account balances inside our smart contract. As seen above, once the contract has been deployed, we can make use of the balanceOf() function to access the balance of a particular account. The Truffle framework gives us access to the accounts that we can then inject inside our test script. We can get the first account inside our ganache local blockchain (account[0]) and we will consider it to be the owner of the contract. In our example, we will use the Chai assertion library once again the compare both values.
if we run the test once again, it will fail because we expect the owner account (accounts[0]) to have 0 tokens as total supply, but our test has our owner account with 1000000 tokens. Another reason is that when we declared our mapping balanceOf, we did not initialize it and by default, mappings in Truffle are initialized to 0.
ERC20-Token $ truffle test --network ganache
// The output should look like this
Compiling your contracts...
===========================
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--68060-9COG6GGUaD2d
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: NzouaToken
✓ Sets the total supply on deployment (83ms)
1) Allocates the total supply to Contract Owner
> No events were emitted
1 passing (183ms)
1 failing
1) Contract: NzouaToken
Allocates the total supply to Contract Owner:
Total supply allocated correctly to account 0x627306090abaB3A6e1400e9345bC60c78a8BEf57
+ expected - actual
+ 1000000 -0
at Context.<anonymous> (test/NzouaToken.test.js:18:16)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
To ensure that our tests pass, we will set the balance of the owner of the smart contract to the initial supply. We will do so inside the constructor, so the balance is assigned upon deployment. Given that the constructor is only called once (When the contract is first deployed), we are sure that the owner of the contract deployed it. To access the account that executed the constructor function, we will use a global variable inside Solidity called msg and access the account with msg.sender.
Inside our ./contracts/NzouaToken.sol, we will add the following line of code:
[...omitted code...]
constructor(uint256 _initialSupply) {
balanceOf[msg.sender] = _initialSupply;
totalSupply = _initialSupply;
}[...omitted code...]
If we run the test once again, they will now pass as expected:
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--68335-zk2tlhCpTmna
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: NzouaToken
✓ Sets the total supply on deployment (91ms)
✓ Allocates the total supply to Contract Owner (52ms)
2 passing (205ms)
Adding more attributes to our ERC20 Token Smart Contract
In the previous section, we learned how to add totalSupply to our smart contract and initialize it on deployment. We also learned to allocate those funds to a particular address (owner of the contract) upon deployment. Now we are going to expand on our smart contract and add more attributes.
ERC20 Token attributes
In this section, we will add the following attributes to our smart contracts, which are part of the ERC20 standard: name, symbol, decimals.
The first thing we will do is write corresponding tests for these attributes.
Inside our ./test/NzouaToken.test.js, let’s add the following code:
[...omitted code...]contract('NzouaToken', async (accounts) => {
it('Initializes the contract with the appropriate attributes', async () => {
const token = await NzouaToken.deployed();
let name = await token.name();
assert.equal(name, "Nzouat Token", 'Nzouat Token has the correct name.');
}),
it('Sets the total supply on deployment', async () => {[...omitted code...]
}),
it('Allocates the total supply to Contract Owner', async () => {[...omitted code...]
})
})[...omitted code...]
When we run the test once again, it will fail because we have not declared our variable name inside our smart contract yet.
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
> Compiling ./contracts/NzouaToken.sol
> Artifacts written to /var/folders/0s/30jwb8rs4mn_p_61st6_78j80000gn/T/test--68651-hu6EYZQ9JbXT
> Compiled successfully using:
- solc: 0.8.9+commit.e5eed63a.Emscripten.clang
Contract: NzouaToken
1) Initializes the contract with the appropriate attributes
> No events were emitted
✓ Sets the total supply on deployment (50ms)
✓ Allocates the total supply to Contract Owner (45ms)
2 passing (177ms)
1 failing
1) Contract: NzouaToken
Initializes the contract with the appropriate attributes:
TypeError: token.name is not a function
at Context.<anonymous> (test/NzouaToken.test.js:6:36)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Inside our ./contracts/NzouaToken.sol file, let’s name our token like so:
[...omitted code...]
string public name = "Nzouat Token";
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(uint256 _initialSupply) {[...omitted code...]
}[...omitted code...]
Now our tests should all pass
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
[...omitted code]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (60ms)
✓ Sets the total supply on deployment (40ms)
✓ Allocates the total supply to Contract Owner (103ms)
3 passing (264ms)
By the same token, we will add a test for another attribute: the symbol. Inside our ./test/NzouaToken.test.js, let’s add the following code:
[...omitted code...]contract('NzouaToken', async (accounts) => {
it('Initializes the contract with the appropriate attributes', async () => {
const token = await NzouaToken.deployed();
let name = await token.name();
let symbol = await token.symbol();
assert.equal(name, "Nzouat Token", 'Nzouat Token has the correct name.'); assert.equal(symbol, "NZT", 'Nzouat Token has the correct symbol.');
}),
it('Sets the total supply on deployment', async () => {[...omitted code...]
}),
it('Allocates the total supply to Contract Owner', async () => {[...omitted code...]
})
})[...omitted code...]
If we run the test at this point, it will still fail for the reason mentioned above: we have not yet declared the state variable symbol inside our smart contract. So let’s do that before we run the test:
[...omitted code...]
string public name = "Nzouat Token"; string public symbol = "NZT";
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(uint256 _initialSupply) {[...omitted code...]
}[...omitted code...]
If we run the test, it will pass
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
[...omitted code]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (60ms)
✓ Sets the total supply on deployment (40ms)
✓ Allocates the total supply to Contract Owner (103ms)
3 passing (264ms)
Now that we have added some basic attributes to our ERC20 Token smart contract, we are going to expand the contract itself and add more functions that are required for an ERC20 token implementation.
One of the most used functions of an ERC20 token is the transfer function, which allows us to move funds between accounts. That is what we will implement next.
Implementing the transfer() inside an ERC20 Token Smart Contract
From a previous section, we talked about the transfer() function, which takes two arguments/parameters (the recipient’s address, and the amount being transferred). The transfer() function will throw an exception if there are not enough funds in the sender’s account or will execute the transfer() otherwise; it will emit a Transfer() Event at the end of the process, and finally, it will return a boolean which will be true if the transfer was successful and false if the transfer was unsuccessful.
Inside our ./test/NzouaToken.test.js, let’s define new tests entries for our transfer() function. We will test a scenario where an account tries to send more tokens than it owns. the transfer will fail and we should expect a “revert” message. Let’s do it.
[...omitted code...]contract('NzouaToken', async (accounts) => {
[...omitted code...]
it('Allocates the total supply to Contract Owner', async () => {[...omitted code...]
}), it('Transfers tokens', async () => {const token = await NzouaToken.deployed();
try {
const transferred = await token.transfer(accounts[1], 1000000000);
assert.equal(transferred, true, 'The account does not have enough funds to make the transfer to account ' + accounts[1] + '.');
} catch (error) {
// catch the transfer error message
assert(error.message.indexOf('revert') >= 0, 'error must contain the term revert');
}
})
})[...omitted code...]
In the example above, we add a try/catch function because we want to catch the error that will be thrown when we execute the transfer function. We expect an error because the transfer() function will try and send1000000000 tokens to accounts[1] and this will revert because the caller of the function(accounts[0]) does not have enough funds on its balance to successfully execute the transfer.
if we run our test it will fail and the output will be the following:
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
[...omitted code]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (122ms)
✓ Sets the total supply on deployment
✓ Allocates the total supply to Contract Owner (39ms)
1) Transfers tokens
> No events were emitted
3 passing (392ms)
1 failing
1) Contract: NzouaToken
Transfers tokens:
AssertionError: error must contain the term revert
at Context.<anonymous> (test/NzouaToken.test.js:35:17)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
To make our test pass, let’s go back to ./contracts/NzouaToken.sol, declare our transfer() function and add a require() statement inside that function. In Solidity, the require() statement works a little bit like an if…else statement in JavaScript, but has its specifications.
The require() statement will ensure that the account calling the transfer() function has more funds than the amount it is transferring. If this condition is not met, the transfer() function not continue execution and will revert as we saw in the error message above.
// ./contracts/NzouaToken.sol
[...omitted code...]
[...omitted code...]
constructor(uint256 _initialSupply) {[...omitted code...]
}
[...omitted code...]
function transfer(address _to, uint256 _value) public returns(bool success){
require(balanceOf[msg.sender] >= _value, 'The account has low funds');
}
[...omitted code...][...omitted code...]
Our test should now pass
ERC20-Token $ truffle test --network ganache
Compiling your contracts...
===========================
[...omitted code]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (163ms)
✓ Sets the total supply on deployment (38ms)
✓ Allocates the total supply to Contract Owner (83ms)
✓ Transfers tokens (2150ms)
4 passing (3s)
Now, let’s test our transfer() function inside the Truffle console:
ERC20-Token $ truffle console
// See the list of our 10 accounts, which is an array
truffle(ganache)> accounts
[
'0x5D16d433aFDB957ceF88231da5CDcf12b083E094', // accounts[0]
'0x838235F38b782Ce6d40e04469767842D91DfA162', // accounts[1]
'0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa',
'0x8429659DEcc8BEB05d77e37c1ff8fE222973a276',
'0xFCb19b549c4628d77313a2Bec6a487B1BA10cD4e',
'0x3BD3EB016b0EA678DF71a0e3a5cA305d37113993',
'0x833D818A07d83509662f353DC15484d08DDa2e09',
'0xF1691d27c4f81750c931AF4a3d6e3746d013FdF8',
'0xfEE09E4e9a94D33ffB3db84A978c5215B2E7F5A3',
'0xb572100594839A716c2219597bBbc76f41A69c19'
]truffle(ganache)>
Inside the console, let’s run the following code
truffle(ganache)> const token = await NzouaToken.deployed();
undefinedtruffle(ganache)> totalSupply = await token.totalSupply()
undefined
truffle(ganache)> totalSupply.toNumber()
👉 1000000
truffle(ganache)> balance0 = await token.balanceOf(accounts[0]);
undefined
truffle(ganache)> balance0.toNumber();
👉 1000000
truffle(ganache)> balance1 = await token.balanceOf(accounts[1]);
undefined
truffle(ganache)> balance1.toNumber();
👉 0
truffle(ganache)>
As we can see from the console, after deployment to ganache, the total supply of NZT is 1,000,000. The balance of accounts[0] (The owner of the smart contract) is 1,000,000 and the balance of accounts[1] is 0.
Now, let’s try to send more funds that accounts[0] currently has to accounts[1], and analyze the output on the console:
truffle(ganache)> await token.transfer(accounts[1], 1000000000);
// Output on the console
Uncaught:
Error: Returned error: VM Exception while processing transaction: revert The account has low funds -- Reason given: The account has low funds.
[...omitted code...]
{
data: {
'0x9162446ed6efd6bfdcfcecc2d1250105c441b20c4e291e624eb02401fa814606': {
👉 error: 'revert',
program_counter: 724,
return: '[...omitted code...]',
👉 reason: 'The account has low funds'
},
stack: 'RuntimeError: VM Exception while processing transaction: revert The account has low funds\n' +
' [...omitted code...]',
name: 'RuntimeError'
},
👉 reason: 'The account has low funds',
hijackedStack: '[...omitted code...]'
}
As we can see, the transfer did not go through and was interrupted by
require(balanceOf[msg.sender] >= _value, ‘The account has low funds’);
So this means that our require() statement can successfully capture errors and revert transactions.
Now let’s try and send legit transactions that have the chance to get executed. We will send 250000 NZT from accounts[0] to accounts[1]. This transaction should not execute because we have not written our debit and credit logic inside our smart contract yet, and our balances should not be updated accordingly
truffle(ganache)> const receipt = await token.transfer(accounts[1], 250000, { from: accounts[0] })
undefined
truffle(ganache)> receipt
{
tx: '0x6421ed8c608709153709e08a10a2fb8d6f5913002c526444867183166ab3a306',
receipt: {
transactionHash: '0x6421ed8c608709153709e08a10a2fb8d6f5913002c526444867183166ab3a306',
transactionIndex: 0,
blockHash: '0xf0071034f62d7cd875e97e51a9e0715fd434e57ecfb8ec3a8be8df3f532e0fea',
blockNumber: 140,
👉 from: '0x5d16d433afdb957cef88231da5cdcf12b083e094',👉 to: '0x664f2f63bc3fae3b02c641ee99b577a1bf449109',
gasUsed: 50614,
cumulativeGasUsed: 50614,
contractAddress: null,
logs: [],
status: true,
logsBloom: '[...omitted code...]',
rawLogs: []
},
logs: []
}
truffle(ganache)> balance0 = await token.balanceOf(accounts[0])
undefined
truffle(ganache)> balance1 = await token.balanceOf(accounts[1])
undefined
truffle(ganache)> balance0.toNumber()
👉 1000000
truffle(ganache)> balance1.toNumber()
👉 0
truffle(ganache)>
As we can see, the transaction was not successful, but a transaction receipt was generated anyway.
Now let’s go back to ./contracts/NzouaToken.sol and make sure that we debit the sender’s account and we credit the recipient’s account:
// ./contracts/NzouaToken.sol
[...omitted code...]
[...omitted code...]
constructor(uint256 _initialSupply) {[...omitted code...]
}
[...omitted code...]
function transfer(address _to, uint256 _value) public returns(bool success){
// Throw an exception if sender has low funds
require(balanceOf[msg.sender] >= _value, 'The account has low funds');
// Implement the Debit & Credit feature
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
// Emit a transfer Event
// Return a the success Boolean value
} [...omitted code...][...omitted code...]
Now, let’s re-migrate our smart contract to know about the changes we have made
ERC20-Token $ truffle migrate --network ganache --reset
// output on the console
Compiling your contracts...
===========================
[...omitted code...]
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)[...omitted code...]
Summary
=======
> Total deployments: 2
> Final cost: 0.01448938 ETH
After the contract is successfully deployed, let’s go back to the console and test our legit transfer() function once again:
ERC20-Token $ truffle console
truffle(ganache)> let token = await NzouaToken.deployed();
truffle(ganache)> const receipt = await token.transfer(accounts[1], 250000, { from: accounts[0] })
undefined
truffle(ganache)> receipt
{
tx: '0x6421ed8c608709153709e08a10a2fb8d6f5913002c526444867183166ab3a306',
receipt: {
transactionHash: '0x6421ed8c608709153709e08a10a2fb8d6f5913002c526444867183166ab3a306',
transactionIndex: 0,
blockHash: '0xf0071034f62d7cd875e97e51a9e0715fd434e57ecfb8ec3a8be8df3f532e0fea',
blockNumber: 140,
👉 from: '0x5d16d433afdb957cef88231da5cdcf12b083e094',👉 to: '0x664f2f63bc3fae3b02c641ee99b577a1bf449109',
gasUsed: 50614,
cumulativeGasUsed: 50614,
contractAddress: null,
logs: [],
status: true,
logsBloom: '[...omitted code...]',
rawLogs: []
},
logs: []
}
truffle(ganache)> balance0 = await token.balanceOf(accounts[0])
undefined
truffle(ganache)> balance1 = await token.balanceOf(accounts[1])
undefined
truffle(ganache)> balance0.toNumber()
👉 750000
truffle(ganache)> balance1.toNumber()
👉 250000
truffle(ganache)>
As we can see, our transfer was successful. accounts[0] was debited 250000 NZT (1000000NZT – 250000NZT), while accounts[1] was credited 250000 NZT (0 NZT + 250000 NZT)
So far, the “Transfers tokens” test inside ./test/NzouaToken.test.js should look like this
[...omitted code...]contract('NzouaToken', async (accounts) => {
[...omitted code...]
it('Allocates the total supply to Contract Owner', async () => {[...omitted code...]
}), it('Transfers tokens', async () => {
const token = await NzouaToken.deployed();
try {
const transferred = await token.transfer(accounts[1], 1000000000);
assert.equal(transferred, false, 'The account does not have enough funds to make the transfer to account ' + accounts[1] + '.');
// Transfer from accounts[0] to accounts[1]
const receipt = await token.transfer(accounts[1], 250000, {
from: accounts[0]
});
// grabbing each balances
const balanceAccount0 = await token.balanceOf(accounts[0])
const balanceAccount1 = await token.balanceOf(accounts[1])
// Checking that balances were updated
assert.equal(balanceAccount0.toNumber(), 750000, 'debits account ' + accounts[0] + ' the amount of ' + balanceAccount0.toNumber() + '.')
assert.equal(balanceAccount1.toNumber(), 250000, 'credits account ' + accounts[1] + ' the amount of ' + balanceAccount1.toNumber() + '.')
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'error must contain the term revert');
}
})
})[...omitted code...]
Now that we can successfully transfer funds from one account to the other, we are going to emit/trigger a Transfer() Event which will be emitted when the amount of tokens/funds (value) is sent from accounts[0]’s address to accounts[1]’s address. this is required for any ERC20 token implementation to work properly.
Implementing the Transfer() Event inside an ERC20 Token Smart Contract
Declaring the Transfer() Event
As stated above, we are going to emit a Transfer() event when funds are sent from accounts[0] to accounts[1]. Inside out ./contracts/NzouaToken.sol, let’s declare a new event like so:
// ./contracts/NzouaToken.sol
[...omitted code...]
string public name = "Nzouat Token"; string public symbol = "NZT";
uint256 public totalSupply;
// Transfer() event declaration
event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
);
mapping(address => uint256) public balanceOf;
constructor(uint256 _initialSupply) {[...omitted code...]
}[...omitted code...]
Writing tests for the Transfer() Event
To write tests for our Transfer() Event, let’s go back to ./test/NzouaToken.test.js and modify the “Transfers tokens” test entry like so:
[...omitted code...]contract('NzouaToken', async (accounts) => {
[...omitted code...]
it('Allocates the total supply to Contract Owner', async () => {[...omitted code...]
}), it('Transfers tokens', async () => {
const token = await NzouaToken.deployed();
try {
[...omitted code...]
// Transfer from accounts[0] to accounts[1]
const receipt = await token.transfer(accounts[1], 250000, {
from: accounts[0]
});
// Verify transaction logs
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Transfer', 'should be the "Transfer()" event');
assert.equal(receipt.logs[0].args._from, accounts[0], 'logs the account the tokens are transferred from');
assert.equal(receipt.logs[0].args._to, accounts[1], 'logs the account the tokens are transferred to');
assert.equal(receipt.logs[0].args._value.toNumber(), 250000, 'logs the transfer amount');
[..omitted code...]
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'error must contain the term revert');
}
})
})[...omitted code...]
Here, we verify that a transaction receipt was issued and that certain fields are present, such as the logs array. Inside the logs array, we assert that
- There is at least one event that was triggered.
- The name of the event is Transfer
- The sender (_from) inside the args field is accounts[0]
- The recipient (_to) inside the args field is accounts[1]
- The amount (_value) sent inside the args field is 250000
And Inside our ./contracts/NzouaToken.sol, let’s update our transfer() function to emit the Transfer() event, like so:
[...omitted code...]
// Implement the Debit & Credit feature
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
// Emit a transfer Event
emit Transfer(msg.sender, _to, _value);
[...omitted code...]
After making all these changes, let’s re-migrate our files to the blockchain. Everything should still compile and migrate as expected
ERC20-Token $ truffle migrate --network ganache --reset
// output on the console
Compiling your contracts...
===========================
[...omitted code...]
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)[...omitted code...]
Summary
=======
> Total deployments: 2
> Final cost: 0.01648124 ETH
If we run truffle test it will pass as expected.
Now let’s open the Truffle console and test our transfer() functions to see what happens:
Testing the Transfer() Event
ERC20-Token $ truffle console
truffle(ganache)> receipt = await token.transfer(accounts[1], 250000)
undefined
truffle(ganache)> receipt
{
tx: '0x229d2428049021429648ce5dcd276f48f6cb35b6b6eaaa66bb48513508b04f7a',
receipt: {
transactionHash: '0x229d2428049021429648ce5dcd276f48f6cb35b6b6eaaa66bb48513508b04f7a',
transactionIndex: 0,
blockHash: '0x9fde08d04fb8a3173cc06c839c726f966724af6f543eea3cde3291c14975253c',
blockNumber: 261,
from: '0x5d16d433afdb957cef88231da5cdcf12b083e094',
to: '0x1e94088fedf476e647d26d858cbf356fe086679a',
gasUsed: 52574,
cumulativeGasUsed: 52574,
contractAddress: null,
logs: [ [Object] ],
status: true,
logsBloom: '[..omitted code...]',
rawLogs: [ [Object] ]
},
logs: [
{
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x229d2428049021429648ce5dcd276f48f6cb35b6b6eaaa66bb48513508b04f7a',
blockHash: '0x9fde08d04fb8a3173cc06c839c726f966724af6f543eea3cde3291c14975253c',
blockNumber: 261,
address: '0x1E94088fedF476E647D26d858Cbf356Fe086679A',
type: 'mined',
id: 'log_cda3069a',
event: 'Transfer',
args: [Result]
}
]
}
truffle(ganache)>
If we analyze this receipt, compared to previous receipts we printed, we will notice that the logs[] field has more entries. Inside the logs field, we have a couple of fields:
- A transactionHash
- An address
- An event of type Transfer
- An args[] array. Let’s explore that in details
truffle(ganache)> receipt.logs[0].args
// Output on the console
Result {
'0': '0x5D16d433aFDB957ceF88231da5CDcf12b083E094',
'1': '0x838235F38b782Ce6d40e04469767842D91DfA162',
'2': BN {
negative: 0,
words: [ 250000, <1 empty item> ],
length: 1,
red: null
},
__length__: 3,
_from: '0x5D16d433aFDB957ceF88231da5CDcf12b083E094',
_to: '0x838235F38b782Ce6d40e04469767842D91DfA162',
_value: BN {
negative: 0,
words: [ 250000, <1 empty item> ],
length: 1,
red: null
}
}
Returning a Boolean value after the transfer()
Every single ERC20 token implementation is required to return a boolean value after the transfer() is complete. So inside our ./contracts/NzouaToken.sol, let’s update our contract like so:
[...omitted code...]
function transfer(address _to, uint256 _value) public returns(bool success){
[...omitted code...]
// Emit a transfer Event
emit Transfer(msg.sender, _to, _value);
// Return a Boolean
return true;
}
[...omitted code...]
So far, we have been able to transfer from one account (a sender) to another (a receiver), in which the sender is the one executing the transfer. In certain cases, we might want to delegate this task to another account (a spender) and allow them to spend funds on the sender’s behalf. This is referred to as delegated transfer. A delegated transfer will be composed of three functions: a transferFrom() function, a approve() function and an allowance() function.
- approve(): will allow a sender to approve a delegate account (spender) to withdraw tokens from his account and to transfer them to other accounts.
- allowance(): will returns the current approved number of tokens by an owner to a specific delegate, as set in the approve() function.
- transferFrom(): will allow a delegate approved for withdrawal to transfer owner funds to a third-party account.
And that is what we will do next.
Implementing the approve(), allowance(), and transferFrom() functions and a Approval() Event inside an ERC20 Token Smart Contract
Declaring the approve() function
Inside our ./contracts/NzouaToken.sol, let’s declare our function like so:
[...omitted code...]
function transfer(address _to, uint256 _value) public returns(bool success){
[...omitted code...]
} function approve(address _spender, uint256 _value) public returns(bool success){
}
[...omitted code...]
Writing the test script for approve() function
Inside our ./test/NzouaToken.test.js, let’s write a new test entry like so:
// ./test/NzouaToken.test.js
[...omitted code...]
it('Transfers tokens', async () => {[...omitted code...]
}),
it('Approves tokens for delegated tranfers', async () => {
const token = await NzouaToken.deployed();
let approved = await token.approve.call(accounts[1], 100);
assert.equal(approved, true, 'it returns true');
})
[...omitted code...]
If we run the test it will fail like so:
ERC20-Token $ truffle test --network ganache
[...omitted code...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (110ms)
✓ Sets the total supply on deployment (62ms)
✓ Allocates the total supply to Contract Owner (50ms)
✓ Transfers tokens (1629ms)
1) approves tokens for delegated tranfers
> No events were emitted
4 passing (2s)
1 failing
1) Contract: NzouaToken
approves tokens for delegated tranfers:
it returns true
+ expected - actual
+ true - false
at Context.<anonymous> (test/NzouaToken.test.js:64:16)
at processTicksAndRejections (node:internal/process/task_queues:96:5)[...omitted code...]
The function is supposed to return true, but failed to. So let’s fix that inside ./contracts/NzouaToken.sol
function approve(address _spender, uint256 _value) public returns(bool success) {
return true;
}
Our test will now pass
ERC20-Token $ truffle test --network ganache
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (169ms)
✓ Sets the total supply on deployment (38ms)
✓ Allocates the total supply to Contract Owner (41ms)
✓ Transfers tokens (2361ms)
✓ Approves tokens for delegated tranfers
5 passing (3s)
Let’s now expand the approve() function by declaring an event Approval() that we will use inside the approve() function
Implementing the Approval() Event inside an ERC20 Token Smart Contract
Inside ./contracts/NzouaToken.sol, let’s declare the event Approval() like so:
// ./contracts/NzouaToken.sol
[...omitted code...]
string public name = "Nzouat Token"; string public symbol = "NZT";
uint256 public totalSupply;
// Transfer() event declaration
event Transfer(
address indexed _from,
address indexed _to,
uint256 _value
); // Approval() event declaration
eventApproval(
address indexed _owner,
address indexed _spender,
uint256 _value
);
mapping(address => uint256) public balanceOf;
constructor(uint256 _initialSupply) {[...omitted code...]
}[...omitted code...]
Now, let’s write our test script for the Approval() event inside ./test/NzouaToken.test.js
// ./test/NzouaToken.test.js
[...omitted code...]
it('Transfers tokens', async () => {[...omitted code...]
}),
it('Approves tokens for delegated tranfers', async () => {
const token = await NzouaToken.deployed();
let approved = await token.approve.call(accounts[1], 100);
assert.equal(approved, true, 'it returns true');
// Test the Approval() Event
let receipt = await token.approve(accounts[1], 100);
// Verify transaction logs
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Approval', 'should be the "Approval()" event');
assert.equal(receipt.logs[0].args._owner, accounts[0], 'logs the account the tokens/funds are authorized by');
assert.equal(receipt.logs[0].args._spender, accounts[1], 'logs the account the tokens/funds are authorized to');
assert.equal(receipt.logs[0].args._value.toNumber(), 100, 'logs the transfer amount');
})
[...omitted code...]
If we run our test script, it will fail, because we have not triggered the Approval() event inside our approve() function, which expects one.
ERC20-Token $ truffle test --network ganache
// Output of the console
[...omitted code...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (133ms)
✓ Sets the total supply on deployment (60ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1504ms)
1) Approves tokens for delegated tranfers
> No events were emitted
4 passing (2s)
1 failing
1) Contract: NzouaToken
Approves tokens for delegated tranfers:
triggers one event
+ expected - actual
+ 1 - 0
at Context.<anonymous> (test/NzouaToken.test.js:72:20)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
[...omitted code...]
Let’s go back inside ./contracts/NzouaToken.sol and trigger the Approval() event.
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
Our tests will now pass successfully.
ERC20-Token $ truffle test --network ganache
// Output of the console
[...omitted code...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (153ms)
✓ Sets the total supply on deployment (80ms)
✓ Allocates the total supply to Contract Owner (50ms)
✓ Transfers tokens (1689ms)
✓ Approves tokens for delegated tranfers (185ms)
5 passing (2s)
[...omitted code...]
Implementing the allowance() function inside an ERC20 Token Smart Contract
The first thing we need to do is to declare a new mapping() that will track allowance for each spender. So inside ./contracts/NzouaToken.sol, let’s declare our allowance mapping like so:
// ./contracts/NzouaToken.sol
[...omitted code...]
mapping(address => uint256) public balanceOf;
// Mapping allowances mapping(address => mapping(address => uint256)) public allowance;
constructor(uint256 _initialSupply) {[...omitted code...]
}[...omitted code...]
If that mapping is a little confusing, let’s take a look at this JavaScript representation of the same mapping:
const allowance = {
"0x023a...": {
"0x014d...": 200, // 200 NZT
"0x056d...": 1000,
},
"0x0903...": {
"0x0fg3...": 13000,
"0x0h1e...": 25,
},
...
}
We could explain this mapping the following way:
- account 0x023a… approves account 0x014d… to spend 200 NZT and account 0x56d… to spend 1000 NZT on its behalf.
- account 0x0903… approves account 0x0fg3… to spend 200 NZT and account 0x0h1e… to spend 1000 NZT on its behalf.
Now, let’s write our test script for the allowance() function inside ./test/NzouaToken.test.js
[...omitted code...]
it('Approves tokens for delegated tranfers', async () => {[...omitted code...]
// Verify transaction logs
assert.equal(receipt.logs.length, 1, 'triggers one event');
assert.equal(receipt.logs[0].event, 'Approval', 'should be the "Approval()" event');
assert.equal(receipt.logs[0].args._owner, accounts[0], 'logs the account the tokens/funds are authorized by');
assert.equal(receipt.logs[0].args._spender, accounts[1], 'logs the account the tokens/funds are authorized to');
assert.equal(receipt.logs[0].args._value.toNumber(), 100, 'logs the transfer amount');
// Test the allowance() function
let allowance = await token.allowance(accounts[0], accounts[1]);
// Verify transaction logs
assert.equal(allowance.toNumber(), 100, 'stores the allowance for delegated transfer');
})
[...omitted code...]
If we run our test, it will fail, because we have not set the allowance inside our approve() function yet. But one thing we should notice is a new entry inside the console for emitted events. In our example, we can see that the Approval() event was fired successfully.
ERC20-Token $ truffle test --network ganache
// Output of the console
[...omitted code...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (252ms)
✓ Sets the total supply on deployment (73ms)
✓ Allocates the total supply to Contract Owner (42ms)
✓ Transfers tokens (1135ms)
1) Approves tokens for delegated tranfers
Events emitted during test:
---------------------------
NzouaToken.Approval(
_owner: <indexed> 0x5D16d433aFDB957ceF88231da5CDcf12b083E094 (type: address),
_spender: <indexed> 0x838235F38b782Ce6d40e04469767842D91DfA162 (type: address),
_value: 100 (type: uint256)
)
---------------------------
4 passing (2s)
1 failing
1) Contract: NzouaToken
Approves tokens for delegated tranfers:
stores the allowance for delegated transfer
+ expected - actual
+ 100 - 0
at Context.<anonymous> (test/NzouaToken.test.js:85:20)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
[...omitted code...]
To ensure that our tests pass, let’s implement the allowance functionality inside the approve() function. Go inside ./contracts/NzouaToken.sol and update the approve() function that will make use of allowance() function and set spending amounts:
function approve(address _spender, uint256 _value) public returns(bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
If we run our tests once again, they will now pass as expected:
ERC20-Token $ truffle test --network ganache
// Output of the console
[...omitted code...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (161ms)
✓ Sets the total supply on deployment (43ms)
✓ Allocates the total supply to Contract Owner (47ms)
✓ Transfers tokens (1435ms)
✓ Approves tokens for delegated tranfers (229ms)
5 passing (2s)
[...omitted code...]
Now that we have written our approve() function, which under the hood makes use of the allowance() function and the Approval() event, we are now going to implement the transferFrom() function which needs the approve() function.
Implementing the transferFrom() function inside an ERC20 Token Smart Contract
The first thing we will do is declare the function inside our smart contract. So go to ./contracts/NzouaToken.sol and add the following function declaration
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT // require allowance has enough NZT funds // update balances // update allowance // emit Transfer event // return a boolean
}
[... omitted code...]
Next, let’s implement our test script inside ./test/NzouaToken.test.js
[...omitted code...]
it('Approves tokens for delegated tranfers', async () => {[...omitted code...]
}),it('Handles delegated NZT tranfers', async () => {
})
[...omitted code...]
Inside this test script, we will do a couple of things:
- We will set the account variables we will need for our tests: fromAccount (Owner of the spending account), toAccount (the Recipient of the transfer), spendingAccount (the deledate Spender of the funds from the fromAccount), and adminAccount (the developer, who will fund the fromAccount for future spendings)
- Next, we will fund the fromAccount with tokens from the adminAccount.
- Then we will approve spendingAccount to spend up to X NZT from fromAccount
- We will ensure that the allowance of spendingAccount that was approved by fromAccount is the the right amount.
- Finally, we will ensure that spendingAccount cannot spend over its allowance.
Let’s get it!
Let’s do it gradually. The first thing we will do is set up account variables. Inside ./test/NzouaToken.test.js add the following code:
// ./test/NzouaToken.test.js
[... omitted code ...]it('Approves tokens for delegated tranfers', async () => {[...omitted code...]
}),
it('Handles delegated NZT tranfers', async () => {
// Get a deployed instance of the token
const token = await NzouaToken.deployed();
// Set account variables adminAccount = accounts[0]; // The Admin
fromAccount = accounts[2]; // The Owner of the account
toAccount = accounts[3]; // The Recipient of the funds
spendingAccount = accounts[4]; // The delegate Spender of the funds
});
[... omitted code ...]
Next, we will fund the fromAccount with tokens from the adminAccount, so fromAccount can delegate funds to spendingAccount. we will make use of the transfer() function to make this happen.
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
// Get a deployed instance of the token
const token = await NzouaToken.deployed();
// Set account variables adminAccount = accounts[0]; // The Admin
fromAccount = accounts[2]; // The Owner of the account
toAccount = accounts[3]; // The Recipient of the funds
spendingAccount = accounts[4]; // The delegate Spender of the funds
// Transfer 100 NZT tokens to fromAccount so it can spend
transferReceipt = await token.transfer(fromAccount, 100, {
from: adminAccount
});
assert.equal(transferReceipt.logs[0].args._value.toNumber(), 100, 'transfers 100 NZT to fromAccount')
});
[... omitted code ...]
The next thing we will add is the ability for spendingAccount to spend up to 10 NZT at a time from fromAccount. To make this happen, we will use the approve() function. Let’s do that like so:
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Transfer 100 NZT tokens to fromAccount so it can spend
transferReceipt = await token.transfer(fromAccount, 100, {
from: adminAccount
});
assert.equal(transferReceipt.logs[0].args._value.toNumber(), 100, 'transfers 100 NZT to fromAccount')
// Approve spendingAccount to spend 10 NZT tokens on behalf of fromAccount
approvalReceipt = await token.approve(spendingAccount, 10, {
from: fromAccount
})
assert.equal(approvalReceipt.logs[0].args._value.toNumber(), 10, '10 NZT Tokens were approved by fromAccount for spendingAccount to spend')
});
[... omitted code ...]
The next thing we will do is ensure that spendingAccount cannot spend more than the balance inside fromAccount. We will use the transferFrom() function and try to send more NZT tokens than fromAccount can spend, using spendingAccount to execute the transfer:
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Approve spendingAccount to spend 10 NZT tokens on behalf of fromAccount
approvalReceipt = await token.approve(spendingAccount, 10, {
from: fromAccount
})
assert.equal(approvalReceipt.logs[0].args._value.toNumber(), 10, '10 NZT Tokens were approved by fromAccount for spendingAccount to spend')
// Ensure that spender cannot spend more than the current balance inside fromAccount and catch the error if anything
try {
failReceipt = await token.transferFrom.call(fromAccount, toAccount, 9999, {
from: spendingAccount
})
assert.equal(failReceipt, true, 'Spender CAN spend more than their balance.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot transfer funds larger than balance');
}
});
[... omitted code ...]
inside this try {} catch (error) {}, we execute the transferFrom() function that should fail and the error will be caught inside the catch(){} statement. We use transferFrom.call() because we do not want the full receipt of the transaction back. We simply want to know if the transaction was successful. In this instance, failReceipt should expect true or false.
Looking closely at this assertion statement:
// ./test/NzouaToken.test.js
[... omitted code ...]
assert.equal(failReceipt, true, 'Spender CAN spend more than their balance.');
[... omitted code ...]
The goal of this assertion is to affirm that failReceipt was not a failed transaction (because of: failReceipt == true). But this will make the test fail because failReceipt will return false:
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (86ms)
✓ Sets the total supply on deployment (200ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1646ms)
✓ Approves tokens for delegated tranfers (358ms)
1) Handles delegated NZT tranfers
Events emitted during test:
---------------------------
NzouaToken.Transfer(
_from: <indexed> 0x5D16d433aFDB957ceF88231da5CDcf12b083E094 (type: address),
_to: <indexed> 0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa (type: address),
_value: 100 (type: uint256)
)
NzouaToken.Approval(
_owner: <indexed> 0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa (type: address),
_spender: <indexed> 0xFCb19b549c4628d77313a2Bec6a487B1BA10cD4e (type: address),
_value: 10 (type: uint256)
)
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
AssertionError: Cannot transfer funds larger than balance
at Context.<anonymous> (test/NzouaToken.test.js:118:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
This is exactly what we expect to happen when we call the transferFrom() function without enough NZT tokens inside the fromAccount. To make our tests pass, let’s go inside ./contracts/NzouaToken.sol and require the fromAccount balance to have more funds than what they are sending.
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]);
// require allowance to have enough NZT funds // update balances // update allowance // emit Transfer event // return a boolean
}
[... omitted code...]
Our test will now pass successfully.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (163ms)
✓ Sets the total supply on deployment (160ms)
✓ Allocates the total supply to Contract Owner (63ms)
✓ Transfers tokens (2563ms)
✓ Approves tokens for delegated tranfers (306ms)
✓ Handles delegated NZT tranfers (868ms)
6 passing (4s)
The next thing we will ensure is that the spender cannot spend more than the approved amount.
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Ensure that spender cannot spend more than the current balance inside fromAccount and catch the error if anything
try {
failReceipt = await token.transferFrom.call(fromAccount, toAccount, 9999, {
from: spendingAccount
})
assert.equal(failReceipt, true, 'Spender CAN spend more than their balance.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot transfer funds larger than balance');
}
// Ensure that spender cannot spend more than the allowance amount
try {
failReceipt2 = await token.transferFrom.call(fromAccount, toAccount, 20, {
from: spendingAccount
});
assert.equal(failReceipt2, true, 'Spender CAN spend more than the approved amount.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot spend funds larger than approved amount');
}
});
[... omitted code ...]
If we run the test, it will fail with the following output:
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (86ms)
✓ Sets the total supply on deployment (200ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1646ms)
✓ Approves tokens for delegated tranfers (358ms)
1) Handles delegated NZT tranfers
Events emitted during test:
--------------------------- [... omitted output ...]
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
AssertionError: Cannot spend funds larger than approved amount
[... omitted output ...]
To make our test pass, let’s expand our transferFrom() function and make sure that the allowance of spendingAccount has enough NZT funds to spend. Go to ./contracts/NzouaToken.sol and add the following inside transferFrom() function:
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]);
// require allowance to have enough NZT funds
require(_value <= allowance[_from][msg.sender]);
// update balances // update allowance // emit Transfer event // return a boolean
}
[... omitted code...]
Our test will now pass successfully.
✓ Handles delegated NZT tranfers (868ms)
In the next step, we will ensure that the spender can spend the right amount from the allowance.
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Ensure that spender cannot spend more than the allowance amount
try {
failReceipt2 = await token.transferFrom.call(fromAccount, toAccount, 20, {
from: spendingAccount
});
assert.equal(failReceipt2, true, 'Spender CAN spend more than the approved amount.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot spend funds larger than approved amount');
}
// Ensure that spender can spend the right allowance amount
try {
successResult = await token.transferFrom.call(fromAccount, toAccount, 10, {
from: spendingAccount
});
assert.equal(successResult, true, 'Spender CAN spend up to the approved amount.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot spend funds larger than approved amount');
}
});
[... omitted code ...]
Our test will fail with the following output:
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (86ms)
✓ Sets the total supply on deployment (200ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1646ms)
✓ Approves tokens for delegated tranfers (358ms)
1) Handles delegated NZT tranfers
Events emitted during test:
--------------------------- [... omitted output ...]
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
AssertionError: Cannot spend funds larger than approved amount
[... omitted output ...]
This is the case of a test that should pass because we are sending the right amount from, to, and by the right accounts. It is failing because the transferFrom() function should return a boolean value of true for valid transactions.
So let’s go back to ./contracts/NzouaToken.sol and update the transferFrom() function to return true;
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]);
// require allowance to have enough NZT funds
require(_value <= allowance[_from][msg.sender]);
// update balances // update allowance // emit Transfer event // return a boolean
return true;
}
[... omitted code...]
Our test will now pass successfully
✓ Handles delegated NZT tranfers (868ms)
6 passing (4s)
Next, we will ensure that a transaction receipt is issued for successful transfers, and we will analyze the logs.
So Inside ./test/NzouaToken.test.js let’s add the following code:
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Ensure that spender can spend the right allowance amount
try {
successResult = await token.transferFrom.call(fromAccount, toAccount, 10, {
from: spendingAccount
});
assert.equal(successResult, true, 'Spender CAN spend up to the approved amount.');
} catch (error) {
assert(error.message.indexOf('revert') >= 0, 'Cannot spend funds larger than approved amount');
}
// Inspect the transaction logs of a valid transaction
logsReceipt = await token.transferFrom(fromAccount, toAccount, 10, {
from: spendingAccount
});
// Verify transaction logs
assert.equal(logsReceipt.logs.length, 1, 'triggers one event');
assert.equal(logsReceipt.logs[0].event, 'Transfer', 'should be the "Transfer()" event');
assert.equal(logsReceipt.logs[0].args._from, fromAccount, 'logs the account the tokens are transferred from');
assert.equal(logsReceipt.logs[0].args._to, toAccount, 'logs the account the tokens are transferred to');
assert.equal(logsReceipt.logs[0].args._value.toNumber(), 10, 'logs the transfer amount');
});
[... omitted code ...]
Notice that in this example, we are using transferFrom() and not transferFrom.call() because we would like to inspect the returned value (transaction receipt).
The test will fail because our first assertion is expecting at least one event inside the logs[] array. But zero event is found, which means that logsReceipt.logs.length == 0 != 1.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (86ms)
✓ Sets the total supply on deployment (200ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1646ms)
✓ Approves tokens for delegated tranfers (358ms)
1) Handles delegated NZT tranfers
Events emitted during test:
--------------------------- [... omitted output ...]
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
triggers one event
+ expected - actual
+ 1 - 0
[... omitted output ...]
To make our test pass, we are going to emit/trigger a Transfer() event inside the transferFrom() function. Open ./contracts/NzouaToken.sol and add the following code:
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]); // require allowance to have enough NZT funds
require(_value <= allowance[_from][msg.sender]);
// update balances // update allowance // emit a Transfer event
emit Transfer(_from, _to, _value);
// return a boolean
return true;
}
[... omitted code...]
Our test will now pass as expected.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (91ms)
✓ Sets the total supply on deployment (160ms)
✓ Allocates the total supply to Contract Owner (72ms)
✓ Transfers tokens (1974ms)
✓ Approves tokens for delegated tranfers (316ms)
✓ Handles delegated NZT tranfers (942ms)
6 passing (4s)
The next thing we will do is update the balance of the accounts that are involved in the transfer.
First, we will write a test. Inside ./test/NzouaToken.test.js add the following code:
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Inspect the transaction logs of a valid transaction
logsReceipt = await token.transferFrom(fromAccount, toAccount, 10, {
from: spendingAccount
});
// Verify transaction logs
assert.equal(logsReceipt.logs.length, 1, 'triggers one event');
assert.equal(logsReceipt.logs[0].event, 'Transfer', 'should be the "Transfer()" event');
assert.equal(logsReceipt.logs[0].args._from, fromAccount, 'logs the account the tokens are transferred from');
assert.equal(logsReceipt.logs[0].args._to, toAccount, 'logs the account the tokens are transferred to');
assert.equal(logsReceipt.logs[0].args._value.toNumber(), 10, 'logs the transfer amount');
// Verify Account Balances are updated accordingly
fromBalance = await token.balanceOf(fromAccount);
toBalance = await token.balanceOf(toAccount);
assert.equal(fromBalance.toNumber(), 90, 'fromAccount was debited 10 NZT Tokens and its new balance is 90')
assert.equal(toBalance.toNumber(), 10, 'toAccount was credited with 10 NZT Tokens and its new balance is 10')
});
[... omitted code ...]
Because balances were not updated inside the transferFrom() function, the test will fail. The transfer did not happen.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (86ms)
✓ Sets the total supply on deployment (200ms)
✓ Allocates the total supply to Contract Owner (67ms)
✓ Transfers tokens (1646ms)
✓ Approves tokens for delegated tranfers (358ms)
1) Handles delegated NZT tranfers
Events emitted during test:
--------------------------- [... omitted output ...]
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
fromAccount was debited 10 NZT Tokens and its new balance is 90
+ expected - actual
+ 90 - 100
[... omitted output ...]
To fix this, let’s go inside ./contracts/NzouaToken.sol and update our transferFrom() function like so:
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]); // require allowance to have enough NZT funds
require(_value <= allowance[_from][msg.sender]);
// update balances
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
// update allowance // emit a Transfer event
emit Transfer(_from, _to, _value); // return a boolean
return true;
}
[... omitted code...]
Our test will now pass as expected.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (148ms)
✓ Sets the total supply on deployment (44ms)
✓ Allocates the total supply to Contract Owner (121ms)
✓ Transfers tokens (1746ms)
✓ Approves tokens for delegated tranfers (252ms)
✓ Handles delegated NZT tranfers (897ms)
6 passing (4s)
The last thing we will do is update the allowances. After a successful transfer, the allowance() should be updated accordingly (deduct the amount from the transferFrom() inside the allowance). Let’s go to ./test/NzouaToken.test.js and add the following code:
// ./test/NzouaToken.test.js
[... omitted code ...]
it('Handles delegated NZT tranfers', async () => {
[... omitted code ...]
// Verify Account Balances are updated accordingly
fromBalance = await token.balanceOf(fromAccount);
toBalance = await token.balanceOf(toAccount);
assert.equal(fromBalance.toNumber(), 90, 'fromAccount was debited 10 NZT Tokens and its new balance is 90')
assert.equal(toBalance.toNumber(), 10, 'toAccount was credited with 10 NZT Tokens and its new balance is 10')
// get the allowance and assert it is the right amount
allowanceAmount = await token.allowance(fromAccount, spendingAccount)
assert.equal(allowanceAmount.toNumber(), 0, 'The amount was successfully deducted from the allowance')
});[... omitted code ...]
The test will fail for obvious reasons. The allowance() was not updated after the transfer.
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (132ms)
✓ Sets the total supply on deployment (46ms)
✓ Allocates the total supply to Contract Owner (56ms)
✓ Transfers tokens (1924ms)
✓ Approves tokens for delegated tranfers (310ms)
1) Handles delegated NZT tranfers
Events emitted during test:
--------------------------- [... omitted output ...]
---------------------------
5 passing (3s)
1 failing
1) Contract: NzouaToken
Handles delegated NZT tranfers:
fromAccount was debited 10 NZT Tokens and its new balance is 90
+ expected - actual
+ 0 - 10
[... omitted output ...]
Let’s go back to ./contracts/NzouaToken.sol and inside the transferFrom() function, let’s ensure the allowance() is updated accordingly.
[... omitted code...]
function approve(address _spender, uint256 _value) public returns(bool success) {
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// require _from has enough NZT
require(_value <= balanceOf[_from]); // require allowance to have enough NZT funds
require(_value <= allowance[_from][msg.sender]);
// update balances
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
// update allowance
allowance[_from][msg.sender] -= _value;
// emit a Transfer event
emit Transfer(_from, _to, _value); // return a boolean
return true;
}
[... omitted code...]
If we run the test, it will run:
ERC20-Token $ truffle test --network ganache
// output on the console
[... omitted output ...]
Contract: NzouaToken
✓ Initializes the contract with the appropriate attributes (159ms)
✓ Sets the total supply on deployment (71ms)
✓ Allocates the total supply to Contract Owner (42ms)
✓ Transfers tokens (2105ms)
✓ Approves tokens for delegated tranfers (417ms)
✓ Handles delegated NZT tranfers (1242ms)
6 passing (4s)
With this last test passed, we were able to create an ERC20 Token using Solidity. In this section, we were able to write a smart contract that follows the EIP20 Specification for building fungible tokens. Throughout this section, we used a TDD (Test Driven Development) approach to ensure that our ERC20 Token Smart Contract behaves as expected. So far we have been able to write tests and execute them from the command line. In the next section, we will learn how to interact with our ERC20 Token Smart Contract inside the Truffle console.
Interacting With en ERC20 Token Smart Contract Using Truffle Console
In this section, will learn how to interact with an ERC20 Token smart contract using the truffle console. We will run the same tests we ran from inside the ./test/NzouaToken.test.js file. Let’s get it.
Prerequisites
- Make sure your local blockchain is running (Ganache)
- Run the migration script to ensure the latest version of our smart contract is deployed to the blockchain
- ERC20-Token $ truffle migrate –network ganache –reset
- Open the truffle console with this command from your project folder (ERC20-Token)
- ERC20-Token $ truffle console
Get a deployed version of the token like so:
truffle(ganache)> token = await NzouaToken.deployed();
// output on the console
undefinedtruffle(ganache)> token
// Output on the console
TruffleContract {
constructor: [Function: TruffleContract] {/* omitted code */},
methods: {/* omitted code */},
abi: [/* omitted code */]
address: '0x35165a0D59821a28CB03f7A20d2278D61FD6a768',
transactionHash: undefined,
contract: Contract {/* omitted code */},
Approval: [Function(anonymous)],
Transfer: [Function(anonymous)],
allowance: [Function(anonymous)] {/* omitted code */},
balanceOf: [Function(anonymous)] {/* omitted code */},
name: [Function(anonymous)] {/* omitted code */},
symbol: [Function(anonymous)] {/* omitted code */},
totalSupply: [Function(anonymous)] {/* omitted code */},
transfer: [Function(anonymous)] {/* omitted code */},
approve: [Function(anonymous)] {/* omitted code */},
transferFrom: [Function(anonymous)] {/* omitted code */},
sendTransaction: [Function(anonymous)],
send: [Function(anonymous)],
allEvents: [Function(anonymous)],
getPastEvents: [Function(anonymous)]
}
Reading State Properties
This returns a massive object with attributes and functions as properties. We will explore some of those properties. The following commands, return the name, symbol, and totalSupply of the Smart Contract:
truffle(ganache)> await token.name()
// Output name the console
'Nzouat Token'
_______________________________
truffle(ganache)> await token.symbol()
// Output symbol the console
'NZT'
_______________________________
truffle(ganache)> totalSupply = await token.totalSupply()
undefined
truffle(ganache)> totalSupply.toNumber()
// Output total supply the console
1000000
_______________________________
truffle(ganache)>
Reading List of accounts
Depending on the version of truffle this command might be slightly different. To list accounts in Truffle v5.4.18 (core: 5.4.18), simply type
// Truffle v5.4.18 (core: 5.4.18)
truffle(ganache)> accounts
// Output accounts to the console
[
'0x5D16d433aFDB957ceF88231da5CDcf12b083E094',
'0x838235F38b782Ce6d40e04469767842D91DfA162',
'0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa',
'0x8429659DEcc8BEB05d77e37c1ff8fE222973a276',
'0xFCb19b549c4628d77313a2Bec6a487B1BA10cD4e',
'0x3BD3EB016b0EA678DF71a0e3a5cA305d37113993',
'0x833D818A07d83509662f353DC15484d08DDa2e09',
'0xF1691d27c4f81750c931AF4a3d6e3746d013FdF8',
'0xfEE09E4e9a94D33ffB3db84A978c5215B2E7F5A3',
'0xb572100594839A716c2219597bBbc76f41A69c19'
]
_______________________________
// For older versions
truffle(ganache)> web3.eth.accounts
// Output accounts to the console
[
'0x5D16d433aFDB957ceF88231da5CDcf12b083E094',
'0x838235F38b782Ce6d40e04469767842D91DfA162',
'0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa',
'0x8429659DEcc8BEB05d77e37c1ff8fE222973a276',
'0xFCb19b549c4628d77313a2Bec6a487B1BA10cD4e',
'0x3BD3EB016b0EA678DF71a0e3a5cA305d37113993',
'0x833D818A07d83509662f353DC15484d08DDa2e09',
'0xF1691d27c4f81750c931AF4a3d6e3746d013FdF8',
'0xfEE09E4e9a94D33ffB3db84A978c5215B2E7F5A3',
'0xb572100594839A716c2219597bBbc76f41A69c19'
]
_______________________________
To read the first account simply type accounts[0] or web3.eth.accounts[0].
Inspect Balances
Check the balance of adminAccount (that we will declare first) to ensure the initial supply was successfully transferred:
truffle(ganache)> adminAccount = accounts[0]
undefinedtruffle(ganache)> adminBal = await token.balanceOf(adminAccount)
undefined
truffle(ganache)> adminBal.toNumber()
1000000
truffle(ganache)>
Set up State Variables
truffle(ganache)> adminAccount = accounts[0]
'0x5D16d433aFDB957ceF88231da5CDcf12b083E094'
truffle(ganache)> fromAccount = accounts[2]
'0x74a6C747f87DAa5a75b3D8ba2549d2489C072FFa'
truffle(ganache)> toAccount = accounts[3]
'0x8429659DEcc8BEB05d77e37c1ff8fE222973a276'
truffle(ganache)> spendingAccount = accounts[4]
'0xFCb19b549c4628d77313a2Bec6a487B1BA10cD4e'
truffle(ganache)>
Fund the Spending account
truffle(ganache)> await token.transfer(fromAccount, 100, {from: adminAccount});
// output on the console
{
tx: '0x92d28423b5c10baa416463c59ea82baaef810e09d37269de0ec60391eddfbc74',
receipt: {
transactionHash: '0x92d28423b5c10baa416463c59ea82baaef810e09d37269de0ec60391eddfbc74',
transactionIndex: 0,
blockHash: '0x89833e2511359d49b2206a3768d04b04dc33c51107bf037bbe8532f0962d94c2',
blockNumber: 1248,
from: '0x5d16d433afdb957cef88231da5cdcf12b083e094',
to: '0x35165a0d59821a28cb03f7a20d2278d61fd6a768',
gasUsed: 52528,
cumulativeGasUsed: 52528,
contractAddress: null,
logs: [ [Object] ],
status: true,
logsBloom: '[/*... omitted code ... */]',
rawLogs: [ [Object] ]
},
logs: [
{
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x92d28423b5c10baa416463c59ea82baaef810e09d37269de0ec60391eddfbc74',
blockHash: '0x89833e2511359d49b2206a3768d04b04dc33c51107bf037bbe8532f0962d94c2',
blockNumber: 1248,
address: '0x35165a0D59821a28CB03f7A20d2278D61FD6a768',
type: 'mined',
id: 'log_bf8053c0',
event: 'Transfer',
args: [Result]
}
]
}
Looking at the output on the console we can see that a transaction was generated, and most importantly, the logs[] array is not empty, which means that our transfer was successful.
Now let’s inspect the accounts and see the balances
truffle(ganache)> adminBal = await token.balanceOf(adminAccount)
undefined
truffle(ganache)> fromBal = await token.balanceOf(fromAccount)
undefined
truffle(ganache)> adminBal.toNumber()
999900
truffle(ganache)> fromBal.toNumber()
100
truffle(ganache)>
Looking at the console, we can confirm that the balances were updated correctly.
Unauthorized Transfers
Let’s try and send more funds than fromAccount can spend/afford. This transaction will fail as expected
truffle(ganache)> await token.transferFrom.call(fromAccount, toAccount, 9999, {from: spendingAccount});
// output on the console
Uncaught Error: Returned error: VM Exception while processing transaction: revert
at evalmachine.<anonymous>:1:28
[.. omitted output ...]
Even if we send a valid amount, the transaction will still fail because we have no allowance set for the spender
truffle(ganache)> await token.transferFrom.call(fromAccount, toAccount, 1, {from: spendingAccount});
// output on the console
Uncaught Error: Returned error: VM Exception while processing transaction: revert
at evalmachine.<anonymous>:1:28
[.. omitted output ...]
Approving Spending
Let’s try and approve spendingAccount to spend 10 NZT tokens on behalf of fromAccount
truffle(ganache)> await token.approve(spendingAccount, 10, {from: fromAccount})
// output on the console{
tx: '0x37c0807835219b01b6ff421ea19acc7698f79c8ce70eaf924afb1a314d6af61e',
receipt: {
transactionHash: '0x37c0807835219b01b6ff421ea19acc7698f79c8ce70eaf924afb1a314d6af61e',
transactionIndex: 0,
blockHash: '0x3c23532baed80aae03454de3b5421eb5fab4fd32a0e3c40ccf24120857edd083',
blockNumber: 1249,
from: '0x74a6c747f87daa5a75b3d8ba2549d2489c072ffa',
to: '0x35165a0d59821a28cb03f7a20d2278d61fd6a768',
gasUsed: 44570,,
cumulativeGasUsed: 44570,
contractAddress: null,
logs: [ [Object] ],
status: true,
logsBloom: '[/*... omitted code ... */]',
rawLogs: [ [Object] ]
},
logs: [
{
logIndex: 0,
transactionIndex: 0,
transactionHash: '0x37c0807835219b01b6ff421ea19acc7698f79c8ce70eaf924afb1a314d6af61e',
blockHash: '0x3c23532baed80aae03454de3b5421eb5fab4fd32a0e3c40ccf24120857edd083',
blockNumber: 1249,
address: '0x35165a0D59821a28CB03f7A20d2278D61FD6a768',
type: 'mined',
id: 'log_34d108dc',
event: 'Approval',
args: [Result]
}
]
}
Checking Allowances
Now that we successfully executed the approve() function, let’s check the allowance of spendingAccount
truffle(ganache)> allowanceAmount = await token.allowance(fromAccount, spendingAccount)
undefined
truffle(ganache)> allowanceAmount.toNumber()
10
truffle(ganache)>
The allowance was successfully updated as seen on the console.
Delegated Transfer
Now that we have allowed spendingAccount to spend up to 10 NZT tokens on the fromAccount‘s behalf, we are now going to make an actual delegated transfer. We will transfer 5 NZT tokens from fromAccount to toAccount and spendingAccount will be in charge of executing the transfer with the correct allowance:
truffle(ganache)> await token.transferFrom(fromAccount, toAccount, 5, {from: spendingAccount});
{
tx: '0xf8f4bd8233f95e2ddc195c4c5fbdf03eed5271882b32a9fe54687d1848a22781',
receipt: {
transactionHash: '0xf8f4bd8233f95e2ddc195c4c5fbdf03eed5271882b32a9fe54687d1848a22781',
transactionIndex: 0,
blockHash: '0x44b4551a3f28af281716d33f6d251734f637ae50bf806b415fc64cb074d95978',
blockNumber: 1250,
from: '0xfcb19b549c4628d77313a2bec6a487b1ba10cd4e',
to: '0x35165a0d59821a28cb03f7a20d2278d61fd6a768',
gasUsed: 60340,
cumulativeGasUsed: 60340,
contractAddress: null,
logs: [ [Object] ],
status: true,
logsBloom: '[/* Omitted code */]',
rawLogs: [ [Object] ]
},
logs: [
{
logIndex: 0,
transactionIndex: 0,
transactionHash: '0xf8f4bd8233f95e2ddc195c4c5fbdf03eed5271882b32a9fe54687d1848a22781',
blockHash: '0x44b4551a3f28af281716d33f6d251734f637ae50bf806b415fc64cb074d95978',
blockNumber: 1250,
address: '0x35165a0D59821a28CB03f7A20d2278D61FD6a768',
type: 'mined',
id: 'log_bc78e5b9',
event: 'Transfer',
args: [Result]
}
]
}
truffle(ganache)>
Our console output indicates that we have at least one entry inside the logs[] array, which means that our transferFrom() function was successful.
Verify Balances and Allowances after transferFrom()
truffle(ganache)> fromBal = await token.balanceOf(fromAccount)
undefined
truffle(ganache)> fromBal.toNumber()
95
truffle(ganache)> toBal = await token.balanceOf(toAccount)
undefined
truffle(ganache)> toBal.toNumber()
5
truffle(ganache)> allowanceAmount = await token.allowance(fromAccount, spendingAccount)
undefined
truffle(ganache)> allowanceAmount.toNumber()
5
truffle(ganache)>
Let’s make sure the balance of the spendingAccount has not changed, even though it’s the account making the transfers
truffle(ganache)> spenBal = await token.balanceOf(spendingAccount)
undefined
truffle(ganache)> spenBal.toNumber()
0
truffle(ganache)>
That’s it. We have now successfully used the truffle console to interact with our ERC20 Token smart contract and execute functions. In the following section, we be building a crowd sale smart contract that will help manage our NZT Tokens.
You can find the complete source code of both smart contracts on the github page
If you would like to learn how to deploy your ERC20 Token Smart contract on a public test network such as Rinkeby, read the following blog post, which explains in details how to go about. I will se you in the next section!
How To Create An ERC-20 Token Sale With Solidity And Deploy On Ethereum – Part II