Skip to main content

Ethers.js: How to send ERC-20 Tokens

In this post we will explore how to transfer an ERC-20 token from one address to another with ethers.js. Ethers is a JavaScript library that can send an EIP-1559 transaction without the need to manually specify 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 can manage the nonce for you.

So, let's use this MetaMask Ethers tutorial 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 Sepolia from this faucet. 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_API_KEY)

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, we mentioned 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 the sending process change:

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 validators. You will also notice that the estimated gasLimit is well above the 21000 gas units needed for a normal ETH transfer, as contract transactions require higher values of gas units. Also, make sure that when you do the gas estimation you don't try to estimate a gas cost for sending more tokens than you actually own, as in this particular case Ethers.js will throw an error.

Use a signer

To take advantage of the full capabilities of the Ethers library and to simplify your code, you can use a signer.

After you have defined your contract instance, it 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_API_KEY)

// 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()
Was this helpful?
Connect MetaMask to provide feedback
What is this?
This is a trial feedback system that uses Verax to record your feedback as onchain attestations on Linea Mainnet. When you vote, submit a transaction in your wallet.