Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/circlefin/evm-cctp-contracts/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This guide demonstrates how to transfer USDC from Ethereum Sepolia testnet to Avalanche Fuji testnet using Circle’s Cross-Chain Transfer Protocol (CCTP).

Prerequisites

  • Node.js installed
  • Web3.js library
  • Private keys for source and destination addresses
  • USDC on the source chain (Sepolia testnet)

Testnet Contract Addresses

Ethereum Sepolia

const ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS = '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5';
const USDC_ETH_CONTRACT_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238';
const ETH_MESSAGE_CONTRACT_ADDRESS = '0x80537e4e8bab73d21096baa3a8c813b45ca0b7c9';

Avalanche Fuji

const AVAX_MESSAGE_TRANSMITTER_CONTRACT_ADDRESS = '0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79';
const AVAX_DESTINATION_DOMAIN = 1;

Setup

1

Install Dependencies

npm install web3 dotenv
2

Configure Environment

Create a .env file with the following variables:
ETH_TESTNET_RPC=<ETH_TESTNET_RPC_URL>
AVAX_TESTNET_RPC=<AVAX_TESTNET_RPC_URL>
ETH_PRIVATE_KEY=<ORIGINATING_ADDRESS_PRIVATE_KEY>
AVAX_PRIVATE_KEY=<RECIPIENT_ADDRESS_PRIVATE_KEY>
RECIPIENT_ADDRESS=<RECIPIENT_ADDRESS_FOR_AVAX>
AMOUNT=<AMOUNT_TO_TRANSFER>

Transfer Process

The cross-chain transfer involves 5 main steps:

Step 1: Approve Token Messenger

Approve the TokenMessenger contract to withdraw USDC from your Ethereum address:
const { Web3 } = require('web3');
const web3 = new Web3(process.env.ETH_TESTNET_RPC);

// Initialize signer
const ethSigner = web3.eth.accounts.privateKeyToAccount(process.env.ETH_PRIVATE_KEY);
web3.eth.accounts.wallet.add(ethSigner);

// Initialize USDC contract
const usdcEthContract = new web3.eth.Contract(
  usdcAbi, 
  USDC_ETH_CONTRACT_ADDRESS, 
  { from: ethSigner.address }
);

// Approve
const approveTxGas = await usdcEthContract.methods
  .approve(ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS, amount)
  .estimateGas();

const approveTx = await usdcEthContract.methods
  .approve(ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS, amount)
  .send({ gas: approveTxGas });

Step 2: Burn USDC on Source Chain

Call depositForBurn on the TokenMessenger contract:
// Initialize TokenMessenger contract
const ethTokenMessengerContract = new web3.eth.Contract(
  tokenMessengerAbi,
  ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS,
  { from: ethSigner.address }
);

// Convert recipient address to bytes32
const ethMessageContract = new web3.eth.Contract(
  messageAbi,
  ETH_MESSAGE_CONTRACT_ADDRESS,
  { from: ethSigner.address }
);

const destinationAddressInBytes32 = await ethMessageContract.methods
  .addressToBytes32(mintRecipient)
  .call();

// Burn USDC
const burnTxGas = await ethTokenMessengerContract.methods
  .depositForBurn(
    amount,
    AVAX_DESTINATION_DOMAIN,
    destinationAddressInBytes32,
    USDC_ETH_CONTRACT_ADDRESS
  )
  .estimateGas();

const burnTx = await ethTokenMessengerContract.methods
  .depositForBurn(
    amount,
    AVAX_DESTINATION_DOMAIN,
    destinationAddressInBytes32,
    USDC_ETH_CONTRACT_ADDRESS
  )
  .send({ gas: burnTxGas });

Step 3: Retrieve Message Bytes

Extract the messageBytes from the MessageSent event:
const transactionReceipt = await web3.eth.getTransactionReceipt(
  burnTx.transactionHash
);

// Find MessageSent event
const eventTopic = web3.utils.keccak256('MessageSent(bytes)');
const log = transactionReceipt.logs.find((l) => l.topics[0] === eventTopic);

// Decode message bytes
const messageBytes = web3.eth.abi.decodeParameters(['bytes'], log.data)[0];
const messageHash = web3.utils.keccak256(messageBytes);

console.log(`MessageHash: ${messageHash}`);

Step 4: Poll for Attestation

Request attestation from Circle’s attestation service:
let attestationResponse = { status: 'pending' };

while (attestationResponse.status !== 'complete') {
  const response = await fetch(
    `https://iris-api-sandbox.circle.com/attestations/${messageHash}`
  );
  attestationResponse = await response.json();
  
  await new Promise(r => setTimeout(r, 2000));
}

const attestationSignature = attestationResponse.attestation;
console.log(`Attestation: ${attestationSignature}`);
The attestation service is rate-limited. Please limit your requests to less than 1 per second.

Step 5: Receive Message on Destination Chain

Call receiveMessage on the destination chain’s MessageTransmitter:
// Switch to Avalanche network
web3.setProvider(process.env.AVAX_TESTNET_RPC);

// Initialize AVAX signer
const avaxSigner = web3.eth.accounts.privateKeyToAccount(
  process.env.AVAX_PRIVATE_KEY
);
web3.eth.accounts.wallet.add(avaxSigner);

// Initialize MessageTransmitter contract
const avaxMessageTransmitterContract = new web3.eth.Contract(
  messageTransmitterAbi,
  AVAX_MESSAGE_TRANSMITTER_CONTRACT_ADDRESS,
  { from: avaxSigner.address }
);

// Receive message
const receiveTxGas = await avaxMessageTransmitterContract.methods
  .receiveMessage(messageBytes, attestationSignature)
  .estimateGas();

const receiveTx = await avaxMessageTransmitterContract.methods
  .receiveMessage(messageBytes, attestationSignature)
  .send({ gas: receiveTxGas });

const receiveTxReceipt = await waitForTransaction(web3, receiveTx.transactionHash);
console.log('Transfer complete!');

Complete Example

Here’s the complete transfer script:
require('dotenv').config();
const { Web3 } = require('web3');

const main = async () => {
  const web3 = new Web3(process.env.ETH_TESTNET_RPC);
  
  // Setup signers
  const ethSigner = web3.eth.accounts.privateKeyToAccount(
    process.env.ETH_PRIVATE_KEY
  );
  web3.eth.accounts.wallet.add(ethSigner);
  
  const avaxSigner = web3.eth.accounts.privateKeyToAccount(
    process.env.AVAX_PRIVATE_KEY
  );
  web3.eth.accounts.wallet.add(avaxSigner);
  
  // Contract addresses and configuration
  const ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS = 
    '0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5';
  const USDC_ETH_CONTRACT_ADDRESS = 
    '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238';
  const AVAX_MESSAGE_TRANSMITTER_CONTRACT_ADDRESS = 
    '0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79';
  const AVAX_DESTINATION_DOMAIN = 1;
  
  // Initialize contracts
  const ethTokenMessengerContract = new web3.eth.Contract(
    tokenMessengerAbi,
    ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS,
    { from: ethSigner.address }
  );
  
  const usdcEthContract = new web3.eth.Contract(
    usdcAbi,
    USDC_ETH_CONTRACT_ADDRESS,
    { from: ethSigner.address }
  );
  
  // Transfer amount
  const amount = process.env.AMOUNT;
  
  // Step 1: Approve
  console.log('Step 1: Approving USDC...');
  const approveTx = await usdcEthContract.methods
    .approve(ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS, amount)
    .send();
  
  // Step 2: Burn
  console.log('Step 2: Burning USDC...');
  const burnTx = await ethTokenMessengerContract.methods
    .depositForBurn(
      amount,
      AVAX_DESTINATION_DOMAIN,
      destinationAddressInBytes32,
      USDC_ETH_CONTRACT_ADDRESS
    )
    .send();
  
  // Step 3: Get message hash
  console.log('Step 3: Retrieving message hash...');
  const messageHash = getMessageHashFromTx(burnTx);
  
  // Step 4: Fetch attestation
  console.log('Step 4: Polling for attestation...');
  const attestation = await pollForAttestation(messageHash);
  
  // Step 5: Receive on destination
  console.log('Step 5: Receiving USDC on destination chain...');
  web3.setProvider(process.env.AVAX_TESTNET_RPC);
  const receiveTx = await avaxMessageTransmitterContract.methods
    .receiveMessage(messageBytes, attestation)
    .send();
  
  console.log('Transfer complete!');
};

main();

Running the Script

node index.js

Domain IDs

ChainDomain ID
Ethereum0
Avalanche1
Arbitrum3
Optimism2
Base6
Polygon7

Troubleshooting

Approval Fails

  • Check that you have sufficient USDC balance
  • Verify the token messenger contract address
  • Ensure your wallet has enough ETH for gas

Burn Transaction Fails

  • Verify approval was successful
  • Check that amount is within burn limits
  • Ensure destination domain is valid

Attestation Not Available

  • Wait longer (attestations typically take 10-20 seconds)
  • Verify the message hash is correct
  • Check that the burn transaction was successful

Receive Message Fails

  • Ensure attestation is complete
  • Verify message hasn’t already been received (check nonce)
  • Check that destination contract address is correct

Next Steps

Build docs developers (and LLMs) love