Skip to content
General Message Passing

Cross Chain Swaps with Circle's CCTP and Axelar

Circle has created CCTP (opens in a new tab) to natively transfer USDC across supported blockchains (opens in a new tab). In this tutorial, we’ll learn how to build a cross-chain USDC dApp using Circle’s Cross-Chain Transfer Protocol (CCTP) and Axelar’s General Message Passing (GMP).

With this approach, your users will be able to issue a single transaction with a GMP payload. On the backend, the application takes care of USDC bridging, plus any other action that the user wishes — as indicated in the payload. Axelar services, also working on the backend, can handle conversion and payment for destination-chain gas fees, so the user only has to transact once, using one gas token.

In this example, we will build a cross-chain swap dApp. It converts a native token from one chain to another chain, using native USDC as a routing asset. For example: send ETH to a contract on Ethereum and receive AVAX on Avalanche, or vice versa.

There are two parts we have to learn to achieve this:

  1. Sending a native USDC token cross-chain.
  2. Sending a swap payload cross-chain.

Part 1: Sending a native USDC token cross-chain

There are three components from Circle that we’ll use in this part:

  1. MessageTransmitter contract – to mint USDC at the destination chain.
  2. CircleBridge contract  – to burn USDC at the source chain.
  3. Attestation API – to retrieve attestation to be used for minting USDC at the destination chain.

Let’s take a look at how to implement this step-by-step:

  1. Burn the given amount of USDC by calling the function depositForBurnat the TokenMessenger contract. The example Solidity code is below. At this step, the contract does nothing except provide a function to burn the USDC in _depositForBurn function.


// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ITokenMessenger} from "./ITokenMessenger.sol";
import "./StringAddressUtils.sol";
contract CrosschainNativeSwap is AxelarExecutable, Ownable {
    IERC20 public usdc;
    ITokenMessenger public tokenMessenger;
    // mapping chain name => domain number;
    mapping(string => uint32) public circleDestinationDomains;
        address usdc_,
        address tokenMessenger_
    ) AxelarExecutable(gateway_) Ownable() {
        usdc = IERC20(usdc_);
        circleDestinationDomains["ethereum"] = 0;
        circleDestinationDomains["avalanche"] = 1;
    function _sendViaCCTP(
        uint256 amount,
        string memory destinationChain,
        address recipient
    ) private isValidChain(destinationChain) {
        IERC20(address(usdc)).approve(address(tokenMessenger), amount);


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface ITokenMessenger {
    // this event will be emitted when `depositForBurn` function is called.
    event MessageSent(bytes message);
    * @param _amount amount of tokens to burn
    * @param _destinationDomain destination domain
    * @param _mintRecipient address of mint recipient on destination domain
    * @param _burnToken address of contract to burn deposited tokens, on local
    * @return _nonce uint64, unique nonce for each burn
    function depositForBurn(
        uint256 _amount,
        uint32 _destinationDomain,
        bytes32 _mintRecipient,
        address _burnToken
    ) external returns (uint64 _nonce);

That's it for the initial contract using CCTP. We'll continue to add our business logic. Let's try to integrate this with Axelar network to complete our cross-chain swap dApp.

Part 2: Sending a swap payload cross-chain

In this part, we’ll add logic in our contract to send a payload cross-chain with Axelar network.

  1. Upgrade our contract to include business logic and integrate with Axelar network.


// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import {