CCM Swaps to Solana
Solana Considerations
Besides the general implementation checklist, there are some specific considerations for Solana.
- The compute unit budget on Solana will be capped at 600k compute units.
- Some extra parameters are needed when initiating a CCM swap to Solana. See Solana parameters.
- There is a limit on the message length and the number of accounts that can be passed in a CCM swap as explained in transaction length limitation.
- The receiver program on the destination chain must implement the described interface.
- If the amount of gas provided is not enough to cover the costs of the receiver's logic on the destination chain, the transaction will revert on-chain. An example of a compute unit estimation is provided below.
- In the event of a transaction reverting due to insufficient gas the nonce will be consumed making the transaction invalid.
- Please pay extra attention to ensure that the transaction doesn't revert due to a lack of "compute units" or the receiver's logic.
- While Chainflip will do its best to successfully execute any swap with cross-chain messaging, CCM transactions that revert on Solana will result in funds not being egressed.
Fallback mechanism
In the event that a CCM transaction is reverted (usually due to an invalid CCM message or insufficient compute unit), assets won't be able to be egressed to the intended destination. The Fallback mechanism is there to ensure funds are safely returned. A fallback address is provided as part of the CCM Parameters(see below) and funds from failed CCM egresses will be automatically sent there via a "Transfer" transaction. This ensures the protection of user's funds, giving the user the peace of mind when using our CCM feature.
Solana CCM Parameters
In Solana, all accounts accessed in a transaction need to be specified in an account list. Therefore, users need to specify the accounts that the receiver program will access in the transaction. Moreover, since the program that receives the Cross Program Invocation can't hold the asset in the same address, the program receiver's address must be passed separately.
To accommodate for that, a list of accounts needs to be passed when initiating a CCM swap via the cf_parameters
(Vec<u8>) parameter when opening a deposit channel or initiating a swap via contract call. That list must contain:
- The receiver program's address.
- The accounts that the receiver program will access in the transaction.
- The fallback address the funds to be transferred to if the Ccm call reverted.
The list shall be a (Vec<u8>) obtained from encoding CcmAccounts
type using scale encoding, where:
- The first address is the receiver program's address
- This is followed by a list of accounts that the receiver program will access in the transaction
- Finally, the last address is the "fallback_address". If the list is invalid or the encoding is incorrect the opening of a deposit channel will fail.
The type definition of CcmAccounts
and CcmAddress
is as follows:
pub struct CcmAddress {
pub pubkey: Pubkey,
pub is_writable: bool,
}
pub struct CcmAccounts {
pub cf_receiver: CcmAddress,
pub remaining_accounts: Vec<CcmAddress>,
pub fallback_address: Pubkey,
}
Receive call and asset on the receiver program
Chainflip's Vault will transfer the destination asset's amount to the specified address in the instruction previous to the one calling the receiver program. Then it will call the receiver program's address with the following parameters:
Param | Description | Data type |
---|---|---|
source_chain | Source chain for the swap | u32 |
source_address | Address that initiated the swap on the source chain. Addresses are encoded into a Vec<u8> type | Vec<u8> |
message | Message passed to the receiver program. | Vec<u8> |
amount | Amount of the destination asset transferred to the receiver address in the previous instruction. | uint |
pub fn cf_receive_token<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, CfReceiveToken<'info>>,
source_chain: u32,
source_address: Vec<u8>,
message: Vec<u8>,
amount: u64,
) -> Result<()>;
#[derive(Accounts)]
pub struct CfReceiveToken<'info> {
#[account(mut, token::mint = mint)]
pub receiver_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub mint: Account<'info, Mint>,
#[account(address = sysvar::instructions::ID)]
pub instruction_sysvar: UncheckedAccount<'info>,
};
pub fn cf_receive_native<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, CfReceiveNative<'info>>,
source_chain: u32,
source_address: Vec<u8>,
message: Vec<u8>,
amount: u64,
) -> Result<()>;
#[derive(Accounts)]
pub struct CfReceiveNative<'info> {
#[account(mut)]
pub receiver_native: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
#[account(address = sysvar::instructions::id())]
instruction_sysvar: UncheckedAccount<'info>,
};
Notice that the receiver interface is written using the Anchor framework (opens in a new tab).
Safety considerations on the user's logic
There is some safety considerations around the receiver program's logic that should be taken into account. In CCM swaps to Solana the receiver program is called on the receiver's chain with the expected cf_receive_* interface. As described, the funds will be transferred in one instruction and the CPI call will be done in the following one.
Those receiver functions could be called by a malicious actor and therefore it's safety needs to be considered. For some applications that don't hold any funds and atomically move them upon receival, such as DEX Aggregators, there is no real necessity to check that the caller is Chainflip or that the funds have been transferred correctly to the receiver as the call will revert otherwise. However, some applications might want to check that it's not a malicious attacker that is calling the cf_receive_* function. If so, there are two main ways to mitigate this. Either (or both) can be used:
- The receiver can check that the call comes from Chainflip using instruction introspection. An example can be found in the CF Tester's
check_chainflip_cpi
function here (opens in a new tab). - Do like DEX aggregators and ensure that the CPI call will revert if no funds are being transferred. For instance, that can be done by having a program that doesn't hold any funds and atomically checks, fetches or moves them upon receiving a CPI call. That will inherently make sure that the caller has sent them as otherwise it will revert.
Compute budget estimation
The user has to provide a gas budget for the destination chain that must cover the entire transaction on the destination chain.
The transaction cost in a Solana transaction is computed as follows, given that Chainflip transactions only contain one signature:
transaction_cost = LAMPORTS_PER_SIGNATURE + prioritization_fee * compute_unit_limit
where LAMPORTS_PER_SIGNATURE
is 5000 lamports.
Therefore the compute unit limit that Chainflip will set to egress CCM transactions is computed as follows:
compute_unit_limit = (gasBudgetAfterSwap - LAMPORTS_PER_SIGNATURE) / prioritization_fee
where:
prioritization_fee
is the current median priority fee of the network with a minimum of 10 micro-lamports per compute unit.gasBudgetAfterSwap
is the gas budget provided by the user swapped into native Sol (1 SOL = 1e9 lamports).
The compute unit limit is capped at 600k compute units.
Given that it's essential for the CCM transfer transactions to succeed and that transactions in Solana are extremely cheap, it's recommended to use a high gas budget to ensure the transaction will not revert due to insufficient compute units.
Transaction length limitation
A Solana transaction is limited to 1232 bytes. That includes all instructions, accounts, data, signatures etc... Therefore, the message and the accounts list are also limited.
Some of those bytes are used as part of the asset transfer and CPI call to the receiver program and, the rest are left for the message and the accounts list in a CCM swap. Therefore, the following requirement must be fulfilled, given that each account passed takes up 33 bytes:
number_of_accounts * 33 + message_length < MAX_LENGTH
where MAX_LENGTH
is:
- 705 bytes for CCM swaps to native Sol
- 492 bytes for CCM swaps to Solana USDC.
If that condition is not met the opening of a deposit channel will fail.