Create A Smart Contract That Transfers ERC20 Tokens To Any ERC20 Compliant Address
Repository
https://github.com/trufflesuite/truffle
Overview
In this tutorial you would be building a smart contract for transfering an ERC20 token to any ERC20 compliant wallet including metamask and exchange addresses. The method shown in this tutorial is a simplified version of what decentralised exchanges such as forkdelta uses.
What Will I Learn?
You would be learning how to write and test a smart contract that can trasfer tokens from your wallet to other ERC20 compatible addresses.
- How decentralised exchanges deposit and withdrawl feature mechanism works
- How to write a smart contract in solidity to transfer ERC20 tokens
- Testing your smart contract and Interacting with it through the truffle console
Requirements
This tutorial is not a Dapps beginners friendly, so you are advise to have some prior knowledge of how ERC20 token works and basic knowledge of writing solidity code is required.
The following are required to be installed on your system for the purpose of this tutorial.
- Trufflesuite installation guide here
- Ganache private blockchain server
- Metamask install for chrome,firefox,opera
This tutorial assumes you are using a UNIX operating system
Difficulty
- Intermediate
Writing Our Contract
This diagram illustrate the steps of how the process works.
- First an ERC20 token is published to the ethereum network
- The token is assigned a contract address and has an ABI
- An address that holds the token approves a contract to spend on its behalf
- The contract then sends the token to a receiving address
Clear enough!!
Enough With The Talk, Lets Start Coding
By now i assume you must have installed all requirements, so start Ganache then navigate to your working directory and create a folder, name it TokenZendR. Navigate to the TokenZendR directory from your terminal and run command truffle init
, you should now be presented with an output similar to this.
Open the project directory in any of your prefered editor, I prefer using PhpStorm (am addicted to this editor) with the solidity plugin activated. Create a package.json file in the root directory and paste the following into it.
{
"dependencies": {
"babel-register": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.18.0"
},
"devDependencies": {
"openzeppelin-solidity": "^1.10.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chai-bignumber": "^2.0.2"
}
}
Next open your project truffle.js and replace it with the following content
require('babel-register');
require('babel-polyfill');
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*" // Match any network id
}
}};
run npm install
from your console in project root directory to install all the packages specified in the package.json.
By now you might be wondering why you need the dependecies in your package.json. We need babel and chai when writing our test, and we will be needing to extend the ERC20 token contract interface of the openzeppelin-solidity framework.
No we need to create two ERC20 token smart contract for the benefit of this tutorial and deploy them on our private blockchain (Ganache), so we can use them to test our token sender contract as we cannot access already deployed token outside our network (5777) e.g testnet, mainnet
In the contact folder create two files BearToken.sol and CubToken.sol and paste the below content inside them.
BearToken.sol
pragma solidity ^0.4.19;
import "openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
/**
* @title BearToken is a basic ERC20 Token
*/
contract BearToken is StandardToken, Ownable{
uint256 public totalSupply;
string public name;
string public symbol;
uint32 public decimals;
/**
* @dev assign totalSupply to account creating this contract */ constructor() public {
symbol = "BEAR";
name = "BearToken";
decimals = 5;
totalSupply = 100000000000;
owner = msg.sender;
balances[msg.sender] = totalSupply;
emit Transfer(0x0, msg.sender, totalSupply);
}}
This is simply an ERC20 token BearToken, with a symbol of BEAR and a total supply of 100 billion with the total supply assigned to the creator of the contract. It extends the StandardToken and Ownable contract.
Cub.sol
pragma solidity ^0.4.19;
import "openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
/**
* @title BearToken is a basic ERC20 Token
*/
contract CubToken is StandardToken, Ownable{
uint256 public totalSupply;
string public name;
string public symbol;
uint32 public decimals;
/**
* @dev assign totalSupply to account creating this contract
*/
constructor() public {
symbol = "CUB";
name = "CubToken";
decimals = 5;
totalSupply = 100000000000;
owner = msg.sender;
balances[msg.sender] = totalSupply;
emit Transfer(0x0, msg.sender, totalSupply);
}}
This is simply an ERC20 token CubToken, with a symbol of CUB and a total supply of 100 billion with the total supply assigned to the creator of the contract. It extends the StandardToken and Ownable contract.
Let us add migrations for both contracts. run the following commands in the following order.
truffle console
truffle(development)> create migration bear_token_migration
truffle(development)> create migration cub_token_migration
The above command will create two migration files for both of our contract in the format {timestamp}_bear_token_migration.js and {timestamp}_cub_token_migration.js. Next replace the contents of both files with this.
*{timestamp}_bear_token_migration.js*
let BearToken = artifacts.require("./BearToken.sol");
module.exports = function(deployer) {
deployer.deploy(BearToken);
};
*{timestamp}_cub_token_migration.js*
let CubToken = artifacts.require("./CubToken.sol");
module.exports = function(deployer) {
deployer.deploy(CubToken);
};
Now that we have that set, proceed to compile and migrate your contracts.
truffle(development)> compile
truffle(development)> migrate
The image above shows both contract been compiled & deployed on the network , your project directory should now have a build
folder containing json files, a closer look at one of the file, you will see that it contains the abi of the contract and it code compiled to bytecode with several other informations.
Before we proceed to write test for both contract, lets play around with the contract from the console. It gives you more confidence when you have interacted with your contract before writing your test and fixing bugs if need be. Its important to note the contract addresses of both contract on the console
BearToken: 0xeec918d74c746167564401103096d45bbd494b74
CubToken: 0xecfcab0a285d3380e488a39b4bb21e777f8a4eac
Lets simply check the name,symbol and total supply of the BearToken and log the balance of the contract creator, remember in our contract we total supply to the contract creator account.
truffle(development)> Bear = BearToken.at("0xeec918d74c746167564401103096d45bbd494b74")
truffle(development)> Bear.name()
truffle(development)> Bear.totalSupply()
truffle(development)> Bear.balanceOf(web3.eth.accounts[0])
Good Job 👌
Now that we have two ERC20 token to test with, next lets create the TokenZendR contract that handles the transfer.
Create a new contract, TokenZendR.sol
pragma solidity ^0.4.23;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol";
contract TokenZendR is Ownable, Pausable {
}
The contract extends Ownable and Pausable contracts from the openzeppelin library, you would soon see the methods from this contracts that we used.
ERC20.sol is the ERC20 standard interface that all ERC20 compliant tokens must implement, see https://github.com/ethereum/EIPs/issues/20 for more details.
Next define the data structure to hold each transfer , update the contract with this.
/**
* @dev Details of each transfer
* @param contract_ contract address of ER20 token to transfer
* @param to_ receiving account
* @param amount_ number of tokens to transfer to_ account
* @param failed_ if transfer was successful or not
*/
struct Transfer {
address contract_;
address to_;
uint amount_;
bool failed_;
}
Add a mapping that holds all transfer index on the transactions array for every user, where each user address is a key to an array of all their transaction indexes.
/**
* @dev a mapping from transaction ID's to the sender address
* that initiates them. Owners can create several transactions
*/
mapping(address => uint[]) public transactionIndexesToSender;
/**
* @dev a list of all transfers successful or unsuccessful
*/
Transfer[] public transactions;
We define a owner property of type address that hold the address that launched this contract, while the tokens will hold addresses of all the ERC20 tokens the contract support to transfer.
One important thing to note is these beautiful peice of code
ERC20 public ERC20Interface;
These implements the ERC20 interface allowing us to call the methods approve
and transferFrom
on while using the token contract address. This will be clearer soon in the transferToken method futher down this tutorial.
address public owner;
/**
* @dev list of all supported tokens for transfer
* @param string token symbol
* @param address contract address of token
*/
mapping(bytes32 => address) public tokens;
ERC20 public ERC20Interface;
Events to be emitted when transfer is successful or failed
/**
* @dev Event to notify if transfer successful or failed
* after account approval verified
*/
event TransferSuccessful(address indexed from_, address indexed to_, uint256 amount_);
event TransferFailed(address indexed from_, address indexed to_, uint256 amount_);
In this contract we majorly want to do 3 things, which are :
- To add address of a new token to be supported by this contract
- If we can add a token, we also want to be able to remove it
- Finally transfer token, which is the major thing this contract is meant to perform.
We might maybe consider adding a payable method to withdraw funds from the contract, just incase someone feels charitable... 😉
/**
* @dev add address of token to list of supported tokens using
* token symbol as identifier in mapping
*/
function addNewToken(bytes32 symbol_, address address_) public onlyOwner returns (bool) {
tokens[symbol_] = address_;
return true;
}
/**
* @dev remove address of token we no more support
*/
function removeToken(bytes32 symbol_) public onlyOwner returns (bool) {
require(tokens[symbol_] != 0x0);
delete(tokens[symbol_]);
return true;
}
/**
* @dev method that handles transfer of ERC20 tokens to other address
* it assumes the calling address has approved this contract
* as spender
* @param symbol_ identifier mapping to a token contract address
* @param to_ beneficiary address
* @param amount_ numbers of token to transfer
*/
function transferTokens(bytes32 symbol_, address to_, uint256 amount_) public whenNotPaused{
require(tokens[symbol_] != 0x0);
require(amount_ > 0);
address contract_ = tokens[symbol_];
address from_ = msg.sender;
ERC20Interface = ERC20(contract_);
uint256 transactionId = transactions.push(
Transfer({
contract_: contract_,
to_: to_,
amount_: amount_,
failed_: true
})
);
transactionIndexesToSender[from_].push(transactionId - 1);
if(amount_ > ERC20Interface.allowance(from_, address(this))) {
emit TransferFailed(from_, to_, amount_);
revert();
}
ERC20Interface.transferFrom(from_, to_, amount_);
transactions[transactionId - 1].failed_ = false;
emit TransferSuccessful(from_, to_, amount_);
}
/**
* @dev allow contract to receive funds
*/
function() public payable {}
/**
* @dev withdraw funds from this contract
* @param beneficiary address to receive ether
*/
function withdraw(address beneficiary) public payable onlyOwner whenNotPaused {
beneficiary.transfer(address(this).balance);
}
Putting It All Together
pragma solidity ^0.4.23;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol";
contract TokenZendR is Ownable, Pausable {
/**
* @dev Details of each transfer * @param contract_ contract address of ER20 token to transfer * @param to_ receiving account * @param amount_ number of tokens to transfer to_ account * @param failed_ if transfer was successful or not */ struct Transfer {
address contract_;
address to_;
uint amount_;
bool failed_;
}
/**
* @dev a mapping from transaction ID's to the sender address * that initiates them. Owners can create several transactions */ mapping(address => uint[]) public transactionIndexesToSender;
/**
* @dev a list of all transfers successful or unsuccessful */ Transfer[] public transactions;
address public owner;
/**
* @dev list of all supported tokens for transfer * @param string token symbol * @param address contract address of token */ mapping(bytes32 => address) public tokens;
ERC20 public ERC20Interface;
/**
* @dev Event to notify if transfer successful or failed * after account approval verified */ event TransferSuccessful(address indexed from_, address indexed to_, uint256 amount_);
event TransferFailed(address indexed from_, address indexed to_, uint256 amount_);
constructor() public {
owner = msg.sender;
}
/**
* @dev add address of token to list of supported tokens using * token symbol as identifier in mapping */ function addNewToken(bytes32 symbol_, address address_) public onlyOwner returns (bool) {
tokens[symbol_] = address_;
return true;
}
/**
* @dev remove address of token we no more support */ function removeToken(bytes32 symbol_) public onlyOwner returns (bool) {
require(tokens[symbol_] != 0x0);
delete(tokens[symbol_]);
return true;
}
/**
* @dev method that handles transfer of ERC20 tokens to other address * it assumes the calling address has approved this contract * as spender * @param symbol_ identifier mapping to a token contract address * @param to_ beneficiary address * @param amount_ numbers of token to transfer */ function transferTokens(bytes32 symbol_, address to_, uint256 amount_) public whenNotPaused{
require(tokens[symbol_] != 0x0);
require(amount_ > 0);
address contract_ = tokens[symbol_];
address from_ = msg.sender;
ERC20Interface = ERC20(contract_);
uint256 transactionId = transactions.push(
Transfer({
contract_: contract_,
to_: to_,
amount_: amount_,
failed_: true
})
);
transactionIndexesToSender[from_].push(transactionId - 1);
if(amount_ > ERC20Interface.allowance(from_, address(this))) {
emit TransferFailed(from_, to_, amount_);
revert();
}
ERC20Interface.transferFrom(from_, to_, amount_);
transactions[transactionId - 1].failed_ = false;
emit TransferSuccessful(from_, to_, amount_);
}
/**
* @dev allow contract to receive funds */ function() public payable {}
/**
* @dev withdraw funds from this contract * @param beneficiary address to receive ether */ function withdraw(address beneficiary) public payable onlyOwner whenNotPaused {
beneficiary.transfer(address(this).balance);
}}
Writing Our Test
If you are still on the truffle console run the create test
command or return back with command truffle console
and run the commands as shown below to create two test files.
truffle(development)> create test token_management
truffle(development)> create test token_transfer
If you check your project test
your two test files would have been created with a default assertion. Open the token_management.js
file, clear the current content, then import chai and the contract in the begining of the file.
PS: To avoid running into errors while running your first test, empty the content of token_transfer.js aslo.
const TokenZendR = artifacts.require('./TokenZendR.sol');
const should = require('chai')
.use(require('chai-as-promised'))
.should();
let sender;
contract('token_management', async (accounts) => {
}
This test is to run test to check that adding,updating and removing tokens from contract works well. Before running each test we want to add a token as default and if you are not used to usage of asyn/await
yet, you might want to read up on that.
beforeEach(async () => {
sender = await TokenZendR.new();
await sender.addNewToken('OPEN', '0x69c4bb240cf05d51eeab6985bab35527d04a8c64');
});
Next is our first assertion for adding token
it("should add new supported token", async() => {
let address = await sender.tokens.call('OPEN');
address.should.equal('0x69c4bb240cf05d51eeab6985bab35527d04a8c64');
});
truffle(development)> test
Assert if it properly update token address
it("should update supported token address", async() => {
await sender.addNewToken('OPEN', '0x3472059945ee170660a9a97892a3cf77857eba3a');
let address = await sender.tokens.call('OPEN');
address.should.equal('0x3472059945ee170660a9a97892a3cf77857eba3a');
});
truffle(development)> test
Lastly to assert that a token is removed
it("should remove unused supported token address", async() => {
await sender.removeToken('OPEN');
let address = await sender.tokens.call('OPEN');
address.should.equal('0x0000000000000000000000000000000000000000');
});
truffle(development)> test
Putting it all together
const TokenZendR = artifacts.require('./TokenZendR.sol');
const should = require('chai')
.use(require('chai-as-promised'))
.should();
let sender;
contract('token_management', async (accounts) => {
beforeEach(async () => {
sender = await TokenZendR.new();
await sender.addNewToken('OPEN', '0x69c4bb240cf05d51eeab6985bab35527d04a8c64');
});
it("should add new supported token", async() => {
let address = await sender.tokens.call('OPEN');
address.should.equal('0x69c4bb240cf05d51eeab6985bab35527d04a8c64');
});
it("should update supported token address", async() => {
await sender.addNewToken('OPEN', '0x3472059945ee170660a9a97892a3cf77857eba3a');
let address = await sender.tokens.call('OPEN');
address.should.equal('0x3472059945ee170660a9a97892a3cf77857eba3a');
});
it("should remove unused supported token address", async() => {
await sender.removeToken('OPEN');
let address = await sender.tokens.call('OPEN');
address.should.equal('0x0000000000000000000000000000000000000000');
});
});
Finally let test that the contract can actually transfer a token, to confirm that everything actually works fine. Import the three contracts and chai at the top the file.
const TokenZendR = artifacts.require('./TokenZendR.sol');
const BearToken = artifacts.require('./BearToken.sol');
const CubToken = artifacts.require('./CubToken.sol');
const BigNumber = web3.BigNumber;
const should = require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
Create a new instance of all contract before you run each test.
const TokenZendR = artifacts.require('./TokenZendR.sol');
const BearToken = artifacts.require('./BearToken.sol');
const CubToken = artifacts.require('./CubToken.sol');
const BigNumber = web3.BigNumber;
const should = require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
let sender, bear, cub;
contract('token_management', async (accounts) => {
let accountA, accountB, accountC, accountD;
[accountA, accountB, accountC, accountD ] = accounts;
beforeEach(async () => {
sender = await TokenZendR.new();
bear = await BearToken.new();
cub = await CubToken.new();
await sender.addNewToken('BEAR', bear.address);
await sender.addNewToken('CUB', cub.address);
});
}
Test that the balance of address two, is equal amount of BEAR token successfully transfered by the contract.
it("should be able to transfer sender token to another wallet", async() => {
// When transfering token, multiple by
//figure of decimal to get exact token e.g
//to send 5 BEAR = 5e5, where 5 is the decimal places
let amount = new BigNumber(500000e5);
//Account a approve contract to spend on behalf
await bear.approve(sender.address, amount,{from: accountA});
await sender.transferTokens('BEAR',accountB, amount,{from: accountA});
let balance = ((await bear.balanceOf(accountB)).toString());
balance.should.equal(amount.toString())
});
truffle(development)> test
This tutorial has been able to successfully show you how to build a contract that transfers ERC20 tokens from your address to any other ERC20 complient wallet even echange. You can test it on testnet or main net with existing tokens such as GTO or TRX.
Code Repository
Complete smart contract code can be found & cloned here https://github.com/slim12kg/tokenzendr-contract
Thank you for your contribution.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Thank you very much @portugalcoin
Hey @alofe.oluwafemi
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!