Swapping
Integrations
Advanced Scenarios
Cross-Chain Messaging
To EVM

CCM Swaps to EVM Chains

EVM Considerations

Besides the general implementation checklist, there are some specific considerations for EVM chains.

  • The gas limit on the destination chain (EVM) will be capped to 10 million gas.
  • The receiver contract on the destination chain must implement the cfReceive function specified in this section.
  • If the amount of gas provided is not enough to cover the gas costs of the receiver's logic on the destination chain, the transaction won't be broadcasted. An example of a gas estimation is provided below.
  • In the event of a transaction not being broadcasted due to insufficient gas, the signed payload will be available for the user to broadcast as described here.

CCM Receiver Smart contract interface

Chainflip's Vault will transfer the destination token amount to the specified address either within the same call (for native token) or by transferring the ERC20 token to it. Then it will call the destination address with the following parameters:

ParamDescriptionData Type
srcChainSource chain ID for the swap.uint32
srcAddressAddress that initiated the swap on the source chain. Addresses are encoded into a bytes type.address
messageMessage that is passed to the destination address on the destination chain..bytes
tokenAddress of the token transferred to the receiver. A value of 0xEeee...eeEEeE represents the native token.address
amountAmount of the destination token transferred to the receiver. If it's the native token, the amount value will equal the msg.value.uint

In order for a contract to be a valid receiver it must implement the Solidity function signature below on the destination address.

function cfReceive(
    uint32 srcChain,
    bytes calldata srcAddress,
    bytes calldata message,
    address token,
    uint256 amount
) external payable;

It's the receiver's responsibility to correctly implement the function's interface and to ensure the call doesn't revert. If the receiver can't guarantee that the receiving logic won't revert, it is recommended to use try/catch-like structure to handle the reversion. This is to avoid the full transaction reverting and therefore the tokens failing to be transferred.

IMPORTANT! Chainflip will transfer tokens to the receiver and then make the call. For ERC-20 tokens, the logic has to assume the amount has been transferred. An attacker could call this function and fake the transfer, exploiting the receiver. We strongly suggest that only the Chainflip Vault can call your function, unless you're transferring all tokens out of the receiver in the same call (like DEX Aggregators).

Here is an example of the function with the adequate access control:

contract CFReceiver {
 
    function cfReceive(
        uint32 srcChain,
        bytes calldata srcAddress,
        bytes calldata message,
        address token,
        uint256 amount
    ) external payable {
        require(msg.sender == cfVault, "CFReceiver: caller not CF Vault");
    }
 
    ...
}

You can find an example of an implementation of a receiver contract with the mentioned interface here (opens in a new tab). The contract also has other logic that is not necessary like the cfReceivexCall (currently unsupported) or the logic to update the cfVault address.

If the receiver is not a contract, doesn't have the specified interface or the logic in the receiver reverts, the Chainflip protocol will not submit the transaction to the destination chain. This may result in a loss of funds.

Gas budget estimation

The gas budget represents the amount of gas required by the user logic on the destination chain. The Chainflip protocol will automatically add the gas requirements for the overhead of the on-chain transaction verification and call.

The simplest way to estimate the gas needed on the destination chain is to simulate the execution of the receiver's logic with a direct call to the receiver contract with the parameters expected from the Chainflip call. The base 21k gas can be subtracted as it will automatically be added by the protocol as part of the overhead.

An overestimation, overestimationRatio, is advised due to the not fully predictable nature of gas usage in EVM as well as to avoid transactions running out of gas. Currently Chainflip does not issue refunds of unspent gas.

import Web3 from 'web3';
 
const web3 = new Web3('your-endpoint');
 
const EVM_BASE_GAS_LIMIT = 21000;
// Use any framework you want (Brownie, Forge etc..) to estimate instead.
// In this example just using ABI encoded data call to `cfReceive` with the CCM message, srcChain...
const data = '0x4904ac5f0000...';
// Estimate needs to be done using "from: vault" to prevent logic reversion
const userGasEstimation = (await web3.eth.estimateGas({ data, to: usersReceiverContractAddress, from: ChainflipVaultAddress})) - EVM_BASE_GAS_LIMIT;
const overestimationRatio = 1.1;
const gasBudget = userGasEstimation * overestimationRatio;

Arbitrum uses the same eth_estimateGas for gas estimations as described in the Arbitrum docs (opens in a new tab) and it also accounts for the gas units needed to pay to the L1. That can fluctuate over time. Therefore it's recommended to always run a new gas estimation and use a slightly higher overestimation ratio.

Gas budget estimation example

  • User wants to swap BTC to ETH, destination chain being Ethereum, with a CCM call to a receiver contract.
  • Estimate of the gas needed to execute the receiver's logic on the destination chain with the corresponding message to be passed via eth_estimateGas
  • Apply the desired overestimation ratio,
  • Start a swap in Chainflip with the calculated gasBudget amount.

BTC to ETH (Ethereum) Example

  1. A user has BTC on the Bitcoin blockchain and wants to swap it to ETH on Ethereum blockchain and then execute some logic on a smart contract.
  2. The user requests a bitcoin deposit address as described here (only when the source chain is EVM-compatible, this can be initiated via a smart contract call).
  3. The user defines, as part of requesting a deposit address, two additional parameters: message and gasBudget. The message can be an arbitrary HEX-encoded sequence of bytes.
  4. The user transfers an amount to swap.
  5. Chainflip will swaps the input amount to the destination asset ('ETH'). It will then estimate how much gas the transaction needs taking into account the transaction overhead and the user's gas budget.
  6. The Chainflip protocol will then subtract from the final amount the destination asset amount needed to pay for the gas costs of the transaction on the destination chain.
  7. After the threshold signature is completed, a transaction originating from the Vault contract transfers the destination asset to the specified receiver address, on the destination chain, and makes a call to that address with a specific interface passing the user's message. The gas limit on that call is set according to the gasBudget swap output and the current gas price.
  8. The receiver contract executes its logic, which can entail decoding the received message from the source chain and execute logic accordingly, i.e. swap ETH into <any-long-tail-asset> in a Uniswap pool.

CCM broadcast failure

While Chainflip will do it's best to broadcast and successfully execute any swap with cross-chain messaging, there are some factors that can make the swap fail that are external to Chainflip. Here are some examples.

  • The gas budget provided is not enough to cover the gas costs of the receiver's logic on the destination chain.
  • The receiver contract not implementing the correct interface (or being an EOA).
  • The logic executed on the receiver's contract reverts.

Chainflip will use TSS to sign over a valid payload for the destination chain and try to broadcast it. If a transaction can't be broadcasted for any reason, the signed payload will be publicly accessible on the Chainflip State Chain for the user. The user can then sign over the payload and broadcast the transaction.

If the transaction reverts due to not enough gas, the user can sign that payload and broadcast it with a higher amount of gas. If it's due to the receiving logic reverting, the user can try to modify the state of the receiving contract before broadcasting it again. The payload will be valid until two key rotations have taken place.

Below you can find a very simple example on how to broadcast a signed payload that has failed to broadcast due to insufficient gas. It users ethers sendTransaction to automatically estimate the required gas, send it and broadcast it.

import { ethers, Wallet } from "ethers";
 
const connection = new ethers.JsonRpcProvider(<your-rpc-provider>);
const wallet = new Wallet(<your-keys>);
const signer = wallet.connect(connection);
const tx = {
    from:  wallet.address,
    to:    "<signed-transaction-payload-contract>", // "0x..."
    value: ethers.parseUnits('0', 'ether'),
    data:  "<signed-transaction-payload-data>"      // "0x..."
};
 
await signer.sendTransaction(tx);

You will find the "Signed transaction payload" in the Swaps page as part of the Broadcast Status. Only the contract, data and value fields are needed.

Signed Transaction Payload

The signed payload can be broadcasted with different gas settings but it can't modify any value in that payload (data) such as the message. Therefore, always make sure to test the receiving logic and ensure it won't revert before initiating a swap.

CCM Additional Data

For swaps with CCM attached on the EVM chains, the ccm_additional_data is not used by the Smart Contract call. This means that when submitting swaps, the ccm_additional_data field in the channel_metadata must be left empty. Passing a non-empty field will cause the swap to be rejected.