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
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:
Address of the token owner
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:
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:
Address to grant spending permission to
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:
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:
Return data from the balanceOf call
Returns:
*big.Int - Account balance
error - Error if unpacking fails
Complete Workflow: Checking Allowance and Approving
Create binding and instance
ierc20 := tokenmessenger.NewIERC20()
usdcAddress := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
instance := ierc20.Instance(client, usdcAddress)
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())
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())
}
Wait for confirmation
receipt, err := bind.WaitMined(ctx, client, approveTx.Hash())
if err != nil {
log.Fatal(err)
}
log.Println("Approval confirmed")
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