Ethers.js: How to send ERC-20 Tokens

Hello everyone,

In this post we will explore how to transfer an ERC-20 token from one address to another with ethers.js.
Ethers is a pretty cool JavaScript library which is able to send an EIP-1559 transaction without the need of manually specifying gas properties. It will determine the gasLimit and use a maxPriorityFeePerGas of 1.5 Gwei by default, starting with v5.6.0.
Also, if you use a signer class it would know also how to manage the nonce for you.

So, let’s use this ethers send transaction example as a reference and see how the code changes when we want to send an ERC-20 token instead of ETH.

But first things first, let’s get some LINK test ERC-20 tokens on Goerli from this faucet https://faucets.chain.link/. Once you get your test tokens you’ll be ready to roll.

And before taking a look at the code let’s find out the essential difference between sending ETH and an ERC-20 token: the transaction will not send any value but instead it will call the token contract Transfer function with the relevant parameters (to_address, amount).

Now let’s see what we need to modify in our reference code. First we’ll need to create a contract instance and load the contract ABI from a file:

const fs = require('fs');
const jsonFile = "/path/to/ABI/file/ct_abi.json";
const abi=JSON.parse(fs.readFileSync(jsonFile));
const tokenContract = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" //LINK
const contract = new ethers.Contract(tokenContract, abi, provider)

Define the token amount which we will be sending. Note that we need to parse the amount as each token has 18 decimal places. Basically we will send 1 Link token to our destination address.

const amount = ethers.utils.parseUnits("1.0", 18);

Define the data parameter which will be included in the transaction (tx) object and which is an encoding of the ABI transfer function call. Also note that in the same transaction object the to parameter is actually the address of the token contract.

const data = contract.interface.encodeFunctionData("transfer", [toAddress, amount] )

With all these said, below you can find the full code which creates and signs the transaction.

const { ethers } = require("ethers");
require('dotenv').config({path:"/path/to/.env"});
 
async function main() {

  // Configuring the connection to an Ethereum node
  const network = process.env.ETHEREUM_NETWORK;
  const provider = new ethers.providers.InfuraProvider(
    network,
    process.env.INFURA_PROJECT_ID
  );

const fs = require('fs');
const jsonFile = "/path/to/ABI/file/ct_abi.json";
const abi=JSON.parse(fs.readFileSync(jsonFile));

const tokenContract = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" //LINK
const toAddress = "<insert_token_destination_address_here>"

  // Define the ERC-20 token contract
const contract = new ethers.Contract(tokenContract, abi, provider)

// Creating a signing account from a private key
const signer = new ethers.Wallet(process.env.SIGNER_PRIVATE_KEY, provider);

// Define and parse token amount. Each token has 18 decimal places. In this example we will send 1 LINK token
const amount = ethers.utils.parseUnits("1.0", 18);

//Define the data parameter
const data = contract.interface.encodeFunctionData("transfer", [toAddress, amount] )

// Creating and sending the transaction object

const tx = await signer.sendTransaction({
    to: tokenContract,
    from: signer.address,
    value: ethers.utils.parseUnits("0.000", "ether"),
    data: data  
      });

      console.log("Mining transaction...");
      console.log(`https://${network}.etherscan.io/tx/${tx.hash}`);

      // Waiting for the transaction to be mined
      const receipt = await tx.wait();

      // The transaction is now on chain!
      console.log(`Mined in block ${receipt.blockNumber}`);
    }

    main();

At the beginning of the tutorial, I was mentioning that the ethers library automatically controls the transaction’s gas properties. However if you want to manually set them yourself, this is how the transaction object and its sending changes.

const limit = await provider.estimateGas({
  from: signer.address,
  to: tokenContract,
  value: ethers.utils.parseUnits("0.000", "ether"),
  data: data
 
});

console.log("The gas limit is " + limit)

const tx = await signer.sendTransaction({
        to: tokenContract,
        value: ethers.utils.parseUnits("0.000", "ether"),
        data: data,
        from: signer.address,
        nonce: signer.getTransactionCount(),
        maxPriorityFeePerGas: ethers.utils.parseUnits("3", "wei"),
        gasLimit: limit,
        chainId: 5

       });

Just make sure that you specify a high enough maxPriorityFeePerGas so that your transaction will be picked up by miners. You will also notice that the estimated gasLimit is well above the 21000 gas units, which would be necessary for a normal ETH transfer, as contract transactions require higher values of gas units.
Also you need to make sure that when you do the gas estimation you don’t try to estimate the sending of more tokens than what you actually own as in this particular case ethers.js will throw an error.

:wave: Hope you are not bored yet as there’s still a neat feature that we still have to explore if you want to take advantage of the full capabilities of the ethers library and to simplify your code.

After you have defined your contract instance, this is connected to the provider, which is read-only. You will need to connect it to a signer so that you can pay to send state-changing transactions and then use the contract transfer function to send the ERC-20 token.

Basically the whole process of creating and sending the transaction object reduces to two lines of code:

//Connect to a signer so that you can pay to send state changing txs
const contractSigner = contract.connect(signer)
//Define tx and transfer token amount to the destination address
const tx = await contractSigner.transfer(toAddress, amount);

And here’s the code modified to accommodate the above changes:

const { ethers } = require("ethers");
require('dotenv').config({path:"/path/to/.env"});

async function main() {

const fs = require('fs');
const jsonFile = "/path/to/ABI/file/ct_abi.json";
const abi=JSON.parse(fs.readFileSync(jsonFile));
const tokenContract = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" //LINK
const toAddress = "<insert_token_destination_address_here>"

// Configuring the connection to an Ethereum node
      const network = process.env.ETHEREUM_NETWORK;
      const provider = new ethers.providers.InfuraProvider(
        network,
       process.env.INFURA_PROJECT_ID
      );

// Define the ERC-20 token contract
const contract = new ethers.Contract(tokenContract, abi, provider)

// Creating a signing account from a private key
const signer = new ethers.Wallet(process.env.SIGNER_PRIVATE_KEY, provider);

// Define and parse token amount. Each token has 18 decimal places. In this example we will send 1 LINK token
const amount = ethers.utils.parseUnits("1.0", 18);

//Connect to a signer so that you can pay to send state changing txs
const contractSigner = contract.connect(signer)

//Define tx and transfer token amount to the destination address
const tx = await contractSigner.transfer(toAddress, amount);

      console.log("Mining transaction...");
      console.log(`https://${network}.etherscan.io/tx/${tx.hash}`);
      // Waiting for the transaction to be mined
      const receipt = await tx.wait();
      // The transaction is now on chain!
      console.log(`Mined in block ${receipt.blockNumber}`);
    }
    main();

Hope this was helpful!

2 Likes