In the world of Ethereum, transferring native ETH and ERC-20 tokens are fundamentally different processes. While sending ETH involves a straightforward transaction between externally owned accounts, token transfers require interacting with a smart contract. This guide will walk you through the process of transferring ERC-20 tokens programmatically using the Go programming language.
Before diving in, ensure you have a basic understanding of connecting to an Ethereum client, loading private keys, and setting gas prices. If these concepts are unfamiliar, please review the fundamentals of Ethereum transactions first. For interacting with non-ERC-20 tokens or custom smart contracts, you'll need to understand general smart contract interaction, which follows different patterns.
Prerequisites for Token Transfers
To transfer an ERC-20 token, you'll need:
- A connected Ethereum client (using a service like Infura or a local node)
- A loaded private key with sufficient ETH to pay for transaction gas
- The token contract address
- The recipient's address
- The amount of tokens you wish to send
Unlike ETH transfers, token transfers require setting the transaction value to 0 since you're not sending Ether itself, but rather instructing the token contract to update its internal ledger.
value := big.NewInt(0)Setting Up Address Variables
First, store the recipient's address in a variable:
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")Next, assign the token contract address to another variable. For demonstration purposes, we're using a test token (HelloToken HTN) deployed on the Rinkeby testnet:
tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")Constructing the Transaction Data
The most critical part of token transfer is constructing the transaction data field. This data tells the token contract which function to execute and with what parameters.
For ERC-20 transfers, we need to call the transfer function with two parameters: the recipient's address (type address) and the amount to send (type uint256).
We start by creating the function signature:
transferFnSignature := []byte("transfer(address,uint256)")We then generate the Keccak-256 hash of this signature and take the first 4 bytes to get the method ID:
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]The recipient address needs to be left-padded to 32 bytes:
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)The token amount must be formatted correctly. ERC-20 tokens use fixed-point arithmetic, so you need to account for the token's decimals. For example, if transferring 1000 tokens with 18 decimals:
amount := new(big.Int)
amount.SetString("1000000000000000000000", 10) // 1000 tokensThis amount also needs to be left-padded to 32 bytes:
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)Finally, we concatenate the method ID, padded address, and padded amount to form our data field:
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)Estimating Gas and Building the Transaction
Gas estimation is crucial for token transfers. The Ethereum client can estimate the required gas using the EstimateGas method:
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
To: &toAddress,
Data: data,
})Now we build the transaction. Note that the to field should be the token contract address, not the recipient's address—a common point of confusion:
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)Signing and Broadcasting the Transaction
The transaction must be signed with the sender's private key. This requires an EIP155 signer, which needs the chain ID:
chainID, err := client.NetworkID(context.Background())
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)Finally, broadcast the signed transaction to the network:
err = client.SendTransaction(context.Background(), signedTx)You can track your transaction on Etherscan using the returned transaction hash.
👉 Explore more blockchain development strategies
Complete Go Implementation
Here's the complete code for transferring ERC-20 tokens using Go:
package main
import (
"context"
"crypto/ecdsa"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("https://rinkeby.infura.io")
if err != nil {
log.Fatal(err)
}
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
if err != nil {
log.Fatal(err)
}
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
value := big.NewInt(0) // in wei (0 eth)
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")
transferFnSignature := []byte("transfer(address,uint256)")
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
amount := new(big.Int)
amount.SetString("1000000000000000000000", 10) // 1000 tokens
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
To: &toAddress,
Data: data,
})
if err != nil {
log.Fatal(err)
}
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("tx sent: %s", signedTx.Hash().Hex())
}Frequently Asked Questions
What's the difference between transferring ETH and ERC-20 tokens?
ETH transfers involve sending the native cryptocurrency directly between accounts, while ERC-20 token transfers require interacting with a smart contract that updates its internal balance mapping. ETH transfers use the transaction's value field, while token transfers use the data field to call contract functions.
Why do I need ETH to transfer ERC-20 tokens?
Although you're not sending ETH itself, every transaction on the Ethereum network requires gas fees paid in ETH. This compensates miners/validators for processing your transaction and executing the smart contract code.
How do I handle tokens with different decimal places?
ERC-20 tokens can have varying decimal places (typically 18, but ranging from 0 to 18). Always multiply the token amount by 10^decimals when formatting the transfer amount. You can usually find the decimal information through the token's contract or explorers.
What happens if my gas estimation is too low?
If your gas limit is set too low, the transaction may run out of gas and fail (reverting all changes), but you'll still lose the gas spent. Always use proper gas estimation techniques and consider adding a small buffer for network congestion.
Can I batch multiple token transfers?
While the standard ERC-20 transfer function only handles single transfers, you can create custom smart contracts that batch multiple transfers in a single transaction, potentially saving gas costs for multiple operations.
How do I confirm a token transfer was successful?
After broadcasting your transaction, wait for it to be included in a block. You can then check the transaction receipt for status (0 for failure, 1 for success). Alternatively, check the recipient's token balance using a blockchain explorer or by calling the balanceOf function on the token contract.