Web3.js: How to Send ERC-20 Tokens

Hey everyone,

In this quick guide we’ll be looking at how to send an ERC-20 token with web3.js
We’ll use as a reference this example that sends value from one address to another and look at what actually changes if we want to send an ERC-20 token instead of ETH.

But before that let’s get some test ERC-20 tokens - we’ll be using Goerli as a testnet and get some LINK tokens from this faucet https://faucets.chain.link/

Good, so I’m hoping that the faucet transaction went through and we all have some test tokens to play with.
Let’s start with pointing out the essential difference between sending ETH and a different 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).

How does this changes the referenced code ? Well, firstly we need to create a contract instance and use the token contract ABI which is going to be loaded from a file.

var fs = require('fs');
var jsonFile = "ct_abi.json";
var parsed=JSON.parse(fs.readFileSync(jsonFile));
var abi = parsed.abi;
const tokenAddress = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB";
const contract = new web3.eth.Contract(abi, tokenAddress, { from: signer.address } )

It worth pointing out how we created an account object from the private key:

const signer = web3.eth.accounts.privateKeyToAccount(
    process.env.SIGNER_PRIVATE_KEY
);
web3.eth.accounts.wallet.add(signer);

Let’s also define an amount that we want to send:

let amount = web3.utils.toHex(web3.utils.toWei("1"));

Now the transaction object would have the following params:

const tx = {
    from: signer.address,
    to: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB",
    value: "0x0",
    data: contract.methods.transfer(toAddress, amount).encodeABI(),
    gas: web3.utils.toHex(5000000),
    nonce: web3.eth.getTransactionCount(signer.address),
    maxPriorityFeePerGas: web3.utils.toHex(web3.utils.toWei('2', 'gwei')),
    chainId: 5,
    type: 0x2
};

Notice the data parameter which is an encoding of the Transfer function call. Also, notice that the to parameter is the token contract address and not the account address where you wish to transfer the tokens.

So the full code that creates and signs the raw transaction becomes:

const Web3 = require("web3");

async function main() {
    // Configuring the connection to an Ethereum node
    const network = process.env.ETHEREUM_NETWORK;
    const web3 = new Web3(
        new Web3.providers.HttpProvider(
            `https://${network}.infura.io/v3/${process.env.INFURA_PROJECT_ID}`
        )
    );

    var fs = require('fs');
    var jsonFile = "ct_abi.json";

    var parsed=JSON.parse(fs.readFileSync(jsonFile));
    var abi = parsed.abi;

    const tokenAddress = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB";
    const toAddress = "<insert_here_the_token_destination_address>"

    // Creating a signing account from a private key
    const signer = web3.eth.accounts.privateKeyToAccount(
        process.env.SIGNER_PRIVATE_KEY
    );
    web3.eth.accounts.wallet.add(signer);

    const contract = new web3.eth.Contract(abi, tokenAddress, { from: signer.address } )
    let amount = web3.utils.toHex(web3.utils.toWei("1"));  
 
     // Creating the transaction object
     const tx = {
         from: signer.address,
         to: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB",
         value: "0x0",
         data: contract.methods.transfer(toAddress, amount).encodeABI(),
         gas: web3.utils.toHex(5000000),
         nonce: web3.eth.getTransactionCount(signer.address),
         maxPriorityFeePerGas: web3.utils.toHex(web3.utils.toWei('2', 'gwei')),
         chainId: 5,
         type: 0x2
     };
 
     signedTx = await web3.eth.accounts.signTransaction(tx, signer.privateKey)
     console.log("Raw transaction data: " + signedTx.rawTransaction)
 
     // Sending the transaction to the network
     const receipt = await web3.eth
         .sendSignedTransaction(signedTx.rawTransaction)
         .once("transactionHash", (txhash) => {
             console.log(`Mining transaction ...`);
             console.log(`https://${network}.etherscan.io/tx/${txhash}`);
         });
     // The transaction is now on chain!
     console.log(`Mined in block ${receipt.blockNumber}`);
 
 }

require("dotenv").config();
main();

Hang on, we’re not done just yet :upside_down_face:

If you wanna simplify your code above and take full advantage of the web3.js library, you could simply use the contract send() method instead of crafting, signing and sending the raw transaction. Note that this is possible because we’ve unlocked the sender account with the web3 native web3.eth.accounts.wallet.add method.

So how does the full code changes:

const Web3 = require("web3");

async function main() {
    // Configuring the connection to an Ethereum node
    const network = process.env.ETHEREUM_NETWORK;
    const web3 = new Web3(
        new Web3.providers.HttpProvider(
            `https://${network}.infura.io/v3/${process.env.INFURA_PROJECT_ID}`
        )
    );

    var fs = require('fs');
    var jsonFile = "ct_abi.json";

    var parsed=JSON.parse(fs.readFileSync(jsonFile));
    var abi = parsed.abi;

    const tokenAddress = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB";
    const toAddress = "0xF1B792820b52e6503208CAb98ec0B7b89ac64D6A"

    // Creating a signing account from a private key
    const signer = web3.eth.accounts.privateKeyToAccount(
        process.env.SIGNER_PRIVATE_KEY
    );
    web3.eth.accounts.wallet.add(signer);

    const contract = new web3.eth.Contract(abi, tokenAddress, { from: signer.address } )
    let amount = web3.utils.toHex(web3.utils.toWei("1"));

    contract.methods.transfer(toAddress, amount).send({
        from: signer.address,
        gas: 5000000
    }).then(console.log).catch(console.error);

 }

require("dotenv").config();
main();

I hope that you’ve enjoyed it, see you next time !

3 Likes