Skip to main content

What You’ll Build

In this quickstart, you’ll execute a cross-chain USDC transfer from Ethereum Sepolia testnet to Avalanche Fuji testnet using Circle’s Cross-Chain Transfer Protocol (CCTP). You’ll learn how to interact with the CCTP smart contracts to burn USDC on the source chain and mint it on the destination chain.

Prerequisites

Before you begin, ensure you have:
  • Node.js installed on your system
  • USDC tokens on Ethereum Sepolia testnet
  • RPC endpoints for both Ethereum Sepolia and Avalanche Fuji
  • Private keys for both source and destination addresses
  • web3.js library installed (npm install web3)

Contract Addresses

The CCTP contracts are deployed on testnet at the following addresses:
  • Ethereum Sepolia
    • TokenMessenger: 0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5
    • USDC: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
  • Avalanche Fuji
    • MessageTransmitter: 0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79

Setup Environment

Create a .env file with the following variables:
ETH_TESTNET_RPC=<YOUR_ETH_SEPOLIA_RPC_URL>
AVAX_TESTNET_RPC=<YOUR_AVAX_FUJI_RPC_URL>
ETH_PRIVATE_KEY=<YOUR_SOURCE_ADDRESS_PRIVATE_KEY>
AVAX_PRIVATE_KEY=<YOUR_RECIPIENT_ADDRESS_PRIVATE_KEY>
RECIPIENT_ADDRESS=<YOUR_RECIPIENT_ADDRESS_ON_AVAX>
AMOUNT=<AMOUNT_TO_TRANSFER_IN_SMALLEST_UNIT>
1

Approve USDC

First, approve the TokenMessenger contract to spend your USDC tokens on Ethereum Sepolia.
const Web3 = require('web3');
const web3 = new Web3(process.env.ETH_TESTNET_RPC);

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

// USDC contract ABI (approve function)
const usdcAbi = [{
  "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}],
  "name": "approve",
  "outputs": [{"name": "", "type": "bool"}],
  "type": "function"
}];

const usdcEthContract = new web3.eth.Contract(usdcAbi, USDC_ETH_CONTRACT_ADDRESS);
const amount = process.env.AMOUNT;

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

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

console.log('Approval transaction:', approveTx.transactionHash);
2

Call depositForBurn

Initiate the cross-chain transfer by calling depositForBurn on the TokenMessenger contract. This burns USDC on Ethereum and emits a message for the destination chain.
const AVAX_DESTINATION_DOMAIN = 1; // Avalanche domain ID

// Convert recipient address to bytes32 format
const destinationAddressInBytes32 = web3.eth.abi.encodeParameter(
  'address',
  process.env.RECIPIENT_ADDRESS
);

// TokenMessenger contract ABI (depositForBurn function)
const tokenMessengerAbi = [{
  "inputs": [
    {"name": "amount", "type": "uint256"},
    {"name": "destinationDomain", "type": "uint32"},
    {"name": "mintRecipient", "type": "bytes32"},
    {"name": "burnToken", "type": "address"}
  ],
  "name": "depositForBurn",
  "outputs": [{"name": "nonce", "type": "uint64"}],
  "type": "function"
}];

const ethTokenMessengerContract = new web3.eth.Contract(
  tokenMessengerAbi,
  ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS
);

// Execute depositForBurn
const burnTx = await ethTokenMessengerContract.methods
  .depositForBurn(
    amount,
    AVAX_DESTINATION_DOMAIN,
    destinationAddressInBytes32,
    USDC_ETH_CONTRACT_ADDRESS
  )
  .send();

console.log('Burn transaction:', burnTx.transactionHash);
3

Retrieve Message Bytes from Logs

Extract the messageBytes from the MessageSent event emitted during the burn transaction. Hash these bytes to prepare for attestation.
// Get transaction receipt
const transactionReceipt = await web3.eth.getTransactionReceipt(
  burnTx.transactionHash
);

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

// Decode the message bytes from event data
const messageBytes = web3.eth.abi.decodeParameters(['bytes'], log.data)[0];

// Hash the message bytes
const messageHash = web3.utils.keccak256(messageBytes);

console.log('Message hash:', messageHash);
4

Poll Attestation Service

Query Circle’s attestation service using the message hash. The service will return a signature once the attestation is complete.
The attestation service is rate-limited. Please limit requests to less than 1 per second.
let attestationResponse = { status: 'pending' };

while (attestationResponse.status !== 'complete') {
  const response = await fetch(
    `https://iris-api-sandbox.circle.com/attestations/${messageHash}`
  );
  attestationResponse = await response.json();
  
  if (attestationResponse.status === 'complete') {
    console.log('Attestation received!');
    break;
  }
  
  // Wait 2 seconds before next poll
  await new Promise(r => setTimeout(r, 2000));
}

const attestationSignature = attestationResponse.attestation;
5

Call receiveMessage on Destination Chain

Finally, call receiveMessage on the MessageTransmitter contract on Avalanche Fuji to mint USDC on the destination chain.
const avaxWeb3 = new Web3(process.env.AVAX_TESTNET_RPC);
const AVAX_MESSAGE_TRANSMITTER_CONTRACT_ADDRESS = 
  '0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79';

// MessageTransmitter contract ABI (receiveMessage function)
const messageTransmitterAbi = [{
  "inputs": [
    {"name": "message", "type": "bytes"},
    {"name": "attestation", "type": "bytes"}
  ],
  "name": "receiveMessage",
  "outputs": [{"name": "success", "type": "bool"}],
  "type": "function"
}];

const avaxMessageTransmitterContract = new avaxWeb3.eth.Contract(
  messageTransmitterAbi,
  AVAX_MESSAGE_TRANSMITTER_CONTRACT_ADDRESS
);

// Execute receiveMessage with message bytes and signature
const receiveTx = await avaxMessageTransmitterContract.methods
  .receiveMessage(messageBytes, attestationSignature)
  .send();

console.log('Receive transaction:', receiveTx.transactionHash);
console.log('USDC successfully transferred to', process.env.RECIPIENT_ADDRESS);

Next Steps

Congratulations! You’ve successfully transferred USDC cross-chain using CCTP. Here are some things to explore next:
  • Try transferring between different chain pairs
  • Explore the contract interfaces in detail
  • Learn about the security model and attestation process
  • Integrate CCTP into your own applications

Build docs developers (and LLMs) love