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:
- Sending a native USDC token cross-chain.
- 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:
- MessageTransmitter contract – to mint USDC at the destination chain.
- CircleBridge contract – to burn USDC at the source chain.
- 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:
- Burn the given amount of USDC by calling the function
depositForBurn
at theTokenMessenger
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.
CrosschainNativeSwap.sol
// 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;
constructor(
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);
tokenMessenger.depositForBurn(
amount,
this.circleDestinationDomains(destinationChain),
bytes32(uint256(uint160(recipient))),
address(usdc)
);
}
}
ITokenMessenger.sol
// 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
domain
* @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.
- Upgrade our contract to include business logic and integrate with Axelar network.
CrosschainNativeSwap.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import {