Skip to main content

Overview

The IERC20 binding provides Go functions to interact with ERC20 token contracts (like USDC). This binding uses the abigen V2 pattern with bind.Call and bind.Transact for type-safe contract interactions. Primary use cases in CCTP:
  • Checking USDC balance before initiating a transfer
  • Approving TokenMessenger to spend USDC
  • Verifying allowance before burning tokens

Creating a Binding

NewIERC20

Creates a new IERC20 binding instance.
import "github.com/circlefin/cctp-go/tokenmessenger"

// Create the binding
ierc20 := tokenmessenger.NewIERC20()
Returns:
  • *IERC20 - A new binding instance
Note: This only creates the ABI wrapper. You must call Instance() to create a bound contract instance.

Creating Contract Instances

Instance

Creates a wrapper for a deployed ERC20 token contract at the given address.
import (
    "github.com/circlefin/cctp-go/tokenmessenger"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
)

client, _ := ethclient.Dial("https://eth-mainnet.g.alchemy.com/v2/...")
ierc20 := tokenmessenger.NewIERC20()

// Create bound contract instance for USDC
usdcAddress := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
instance := ierc20.Instance(client, usdcAddress)
Parameters:
backend
bind.ContractBackend
required
Ethereum client connection
addr
common.Address
required
ERC20 token contract address (e.g., USDC address)
Returns:
  • *bind.BoundContract - Contract instance for use with bind.Call and bind.Transact

Main Functions

PackAllowance

Packs parameters for checking the allowance granted to a spender.
import (
    "github.com/circlefin/cctp-go/tokenmessenger"
    "github.com/ethereum/go-ethereum/common"
)

ierc20 := tokenmessenger.NewIERC20()

// Pack the allowance call
allowanceData := ierc20.PackAllowance(
    ownerAddr,       // Token owner address
    spenderAddr,     // Spender address (e.g., TokenMessenger)
)
Parameters:
owner
common.Address
required
Address of the token owner
spender
common.Address
required
Address that has been granted spending permission
Returns:
  • []byte - Packed call data

UnpackAllowance

Unpacks the allowance amount returned from the contract.
allowance, err := ierc20.UnpackAllowance(data)
if err != nil {
    log.Fatal(err)
}
log.Printf("Allowance: %s", allowance.String())
Parameters:
data
[]byte
required
Return data from the allowance call
Returns:
  • *big.Int - Current allowance amount
  • error - Error if unpacking fails

PackApprove

Packs parameters for approving a spender to transfer tokens.
import "math/big"

ierc20 := tokenmessenger.NewIERC20()

// Pack the approve call
approveData := ierc20.PackApprove(
    spenderAddr,              // Address to approve (e.g., TokenMessenger)
    big.NewInt(1000000),      // Amount to approve (1 USDC)
)
Parameters:
spender
common.Address
required
Address to grant spending permission to
amount
*big.Int
required
Amount of tokens to approve (in token’s smallest unit)
Returns:
  • []byte - Packed transaction data

UnpackApprove

Unpacks the success boolean returned from an approve call.
success, err := ierc20.UnpackApprove(data)
if err != nil {
    log.Fatal(err)
}
if success {
    log.Println("Approval successful")
}

PackBalanceOf

Packs parameters for checking an account’s token balance.
ierc20 := tokenmessenger.NewIERC20()

// Pack the balanceOf call
balanceData := ierc20.PackBalanceOf(accountAddr)
Parameters:
account
common.Address
required
Address to check balance for
Returns:
  • []byte - Packed call data

UnpackBalanceOf

Unpacks the balance amount returned from the contract.
balance, err := ierc20.UnpackBalanceOf(data)
if err != nil {
    log.Fatal(err)
}
log.Printf("Balance: %s USDC", new(big.Float).Quo(
    new(big.Float).SetInt(balance),
    big.NewFloat(1000000),
))
Parameters:
data
[]byte
required
Return data from the balanceOf call
Returns:
  • *big.Int - Account balance
  • error - Error if unpacking fails

Complete Workflow: Checking Allowance and Approving

1

Create binding and instance

ierc20 := tokenmessenger.NewIERC20()
usdcAddress := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
instance := ierc20.Instance(client, usdcAddress)
2

Check current allowance

import "github.com/ethereum/go-ethereum/accounts/abi/bind/v2"

// Pack and call allowance
allowance, err := bind.Call(instance, &bind.CallOpts{Context: ctx},
    ierc20.PackAllowance(walletAddr, tokenMessengerAddr),
    ierc20.UnpackAllowance,
)
if err != nil {
    log.Fatal(err)
}
log.Printf("Current allowance: %s", allowance.String())
3

Approve if needed

if allowance.Cmp(amountToTransfer) < 0 {
    // Create transaction options
    auth, _ := bind.NewKeyedTransactorWithChainID(privateKey, chainID)

    // Pack and send approve transaction
    approveTx, err := bind.Transact(
        instance,
        auth,
        ierc20.PackApprove(tokenMessengerAddr, amountToTransfer),
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Approve tx: %s", approveTx.Hash().Hex())
}
4

Wait for confirmation

receipt, err := bind.WaitMined(ctx, client, approveTx.Hash())
if err != nil {
    log.Fatal(err)
}
log.Println("Approval confirmed")
5

Verify new allowance

newAllowance, err := bind.Call(instance, &bind.CallOpts{Context: ctx},
    ierc20.PackAllowance(walletAddr, tokenMessengerAddr),
    ierc20.UnpackAllowance,
)
if err != nil {
    log.Fatal(err)
}
if newAllowance.Cmp(amountToTransfer) < 0 {
    log.Fatal("Allowance not updated correctly")
}

Usage Examples from SDK

Checking Allowance

From transfer.go:158-164:
// Create USDC contract instance for allowance and approval operations
ierc20 := tokenmessenger.NewIERC20()
usdcAddress := common.HexToAddress(t.params.SourceChain.USDC)
usdcInstance := ierc20.Instance(t.sourceClient, usdcAddress)

// Use V2 bindings to check allowance
allowance, err := bind.Call(usdcInstance, &bind.CallOpts{Context: ctx},
    ierc20.PackAllowance(t.wallet.Address, tokenMessengerAddr), ierc20.UnpackAllowance)

Approving Tokens

From transfer.go:182-186:
// Use V2 bindings to send approve transaction
approveTx, err := bind.Transact(usdcInstance, auth, ierc20.PackApprove(tokenMessengerAddr, t.params.Amount))
if err != nil {
    return fmt.Errorf("failed to send approve tx: %w", err)
}
USDC DecimalsUSDC uses 6 decimals:
  • 1 USDC = 1,000,000 (smallest unit)
  • 0.5 USDC = 500,000
  • 100 USDC = 100,000,000
Always use big.Int for token amounts to avoid precision loss.

V2 Binding Pattern Benefits

Using the V2 binding pattern with bind.Call and bind.Transact:

For Read Operations (allowance, balanceOf):

result, err := bind.Call(
    instance,
    &bind.CallOpts{Context: ctx},
    ierc20.PackAllowance(owner, spender),
    ierc20.UnpackAllowance,
)
Benefits:
  • Type-safe return values
  • Automatic error handling
  • Context support for timeouts
  • No gas required

For Write Operations (approve):

tx, err := bind.Transact(
    instance,
    auth,
    ierc20.PackApprove(spender, amount),
)
Benefits:
  • Automatic gas estimation
  • Nonce management
  • Transaction signing
  • Receipt retrieval with bind.WaitMined

Error Handling

Common Errors
  • “insufficient allowance”: Need to call approve before depositForBurn
  • “insufficient balance”: Account doesn’t have enough tokens
  • “ERC20: approve from zero address”: Invalid owner address
  • “ERC20: approve to zero address”: Invalid spender address
Always verify balances and allowances before attempting transfers.

TryPackAllowance, TryPackApprove, TryPackBalanceOf

Non-panicking versions that return errors:
allowanceData, err := ierc20.TryPackAllowance(owner, spender)
if err != nil {
    log.Fatal(err)
}

approveData, err := ierc20.TryPackApprove(spender, amount)
if err != nil {
    log.Fatal(err)
}

balanceData, err := ierc20.TryPackBalanceOf(account)
if err != nil {
    log.Fatal(err)
}

See Also

Build docs developers (and LLMs) love