Solana Migration Guide

Across is expanding support to Solana, enabling seamless USDC bridging between Ethereum Mainnet, other supported chains and Solana. The migration is scheduled to go live soon. All integrators and relayers are encouraged to complete the necessary updates before the migration happens to prevent disruptions.


For Solana routes, check out this utility script demonstrating how to call deposit()with the integratorID for seamless transaction execution.

Building and Executing Transactions

Developers are requested to build transactions by fetching details from the /suggested-fees API, adding integratorID and calling the deposit() function in the SVM SpokePool contract to execute deposit transactions. We will let you know as soon as Solana routes are available in the /suggested-fees API.

When calling the /suggested-fees API, you must provide the recipient parameter in Solana Pubkey format (base-58 encoded string).

If the recipient is missing or not a valid Solana public key, the API will return an error response with status code 400

There are no changes in the /suggested-fees API's response body. However please note that you will be able to see Solana addresses (in Pubkey format) in fields like recipient, inputToken, outputToken, spokePoolAddress, exclusiveRelayer and destinationSpokePoolAddress .

Solana is not currently supported in the Across App-SDK. We’re actively working on expanding support and improving the developer experience for Solana. Stay tuned, we’ll share updates as soon as it becomes available.

Restrictions with Embedded Crosschain Actions (Across+) on Solana

When integrating Solana for crosschain deposits, it's crucial to know that Solana transactions have a maximum size limit of 1232 bytes. This can affect your deposit transactions in the following ways:

  • Deposits from EVM Chains to Solana: Due to Solana's inherent message size limitations, there is a potential for deposit transactions failing when bridging (with an attached message) from EVM chains to Solana.

  • Deposits from Solana to EVM Chains: While the Solana side of the deposit may experience a revert due to message size limitations during the initial deposit, if the transaction successfully completes on Solana, the EVM side is designed to process the message without issues. This means that as long as the deposit transaction is successfully broadcast and confirmed on Solana, the subsequent fill on the EVM destination chain should proceed as expected.

If you need any help adding the integratorID, please reach out to us here.


For Relayers

We encourage relayers to update their codebases and follow the below guidelines as soon as possible and support bridging to and from Solana. Early support on Solana routes will positively affect their nomination percentage. Relayers supporting Solana will need:

  1. A valid Solana address to perform fill operations.

  2. The following package dependencies:

    1. @across-protocol/sdk

    2. @across-protocol/contracts

    3. @solana/kit (previously @solana/web3.js@^2.0.0)

While each relayer can decide how to manage their Solana addresses, the recommended approach is deriving the Solana address from the same private key used for EVM operations. This approach minimizes the need to store additional secrets.

Fill Mechanism Changes

For relayers, the most significant update for relayers is the transition from fillV3Relay to fillRelay for handling crosschain transactions:

  1. For deposits from Solana to EVM chains:

    1. Relayers monitor for deposit events on Solana

    2. Use fillRelay to complete the fill transaction on the EVM destination. You can learn more about it here.

  2. For deposits from EVM chains to Solana:

    1. Monitor for deposit events on EVM chains

    2. Use Solana-specific fill functions to complete the transaction

We use emit_cpi!() method in Anchor to emit events through a Cross Program Invocation (CPI) by including the event data in the instruction data. Here, you need to fetch all transactions involving the SVM Spokepool program and inspect the internal event data. Please checkout the helper in the SDK to implement this.

Testing the Migration

You can check that you have updated your relayer codebase properly by simply following the below script to bridge 10 USDC from Solana to Base.

1

Setup the Testing Environment

Start by making an empty folder and running the following commands on the terminal:

npm init -y
npm i @solana/web3.js @solana/kit @solana/spl-token bn.js @coral-xyz/anchor dotenv bs58 @across-protocol/contracts
2

Running the script

Now, make 2 files in the same directory:

  1. index.js : Main code for the script should be here. feel free to simply copy paste the script given below and add your relayer address on line 102 as the exclusiveRelayer .

  2. .env : Put your Solana Wallet's private key here to be able to bridge funds for testing your relayer.

import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { getU64Encoder } from "@solana/kit";
import {
  ASSOCIATED_TOKEN_PROGRAM_ID,
  TOKEN_PROGRAM_ID,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import BN from "bn.js";
import anchor from "@coral-xyz/anchor";
import fetch from "node-fetch";
import dotenv from "dotenv";
import bs58 from "bs58";
import { SvmSpokeIdl } from "@across-protocol/contracts";

dotenv.config();

const u64Encoder = getU64Encoder();
const { Program, AnchorProvider } = anchor;

// 1️⃣ Provider + Program
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");

const wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_PRIVATE_KEY));

const provider = new AnchorProvider(connection, {
  publicKey: wallet.publicKey,
  signTransaction: tx => wallet.signTransaction(tx),
  signAllTransactions: txs => txs.map(t => wallet.signTransaction(t)),
}, {});

const program = new Program(SvmSpokeIdl, {connection, provider});

// 2️⃣ Fetch quote
const API_URL = "https://app-frontend-v3-git-epic-solana-v1-uma.vercel.app/api/suggested-fees";

function evmToSolanaPK(evmAddress) {
  const hex = evmAddress.replace(/^0x/, "").toLowerCase();
  if (hex.length !== 40) throw new Error("Invalid EVM address");

  const buf = Buffer.alloc(32);
  Buffer.from(hex, "hex").copy(buf, 12); // right-align, zero-pad left 12 bytes
  return new PublicKey(buf);
}

async function getQuote() {
  const params = new URLSearchParams({
    inputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    outputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    destinationChainId: "8453",
    originChainId: "34268394551451",
    amount: "10000000",
    skipAmountLimit: "true",
    allowUnmatchedDecimals: "true",
  });

  const resp = await fetch(`${API_URL}?${params}`);
  if (!resp.ok) throw new Error(`Quote API error ${resp.status}`);
  return resp.json();
}

// 3️⃣ PDAs
async function derivePdas(depositor, inputToken) {
  const seed = 0;
  const programId = new PublicKey(SvmSpokeIdl.address);
  const [statePda] = PublicKey.findProgramAddressSync(
    [Buffer.from("state"), Buffer.from(u64Encoder.encode(seed))],
    programId
  );

  const depositorTokenAccount = getAssociatedTokenAddressSync(
    inputToken,
    depositor,
    true,
    TOKEN_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID
  );

  const [vaultPda] = PublicKey.findProgramAddressSync(
    [statePda.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), inputToken.toBuffer()],
    programId
  );

  const [eventAuthority] = PublicKey.findProgramAddressSync(
    [Buffer.from("__event_authority")],
    programId
  );

  return { statePda, depositorTokenAccount, vaultPda, eventAuthority };
}

// 4️⃣ Call deposit
(async () => {
  const quote = await getQuote();
  console.log("Quote data:", JSON.stringify(quote, null, 2));

  const depositor = wallet.publicKey;
  
  // Convert EVM addresses to Solana base58 addresses
  const recipient = wallet.publicKey;
  const inputToken = new PublicKey(quote.inputToken.address);
  const outputToken = evmToSolanaPK(quote.outputToken.address);
  const exclusiveRelayer = evmToSolanaPK(<add-your-relayer-address-here>); //replace this with your relayer address as a string

  const inputAmount = new BN(quote.inputAmount);
  const outputAmount = new BN(quote.outputAmount); // must be length 32
  const destinationChainId = new BN(quote.outputToken.chainId);
  const quoteTimestamp = quote.quoteTimestamp >>> 0; // u32
  const fillDeadline = quote.fillDeadline >>> 0; // u32
  const exclusivityParameter = quote.exclusivityParameter >>> 0; // u32
  const message = new Uint8Array([]);


  const { statePda, depositorTokenAccount, vaultPda, eventAuthority } =
    await derivePdas(depositor, inputToken);

  const txSig = await program.methods
    .deposit(
      depositor,
      recipient,
      inputToken,
      outputToken,
      inputAmount,
      outputAmount,
      destinationChainId,
      exclusiveRelayer,
      quoteTimestamp,
      fillDeadline,
      exclusivityParameter,
      message
    )
    .accounts({
      signer: depositor,
      state: statePda,
      delegate: depositor,
      depositorTokenAccount,
      vault: vaultPda,
      mint: inputToken,
      tokenProgram: TOKEN_PROGRAM_ID,
      associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
      eventAuthority,
      program: program.programId,
    })
    .signers([wallet])
    .rpc();

  console.log("✅ Deposit TX:", txSig);
})();

To run the script, simply head over to your terminal and run the following command and you can see the deposit transaction go forward and get filled by your relayer:

node index.js

Technical Reference

Deposit Function

The deposit function is used when users want to transfer funds from Solana to an EVM chain:

// This function is called when a user wants to bridge from Solana to another chain
function deposit(
    Pubkey depositor,           // The depositor address in Pubkey format
    Pubkey recipient,           // The recipient address on the destination chain
    Pubkey input_token,          // Token being deposited (on Solana)
    Pubkey output_token,         // Token to receive on destination chain
    u64 input_amount,         // Amount of input token being deposited
    u64 output_amount,        // Amount of output token to receive
    u64 destination_chain_id,  // Destination chain ID
    Pubkey exclusive_relayer,    // Address of exclusive relayer (use Pubkey::default() if none)
    u32 quote_timestamp,       // Timestamp when quote was generated
    u32 fill_deadline,         // Deadline after which deposit can't be filled
    u32 exclusivity_parameter,  // Deadline for exclusive relayer's priority
    Vec<u8> calldata message       // Additional message data
) external payable;

FillRelay Function

Relayers use this function to complete crosschain transactions:

/// This function is called when a relayer wants to fill a crosschain bridge request by sending
/// the specified output tokens to the recipient on the destination chain.
function fillRelay(
    [u8; 32] relay_hash,             // Hash uniquely identifying the deposit to be filled. Computed as hash of relay_data & destination_chain_id.
    V3RelayData relay_data,         // Struct containing all deposit data (matches parameters from FundsDeposited event).
    Pubkey depositor,              // The account credited with the deposit on the origin chain.
    Pubkey recipient,              // The recipient account receiving the funds on this chain.
    Pubkey input_token,            // Token used for deposit on the origin chain.
    Pubkey output_token,           // Token to be received by the recipient on the destination chain.
    u64 input_amount,           // Amount deposited by the user on the origin chain.
    u64 output_amount,          // Amount sent to the recipient on the destination chain.
    u64 origin_chain_id,        // Origin chain identifier.
    Pubkey exclusive_relayer,      // The exclusive relayer allowed to fill the deposit before the exclusivity deadline.
    u32 fill_deadline,           // Deadline after which deposit cannot be filled.
    u32 exclusivity_deadline,    // Deadline for exclusive relayer to maintain priority.
    bytes calldata message,         // Message to be passed.
    u64 repayment_chain_id,     // Chain where the relayer expects repayment after the challenge period.
    Pubkey repayment_address       // Address where relayer wants to receive refund on the repayment chain.
) external;

Support

Want to learn more or need personalized help? Check out developer support and reach out to us!

Last updated