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:
Param | Description | Data Type |
---|---|---|
srcChain | Source chain ID for the swap. | uint32 |
srcAddress | Address that initiated the swap on the source chain. Addresses are encoded into a bytes type. | address |
message | Message that is passed to the destination address on the destination chain.. | bytes |
token | Address of the token transferred to the receiver. A value of 0xEeee...eeEEeE represents the native token. | address |
amount | Amount 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
- A user has
BTC
on the Bitcoin blockchain and wants to swap it toETH
on Ethereum blockchain and then execute some logic on a smart contract. - 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).
- The user defines, as part of requesting a deposit address, two additional parameters:
message
andgasBudget
. Themessage
can be an arbitrary HEX-encoded sequence of bytes. - The user transfers an amount to swap.
- 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.
- 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.
- 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 thegasBudget
swap output and the current gas price. - The receiver contract executes its logic, which can entail decoding the received
message
from the source chain and execute logic accordingly, i.e. swapETH
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.
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.