My first ERC20 token
Introduction
The token I developed is an ERC20 token called $MTK, implementing the EIP-20 token standard: https://eips.ethereum.org/EIPS/eip-20.
We can say that ERC20 is the standard implementation of the Ethereum Improvement Proposal EIP-20, but this definition may not be formally correct.
Anyway, the important thing is that the ERC20 standard is probably the most used standard for fungible token on Ethereum and most of the tokens developed on the Ethereum blockchain are ERC20 token (e.g. $LINK, $USDT, $SHIB, $BAT among others).
The world “fungible” means that any $MTK token has the same value of any other $MTK token. This is the same that happens to standard FIAT currency, where for example 1 EUR coin has the same value as all the 1 EUR coins in circulation.
In the following section I analyze the source code of the smart contracts implementing all the functionalities of the ERC20 token $MTK. The smart contract are written in Solidity programming language and almost all of the functionalities are inherited from OpenZeppelin library, so an analysis of the source code of the library will be performed.
OpenZeppelin Web Site: https://openzeppelin.com/
Finally, we will deploy the smart contract on a local blockchain for testing and make transactions of $MTK from one wallet to another.
The token source code can be found on the Github Repo: https://github.com/lucadidomenico/myERC20Token
ERC20.sol and IERC20.sol
Implementation of the ERC20 Token Standard is contained in ERC20.sol and IERC20.sol source files of OpenZeppelin.
Specifically, the IERC20.sol interface defines the ERC20 functions while ERC20.sol implements them.
IERC20.sol:
pragma solidity ^0.8.0;interface IERC20 {
// returns the amount of token in circulation.
function totalSupply() external view returns (uint256); //returns the balance of an account, i.e. the amount of token the account owns.
function balanceOf(address account) external view returns (uint256); //send amount of token to the recipient address
function transfer(address recipient, uint256 amount) external returns (bool); //returns how much the owner of the token has allowed another account to transfer the token he owns. (ignore for now)
function allowance(address owner, address spender) external view returns (uint256); //the owner of tokens allows another account to transfer the token he owns by amount (ignore for now)
function approve(address spender, uint256 amount) external returns (bool); //transfer token from one account to another. The owner of the token is not the calling address (ignore for now)
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); // event emitted during a transfer (ignore for now)
event Transfer(address indexed from, address indexed to, uint256 value);
// event emitted during an approval (ignore for now)
event Approval(address indexed owner, address indexed spender, uint256 value);}
ERC20.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";contract ERC20 is Context, IERC20, IERC20Metadata {[ . . . ]
The following three functions are also part of ERC20 but are not declared in IERC20.sol:
// returns the name of the token i.e. MyToken
function name() public view virtual override returns (string memory);// return the symbol of the token i.e. MTK
function symbol() public view virtual override returns (string memory);// Returns the number of decimals used to get its user representation (see below).
function decimals() public view virtual override returns (uint8);
I don’t know the specific reason why OpenZeppelin decided to not declare also these 3 functions in IERC20.sol, but defined them directly in the ERC20.sol smart contract.
You can find the source code here:
- IERC20.sol: https://github.com/lucadidomenico/myERC20Token/blob/master/node_modules/%40openzeppelin/contracts/token/ERC20/IERC20.sol#L1
- ERC20.sol: https://github.com/lucadidomenico/myERC20Token/blob/master/node_modules/%40openzeppelin/contracts/token/ERC20/ERC20.sol#L1
Minting.
The _mint() function is defined in ERC20.sol at line 251.
function _mint(address account, uint256 amount) internal virtual {
[ . . . ]
_totalSupply += amount;
_balances[account] += amount;
[ . . . ]
}
the _mint function creates new “amount” of token and increases the total supply. It assigns the new token to the “account” that is calling the function. This function is useful in order to mantain the synchronization between the totalSupply and the balance of the minted account (which increases by amount) and should always be used when increasing the total amount of an ERC20 token.
Burning.
The _burn() function is defined in ERC20.sol at line 274.
function _burn(address account, uint256 amount) internal virtual {
[ . . . ]
uint256 accountBalance = _balances[account];
unchecked {
_balances[account] = accountBalance - amount;
}
_totalSupply -= amount;
[ . . . ]
}
the _burn() function is exactly the opposite of the _mint() function. In fact it is used in order to decrease the total supply of token. The amount of token will be burned from the account balance. When this operation is performed by developers, usually it results in a price increase of remaining tokens. The opposite is true for minting, which usually decreases the price of token.
Transferring tokens between accounts.
It is important to note that when transferring tokens between accounts, the only transaction that happens on the blockchain is the contract call. In fact, when transferring tokens from one account to another (by using transfer() or transferFrom()) what the token contract does is simply update its own internal variable “_balances”, which contains for each account the amount of tokens it owns. This is showed in the snippets of code below.
ERC20.sol:35
mapping(address => uint256) private _balances;
The “_balances” variable contains for each address the amount of token it owns, expressed in unsigned integer.
The transfer() function of the ERC20 smartcontract updates this mapping, as shown in the following code.
function _transfer(address sender, address recipient, uint256 amount ) internal virtual {
[ . . . ]
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[sender] = senderBalance - amount;
}
_balances[recipient] += amount;
[ . . . ]
This is the transfer() function which is implemented at line 220 of ERC20.sol.
A note about decimals.
You may have noticed the following function defined in ERC20.sol:
function decimals() public view virtual override returns (uint8) {
return 18;
}
What is its purpose?
Unfortunately, the solidity language and EVM does not support floating point number data-type: which means that all arithmetic operations in smart contracts happens between integers types. But what happens if you want to transfer 1.5 token? It is not possible in Solidity, so the EIP-20 Token Standard provides the decimals property, which defines how many 0s must be considered for the token.
For example, suppose an EOA wants to transfer 1.5 tokens to another EOA. In the user interface of the wallet it specifies the 1.5 tokens as the amount to transfer, but in reality the amount of token transferred is 1.5 * decimals. To be able to transfer 1.5 tokens, decimals must be at least 1, since that number has a single decimal place.
In OpenZeppelin decimals by default are set to 18, so when transferring 1.5 token, in reality the calculation is performed on 1.5*10¹⁸ = 1500000000000000000.
For more information: (https://docs.openzeppelin.com/contracts/4.x/erc20#a-note-on-decimals)
OpenZeppelin extensions.
Extensions provide useful utilities for token management after the smart contract is deployed. Extensions are smart contract implemented in Solidity language and are part of the OpenZeppein library. I’ve used the extensions in order to give to my token the following capabilities:
- dinamically minting new token;
- dinamically burning token;
- dinamically pausing all token’s operation.
Excluding pausing, the minting and burning capabilities are already implemented in the ERC20.sol smart contract so what is the reason to implement them in another contract? Because the functions in ERC20.sol are internal, thus them can not be called after the contract is deployed. In order for a function to be called by a transaction, the function must be declared public.
Minting
The ERC20PresetMinterPauser.sol contract provides the public mint() function, which can be called by an account with role MINTER_ROLE in order to increase the total supply of the token.
ERC20PresetMinterPauser.sol:51
function mint(address to, uint256 amount) public virtual {
require(hasRole(MINTER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have minter role to mint");
_mint(to, amount);
}
URL: https://github.com/lucadidomenico/myERC20Token/blob/master/contracts/MyTokenERC20.sol#L1
The public mint() function calls the internal _mint() function of ERC20.sol.
Burning
The ERC20Burnable.sol contract provides the public burn() function, which can be called by an account with role BURNER_ROLE in order to decrease the total supply of token.
ERC20Burnable.sol:19
function burn(uint256 amount) public virtual {
_burn(_msgSender(), amount);
}
Pausing
the ERC20PresetMinterPauser.sol contract, by inerithing from ERC20Pausable.sol, provides the pause() function which pauses all the token transfers until unpause() is called. This can be useful for scenarios such as preventing trades until the end of an evaluation period, or having an emergency switch for freezing all token transfers in the event of a large bug.
ERC20PresetMinterPauser.sol:65
function pause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to pause");
_pause();
}
ERC20PresetMinterPauser.sol:79
function unpause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to unpause");
_unpause();
}
The contract Pausable.sol defines the _pause variable:
bool private _paused;
Which is initially set to “false” in the constructor.
constructor() {
_paused = false;
}
And updated by calling the public functions “pause()” and “unpause()”.
Pausable.sol:74
function _pause() internal virtual whenNotPaused {
_paused = true;
emit Paused(_msgSender());
}
Pausable.sol:86
function _unpause() internal virtual whenPaused {
_paused = false;
emit Unpaused(_msgSender());
}
Access Control
In myERC20Token it is implemented a Role-Based Access Control mechanism (https://en.wikipedia.org/wiki/Role-based_access_control) using the AccessControl.sol smart contract of the OpenZeppelin library.
A role authorizes an account to call a specific function inside the token’s smart contract. Accounts can have 0 or more roles.
Defining roles.
Three roles were defined in the MyTokenERC20 implementation:
- Burner role
- Minter role
- Pauser role
The Minter and Pauser roles are defined in the ERC20PresetMinterPauser.sol smart contract at line 26 and 27.
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
And the Burner role is defined in MyTokenERC20.sol smart contract, at line 9.
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
Using roles.
Roles are saved inside the AccessControl.sol smart contract using the “_roles” mapping and the “RoleData” structure, defined at line 49–54.
[ . . . ]
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address => bool) members;
bytes32 adminRole;
}
mapping(bytes32 => RoleData) private _roles;
[ . . . ]
In the above code, the “RoleData” structure contains a mapping between an address and a boolean value. The “_roles” member of the AccessControl smart contract is a mapping between a role and the account having that role. Given an account and a role, you can test if the account has the role by looking at the “members” field of “RoleData”. This is what the “hasRole” function does at line 83 of AccessControl.sol.
function hasRole(bytes32 role, address account) public view override returns (bool) {
return _roles[role].members[account];
}
Finally, the field “adminRole” in “RoleData” contains the admin role for the role. In fact, in OpenZeppelin’s implementation of Access Control mechanism, for each role there is an admin role: an account with admin role has permission to grant or revoke the role to all accounts.
For example: suppose the admin role for the BURNER_ROLE role is called BURNER_ADMIN_ROLE. Account A has the BURNER_ADMIN_ROLE role. Thus account A can grant and revoke the role BURNER_ROLE to any account of the token, including himself.
Granting and revoking roles.
We have already encountered the functions grantRole() and revokeRole() of AccessControl.sol in the section above. As their name says, these functions are used respectively to grant a role to an account and to revoke a role. Remember that only the admin role for the role can grant or revoke the role to other account. However, roles can also be assigned during the smart contract creation, i.e. in the contructor; in this case the internal : _setupRole() function is used.
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
_setupRole(MINTER_ROLE, _msgSender());
_setupRole(PAUSER_ROLE, _msgSender());
}
The contructor of ERC20PresetMinterPauser smart contract assigns the DEFAULT_ADMIN_ROLE, the MINTER_ROLE and the PAUSER_ROLE to the creator of the contract. Than calls the _setupRole() function of AccessControl.sol
function _setupRole(bytes32 role, address account) internal virtual {
_grantRole(role, account);
}
Which in turn calls the internal _grantRole() function.
function _grantRole(bytes32 role, address account) private {
if (!hasRole(role, account)) {
_roles[role].members[account] = true;
emit RoleGranted(role, account, _msgSender());
}
}
Checking the role of accounts to authorize operations on smart contract.
In order to check that an account has the role to perform the specific action the require() function is used. It can be used in two ways:
- by directly calling require() inside the function body of the privileged function;
- by defining the require() statement inside a modifier and including the modifier inside the privileged function.
I prefer the “modifier” way, because I think is less error-prone.
MyTokenERC20.sol:16
function burn(uint256 value) public onlyRole(BURNER_ROLE) override {
super._burn(msg.sender, value);
}
In the OpenZeppelin library, both approaches are used. Specifically, ERC20PresetMinterPauser.sol uses the first approach while AccessControl.sol uses the second approach, as can be seen in the snippet of code below.
ERC20PresetMinterPauser.sol:51
function mint(address to, uint256 amount) public virtual {
require(hasRole(MINTER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have minter role to mint");
_mint(to, amount);
}
ERC20PresetMinterPauser.sol:65
function pause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to pause");
_pause();
}
ERC20PresetMinterPauser.sol:79
function unpause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to unpause");
_unpause();
}
AccessControl.sol:129
function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
AccessControl.sol:142
function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
Note about TimeLock.
Please note that OpenZeppelin provides also a TimeLock mechanism to enforce the security policy, specifically this mecanism can be used in order to not let an administrator perform a malicious task. However i’ve not implemented this mechanism in myERC20Token because…
For example, the timelock security mechanism can be used by legitimate projects to prevent “rug pull” scams.
URL: https://youtu.be/VlIcoKJsIP8?t=349
MyTokenERC20.sol
The base contract that I have personally implemented is MyERC20Token.sol and is composed by a few lines of code, because has we have seen allthe functionalities are implemented in the OpenZeppelin library, which the contract includes.
Specifically, the file includes the ERC20PresetMinterPauser.sol contract, which in turn includes ERC20.sol. ERC20PresetMinterPauser.sol also includes AccessControlEnumerable, ERC20Burnable and ERC20Pausable, the smart contracts providing the functionalities explained in the previous sections.
Here is the source code of MyTokenERC20.sol.
// contracts/MyTokenERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";contract MyTokenERC20 is ERC20PresetMinterPauser { bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");constructor(uint256 initialSupply) ERC20PresetMinterPauser("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
grantRole(BURNER_ROLE, msg.sender);
} function burn(uint256 value) public onlyRole(BURNER_ROLE) override {
super._burn(msg.sender, value);
} fallback() external payable { revert(); }
}
The source code can be found here: https://github.com/lucadidomenico/myERC20Token/blob/master/contracts/MyTokenERC20.sol
Analysis of the contract.
The MyERC20Token.sol defines the BURNER_ROLE role at line 9.
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
An account having this role can burn tokens thus decreasing the total number of token in circulation.
The contructor for my token is at line 11.
constructor(uint256 initialSupply) ERC20PresetMinterPauser("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
grantRole(BURNER_ROLE, msg.sender);
}
The token we will deploy is called “MyToken” with symbol MTK. The constructor assigns the BURNER_ROLE to the creator of the smart contract (i.e. msg.sender). The constructor takes one argument, which is the total supply of the token, expressed in uint256. To assign the initial supply, the _mint() function provided by the contract ERC20.sol is used.
The contract defines the burn() function at line 16.
function burn(uint256 value) public onlyRole(BURNER_ROLE) override {
super._burn(msg.sender, value);
}
Accounts with the BURNER_ROLE role can “burn” tokens. In order to check that the account calling the function burn() has the right role, it uses the “onlyRole” modifier. An account burns tokens from its balance, thus when burning tokens the balance of account must be >= to the quantity of token burned, and this line in the _burn() function of ERC20.sol at line 280 does the check:
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
Finally, the contract implements the fallback() function at line 21.
// REJECT any incoming ether
fallback() external payable { revert(); }
The fallback() function in Solidity is a function invoked in the absence of data or a declared function name in the transaction. In this case I implemented this function in order to prevent locking forever ether in the smart contract. For example, if an account wants to send ethers to another account, but by mistake inserts the contract’s address in the destination address, the ether will be locked forever and no one can withdrawal them from the contract, because the contract does not provide any “withdrawal” function. In order to prevent this scenario, the fallback() function calls revert() thus canceling the transaction and saving the ethers of the caller.
Deploy the myERC20Token on Ethereum blockchain
Smart contracts can be deployed on different networks. The main Ethereum network is called “mainnet” where you use real ETH, so real money. In order to test smart contracts, developers can use other Ethereum test networks. “Ropsten” is the principal test network on Ethereum that provides an environment which works the same way as the main blockchain. Another valid alternative for testing is to use the “ganache” blockchain, which is an Ethereum blockchain that runs on localhost.
For demonstration purpose I will deploy myERC20Token on “ganache” which you can download from the following URL:
Once downloaded run the ganache binary. It shows the following page:

Click on the “Quickstart Ethereum” button and ganache will instantiate a new blockchain which runs on localhost on port 7545.

In the above screenshot you can see that ganache creates 10 new wallets, each one having a balance of 100 ETH.
The “ganache” software is part of “truffle suite”, which essentially is a node.js framework that help developers deploy smart contracts and interact with them by using the web3.js JavaScript library.
Truffle Web Site:
In order to deploy the token, clone the token source code from github repo (https://github.com/lucadidomenico/myERC20Token) and run the following commands inside the repo directory:
$ npm install
$ truffle deploy --network ganache
The output of truffle shows information about the transaction:
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)1_initial_migration.js
======================Replacing 'Migrations'
----------------------
> transaction hash: 0xcc061045784b2b6dc332035b6d01bdcdbfe12313e12472369949d48b9f4597bd
> Blocks: 0 Seconds: 0
> contract address: 0xF1FF9a87e6a270A88C5651222053C9B7C93d3799
> block number: 1
> block timestamp: 1638093841
> account: 0x4526fC5171752CC3eA6A3173E75625553cbC8D1f
> balance: 99.9969218
> gas used: 153910 (0x25936)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.0030782 ETH> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.0030782 ETH2_deploy_all.js
===============Replacing 'MyTokenERC20'
------------------------
> transaction hash: 0x44dd2e171a86b3e6fde7ff8980d6b3b7d6f4a6dbe44f792a3b2eb2fc823c6ef1
> Blocks: 0 Seconds: 0
> contract address: 0x3c0acBad62C03443953c521FBe16375777A1dF80
> block number: 3
> block timestamp: 1638093841
> account: 0x4526fC5171752CC3eA6A3173E75625553cbC8D1f
> balance: 99.95611126
> gas used: 1998237 (0x1e7d9d)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.03996474 ETH> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.03996474 ETHSummary
=======
> Total deployments: 2
> Final cost: 0.04304294 ETH
The amount of ETH spent for the two transaction was 0.04304294 ETH.
We can see that 2 contracts where deployed: Migrations and MyTokenERC20.
Migrations is a smart contract provided by truffle that helps managing deployment of all other smart contracts. The Migrations.sol smart contract has the following source code:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
See the doc of truffle for more information on migrations: https://www.trufflesuite.com/docs/truffle/getting-started/running-migrations
Basically, when you to deploy a smart contract using the “truffle deploy” command you first make a transaction to the Migrations smart contract as can be seen in the above screenshot.

Interacting with the smart contract
In order to interact with the deployed smart contract by using web3.js, run the following command:
$ truffle console --network ganache
This will open a console where you can issue JavaScript code using web3.js. For example, now I will transfer 1000 token from one account (the account that received the total supply of 10¹⁸ MyTokenERC20) to another account. The account are available in the “accounts” array:
truffle(ganache)> accounts
[ '0x4526fC5171752CC3eA6A3173E75625553cbC8D1f',
'0xB4545da269dF6835507D58B14f10B451885c42Ed',
'0x8b40A9E295a87b95336dC2426BC6b473FC86884C',
'0xB33BC04b589F7c3D0d5E70eB3aaC9702e35ca93e',
'0x59b68A348eb5cF437ef8DDA473bAaD3C1EF6488C',
'0x8843d4e156e3E30FFa951488A6F918fD3028d1C3',
'0xEa7d08C13e5041a8fe0A9dce562dA41B1DDC66b7',
'0xF69eE5d9F65F130fB4805E881014B6E429ca0EAC',
'0x44936B4aeBc137b1CC4D9fF2389cc15606D2E4D3',
'0x69A8432E6720d4B4a642b6e7217DaB741F5e0133' ]
In order to interact with the MyTokenERC20 smart contract we need an instance.
truffle(ganache)> var mytoken = await MyTokenERC20.deployed()
undefined
If we call the balanceOf() ERC20 method on mytoken, we can see the balance of accounts.
Balance of accounts[0]:
truffle(ganache)> mytoken.balanceOf(accounts[0]).then(a => a.toString())
'10000000000000000000000000'
Balance of accounts[1]:
truffle(ganache)> mytoken.balanceOf(accounts[1]).then(a => a.toNumber())
'0'
Now we send 1000 token from accounts[0] to account [1]:
truffle(ganache)> mytoken.transfer(accounts[1], 1000)
{ tx:
'0xc0027840e5b740220e0a4de6f818284a7e9820f2e65701f512e6b605af1d37e7',
receipt:
{ transactionHash:
'0xc0027840e5b740220e0a4de6f818284a7e9820f2e65701f512e6b605af1d37e7',
transactionIndex: 0,
blockHash:
'0x98d5711d55ac91d5a1db685f2d42d6b85ada8015d212ad84c05f4ed54ab439c8',
blockNumber: 5,
from: '0x4526fc5171752cc3ea6a3173e75625553cbc8d1f',
to: '0x3c0acbad62c03443953c521fbe16375777a1df80',
gasUsed: 52354,
cumulativeGasUsed: 52354,
contractAddress: null,
logs: [ [Object] ],
status: true,
logsBloom:
'0x00000000000000000000000000000800000000000000000000000801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000001000000012000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000004000000000',
rawLogs: [ [Object] ] },
logs:
[ { logIndex: 0,
transactionIndex: 0,
transactionHash:
'0xc0027840e5b740220e0a4de6f818284a7e9820f2e65701f512e6b605af1d37e7',
blockHash:
'0x98d5711d55ac91d5a1db685f2d42d6b85ada8015d212ad84c05f4ed54ab439c8',
blockNumber: 5,
address: '0x3c0acBad62C03443953c521FBe16375777A1dF80',
type: 'mined',
id: 'log_ca948418',
event: 'Transfer',
args: [Result] } ] }
The transaction details are returned inside the “tx” object. The transaction was successful and the balances are now updated.
Balance of accounts[0]:
truffle(ganache)> mytoken.balanceOf(accounts[0]).then(a => a.toString())
'9999999999999999999999000'
Balance of accounts[1]:
truffle(ganache)> mytoken.balanceOf(accounts[1]).then(a => a.toString())
'1000'
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing