Decode LayerZero SVM

Decode LayerZero SVM

Tags
Web3
Cross-chain
Published
April 14, 2025
Author
Senn

Overview

LayerZero's Solana VM (SVM) implementation introduces a modular architecture for cross-chain messaging, focusing on flexibility, security, and interoperability. Here's a breakdown of its core components and workflows:

1. Core Components

a. OApp (Omnichain Application)

  • Purpose: Acts as the base contract for cross-chain logic (e.g., token transfers via OFT).
  • Registration & Configuration:
    • OApp delegates initialize configurations via Endpoint (e.g., nonce, send/receive libraries).
    • Critical for defining trusted message paths (set_peer) and enforcing message options (set_enforced_options).

b. OFT (Omnichain Fungible Token)

  • Types:
    • Native OFT: Burns/mints tokens directly on-chain.
    • Adapter OFT: Locks/unlocks tokens in an escrow account.
  • Key Features:
    • send() handles cross-chain transfers with fee/dust calculations.
    • Rate limiting per peer to prevent abuse.

c. Endpoint Program

  • Role: Central hub for message lifecycle management.
  • Key Functions:
    • send(): Initiates cross-chain messages via configured send libraries (e.g., ULN).
    • verify(): Marks messages as verified after DVN checks.
    • Nonce tracking to prevent replay attacks.

d. Ultra Light Node (ULN)

  • Function: Manages message delivery via workers (executors/DVNs).
    • Assigns jobs to executors (message delivery) and DVNs (verification).
    • Handles fee distribution to workers and treasuries.
  • Configurable: Supports OApp-specific or default worker sets.

e. Executor & DVN

  • Executor: Executes verified messages by calling the OApp's lzReceive.
  • DVN (Decentralized Verification Node): Verifies message validity (e.g., block confirmations) and submits proofs.

2. Cross-Chain Message Lifecycle

a. Sending a Message

  1. OApp Interaction:
      • Calls Endpoint.send(), triggering token burn/lock (OFT) and message creation.
  1. ULN Processing:
      • Assigns executor/DVNs based on OApp configurations.
      • Calculates fees (gas, treasury cuts) and transfers payments.
  1. Message Emission:
      • Constructs a Packet (nonce, GUID, sender/receiver, payload).
      • Emits event for off-chain relayers to propagate the message.

b. Verification & Execution

  1. Initial Verification:
      • Anyone can initialize a pending message via Endpoint.init_verify.
  1. DVN Verification:
      • DVNs call verify() after confirming on-chain data (e.g., block headers).
  1. Verification Commitment
    1. After enough DVNs have verified x-chain message, Uln.commit_verification can be called permissionlessly to register x-chain message into Endpoint.
  1. Final Execution:
      • Executor calls Executor.execute, which invokes the OApp's lzReceive.
      • OApp call Endpoint.clear() to mark the message as executed (prevents replay via nonce tracking).

3. Security Mechanisms

  • Replay Protection:
    • Payload Hash Storage: Deleted after execution to prevent reuse.
  • Configurable Trust:
    • OApps define trusted DVNs/executors and message libraries.

4. Fee Model

  • Worker Fees:
    • Executors/DVNs are paid in native tokens or LayerZero tokens.
    • Fees calculated based on gas and destination chain costs.
  • Treasury Fees:
    • Protocol-level cut taken from worker fees (configurable %).

6. Developer Flow

  1. OApp Setup: Register with Endpoint, configure libraries/peers.
  1. OFT Deployment: Choose native/adapter mode, set shared decimals.
  1. Cross-Chain Integration:
      • Use send() for transfers, quote() for fee estimation.
      • Handle lzReceive for inbound message processing.

Conclusion

LayerZero's SVM implementation brings EVM-like cross-chain interoperability to Solana, emphasizing configurability and security. By separating concerns (OApp logic, message routing, verification), it enables complex use cases like OFT while maintaining decentralized trust assumptions. Developers can leverage this framework to build omnichain dApps with minimal protocol-level friction.

Architecture

notion image

Basic Process

OApp Register

  1. OApp calls endpoint.register_oapp to register OApp and its delegate. OApp delegate has authority to set OApp’s configuration in Endpoint program.
  1. OApp delegate calls endpoint.init_nonce to initialize x-chain route, where nonce related accounts are initialized.
  1. OApp delegate calls endpoint.init_send_library to initialize send library which will be used to assign jobs to executor and DVNs to deliver x-chain message.
  1. OApp delegate calls endpoint.init_receive_library to initialize receive library which will be used to verify x-chain messages sent from remote chains.
  1. OApp delegate calls endpoint.init_config to initialize send_config and receive_config (control executor and dvn setting) in sender library program.
  1. OApp delegate calls endpoint.set_config to set send_config and receive_config in receiver library program.

Send x-chain Message

  1. OApp calls Endpoint.send to send x-chain message. Endpoint program looks for OApp’s configured sender library program address (message library program/ultra ligh node program), and calls the sender library program to pay fee to workers and assign job to workers (executor and DVNs configured by the OApp) to deliver x-chain message
  1. Any account can call Endpoint.init_verify to initialize space for the inbound message registration waiting for DVNs verification. This is permissionless as wrong registration can be revoked by corresponding DVNs.
  1. DVNs calls receiver library program’s init_verify and verify instructions on remote chain to register x-chain messge with block confirmations.
  1. After enough DVNs have verified x-chain messages, any account can call receiver library program’s commit_verification instruction to verify enough DVNs have verified, then it calls Endpoint.verify instruction to register the x-chain message’s nonce and payload hash, waiting for execution.
  1. executor calls Executor.execute to execute x-chain message where Executor calls OApp directly. And OApp calls Endpoint.clear to verify the x-chain message has been verified by DVNs and not executed before. Endpoint.clear closes account records the x-chain message to prevent it from being replayed.
    1. The logic behind replay preventation is that
      1. Endpoint.Nonce.inbound_nonce records the largest message nonce whose previous nonces have all be verified. Everytime Endpoing.verify is called, it tries to update the inbound_nonce.
      2. to call Endpoint.init_verify to initialize PayloadHash account, Endpoint program requires x-chain message nonce should be larger than recorded inbound_nonce.
      3. when Endpoing.verify is called, it tries to update the inbound_nonce.
      4. when oft.lzReceive is called, it calls Endpoint.clear to delete PayloadHash account which records payload’s hash, and it requires x-chain message nonce should be smaller or equal than inbound_nonce recorded.
      5. so as long as OApp successfully calls Endpoint.clear, which means the message nonce is smaller than inbound_nonce. Then the corresponding PayloadHash is deleted and can never be initialized again.

OFT

Initialize OFT

init_oft function initializes OFT OApp.
/// --- programs/oft/src/lib.rs --- pub fn init_oft(mut ctx: Context<InitOft>, params: InitOftParams) -> Result<()> { InitOft::apply(&mut ctx, &params) }
 
InitOft::apply
  • creates OftConfig pda which represents this OApp.
    • ld2sd_rate: conversion rate between local decimals and shared decimals
    • token_mint: token mint address
    • token_program : token program address of the token mint
    • endpoint_program: endpoint program address
    • bump: bump of this OftConfig pda
    • admin: admin of this OApp
    • ext: type of this omnichain token (native or adapter)
  • creates LzReceiveTypesAccounts pda
  • It requires the token mint’s authority is set to be OftConfig so that the OftConfig can mint and burn the token to facilitate bridge.
/// --- programs/oft/src/instructions/init_oft.rs --- use crate::*; use anchor_spl::token_interface::{Mint, TokenInterface}; /// This instruction should always be in the same transaction as InitializeMint. /// Otherwise, it is possible for your settings to be front-run by another transaction. /// If such a case did happen, you should initialize another mint for this oft. #[derive(Accounts)] #[instruction(params: InitOftParams)] pub struct InitOft<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, space = 8 + OftConfig::INIT_SPACE, seeds = [OFT_SEED, token_mint.key().as_ref()], bump )] pub oft_config: Account<'info, OftConfig>, #[account( init, payer = payer, space = 8 + LzReceiveTypesAccounts::INIT_SPACE, seeds = [LZ_RECEIVE_TYPES_SEED, &oft_config.key().as_ref()], bump )] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, #[account( mint::authority = oft_config, mint::token_program = token_program )] pub token_mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } impl InitOft<'_> { pub fn apply(ctx: &mut Context<InitOft>, params: &InitOftParams) -> Result<()> { ctx.accounts.oft_config.bump = ctx.bumps.oft_config; ctx.accounts.oft_config.token_mint = ctx.accounts.token_mint.key(); ctx.accounts.oft_config.ext = OftConfigExt::Native(params.mint_authority); ctx.accounts.oft_config.token_program = ctx.accounts.token_program.key(); ctx.accounts.lz_receive_types_accounts.oft_config = ctx.accounts.oft_config.key(); ctx.accounts.lz_receive_types_accounts.token_mint = ctx.accounts.token_mint.key(); let oapp_signer = ctx.accounts.oft_config.key(); ctx.accounts.oft_config.init( params.endpoint_program, params.admin, params.shared_decimals, ctx.accounts.token_mint.decimals, ctx.remaining_accounts, oapp_signer, ) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct InitOftParams { pub admin: Pubkey, pub shared_decimals: u8, pub endpoint_program: Option<Pubkey>, pub mint_authority: Option<Pubkey>, } /// --- programs/oft/src/state/oft.rs --- #[account] #[derive(InitSpace)] pub struct OftConfig { // immutable pub ld2sd_rate: u64, pub token_mint: Pubkey, pub token_program: Pubkey, pub endpoint_program: Pubkey, pub bump: u8, // mutable pub admin: Pubkey, pub ext: OftConfigExt, } /// --- programs/oft/src/state/oft.rs --- /// LzReceiveTypesAccounts includes accounts that are used in the LzReceiveTypes /// instruction. #[account] #[derive(InitSpace)] pub struct LzReceiveTypesAccounts { pub oft_config: Pubkey, pub token_mint: Pubkey, }

Initialize OFT Adapter

init_adapter_oft function initializes the OApp as OFT Adpater.
/// --- programs/oft/src/lib.rs --- pub fn init_adapter_oft( mut ctx: Context<InitAdapterOft>, params: InitAdapterOftParams, ) -> Result<()> { InitAdapterOft::apply(&mut ctx, &params) }
 
InitAdapterOft::apply
  • creates OftConfig pda which represents this OApp.
  • creates LzReceiveTypesAccounts pda
  • It initializes OftConfig’s token account of the token mint, which is used to lock/release bridged token.
#[derive(Accounts)] #[instruction(params: InitAdapterOftParams)] pub struct InitAdapterOft<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, space = 8 + OftConfig::INIT_SPACE, seeds = [OFT_SEED, token_escrow.key().as_ref()], bump )] pub oft_config: Account<'info, OftConfig>, #[account( init, payer = payer, space = 8 + LzReceiveTypesAccounts::INIT_SPACE, seeds = [LZ_RECEIVE_TYPES_SEED, &oft_config.key().as_ref()], bump )] pub lz_receive_types_accounts: Account<'info, LzReceiveTypesAccounts>, #[account(mint::token_program = token_program)] pub token_mint: InterfaceAccount<'info, Mint>, #[account( init, payer = payer, token::authority = oft_config, token::mint = token_mint, token::token_program = token_program, )] pub token_escrow: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } impl InitAdapterOft<'_> { pub fn apply(ctx: &mut Context<InitAdapterOft>, params: &InitAdapterOftParams) -> Result<()> { ctx.accounts.oft_config.bump = ctx.bumps.oft_config; ctx.accounts.oft_config.token_mint = ctx.accounts.token_mint.key(); ctx.accounts.oft_config.ext = OftConfigExt::Adapter(ctx.accounts.token_escrow.key()); ctx.accounts.oft_config.token_program = ctx.accounts.token_program.key(); ctx.accounts.lz_receive_types_accounts.oft_config = ctx.accounts.oft_config.key(); ctx.accounts.lz_receive_types_accounts.token_mint = ctx.accounts.token_mint.key(); let oapp_signer = ctx.accounts.oft_config.key(); ctx.accounts.oft_config.init( params.endpoint_program, params.admin, params.shared_decimals, ctx.accounts.token_mint.decimals, ctx.remaining_accounts, oapp_signer, ) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct InitAdapterOftParams { pub admin: Pubkey, pub shared_decimals: u8, pub endpoint_program: Option<Pubkey>, }

Set Peer

set_peer function allows OftConfig's admin to set OApp’s peer on remote chain.
The OApp can only send message to and receive message from peer.
/// --- programs/oft/src/lib.rs --- pub fn set_peer(mut ctx: Context<SetPeer>, params: SetPeerParams) -> Result<()> { SetPeer::apply(&mut ctx, &params) }
 
SetPeer::apply function initializes Peer pda to registered peer information.
/// --- programs/oft/src/instructions/set_peer.rs --- #[derive(Accounts)] #[instruction(params: SetPeerParams)] pub struct SetPeer<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init_if_needed, payer = admin, space = 8 + Peer::INIT_SPACE, seeds = [PEER_SEED, &oft_config.key().to_bytes(), &params.dst_eid.to_be_bytes()], bump )] pub peer: Account<'info, Peer>, #[account( seeds = [OFT_SEED, &get_oft_config_seed(&oft_config).to_bytes()], bump = oft_config.bump, has_one = admin @OftError::Unauthorized )] pub oft_config: Account<'info, OftConfig>, pub system_program: Program<'info, System>, } impl SetPeer<'_> { pub fn apply(ctx: &mut Context<SetPeer>, params: &SetPeerParams) -> Result<()> { ctx.accounts.peer.address = params.peer; ctx.accounts.peer.bump = ctx.bumps.peer; Ok(()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SetPeerParams { pub dst_eid: u32, pub peer: [u8; 32], } /// --- programs/oft/src/state/peer.rs --- #[account] #[derive(InitSpace)] pub struct Peer { pub address: [u8; 32], pub rate_limiter: Option<RateLimiter>, pub bump: u8, }

Set enforced options

set_enforced_options function allows OftConfig admin to set default options for messages sent to remote chains.
User can specify their option to message to be sent, which can override the enforced options configured.
/// --- programs/oft/src/lib.rs --- pub fn set_enforced_options( mut ctx: Context<SetEnforcedOptions>, params: SetEnforcedOptionsParams, ) -> Result<()> { SetEnforcedOptions::apply(&mut ctx, &params) }
 
SetEnforcedOptions::apply initializes EnforcedOptions pda to record enforced options.
It also checks the options byte array is correct.
#[derive(Accounts)] #[instruction(params: SetEnforcedOptionsParams)] pub struct SetEnforcedOptions<'info> { #[account(mut)] pub admin: Signer<'info>, #[account( init_if_needed, payer = admin, space = 8 + EnforcedOptions::INIT_SPACE, seeds = [ENFORCED_OPTIONS_SEED, &oft_config.key().to_bytes(), &params.dst_eid.to_be_bytes()], bump )] pub enforced_options: Account<'info, EnforcedOptions>, #[account( seeds = [OFT_SEED, &get_oft_config_seed(&oft_config).to_bytes()], bump = oft_config.bump, has_one = admin @OftError::Unauthorized )] pub oft_config: Account<'info, OftConfig>, pub system_program: Program<'info, System>, } impl SetEnforcedOptions<'_> { pub fn apply( ctx: &mut Context<SetEnforcedOptions>, params: &SetEnforcedOptionsParams, ) -> Result<()> { oapp::options::assert_type_3(&params.send)?; ctx.accounts.enforced_options.send = params.send.clone(); oapp::options::assert_type_3(&params.send_and_call)?; ctx.accounts.enforced_options.send_and_call = params.send_and_call.clone(); ctx.accounts.enforced_options.bump = ctx.bumps.enforced_options; Ok(()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SetEnforcedOptionsParams { pub dst_eid: u32, pub send: Vec<u8>, pub send_and_call: Vec<u8>, } /// --- programs/oft/src/state/enforced_options.rs --- #[account] #[derive(InitSpace)] pub struct EnforcedOptions { #[max_len(ENFORCED_OPTIONS_SEND_MAX_LEN)] pub send: Vec<u8>, #[max_len(ENFORCED_OPTIONS_SEND_AND_CALL_MAX_LEN)] pub send_and_call: Vec<u8>, pub bump: u8, }

assert_type_3

/// --- libs/oapp/src/options.rs --- pub fn assert_type_3(options: &Vec<u8>) -> anchor_lang::Result<()> { let mut option_type_bytes = [0; 2]; option_type_bytes.copy_from_slice(&options[0..2]); require!(u16::from_be_bytes(option_type_bytes) == 3, ErrorCode::InvalidOptions); Ok(()) }

Set mint authority

set_mint_authority function allows admin to set mint authority of the OFT.
Note that the mint authority of token is the OftConfig pda. And for OFT (not OFT adapter), the OftConfig.ext records the account which can mint tokens using OFT program.
Also, the account can be set to None so that no account can mint token freely, so that token can only be bridged from other chains.
/// --- programs/oft/src/instructions/set_mint_authority.rs --- pub fn set_mint_authority( mut ctx: Context<SetMintAuthority>, params: SetMintAuthorityParams, ) -> Result<()> { SetMintAuthority::apply(&mut ctx, &params) }
 
/// --- programs/oft/src/instructions/set_mint_authority.rs --- #[derive(Accounts)] pub struct SetMintAuthority<'info> { /// The admin or the mint authority pub signer: Signer<'info>, #[account( mut, seeds = [OFT_SEED, oft_config.token_mint.as_ref()], bump = oft_config.bump, constraint = is_valid_signer(signer.key(), &oft_config) @OftError::Unauthorized )] pub oft_config: Account<'info, OftConfig>, } impl SetMintAuthority<'_> { pub fn apply( ctx: &mut Context<SetMintAuthority>, params: &SetMintAuthorityParams, ) -> Result<()> { // the mint authority can be removed by setting it to None ctx.accounts.oft_config.ext = OftConfigExt::Native(params.mint_authority); Ok(()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SetMintAuthorityParams { pub mint_authority: Option<Pubkey>, } /// Check if the signer is the admin or the mint authority /// When the mint authority is set, the signer can be the admin or the mint authority /// Otherwise, no one can set the mint authority fn is_valid_signer(signer: Pubkey, oft_config: &OftConfig) -> bool { if let OftConfigExt::Native(Some(mint_authority)) = oft_config.ext { signer == oft_config.admin || signer == mint_authority } else { false } }

Mint To

mint_to allows mint authority to mint token.
/// --- programs/oft/src/lib.rs --- pub fn mint_to(mut ctx: Context<MintTo>, params: MintToParams) -> Result<()> { MintTo::apply(&mut ctx, &params) }
 
MintTo::apply
use oftConfig pda(mint authority) to sign tx to mint token.
/// --- programs/oft/src/instructions/mint_to.rs --- use crate::*; use anchor_spl::token_interface::{ self, Mint, MintTo as TokenMintTo, TokenAccount, TokenInterface, }; #[derive(Accounts)] pub struct MintTo<'info> { pub minter: Signer<'info>, /// only the non-adapter oft can mint token to the destination account #[account( seeds = [OFT_SEED, oft_config.token_mint.as_ref()], bump = oft_config.bump, constraint = oft_config.ext == OftConfigExt::Native(Some(minter.key())) @OftError::Unauthorized )] pub oft_config: Account<'info, OftConfig>, #[account( mut, token::mint = token_mint, token::token_program = token_program, )] pub token_dest: InterfaceAccount<'info, TokenAccount>, #[account(mut, address = oft_config.token_mint)] pub token_mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, } impl MintTo<'_> { pub fn apply(ctx: &mut Context<MintTo>, params: &MintToParams) -> Result<()> { token_interface::mint_to( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), TokenMintTo { mint: ctx.accounts.token_mint.to_account_info(), to: ctx.accounts.token_dest.to_account_info(), authority: ctx.accounts.oft_config.to_account_info(), }, &[&[ OFT_SEED, ctx.accounts.oft_config.token_mint.as_ref(), &[ctx.accounts.oft_config.bump], ]], ), params.amount, )?; Ok(()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct MintToParams { pub amount: u64, }

Send

/// --- programs/oft/src/lib.rs --- pub fn send(mut ctx: Context<Send>, params: SendParams) -> Result<MessagingReceipt> { Send::apply(&mut ctx, &params) } /// --- programs/oft/src/instructions/send.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub dst_eid: u32, pub to: [u8; 32], pub amount_ld: u64, pub min_amount_ld: u64, pub options: Vec<u8>, pub compose_msg: Option<Vec<u8>>, pub native_fee: u64, pub lz_token_fee: u64, } /// --- programs/messagelib-interface/src/lib.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize, Default)] pub struct MessagingReceipt { pub guid: [u8; 32], pub nonce: u64, pub fee: MessagingFee, }
 
Process
  1. Calculate Remote Chain Received Token Amount
    1. calculate amount can be received on the remote chain(amount_received_ld) using local decimal based on bridge amount specified by sender (params.amount_ld).
      • if token’s TransferFeeConfig extension enabled, transfer fee will be charged.
      • decimals difference between local chain and remote chainincurs dust.
  1. Minimal Bridge Amount Check
    1. check amount can be received on remote chain is bigger than minimal amount specified in params
  1. Re-calculate Bridge Amount
    1. calculate the (minimum) required amount to send to receive exactly amount_received_ld amount of token.
      Since the amount in amount_received_ld has already excluded the dust, we need to recalculate the actual amount the sender needs to bridge based on it. This amount will be less than or equal to params.amount_ld.
  1. Consume Bridge Rate Limitation
    1. consume bridge rate limitation if there is rate limit
  1. Burn/Lock token
      • For Adapter OFT, it transfers token into escrow account
      • For native OFT, it burns token
  1. Send token
    1. calls oapp::endpoint_cpi::send to emit event to send token to remote chain
 
Note:
  1. peer and oft_config and enforced_options should be initialized first
  1. token 2022, it may have TransferFeeConfig extension enabled, which charges fee during token transfer. And OFT adapter uses token’s transfer functionality to lock/release token, so it needs to calcualte accurate token amount needed to bridge amount_received_ld amount of token out.
/// --- programs/oft/src/instructions/send.rs --- use crate::*; use anchor_spl::token_interface::{ self, Burn, Mint, TokenAccount, TokenInterface, TransferChecked, }; use oapp::endpoint::{instructions::SendParams as EndpointSendParams, MessagingReceipt}; #[event_cpi] #[derive(Accounts)] #[instruction(params: SendParams)] pub struct Send<'info> { pub signer: Signer<'info>, #[account( mut, seeds = [ PEER_SEED, &oft_config.key().to_bytes(), &params.dst_eid.to_be_bytes() ], bump = peer.bump )] pub peer: Account<'info, Peer>, #[account( seeds = [ ENFORCED_OPTIONS_SEED, &oft_config.key().to_bytes(), &params.dst_eid.to_be_bytes() ], bump = enforced_options.bump )] pub enforced_options: Account<'info, EnforcedOptions>, #[account( seeds = [OFT_SEED, &get_oft_config_seed(&oft_config).to_bytes()], bump = oft_config.bump )] pub oft_config: Account<'info, OftConfig>, #[account( mut, token::authority = signer, token::mint = token_mint, token::token_program = token_program, )] pub token_source: InterfaceAccount<'info, TokenAccount>, #[account( mut, token::authority = oft_config.key(), token::mint = token_mint, token::token_program = token_program, constraint = oft_config.ext == OftConfigExt::Adapter(token_escrow.key()) @OftError::InvalidTokenEscrow )] pub token_escrow: Option<InterfaceAccount<'info, TokenAccount>>, #[account( mut, address = oft_config.token_mint, mint::token_program = token_program )] pub token_mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, } impl Send<'_> { pub fn apply(ctx: &mut Context<Send>, params: &SendParams) -> Result<MessagingReceipt> { // 1. Quote the amount with token2022 fee and dedust it let amount_received_ld = ctx.accounts.oft_config.remove_dust(get_post_fee_amount_ld( &ctx.accounts.oft_config.ext, &ctx.accounts.token_mint, params.amount_ld, )?); require!(amount_received_ld >= params.min_amount_ld, OftError::SlippageExceeded); // 2. Calculate the (minimum) required amount to send to receive exactly amount_received_ld // amount_sent_ld does not have to be dedusted, because it is collected or burned locally let amount_sent_ld = get_pre_fee_amount_ld( &ctx.accounts.oft_config.ext, &ctx.accounts.token_mint, amount_received_ld, )?; if let Some(rate_limiter) = ctx.accounts.peer.rate_limiter.as_mut() { rate_limiter.try_consume(amount_sent_ld)?; } match &ctx.accounts.oft_config.ext { OftConfigExt::Adapter(_) => { if let Some(escrow_acc) = &mut ctx.accounts.token_escrow { // lock token_interface::transfer_checked( CpiContext::new( ctx.accounts.token_program.to_account_info(), TransferChecked { from: ctx.accounts.token_source.to_account_info(), mint: ctx.accounts.token_mint.to_account_info(), to: escrow_acc.to_account_info(), authority: ctx.accounts.signer.to_account_info(), }, ), amount_sent_ld, ctx.accounts.token_mint.decimals, )?; } else { return Err(OftError::InvalidTokenEscrow.into()); } }, OftConfigExt::Native(_) => { // burn let cpi_accounts = Burn { mint: ctx.accounts.token_mint.to_account_info(), from: ctx.accounts.token_source.to_account_info(), authority: ctx.accounts.signer.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); token_interface::burn(CpiContext::new(cpi_program, cpi_accounts), amount_sent_ld)?; }, }; require!( ctx.accounts.oft_config.key() == ctx.remaining_accounts[1].key(), OftError::InvalidSender ); let amount_sd = ctx.accounts.oft_config.ld2sd(amount_received_ld); let receipt = oapp::endpoint_cpi::send( ctx.accounts.oft_config.endpoint_program, ctx.accounts.oft_config.key(), ctx.remaining_accounts, &[ OFT_SEED, &get_oft_config_seed(&ctx.accounts.oft_config).to_bytes(), &[ctx.accounts.oft_config.bump], ], EndpointSendParams { dst_eid: params.dst_eid, receiver: ctx.accounts.peer.address, message: msg_codec::encode( params.to, amount_sd, ctx.accounts.signer.key(), &params.compose_msg, ), options: ctx .accounts .enforced_options .combine_options(&params.compose_msg, &params.options)?, native_fee: params.native_fee, lz_token_fee: params.lz_token_fee, }, )?; emit_cpi!(OFTSent { guid: receipt.guid, dst_eid: params.dst_eid, from: ctx.accounts.token_source.key(), amount_sent_ld, amount_received_ld }); Ok(receipt) } } pub fn get_oft_config_seed(oft_config: &OftConfig) -> Pubkey { if let OftConfigExt::Adapter(token_escrow) = oft_config.ext { token_escrow } else { oft_config.token_mint } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub dst_eid: u32, pub to: [u8; 32], pub amount_ld: u64, pub min_amount_ld: u64, pub options: Vec<u8>, pub compose_msg: Option<Vec<u8>>, pub native_fee: u64, pub lz_token_fee: u64, }

Rate Limit

Each peer can optionally set bridge rate limit.
  • If not set, then there is no bridge limit from local chain to this peer.
  • Else there is bridge limit to this peer.
Rate limiting supports refilling. The RateLimiter.refill_per_second parameter defines how many tokens are replenished each second. Each time a token is bridged, function refill is called, which calculates the amount to refill based on the time elapsed since the last_refill_time.
/// --- programs/oft/src/state/peer.rs --- #[account] #[derive(InitSpace)] pub struct Peer { pub address: [u8; 32], pub rate_limiter: Option<RateLimiter>, pub bump: u8, } /// --- programs/oft/src/state/peer.rs --- #[derive(Clone, Default, AnchorSerialize, AnchorDeserialize, InitSpace)] pub struct RateLimiter { pub capacity: u64, pub tokens: u64, pub refill_per_second: u64, pub last_refill_time: u64, } impl RateLimiter { pub fn set_rate(&mut self, refill_per_second: u64) -> Result<()> { self.refill(0)?; self.refill_per_second = refill_per_second; Ok(()) } pub fn set_capacity(&mut self, capacity: u64) -> Result<()> { self.capacity = capacity; self.tokens = capacity; self.last_refill_time = Clock::get()?.unix_timestamp.try_into().unwrap(); Ok(()) } pub fn refill(&mut self, extra_tokens: u64) -> Result<()> { let mut new_tokens = extra_tokens; let current_time: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); if current_time > self.last_refill_time { let time_elapsed_in_seconds = current_time - self.last_refill_time; new_tokens += time_elapsed_in_seconds * self.refill_per_second; } self.tokens = std::cmp::min(self.capacity, self.tokens.saturating_add(new_tokens)); self.last_refill_time = current_time; Ok(()) } pub fn try_consume(&mut self, amount: u64) -> Result<()> { self.refill(0)?; match self.tokens.checked_sub(amount) { Some(new_tokens) => { self.tokens = new_tokens; Ok(()) }, None => Err(error!(OftError::RateLimitExceeded)), } } }

endpoint_cpi Library Send

libs/oapp/src/endpoint_cpi.rs is a helper function used by OFT program to send message to endpoint program. The send function checks accounts validity, construct context and cpi-calls endpoint program.
/// --- libs/oapp/src/endpoint_cpi.rs --- pub fn send( endpoint_program: Pubkey, sender: Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], params: SendParams, ) -> Result<MessagingReceipt> { if sender != accounts[1].key() { return Err(ErrorCode::ConstraintAddress.into()); } if accounts.iter().filter(|acc| acc.key == &sender).count() > 1 { return Err(ErrorCode::ConstraintAddress.into()); } let cpi_ctx = Send::construct_context(endpoint_program, accounts)?; let rtn = endpoint::cpi::send(cpi_ctx.with_signer(&[&seeds]), params)?; Ok(rtn.get()) } /// --- programs/endpoint/src/instructions/oapp/send.rs --- #[event_cpi] #[derive(CpiContext, Accounts)] #[instruction(params: SendParams)] pub struct Send<'info> { pub sender: Signer<'info>, /// CHECK: assert this program in assert_send_library() pub send_library_program: UncheckedAccount<'info>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, sender.key.as_ref(), &params.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, &params.dst_eid.to_be_bytes()], bump = default_send_library_config.bump )] pub default_send_library_config: Account<'info, SendLibraryConfig>, /// The PDA signer to the send library when the endpoint calls the send library. #[account( seeds = [ MESSAGE_LIB_SEED, &get_send_library( &send_library_config, &default_send_library_config ).key().to_bytes() ], bump = send_library_info.bump, constraint = !send_library_info.to_account_info().is_writable @LayerZeroError::ReadOnlyAccount )] pub send_library_info: Account<'info, MessageLibInfo>, #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump)] pub endpoint: Account<'info, EndpointSettings>, #[account( mut, seeds = [ NONCE_SEED, &sender.key().to_bytes(), &params.dst_eid.to_be_bytes(), &params.receiver[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, }

Endpoint CPI Send

endpoint program’s send instruction allows OApp to send x-chain message.
/// --- programs/endpoint/src/lib.rs --- pub fn send<'c: 'info, 'info>( mut ctx: Context<'_, '_, 'c, 'info, Send<'info>>, params: SendParams, ) -> Result<MessagingReceipt> { Send::apply(&mut ctx, &params) } /// --- programs/endpoint/src/instructions/oapp/send.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub dst_eid: u32, pub receiver: [u8; 32], pub message: Vec<u8>, pub options: Vec<u8>, pub native_fee: u64, pub lz_token_fee: u64, } /// --- programs/messagelib-interface/src/lib.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize, Default)] pub struct MessagingReceipt { pub guid: [u8; 32], pub nonce: u64, pub fee: MessagingFee, }

Quote OFT

/// --- programs/oft/src/lib.rs --- pub fn quote_oft(ctx: Context<QuoteOft>, params: QuoteOftParams) -> Result<QuoteOftResult> { QuoteOft::apply(&ctx, &params) }
 
/// --- programs/oft/src/instructions/quote.rs --- use crate::*; #[derive(Accounts)] #[instruction(params: QuoteOftParams)] pub struct QuoteOft<'info> { #[account( seeds = [OFT_SEED, &get_oft_config_seed(&oft_config).to_bytes()], bump = oft_config.bump )] pub oft_config: Account<'info, OftConfig>, #[account( seeds = [ PEER_SEED, &oft_config.key().to_bytes(), &params.dst_eid.to_be_bytes() ], bump = peer.bump )] pub peer: Account<'info, Peer>, #[account( address = oft_config.token_mint, )] pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>, } impl QuoteOft<'_> { pub fn apply(ctx: &Context<QuoteOft>, params: &QuoteOftParams) -> Result<QuoteOftResult> { // 1. Quote the amount with token2022 fee and dedust it let amount_received_ld = ctx.accounts.oft_config.remove_dust(get_post_fee_amount_ld( &ctx.accounts.oft_config.ext, &ctx.accounts.token_mint, params.amount_ld, )?); require!(amount_received_ld >= params.min_amount_ld, OftError::SlippageExceeded); // amount_sent_ld does not have to be dedusted let amount_sent_ld = get_pre_fee_amount_ld( &ctx.accounts.oft_config.ext, &ctx.accounts.token_mint, amount_received_ld, )?; let oft_limits = OFTLimits { min_amount_ld: 0, max_amount_ld: 0xffffffffffffffff }; let oft_fee_details = if amount_received_ld < amount_sent_ld { vec![OFTFeeDetail { fee_amount_ld: amount_sent_ld - amount_received_ld, description: "Token2022 Transfer Fee".to_string(), }] } else { vec![] }; let oft_receipt = OFTReceipt { amount_sent_ld, amount_received_ld }; Ok(QuoteOftResult { oft_limits, oft_fee_details, oft_receipt }) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct QuoteOftParams { pub dst_eid: u32, pub to: [u8; 32], pub amount_ld: u64, pub min_amount_ld: u64, pub options: Vec<u8>, pub compose_msg: Option<Vec<u8>>, pub pay_in_lz_token: bool, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct QuoteOftResult { pub oft_limits: OFTLimits, pub oft_fee_details: Vec<OFTFeeDetail>, pub oft_receipt: OFTReceipt, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct OFTFeeDetail { pub fee_amount_ld: u64, pub description: String, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct OFTReceipt { pub amount_sent_ld: u64, pub amount_received_ld: u64, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct OFTLimits { pub min_amount_ld: u64, pub max_amount_ld: u64, }

Quote

pub fn quote<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, Quote<'info>>, params: QuoteParams, ) -> Result<MessagingFee> { Quote::apply(&ctx, &params) }
 
/// --- programs/endpoint/src/instructions/oapp/quote.rs --- use crate::*; use cpi_helper::CpiContext; /// MESSAGING STEP 0 /// don't need to separate quote and quote_with_lz_token as it does not process payment on quote() #[derive(CpiContext, Accounts)] #[instruction(params: QuoteParams)] pub struct Quote<'info> { /// CHECK: assert this program in assert_send_library() pub send_library_program: UncheckedAccount<'info>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, &params.sender.to_bytes(), &params.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, &params.dst_eid.to_be_bytes()], bump = default_send_library_config.bump )] pub default_send_library_config: Account<'info, SendLibraryConfig>, /// The PDA signer to the send library when the endpoint calls the send library. #[account( seeds = [ MESSAGE_LIB_SEED, &get_send_library( &send_library_config, &default_send_library_config ).key().to_bytes() ], bump = send_library_info.bump, constraint = !send_library_info.to_account_info().is_writable @LayerZeroError::ReadOnlyAccount )] pub send_library_info: Account<'info, MessageLibInfo>, #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump)] pub endpoint: Account<'info, EndpointSettings>, #[account( seeds = [ NONCE_SEED, &params.sender.to_bytes(), &params.dst_eid.to_be_bytes(), &params.receiver[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, } impl Quote<'_> { pub fn apply<'c: 'info, 'info>( ctx: &Context<'_, '_, 'c, 'info, Quote<'info>>, params: &QuoteParams, ) -> Result<MessagingFee> { // assert all accounts are non-writable for account in ctx.remaining_accounts { require!(!account.is_writable, LayerZeroError::WritableAccountNotAllowed) } let nonce = ctx.accounts.nonce.outbound_nonce + 1; let packet = Packet { nonce, src_eid: ctx.accounts.endpoint.eid, sender: params.sender, dst_eid: params.dst_eid, receiver: params.receiver, guid: get_guid( nonce, ctx.accounts.endpoint.eid, params.sender, params.dst_eid, params.receiver, ), message: params.message.clone(), }; let send_library = assert_send_library( &ctx.accounts.send_library_info, &ctx.accounts.send_library_program.key, &ctx.accounts.send_library_config, &ctx.accounts.default_send_library_config, )?; // call the send library if params.pay_in_lz_token { require!( ctx.accounts.endpoint.lz_token_mint.is_some(), LayerZeroError::LzTokenUnavailable ); } let quote_params = messagelib_interface::QuoteParams { packet, options: params.options.clone(), pay_in_lz_token: params.pay_in_lz_token, }; let seeds: &[&[&[u8]]] = &[&[MESSAGE_LIB_SEED, send_library.as_ref(), &[ctx.accounts.send_library_info.bump]]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.send_library_program.to_account_info(), messagelib_interface::cpi::accounts::Interface { endpoint: ctx.accounts.send_library_info.to_account_info(), }, seeds, ) .with_remaining_accounts(ctx.remaining_accounts.to_vec()); Ok(messagelib_interface::cpi::quote(cpi_ctx, quote_params)?.get()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct QuoteParams { pub sender: Pubkey, pub dst_eid: u32, pub receiver: [u8; 32], pub message: Vec<u8>, pub options: Vec<u8>, pub pay_in_lz_token: bool, }

Endpoint

Send

endpoint program’s send instruction allows OApp to send x-chain message.
pub fn send<'c: 'info, 'info>( mut ctx: Context<'_, '_, 'c, 'info, Send<'info>>, params: SendParams, ) -> Result<MessagingReceipt> { Send::apply(&mut ctx, &params) } /// --- programs/endpoint/src/instructions/oapp/send.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub dst_eid: u32, pub receiver: [u8; 32], pub message: Vec<u8>, pub options: Vec<u8>, pub native_fee: u64, pub lz_token_fee: u64, } /// --- programs/messagelib-interface/src/lib.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize, Default)] pub struct MessagingReceipt { pub guid: [u8; 32], pub nonce: u64, pub fee: MessagingFee, }
Send::apply
  1. Increate Outbound Nonce
    1. Increase the outbound nonce of this x-chain message route
  1. Calculate Guid
    1. Guid is calculated based on
    2. Outbound nonce
    3. endpoint id
    4. sender
    5. destination endpoint id
    6. receiver address
  1. Construct Packet
  1. Check Send Library Validity
    1. Get the send library(pda) address based on OApp’s config and default config
    2. Check the the send library is derived from passed-in send_library_program .
  1. Call send_library_program (Ultra Light Node Prorgram) to assign job to workers to send x-chain message
  1. Emit Event
 
Note:
  1. sender’s send_library_config pda should be initialized before by calling endpoint.init_send_library
  1. default_send_library_config is initialized by endpoint admin ba calling endpoint.init_default_send_library, which means supported destination chain list should be registered previously.
  1. send_library_info(MessageLibInfo) is registered by endpoint program admin previously.
  1. nonce account should be initialized before using endpoint.init_nonce
/// --- programs/endpoint/src/instructions/oapp/send.rs --- /// MESSAGING STEP 1 #[event_cpi] #[derive(CpiContext, Accounts)] #[instruction(params: SendParams)] pub struct Send<'info> { pub sender: Signer<'info>, /// CHECK: assert this program in assert_send_library() pub send_library_program: UncheckedAccount<'info>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, sender.key.as_ref(), &params.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, &params.dst_eid.to_be_bytes()], bump = default_send_library_config.bump )] pub default_send_library_config: Account<'info, SendLibraryConfig>, /// The PDA signer to the send library when the endpoint calls the send library. #[account( seeds = [ MESSAGE_LIB_SEED, &get_send_library( &send_library_config, &default_send_library_config ).key().to_bytes() ], bump = send_library_info.bump, constraint = !send_library_info.to_account_info().is_writable @LayerZeroError::ReadOnlyAccount )] pub send_library_info: Account<'info, MessageLibInfo>, #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump)] pub endpoint: Account<'info, EndpointSettings>, #[account( mut, seeds = [ NONCE_SEED, &sender.key().to_bytes(), &params.dst_eid.to_be_bytes(), &params.receiver[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, } impl Send<'_> { pub fn apply<'c: 'info, 'info>( ctx: &mut Context<'_, '_, 'c, 'info, Send<'info>>, params: &SendParams, ) -> Result<MessagingReceipt> { /// Increate Outbound Nonce ctx.accounts.nonce.outbound_nonce += 1; /// Calculate Guid let sender = ctx.accounts.sender.key(); let guid = get_guid( ctx.accounts.nonce.outbound_nonce, ctx.accounts.endpoint.eid, sender, params.dst_eid, params.receiver, ); /// Construct Packet let packet = Packet { nonce: ctx.accounts.nonce.outbound_nonce, src_eid: ctx.accounts.endpoint.eid, sender, dst_eid: params.dst_eid, receiver: params.receiver, guid, message: params.message.clone(), }; /// Check Send Library Validity let send_library = assert_send_library( &ctx.accounts.send_library_info, &ctx.accounts.send_library_program.key, &ctx.accounts.send_library_config, &ctx.accounts.default_send_library_config, )?; /// Call send_library_program to assign job to workers to send x-chain message let seeds: &[&[&[u8]]] = &[&[MESSAGE_LIB_SEED, send_library.as_ref(), &[ctx.accounts.send_library_info.bump]]]; let cpi_ctx = CpiContext::new_with_signer( ctx.accounts.send_library_program.to_account_info(), messagelib_interface::cpi::accounts::Interface { endpoint: ctx.accounts.send_library_info.to_account_info(), }, seeds, ) .with_remaining_accounts(ctx.remaining_accounts.to_vec()); // separate send and send_with_lz_token interface to be implemented by message library, for the benefits of: // 1. as different accounts are required, they can be validated through anchor constraints rather than manually handling remaining accounts // 2. idl can be generated and used by sdk to assembled the required accounts // subsequently, due to this design, fee payment is handled in the message library for simplicity let (fee, encoded_packet) = if params.lz_token_fee == 0 { let send_params = messagelib_interface::SendParams { packet, options: params.options.clone(), native_fee: params.native_fee, }; messagelib_interface::cpi::send(cpi_ctx, send_params)?.get() } else { let lz_token_mint = ctx.accounts.endpoint.lz_token_mint.ok_or(LayerZeroError::LzTokenUnavailable)?; let send_params = messagelib_interface::SendWithLzTokenParams { packet, options: params.options.clone(), native_fee: params.native_fee, lz_token_fee: params.lz_token_fee, lz_token_mint, }; messagelib_interface::cpi::send_with_lz_token(cpi_ctx, send_params)?.get() }; /// Emit Event emit_cpi!(PacketSentEvent { encoded_packet, options: params.options.clone(), send_library, }); Ok(MessagingReceipt { guid, nonce: ctx.accounts.nonce.outbound_nonce, fee }) } } pub(crate) fn assert_send_library( send_library_info: &MessageLibInfo, send_library_program: &Pubkey, send_library_config: &SendLibraryConfig, default_send_library_config: &SendLibraryConfig, ) -> Result<Pubkey> { let send_library = get_send_library(send_library_config, default_send_library_config); require!( send_library == Pubkey::create_program_address( &[MESSAGE_LIB_SEED, &[send_library_info.message_lib_bump]], send_library_program ) .map_err(|_| LayerZeroError::InvalidSendLibrary)?, LayerZeroError::InvalidSendLibrary ); Ok(send_library) } pub(crate) fn get_send_library( config: &SendLibraryConfig, default_config: &SendLibraryConfig, ) -> Pubkey { if config.message_lib == DEFAULT_MESSAGE_LIB { default_config.message_lib } else { config.message_lib } } pub fn get_guid( nonce: u64, src_eid: u32, sender: Pubkey, dst_eid: u32, receiver: [u8; 32], ) -> [u8; 32] { hash( &[ &nonce.to_be_bytes()[..], &src_eid.to_be_bytes()[..], &sender.to_bytes()[..], &dst_eid.to_be_bytes()[..], &receiver[..], ] .concat(), ) .to_bytes() } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub dst_eid: u32, pub receiver: [u8; 32], pub message: Vec<u8>, pub options: Vec<u8>, pub native_fee: u64, pub lz_token_fee: u64, }

Init_Verify

Anyone can call endpoint.init_verify to register a pre-flight x-chain message which then waits for DVNs to verify.
/// --- programs/endpoint/src/lib.rs --- pub fn init_verify(mut ctx: Context<InitVerify>, params: InitVerifyParams) -> Result<()> { InitVerify::apply(&mut ctx, &params) }
use crate::*; #[derive(Accounts)] #[instruction(params: InitVerifyParams)] pub struct InitVerify<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( seeds = [ NONCE_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes(), &params.sender[..] ], bump = nonce.bump, constraint = params.nonce > nonce.inbound_nonce )] pub nonce: Account<'info, Nonce>, #[account( init, payer = payer, space = 8 + PayloadHash::INIT_SPACE, seeds = [ PAYLOAD_HASH_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes(), &params.sender[..], &params.nonce.to_be_bytes() ], bump )] pub payload_hash: Account<'info, PayloadHash>, pub system_program: Program<'info, System>, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct InitVerifyParams { pub src_eid: u32, pub sender: [u8; 32], pub receiver: Pubkey, pub nonce: u64, } impl InitVerify<'_> { pub fn apply(ctx: &mut Context<InitVerify>, _params: &InitVerifyParams) -> Result<()> { ctx.accounts.payload_hash.hash = EMPTY_PAYLOAD_HASH; ctx.accounts.payload_hash.bump = ctx.bumps.payload_hash; Ok(()) } }

Verify

Receiver library can call endpoint program to verify x-chain message where the nonce of the x-chain message is marked verified in endpoint program.
Note the receive_library is either the default one or custom one configured by the OApp.
receive_library is pda of receiver program (Ultra light node program) and should be signer. Receiver program ensures only x-chain message have been verified by DVNs set by OApp then it will call endpoint program to verify corresponding x-chain message.
/// --- programs/endpoint/src/lib.rs --- pub fn verify(mut ctx: Context<Verify>, params: VerifyParams) -> Result<()> { Verify::apply(&mut ctx, &params) }

Ultra Light Node (Message Lib)

/// --- programs/endpoint/src/instructions/verify.rs --- use crate::*; use cpi_helper::CpiContext; use solana_program::clock::Slot; /// MESSAGING STEP 2 /// requires init_verify() #[event_cpi] #[derive(CpiContext, Accounts)] #[instruction(params: VerifyParams)] pub struct Verify<'info> { /// The PDA of the receive library. #[account( constraint = is_valid_receive_library( receive_library.key(), &receive_library_config, &default_receive_library_config, Clock::get()?.slot ) @LayerZeroError::InvalidReceiveLibrary )] pub receive_library: Signer<'info>, #[account( seeds = [RECEIVE_LIBRARY_CONFIG_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes()], bump = receive_library_config.bump )] pub receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( seeds = [RECEIVE_LIBRARY_CONFIG_SEED, &params.src_eid.to_be_bytes()], bump = default_receive_library_config.bump )] pub default_receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( mut, seeds = [ NONCE_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes(), &params.sender[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, #[account( mut, seeds = [ PENDING_NONCE_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes(), &params.sender[..] ], bump = pending_inbound_nonce.bump )] pub pending_inbound_nonce: Account<'info, PendingInboundNonce>, #[account( mut, seeds = [ PAYLOAD_HASH_SEED, &params.receiver.to_bytes(), &params.src_eid.to_be_bytes(), &params.sender[..], &params.nonce.to_be_bytes() ], bump = payload_hash.bump, constraint = params.payload_hash != EMPTY_PAYLOAD_HASH @LayerZeroError::InvalidPayloadHash )] pub payload_hash: Account<'info, PayloadHash>, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct VerifyParams { pub src_eid: u32, pub sender: [u8; 32], pub receiver: Pubkey, pub nonce: u64, pub payload_hash: [u8; 32], } impl Verify<'_> { pub fn apply(ctx: &mut Context<Verify>, params: &VerifyParams) -> Result<()> { // don't need initializable() as the Nonce account was initiated by the delegate // don't need verifiable() as the init_verify() already checks the nonce requirement if params.nonce > ctx.accounts.nonce.inbound_nonce { ctx.accounts .pending_inbound_nonce .insert_pending_inbound_nonce(params.nonce, &mut ctx.accounts.nonce)?; } ctx.accounts.payload_hash.hash = params.payload_hash; emit_cpi!(PacketVerifiedEvent { src_eid: params.src_eid, sender: params.sender, receiver: params.receiver, nonce: params.nonce, payload_hash: params.payload_hash, }); Ok(()) } } pub fn is_valid_receive_library( actual_receiver_library: Pubkey, receiver_library_config: &ReceiveLibraryConfig, default_receiver_library_config: &ReceiveLibraryConfig, slot: Slot, ) -> bool { let (expected_receiver_library, is_default) = if receiver_library_config.message_lib == DEFAULT_MESSAGE_LIB { (default_receiver_library_config.message_lib, true) } else { (receiver_library_config.message_lib, false) }; // early return true if the actual_receiver_library is the currently configured one if actual_receiver_library == expected_receiver_library { return true; } // check the timeout condition otherwise // if the Oapp is using default_receiver_library_config, use the default timeout config // otherwise, use the timeout configured by the Oapp let timeout = if is_default { &default_receiver_library_config.timeout } else { &receiver_library_config.timeout }; // requires the actual_receiver_library to be the same as the one in grace period and the grace period has not expired if let Some(timeout) = timeout { if timeout.message_lib == actual_receiver_library && timeout.expiry > slot { return true; } } // returns false by default false }

Send

/// --- programs/uln/src/lib.rs --- pub fn send<'c: 'info, 'info>( mut ctx: Context<'_, '_, 'c, 'info, Send<'info>>, params: SendParams, ) -> Result<(MessagingFee, Vec<u8>)> { Send::apply(&mut ctx, &params) } /// --- programs/messagelib-interface/src/lib.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct SendParams { pub packet: Packet, pub options: Vec<u8>, pub native_fee: u64, } #[derive(Clone, AnchorSerialize, AnchorDeserialize, Default)] pub struct MessagingFee { pub native_fee: u64, pub lz_token_fee: u64, }
 
Process:
  1. Assign job to workers
  1. Quote and Charge Treasury Fee
  1. Emit Event
/// --- programs/uln/src/instructions/endpoint/send.rs --- #[event_cpi] #[derive(Accounts)] #[instruction(params: SendParams)] pub struct Send<'info> { pub endpoint: Signer<'info>, #[account(has_one = endpoint, seeds = [ULN_SEED], bump = uln.bump)] pub uln: Account<'info, UlnSettings>, /// The custom send config account may be uninitialized, so deserialize it only if it's initialized #[account( seeds = [SEND_CONFIG_SEED, &params.packet.dst_eid.to_be_bytes(), &params.packet.sender.to_bytes()], bump )] pub send_config: AccountInfo<'info>, #[account( seeds = [SEND_CONFIG_SEED, &params.packet.dst_eid.to_be_bytes()], bump = default_send_config.bump, )] pub default_send_config: Account<'info, SendConfig>, /// pay for the native fee #[account( mut, constraint = payer.key() != endpoint.key() @UlnError::InvalidPayer, )] pub payer: Signer<'info>, /// The treasury account to receive the native fee #[account(mut)] pub treasury: Option<AccountInfo<'info>>, /// for native fee transfer pub system_program: Program<'info, System>, } impl Send<'_> { pub fn apply<'c: 'info, 'info>( ctx: &mut Context<'_, '_, 'c, 'info, Send<'info>>, params: &SendParams, ) -> Result<(MessagingFee, Vec<u8>)> { let (executor_fee, dvn_fees) = assign_job_to_workers( &ctx.accounts.uln.key(), &ctx.accounts.payer, &params.packet, &params.options, &ctx.accounts.send_config, &ctx.accounts.default_send_config, ctx.remaining_accounts, )?; let worker_fee = executor_fee.fee + dvn_fees.iter().map(|f| f.fee).sum::<u64>(); // treasury fee let treasury_fee = if let Some(treasury) = &ctx.accounts.uln.treasury { let fee = quote_treasury(treasury, worker_fee, false)?; // assert the treasury receiver is the same as the treasury account let treasury_acc = ctx.accounts.treasury.as_ref().ok_or(UlnError::InvalidTreasury)?; require!(treasury_acc.key() == treasury.native_receiver, UlnError::InvalidTreasury); if fee > 0 { program::invoke( &system_instruction::transfer(ctx.accounts.payer.key, treasury_acc.key, fee), &[ctx.accounts.payer.to_account_info(), treasury_acc.to_account_info()], )?; Some(TreasuryFee { treasury: treasury.native_receiver, fee, pay_in_lz_token: false, }) } else { None } } else { None }; let total_fee = worker_fee + treasury_fee.as_ref().map(|f| f.fee).unwrap_or(0); require!(params.native_fee >= total_fee, UlnError::InsufficientFee); emit_cpi!(FeesPaidEvent { executor: executor_fee, dvns: dvn_fees, treasury: treasury_fee }); Ok((MessagingFee { native_fee: total_fee, lz_token_fee: 0 }, encode(&params.packet))) } } pub(crate) fn assign_job_to_workers<'c: 'info, 'info>( uln: &Pubkey, payer: &AccountInfo<'info>, packet: &Packet, options: &[u8], send_config: &AccountInfo, default_send_config: &SendConfig, worker_accounts: &[AccountInfo<'info>], ) -> Result<(WorkerFee, Vec<WorkerFee>)> { let (uln_config, executor_config) = get_send_config(send_config, default_send_config)?; let (executor_options, dvn_options) = decode_options(options)?; // pay executor fee let executor_accounts = &worker_accounts[0..4]; // each worker can have 4 accounts let executor_fee = quote_executor( uln, &executor_config, packet.dst_eid, &packet.sender, packet.message.len() as u64, executor_options, executor_accounts, )?; if executor_fee.fee > 0 { // the account at index 1 is the executor config account, which is the account that needs to be paid program::invoke( &system_instruction::transfer(payer.key, executor_accounts[1].key, executor_fee.fee), &[payer.to_account_info(), executor_accounts[1].to_account_info()], )?; } // pay dvn fees let dvn_accounts = &worker_accounts[4..]; let dvn_fees = quote_dvns( uln, &uln_config, packet.dst_eid, &packet.sender, encode_packet_header(&packet), hash_payload(&packet.guid, &packet.message), dvn_options, dvn_accounts, )?; for (i, chunk) in dvn_accounts.chunks(4).enumerate() { let fee = dvn_fees[i].fee; if fee > 0 { // the account at index 1 is the dvn config account, // which is the account that needs to be paid let dvn_acc = &chunk[1]; program::invoke( &system_instruction::transfer(payer.key, dvn_acc.key, fee), &[payer.to_account_info(), chunk[1].to_account_info()], )?; } } Ok((executor_fee, dvn_fees)) } pub(crate) fn get_send_config( custom_config_acc: &AccountInfo, default_config: &SendConfig, ) -> Result<(UlnConfig, ExecutorConfig)> { let custom_config = local_custom_config::<SendConfig>(custom_config_acc)?; let uln_config = UlnConfig::get_config(&default_config.uln, &custom_config.uln)?; let executor_config = ExecutorConfig::get_config(&default_config.executor, &custom_config.executor); Ok((uln_config, executor_config)) } pub(crate) fn local_custom_config<T: Default + AccountDeserialize>( custom_config_acc: &AccountInfo, ) -> Result<T> { // if the custom config account is not initialized, return the default config let custom_config = if custom_config_acc.owner.key() == ID { let mut config_data: &[u8] = &custom_config_acc.try_borrow_data()?; T::try_deserialize(&mut config_data)? } else { T::default() }; Ok(custom_config) }
 

Assign Job to Workers

Process:
  1. Get OApp’s send config
  1. Decode Options
  1. Quote Executor Fee and Send Fee to Executor
  1. Quote DVNs’s Fees and Send Fee to DVNs
/// --- programs/uln/src/instructions/endpoint/send.rs --- pub(crate) fn assign_job_to_workers<'c: 'info, 'info>( uln: &Pubkey, payer: &AccountInfo<'info>, packet: &Packet, options: &[u8], send_config: &AccountInfo, default_send_config: &SendConfig, worker_accounts: &[AccountInfo<'info>], ) -> Result<(WorkerFee, Vec<WorkerFee>)> { /// Get OApp’s send config let (uln_config, executor_config) = get_send_config(send_config, default_send_config)?; /// Decode Options let (executor_options, dvn_options) = decode_options(options)?; /// Quote Executor Fee and Send Fee to Executor let executor_accounts = &worker_accounts[0..4]; // each worker can have 4 accounts let executor_fee = quote_executor( uln, &executor_config, packet.dst_eid, &packet.sender, packet.message.len() as u64, executor_options, executor_accounts, )?; if executor_fee.fee > 0 { // the account at index 1 is the executor config account, which is the account that needs to be paid program::invoke( &system_instruction::transfer(payer.key, executor_accounts[1].key, executor_fee.fee), &[payer.to_account_info(), executor_accounts[1].to_account_info()], )?; } /// Quote DVNs’s Fees and Send Fee to DVNs let dvn_accounts = &worker_accounts[4..]; let dvn_fees = quote_dvns( uln, &uln_config, packet.dst_eid, &packet.sender, encode_packet_header(&packet), hash_payload(&packet.guid, &packet.message), dvn_options, dvn_accounts, )?; for (i, chunk) in dvn_accounts.chunks(4).enumerate() { let fee = dvn_fees[i].fee; if fee > 0 { // the account at index 1 is the dvn config account, // which is the account that needs to be paid let dvn_acc = &chunk[1]; program::invoke( &system_instruction::transfer(payer.key, dvn_acc.key, fee), &[payer.to_account_info(), chunk[1].to_account_info()], )?; } } Ok((executor_fee, dvn_fees)) }
 

get_send_config

get_send_config aggregates config set by OApp and default config, and returns the final OApp config.
  • tries to load custom_config set by Oapp. If the custom_config is not initialized, it will return config with default value.
  • aggregate custom_config and default_config to get final OApp config.
/// --- programs/uln/src/instructions/endpoint/send.rs --- pub(crate) fn get_send_config( custom_config_acc: &AccountInfo, default_config: &SendConfig, ) -> Result<(UlnConfig, ExecutorConfig)> { let custom_config = local_custom_config::<SendConfig>(custom_config_acc)?; let uln_config = UlnConfig::get_config(&default_config.uln, &custom_config.uln)?; let executor_config = ExecutorConfig::get_config(&default_config.executor, &custom_config.executor); Ok((uln_config, executor_config)) } pub(crate) fn local_custom_config<T: Default + AccountDeserialize>( custom_config_acc: &AccountInfo, ) -> Result<T> { // if the custom config account is not initialized, return the default config let custom_config = if custom_config_acc.owner.key() == ID { let mut config_data: &[u8] = &custom_config_acc.try_borrow_data()?; T::try_deserialize(&mut config_data)? } else { T::default() }; Ok(custom_config) } /// --- programs/uln/src/state/uln.rs --- #[account] #[derive(InitSpace, Default)] pub struct SendConfig { pub bump: u8, pub uln: UlnConfig, pub executor: ExecutorConfig, } /// --- programs/uln/src/state/uln.rs --- // the max data size that can be sent through a CPI is 1280 bytes // the total size of (optional) dvn list is not more than 20 pub const DVN_MAX_LEN: u8 = 16; #[derive(Clone, InitSpace, AnchorSerialize, AnchorDeserialize, Default)] pub struct UlnConfig { pub confirmations: u64, pub required_dvn_count: u8, pub optional_dvn_count: u8, pub optional_dvn_threshold: u8, #[max_len(DVN_MAX_LEN)] pub required_dvns: Vec<Pubkey>, // PDA of DVN program (fees paid to these accounts) #[max_len(DVN_MAX_LEN)] pub optional_dvns: Vec<Pubkey>, // PDA of DVN program (fees paid to these accounts) } impl UlnConfig { /// ... pub fn get_config(default_config: &UlnConfig, custom_config: &UlnConfig) -> Result<UlnConfig> { let mut rtn_config = UlnConfig::default(); if custom_config.confirmations == Self::DEFAULT as u64 { rtn_config.confirmations = default_config.confirmations; } else if custom_config.confirmations != Self::NIL_CONFIRMATIONS { rtn_config.confirmations = custom_config.confirmations; } // else do nothing, rtnConfig.confirmation is 0 if custom_config.required_dvn_count == Self::DEFAULT { if default_config.required_dvn_count > 0 { rtn_config.required_dvns = default_config.required_dvns.clone(); rtn_config.required_dvn_count = default_config.required_dvn_count; } } else { if custom_config.required_dvn_count != Self::NIL_DVN_COUNT { rtn_config.required_dvns = custom_config.required_dvns.clone(); rtn_config.required_dvn_count = custom_config.required_dvn_count; } } if custom_config.optional_dvn_count == Self::DEFAULT { if default_config.optional_dvn_count > 0 { rtn_config.optional_dvns = default_config.optional_dvns.clone(); rtn_config.optional_dvn_count = default_config.optional_dvn_count; rtn_config.optional_dvn_threshold = default_config.optional_dvn_threshold; } } else { if custom_config.optional_dvn_count != Self::NIL_DVN_COUNT { rtn_config.optional_dvns = custom_config.optional_dvns.clone(); rtn_config.optional_dvn_count = custom_config.optional_dvn_count; rtn_config.optional_dvn_threshold = custom_config.optional_dvn_threshold; } } // the final value must have at least one dvn // it is possible that some default config result into 0 dvns Self::assert_at_least_one_dvn(&rtn_config)?; Ok(rtn_config) } } /// --- programs/uln/src/state/uln.rs --- #[derive(Clone, InitSpace, AnchorSerialize, AnchorDeserialize, Default)] pub struct ExecutorConfig { pub max_message_size: u32, pub executor: Pubkey, // PDA of executor program (fees paid to this account) } impl ExecutorConfig { /// ... pub fn get_config( default_config: &ExecutorConfig, custom_config: &ExecutorConfig, ) -> ExecutorConfig { let mut rtn_config = ExecutorConfig::default(); rtn_config.max_message_size = if custom_config.max_message_size == 0 { default_config.max_message_size } else { custom_config.max_message_size }; rtn_config.executor = if custom_config.executor == Pubkey::default() { default_config.executor } else { custom_config.executor }; rtn_config } }

Quote Treasury Fee

/// --- programs/uln/src/instructions/endpoint/quote.rs --- pub(crate) fn quote_treasury( treasury: &Treasury, worker_fee: u64, pay_in_lz_token: bool, ) -> Result<u64> { if pay_in_lz_token { let treasury = treasury.lz_token.as_ref().ok_or(UlnError::LzTokenUnavailable)?; Ok(treasury.fee) } else { // pay in native Ok(worker_fee * treasury.native_fee_bps / BPS_DENOMINATOR) } } /// --- programs/uln/src/state/uln.rs --- #[derive(InitSpace, Clone, AnchorSerialize, AnchorDeserialize)] pub struct Treasury { pub admin: Option<Pubkey>, pub native_receiver: Pubkey, pub native_fee_bps: u64, pub lz_token: Option<LzTokenTreasury>, } #[derive(InitSpace, Clone, AnchorSerialize, AnchorDeserialize)] pub struct LzTokenTreasury { pub receiver: Pubkey, pub fee: u64, // amount passed in is always 10^decimals }

Decode Options

/// --- programs/uln/src/options_codec.rs --- pub fn decode_options(options: &[u8]) -> Result<(Vec<LzOption>, DVNOptions)> { let mut executor_options = Vec::new(); let mut dvn_options = DVNOptions::new(); // the first 2 bytes is the format type let format_type = options.to_u16(0); if format_type < TYPE_3 { executor_options = convert_legacy_options(format_type, &options)?; Ok((executor_options, dvn_options)) } else if format_type == TYPE_3 { // type3 options: [worker_option][worker_option]... // worker_option: [worker_id][option_size][option] // option: [option_type][params] // worker_id: uint8, option_size: uint16, option: bytes, option_type: uint8, params: bytes let mut cursor = 2; while cursor < options.len() { let worker_id = options.to_u8(cursor); cursor += 1; let option_size = options.to_u16(cursor) as usize; cursor += 2; match worker_id { EXECUTOR_WORKER_ID => { let option_type = options.to_u8(cursor); let option_params = &options[cursor + 1..cursor + option_size]; cursor += option_size; executor_options.push(LzOption { option_type, params: option_params.to_vec() }) }, DVN_WORKER_ID => { let idx = options.to_u8(cursor); let option_type = options.to_u8(cursor + 1); let option_params = &options[cursor + 2..cursor + option_size]; cursor += option_size; if let Some(options) = dvn_options.get_mut(&idx) { options.push(LzOption { option_type, params: option_params.to_vec() }); } else { dvn_options.insert( idx, vec![LzOption { option_type, params: option_params.to_vec() }], ); } }, _ => return Err(UlnError::InvalidWorkerId.into()), } } Ok((executor_options, dvn_options)) } else { Err(UlnError::InvalidOptionType.into()) } } #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] pub struct LzOption { pub option_type: u8, pub params: Vec<u8>, }

Quote Executor

/// --- programs/uln/src/instructions/endpoint/quote.rs --- pub fn quote_executor( uln: &Pubkey, executor_config: &ExecutorConfig, dst_eid: u32, sender: &Pubkey, calldata_size: u64, options: Vec<LzOption>, // [executor_program, executor_config, price_feed_program, price_feed_config] accounts: &[AccountInfo], ) -> Result<WorkerFee> { require!( calldata_size <= executor_config.max_message_size as u64, UlnError::ExceededMaxMessageSize ); // assert all accounts are non-signer // executor_config should be writable to receive the fee on send(), so it's not checked for account in accounts { require!(!account.is_signer, UlnError::NonSigner); } let executor_program = &accounts[0]; let executor_acc = &accounts[1]; // assert executor program is owner of executor config require!(executor_program.key() == *executor_acc.owner, UlnError::InvalidExecutorProgram); // assert executor is the same as the executor in the executor config require!(executor_config.executor == executor_acc.key(), UlnError::InvalidExecutor); let params = QuoteExecutorParams { msglib: uln.key(), dst_eid, sender: *sender, calldata_size, options }; let cpi_ctx = CpiContext::new( executor_program.to_account_info(), worker_interface::cpi::accounts::Quote { worker_config: executor_acc.to_account_info(), price_feed_program: accounts[2].to_account_info(), price_feed_config: accounts[3].to_account_info(), }, ); let fee = worker_interface::cpi::quote_executor(cpi_ctx, params)?.get(); Ok(WorkerFee { worker: executor_config.executor, fee }) }

Quote DVN

/// --- programs/uln/src/instructions/endpoint/quote.rs --- pub fn quote_dvns( uln: &Pubkey, uln_config: &UlnConfig, dst_eid: u32, sender: &Pubkey, packet_header: Vec<u8>, payload_hash: [u8; 32], options: DVNOptions, // [dvn_program, dvn_config, price_feed_program, price_feed_config, ...] accounts: &[AccountInfo], ) -> Result<Vec<WorkerFee>> { let length = uln_config.required_dvns.len() + uln_config.optional_dvns.len(); require!(accounts.len() == length * 4, UlnError::InvalidAccountLength); // assert all accounts are non-signer // dvn_config should be writable to receive the fee on send(), so it's not checked for account in accounts { require!(!account.is_signer, UlnError::NonSigner); } let mut fees = Vec::with_capacity(length); for (i, chunk) in accounts.chunks(4).enumerate() { let dvn_program = &chunk[0]; let dvn_acc = &chunk[1]; // assert dvn program is owner of dvn config require!(dvn_program.key() == *dvn_acc.owner, UlnError::InvalidDvnProgram); let dvn = if i < uln_config.required_dvns.len() { uln_config.required_dvns[i] } else { uln_config.optional_dvns[i - uln_config.required_dvns.len()] }; // assert dvn is the same as the dvn in the dvn config require!(dvn == dvn_acc.key(), UlnError::InvalidDvn); let options = options.get(&(i as u8)).cloned().unwrap_or_default(); let params = QuoteDvnParams { msglib: uln.key(), dst_eid, sender: *sender, packet_header: packet_header.clone(), payload_hash, confirmations: uln_config.confirmations, options, }; let cpi_ctx = CpiContext::new( dvn_program.to_account_info(), worker_interface::cpi::accounts::Quote { worker_config: dvn_acc.to_account_info(), price_feed_program: chunk[2].to_account_info(), price_feed_config: chunk[3].to_account_info(), }, ); let fee = worker_interface::cpi::quote_dvn(cpi_ctx, params)?.get(); fees.push(WorkerFee { worker: dvn, fee }); } Ok(fees) }

Send with LayerZero Token

Executor

quote_executor

/// --- programs/executor/src/lib.rs --- /// --------------------------- MsgLib Instructions --------------------------- pub fn quote_executor(ctx: Context<Quote>, params: QuoteExecutorParams) -> Result<u64> { Quote::apply(&ctx, &params) } /// --- programs/executor/src/instructions/quote.rs --- #[derive(Accounts)] #[instruction(params: QuoteExecutorParams)] pub struct Quote<'info> { #[account(seeds = [EXECUTOR_CONFIG_SEED], bump = executor_config.bump)] pub executor_config: Account<'info, ExecutorConfig>, #[account(address = price_feed_config.owner.clone())] pub price_feed_program: AccountInfo<'info>, #[account(address = executor_config.price_feed)] pub price_feed_config: AccountInfo<'info>, } impl Quote<'_> { pub fn apply(ctx: &Context<Quote>, params: &QuoteExecutorParams) -> Result<u64> { require!(!ctx.accounts.executor_config.paused, ExecutorError::Paused); let config = &ctx.accounts.executor_config; config.acl.assert_permission(&params.sender)?; if config.msglibs.len() > 0 { require!( config.msglibs.binary_search(&params.msglib).is_ok(), ExecutorError::MsgLibNotAllowed ); } let dst_config = sorted_list_helper::get_from_sorted_list_by_eid(&config.dst_configs, params.dst_eid)?; require!(dst_config.lz_receive_base_gas > 0, ExecutorError::EidNotSupported); let mut total_dst_amount: u128 = 0; let mut ordered = false; let mut unique_lz_compose_idx = Vec::new(); let mut total_lz_compose_gas: HashMap<u16, u128> = HashMap::new(); let mut total_lz_receive_gas: u128 = 0; for option in params.options.clone() { match option.option_type { OPTION_TYPE_LZRECEIVE => { let (gas, value) = decode_lz_receive_params(&option.params)?; total_dst_amount += value; total_lz_receive_gas += gas; }, OPTION_TYPE_NATIVE_DROP => { let (amount, _) = decode_native_drop_params(&option.params)?; total_dst_amount += amount; }, OPTION_TYPE_LZCOMPOSE => { let (index, gas, value) = decode_lz_compose_params(&option.params)?; total_dst_amount += value; // update lz compose gas by index if let Some(lz_compose_gas) = total_lz_compose_gas.get(&index) { total_lz_compose_gas.insert(index, lz_compose_gas + gas); } else { total_lz_compose_gas.insert(index, gas); unique_lz_compose_idx.push(index); } }, OPTION_TYPE_ORDERED_EXECUTION => { ordered = true; }, _ => return Err(ExecutorError::UnsupportedOptionType.into()), } } require!( total_dst_amount <= dst_config.native_drop_cap, ExecutorError::NativeAmountExceedsCap ); // validate lzComposeGas and lzReceiveGas require!(total_lz_receive_gas > 0, ExecutorError::ZeroLzReceiveGasProvided); let mut total_gas = dst_config.lz_receive_base_gas as u128 + total_lz_receive_gas; for idx in unique_lz_compose_idx { let lz_compose_gas = total_lz_compose_gas.get(&idx).unwrap(); require!(*lz_compose_gas > 0, ExecutorError::ZeroLzComposeGasProvided); total_gas += dst_config.lz_compose_base_gas as u128 + *lz_compose_gas; } if ordered { total_gas = total_gas * 102 / 100; // 2% extra gas for ordered } let get_fee_params = GetFeeParams { dst_eid: params.dst_eid, calldata_size: params.calldata_size, total_gas, }; let cpi_ctx = CpiContext::new( ctx.accounts.price_feed_program.to_account_info(), pricefeed::cpi::accounts::GetFee { price_feed: ctx.accounts.price_feed_config.to_account_info(), }, ); let (mut fee_for_gas, price_ratio, price_ration_denominator, native_token_price_usd) = pricefeed::cpi::get_fee(cpi_ctx, get_fee_params)?.get(); let multiplier_bps = if let Some(multiplier_bps) = dst_config.multiplier_bps { multiplier_bps as u128 } else { config.default_multiplier_bps as u128 }; fee_for_gas = worker_utils::increase_fee_with_multiplier_or_floor_margin( fee_for_gas, multiplier_bps, dst_config.floor_margin_usd, native_token_price_usd, ); let fee_for_amount = total_dst_amount * price_ratio * multiplier_bps / price_ration_denominator / 10000; let total_fee = fee_for_gas + fee_for_amount; Ok(worker_utils::safe_convert_u128_to_u64(total_fee)?) } }

Execute

Execution process is different compated to EVM implemetation:
  • In EVM, executor EOA calls endpoint contract and endpoint contract calls OApp.
  • In SVM
    • executor calls Executor program to execute x-chain message
    • then Executor calls OApp’s lzReceive instruction to execute x-chain message.
    • OApp calls calls Endpoint program to check the validity of the payload (compare hash and check whether the nonce has been verified by DVNs), and marks the x-chain message has been executed in Endpoint program to prevent it from being re-executed.
/// --- programs/executor/src/lib.rs --- pub fn execute(mut ctx: Context<Execute>, params: ExecuteParams) -> Result<()> { Execute::apply(&mut ctx, &params) }
/// --- programs/executor/src/instructions/execute.rs --- #[event_cpi] #[derive(Accounts)] pub struct Execute<'info> { #[account(mut)] pub executor: Signer<'info>, #[account( seeds = [EXECUTOR_CONFIG_SEED], bump = config.bump, constraint = config.executors.contains(executor.key) @ExecutorError::NotExecutor )] pub config: Account<'info, ExecutorConfig>, pub endpoint_program: Program<'info, Endpoint>, /// The authority for the endpoint program to emit events pub endpoint_event_authority: UncheckedAccount<'info>, } #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct ExecuteParams { pub receiver: Pubkey, pub lz_receive: LzReceiveParams, pub value: u64, pub compute_units: u64, } impl Execute<'_> { pub fn apply(ctx: &mut Context<Execute>, params: &ExecuteParams) -> Result<()> { let balance_before = ctx.accounts.executor.lamports(); let program_id = ctx.remaining_accounts[0].key(); let accounts = ctx .remaining_accounts .iter() .skip(1) .map(|acc| acc.to_account_metas(None)[0].clone()) .collect::<Vec<_>>(); let data = get_lz_receive_ix_data(&params.lz_receive)?; let result = invoke(&Instruction { program_id, accounts, data }, ctx.remaining_accounts); if let Err(e) = result { // call lz_receive_alert let params = LzReceiveAlertParams { receiver: params.receiver, src_eid: params.lz_receive.src_eid, sender: params.lz_receive.sender, nonce: params.lz_receive.nonce, guid: params.lz_receive.guid, compute_units: params.compute_units, value: params.value, message: params.lz_receive.message.clone(), extra_data: params.lz_receive.extra_data.clone(), reason: e.to_string().into_bytes(), }; let cpi_ctx = LzReceiveAlert::construct_context( ctx.accounts.endpoint_program.key(), &[ ctx.accounts.config.to_account_info(), // use the executor config as the signer ctx.accounts.endpoint_event_authority.to_account_info(), ctx.accounts.endpoint_program.to_account_info(), ], )?; endpoint::cpi::lz_receive_alert( cpi_ctx.with_signer(&[&[EXECUTOR_CONFIG_SEED, &[ctx.accounts.config.bump]]]), params, )?; } else { // assert that the executor account does not lose more than the expected value let balance_after = ctx.accounts.executor.lamports(); require!( balance_before <= balance_after + params.value, ExecutorError::InsufficientBalance ); } require!( ctx.accounts.executor.owner.key() == system_program::ID, ExecutorError::InvalidOwner ); require!(ctx.accounts.executor.data_is_empty(), ExecutorError::InvalidSize); Ok(()) } } fn get_lz_receive_ix_data(params: &LzReceiveParams) -> Result<Vec<u8>> { let mut data = Vec::with_capacity(92 + params.message.len() + params.extra_data.len()); // 8 + 4 + 32 + 8 + 32 + 4 + 4 data.extend(LZ_RECEIVE_DISCRIMINATOR); params.serialize(&mut data)?; Ok(data) }

DVN

Quote DVN

/// --- programs/dvn/src/lib.rs --- /// --------------------------- MsgLib Instructions --------------------------- pub fn quote_dvn(ctx: Context<Quote>, params: QuoteDvnParams) -> Result<u64> { Quote::apply(&ctx, &params) } /// -- programs/dvn/src/instructions/quote.rs --- #[derive(Accounts)] #[instruction(params: QuoteDvnParams)] pub struct Quote<'info> { #[account(seeds = [DVN_CONFIG_SEED], bump = dvn_config.bump)] pub dvn_config: Account<'info, DvnConfig>, #[account(address = price_feed_config.owner.clone())] pub price_feed_program: AccountInfo<'info>, #[account(address = dvn_config.price_feed)] pub price_feed_config: AccountInfo<'info>, } /// --- programs/worker-interface/src/lib.rs --- #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub struct QuoteDvnParams { pub msglib: Pubkey, pub dst_eid: u32, pub sender: Pubkey, pub packet_header: Vec<u8>, pub payload_hash: [u8; 32], pub confirmations: u64, pub options: Vec<LzOption>, }
 
/// --- programs/dvn/src/instructions/quote.rs --- pub fn apply(ctx: &Context<Quote>, params: &QuoteDvnParams) -> Result<u64> { let config = &ctx.accounts.dvn_config; require!(!config.paused, DvnError::Paused); config.acl.assert_permission(&params.sender)?; if config.msglibs.len() > 0 { require!( config.msglibs.binary_search(&params.msglib).is_ok(), DvnError::MsgLibNotAllowed ); } let total_signature_bytes = config.multisig.quorum as u64 * SIGNATURE_RAW_BYTES as u64; let total_signature_bytes_padded = if total_signature_bytes % 32 == 0 { total_signature_bytes } else { total_signature_bytes + 32 - (total_signature_bytes % 32) }; // getFee should charge on execute(updateHash) // totalSignatureBytesPadded also has 64 overhead for bytes let calldata_size = EXECUTE_FIXED_BYTES + VERIFY_BYTES + total_signature_bytes_padded + 64; let dst_config = sorted_list_helper::get_from_sorted_list_by_eid(&config.dst_configs, params.dst_eid)?; require!(dst_config.dst_gas > 0, DvnError::EidNotSupported); let get_fee_params = GetFeeParams { dst_eid: params.dst_eid, calldata_size, total_gas: dst_config.dst_gas as u128, }; let cpi_ctx = CpiContext::new( ctx.accounts.price_feed_program.to_account_info(), pricefeed::cpi::accounts::GetFee { price_feed: ctx.accounts.price_feed_config.to_account_info(), }, ); let (fee, _, _, native_token_price_usd) = pricefeed::cpi::get_fee(cpi_ctx, get_fee_params)?.get(); let multiplier_bps = if let Some(multiplier_bps) = dst_config.multiplier_bps { multiplier_bps } else { config.default_multiplier_bps }; let fee = worker_utils::increase_fee_with_multiplier_or_floor_margin( fee, multiplier_bps as u128, dst_config.floor_margin_usd, native_token_price_usd, ); Ok(worker_utils::safe_convert_u128_to_u64(fee)?) }