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.

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.

Truffle init

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

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])

Migrate

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 testyour 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

Sort:  

Thank you for your contribution.

  • Your tutorial is very good and easy to understand, thank you very much for your hard work.

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]

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!