Build
Connected Chains
Solana

To interact with universal applications from Solana, use the Solana Gateway. The Solana Gateway supports:

  • Depositing SOL to a universal app or an account on ZetaChain
  • Depositing supported SPL tokens
  • Depositing SOL and calling a universal app
  • Depositing supported SPL tokens and calling a universal app

To deposit SOL to an EOA or a universal contract, call the deposit instruction of the Solana Gateway program:

pub fn deposit(ctx: Context<Deposit>, amount: u64, receiver: [u8; 20], revert_options: Option<RevertOptions>) -> Result<()>

The deposit instruction accepts SOL (in lamports) which will then be sent to a receiver on ZetaChain. Note that 1 SOL equals 1,000,000,000 lamports, so ensure you convert SOL amounts to lamports when specifying the amount parameter.

The receiver can be either an externally-owned account (EOA) or a universal app address on ZetaChain. Even if the receiver is a universal app contract with the standard receive function, the deposit instruction will not trigger a contract call. If you want to deposit and call a universal app, use the deposit_and_call instruction instead.

After the deposit is processed, the receiver receives the ZRC-20 version of the deposited token—for example, ZRC-20 SOL.

To deposit SPL tokens to an EOA or a universal contract, call the deposit_spl_token instruction:

pub fn deposit_spl_token(ctx: Context<DepositSplToken>, amount: u64, receiver: [u8; 20], revert_options: Option<RevertOptions>) -> Result<()>

Only supported SPL tokens can be deposited. The receiver gets the ZRC-20 version of the deposited token (e.g., ZRC-20 USDC.SOL). SPL tokens must be whitelisted before they can be deposited through the gateway.

The amount specifies the quantity of SPL tokens to deposit.

To deposit SOL and call a universal app contract, use the deposit_and_call instruction:

pub fn deposit_and_call(ctx: Context<Deposit>, amount: u64, receiver: [u8; 20], message: Vec<u8>, revert_options: Option<RevertOptions>) -> Result<()>

After the cross-chain transaction is processed, the onCall function of the universal app contract is executed.

The receiver must be the address of a universal app contract.

When calling a universal app, the message is passed to onCall.

The deposit_spl_token_and_call instruction can be used to call a universal app contract and send SPL tokens:

pub fn deposit_spl_token_and_call(ctx: Context<DepositSplToken>, amount: u64, receiver: [u8; 20], message: Vec<u8>, revert_options: Option<RevertOptions>) -> Result<()>

Here, amount specifies the quantity of SPL tokens to deposit.

In the current version of the protocol, only one SPL token can be deposited at a time.

pub fn call(ctx: Context<Call>, receiver: [u8; 20], message: Vec<u8>, revert_options: Option<RevertOptions>) -> Result<()>

Use when you simply need to invoke logic on ZetaChain and no asset movement is required.

The Solana Gateway supports transaction revert options to handle failure scenarios during cross-chain execution. You can pass an optional revert_options argument to all Gateway instructions (deposit, deposit_spl_token, deposit_and_call, etc.). This enables more granular control over what happens when a cross-chain call fails on ZetaChain.

The RevertOptions struct is defined as:

pub struct RevertOptions {
    pub revert_address: Pubkey,
    pub abort_address: [u8; 20],
    pub call_on_revert: bool,
    pub revert_message: Vec<u8>,
    pub on_revert_gas_limit: u64,
}

Fields

  • revert_address: Solana Pubkey that receives the tokens back if the transaction fails on ZetaChain after being processed. This must be a valid SPL token or SOL account depending on the asset deposited.
  • abort_address: 20-byte Ethereum-style address on ZetaChain that receives the tokens if the call to the universal contract’s onCall function fails and the protocol is unable to execute a revert back to Solana (e.g., due to insufficient gas, invalid revert path, or internal errors). This address acts as a final fallback to prevent asset loss. If call_on_revert is true, this address may also receive the revert message via the app’s onRevert function.
  • call_on_revert boolean flag that determines whether the on_revert on Solana or onAbort on ZetaChain hook on the universal app should be called if the transaction fails.
  • revert_message: arbitrary bytes to be passed to the on_revert on Solana and onAbort on ZetaChain functions. This can contain metadata about the original intent, failure reason, or any custom app-specific data.
  • on_revert_gas_limit: the gas limit to allocate for the revert transaction on ZetaChain. Ensure this is sufficient for the onRevert hook to execute.

Notes

  • If revert_options is omitted, the default behavior in case of revert is to transfer the tokens back to the sender.
  • To fully protect assets against loss of funds, we recommend always specifying abort_address.

Implementing on_revert

If a call to a universal contract on ZetaChain reverts, and the protocol is able to execute a revert transaction back to Solana, the Gateway invokes the on_revert function in your Solana program. This lets your app unwind state, emit telemetry, or reimburse the user after a failed cross-chain call.

pub fn on_revert(
    ctx: Context<OnRevert>,
    amount: u64,      // Asset quantity originally deposited (lamports or SPL)
    sender: Pubkey,   // The account that triggered the deposit/call from Solana
    data: Vec<u8>,    // Arbitrary bytes supplied via `revert_message`
) -> Result<()>

Implement this function to make your universal app resilient and transparent in the face of cross-chain failures.

To withdraw ZRC-20 tokens and call a Solana program from a universal app on ZetaChain, use the withdrawAndCall function of the ZetaChain Gateway. The program being called on Solana must implement an on_call function.

The on_call function must have the following signature:

pub fn on_call(
    ctx: Context<OnCall>,
    amount: u64,
    sender: [u8; 20],
    data: Vec<u8>,
) -> Result<()>

The function receives:

  • amount: The amount of tokens being withdrawn
  • sender: The address of the universal app on ZetaChain that initiated the call
  • data: Additional data passed from the universal app

The program can handle both SOL and SPL token withdrawals. For SPL tokens, the program must include the necessary token accounts and mint account in its context.

When calling a Solana program from ZetaChain, the message payload must include both the program accounts and the data to be passed to the program. The payload is ABI-encoded as a tuple containing:

  1. An array of account metadata, where each account is specified as:

    • publicKey: The Solana public key of the account
    • isWritable: Whether the account can be modified by the program
  2. The data to be passed to the program's on_call function

The accounts array must include all required accounts for the program's on_call function.

For SOL token withdrawals, the accounts array must include:

  • Program PDA (writable)
  • Gateway PDA (read-only)
  • System program (read-only)

For SPL token withdrawals, the accounts array must include:

  • Program PDA (writable)
  • Program's associated token account (writable)
  • Mint account (read-only)
  • Gateway PDA (read-only)
  • Token program (read-only)
  • System program (read-only)

The data field can be any bytes that your program's on_call function expects to receive.

For a complete example of how to call a Solana program from a universal app, including message encoding and program implementation, check out the Solana example in the ZetaChain examples repository (opens in a new tab).

A deposit fee of 2,000,000 lamports (0.002 SOL) is charged for all deposits.

The Solana Gateway program includes several error codes to handle different failure scenarios:

  • SignerIsNotAuthority: The signer is not authorized to perform the action.
  • DepositPaused: Deposits are currently paused.
  • NonceMismatch: The provided nonce does not match the expected nonce.
  • TSSAuthenticationFailed: The TSS signature verification failed.
  • DepositToAddressMismatch: The deposit destination address does not match.
  • MessageHashMismatch: The message hash verification failed.
  • MemoLengthExceeded: The memo length exceeds the maximum allowed size.
  • SPLAtaAndMintAddressMismatch: The SPL token account address does not match the expected address.
  • EmptyReceiver: The receiver address is empty.
  • InvalidInstructionData: The instruction data is invalid.