Transferring Tokens (ERC-20)

    This section will walk you through on how to transfer ERC-20 tokens. To learn how to transfer other types of tokens that are non-ERC-20 compliant check out the section on smart contracts to learn how to interact with smart contracts.

    To transfer ERC-20 tokens, we’ll need to broadcast a transaction to the blockchain just like before, but with a few changed parameters:

    • Instead of setting a for the broadcasted transaction, we’ll need to embed the value of tokens to transfer in the data send in the transaction.
    • Construct a contract function call and embed it in the data field of the transaction we’re broadcasting to the blockchain.

    We’ll assume that you’ve already completed the previous , and have a Go application that has:

    1. Connected a client.
    2. Loaded your account private key.
    3. Configured the gas price to use for your transaction.

    You can create a token using the Token Factory https://tokenfactory.surge.sh, a website for conveniently deploying ERC-20 token contracts, to follow the examples in this guide.

    When you create your ERC-20 Token, be sure to note down the address of the token contract.

    For demonstration purposes, I’ve created a token (HelloToken HTN) using the Token Factory and deployed it to the Rinkeby testnet at the token contract address 0x28b149020d2152179873ec60bed6bf7cd705775d.

    You can check it out with a Web3-enabled browser here (make sure to be connected to the Rinkeby testnet in MetaMask):

    First, we’ll set a few variables.

    Set the value of the transaction to 0.

    This value is the amount of ETH to be transferred for this transaction, which should be 0 since we’re transferring ERC-20 Tokens and not ETH. We’ll set the value of Tokens to be transferred in the data field later.

    1. toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")

    Now the fun part. We’ll need to figure out what goes into the data field of the transaction. This is the message that we broadcast to the blockchain as part of the transaction.

    To make a token transfer, we need to use this data field to invoke a function on the smart contract. For more information on the functions available on an ERC-20 token contract, see the ERC-20 Token Standard specification.

    To transfer tokens from our active account to another, we need to invoke the transfer() function in our ERC-20 token in our transactions data field. We do this by doing the following:

    1. Figure out the function signature of the transfer() smart contract function we’ll be calling.
    2. Figure out the inputs for the function — the address of the token recipients, and the value of tokens to be transferred.
    3. Get the first 8 characters (4 bytes) of the Keccak256 hash of that function signature. This is the method ID of the contract function we’re invoking.
    4. Zero-pad (on the left) the inputs of our function call — the address and value. These input values need to be 256-bits (32 bytes) long.

    First, let’s assign the token contract address to a variable.

    1. tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")

    Next, we need to form the smart contract function call. The signature of the function we’ll be calling is the transfer() function in the ERC-20 specification, and the types of the argument we’ll be passing to it. The first argument type is address (the address to which we’re sending tokens), and the second argument’s type is uint256 (the amount of tokens to send). The result is the string transfer(address,uint256) (no spaces!).

    We need this function signature as a byte slice, which we assign to transferFnSignature:

    1. transferFnSignature := []byte("transfer(address,uint256)") // do not include spaces in the string

    We then need to get the methodID of our function. To do this, we’ll import the crypto/sha3 to generate the Keccak256 hash of the function signature. The first 4 bytes of the resulting hash is the methodID:

    Next we’ll zero pad (to the left) the account address we’re sending tokens. The resulting byte slice must be 32 bytes long:

    1. paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    2. fmt.Println(hexutil.Encode(paddedAddress)) // 0x0000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d

    Next we’ll set the value tokens to send as a *big.Int number. Note that the denomination used here is determined by the token contract that you’re interacting with, and not in ETH or wei.

    For example, if we were working with TokenA where 1 token is set as the smallest unit of TokenA (i.e. the decimal() value of the token contract is 0; for more information, see the ), then amount := big.NewInt(1000) would set amount to 1000 units of TokenA.

    1. amount := new(big.Int)
    2. amount.SetString("1000000000000000000000", 10) // sets the value to 1000 tokens, in the token denomination

    There are utility functions available in the utils section to easily do these conversions.

    Left padding to 32 bytes will also be required for the amount since the EVM use 32 byte wide data structures.

    1. paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
    2. fmt.Println(hexutil.Encode(paddedAmount)) // 0x00000000000000000000000000000000000000000000003635c9adc5dea00000

    Now we concanate the method ID, padded address, and padded amount into a byte slice that will be our data field.

    The gas limit will depend on the size of the transaction data and computational steps that the smart contract has to perform. Fortunately the client provides the EstimateGas method which is able to esimate the gas for us based on the most recent state of the blockchain. This function takes a CallMsg struct from the ethereum package where we specify the data and the address of the token contract to which we’re sending the function call message. It’ll return the estimated gas limit units we’ll use to generate the complete transaction.

    1. gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
    2. To: &tokenAddress,
    3. Data: data,
    4. })
    5. if err != nil {
    6. log.Fatal(err)
    7. }
    8. fmt.Println(gasLimit) // 23256

    NOTE: The gas limit set by the EstimateGas() method is based on the current state of the blockchain, and is just an estimate. If your transactions are constantly failing, or if you prefer to have full control over the amount of gas your application spends, you may want to set this value manually.

    Now we have all the information we need to generate the transaction.

    We’ll create a transaction similar the one we used in , EXCEPT that the to field should contain the token smart contract address, and the value field should be set to 0 since we’re not transferring ETH. This is a gotcha that confuses people.

    1. tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)

    The next step is to sign the transaction with the private key of the sender. The method requires the EIP155 signer, which we derive the chain ID from the client.

    1. chainID, err := client.NetworkID(context.Background())
    2. if err != nil {
    3. log.Fatal(err)
    4. }
    5. signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    6. if err != nil {
    7. log.Fatal(err)
    8. }

    And finally, broadcast the transaction:

    You can check the progress on Etherscan: https://rinkeby.etherscan.io/tx/0xa56316b637a94c4cc0331c73ef26389d6c097506d581073f927275e7a6ece0bc

    1. package main
    2. import (
    3. "crypto/ecdsa"
    4. "fmt"
    5. "log"
    6. "math/big"
    7. "golang.org/x/crypto/sha3"
    8. "github.com/ethereum/go-ethereum"
    9. "github.com/ethereum/go-ethereum/common"
    10. "github.com/ethereum/go-ethereum/common/hexutil"
    11. "github.com/ethereum/go-ethereum/core/types"
    12. "github.com/ethereum/go-ethereum/crypto"
    13. "github.com/ethereum/go-ethereum/ethclient"
    14. )
    15. func main() {
    16. client, err := ethclient.Dial("https://rinkeby.infura.io")
    17. if err != nil {
    18. log.Fatal(err)
    19. }
    20. privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
    21. if err != nil {
    22. log.Fatal(err)
    23. }
    24. publicKey := privateKey.Public()
    25. publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    26. if !ok {
    27. log.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
    28. }
    29. fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    30. nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    31. if err != nil {
    32. log.Fatal(err)
    33. }
    34. value := big.NewInt(0) // in wei (0 eth)
    35. gasPrice, err := client.SuggestGasPrice(context.Background())
    36. log.Fatal(err)
    37. }
    38. toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    39. tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")
    40. hash := sha3.NewLegacyKeccak256()
    41. hash.Write(transferFnSignature)
    42. methodID := hash.Sum(nil)[:4]
    43. fmt.Println(hexutil.Encode(methodID)) // 0xa9059cbb
    44. paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    45. fmt.Println(hexutil.Encode(paddedAddress)) // 0x0000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d
    46. amount := new(big.Int)
    47. amount.SetString("1000000000000000000000", 10) // sets the value to 1000 tokens, in the token denomination
    48. paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
    49. fmt.Println(hexutil.Encode(paddedAmount)) // 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
    50. var data []byte
    51. data = append(data, methodID...)
    52. data = append(data, paddedAddress...)
    53. data = append(data, paddedAmount...)
    54. gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
    55. To: &tokenAddress,
    56. Data: data,
    57. })
    58. if err != nil {
    59. log.Fatal(err)
    60. }
    61. fmt.Println(gasLimit) // 23256
    62. tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
    63. chainID, err := client.NetworkID(context.Background())
    64. if err != nil {
    65. log.Fatal(err)
    66. }
    67. signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    68. if err != nil {
    69. log.Fatal(err)
    70. }
    71. err = client.SendTransaction(context.Background(), signedTx)
    72. if err != nil {
    73. log.Fatal(err)
    74. }