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
- OApp Interaction:
- Calls
Endpoint.send()
, triggering token burn/lock (OFT) and message creation.
- ULN Processing:
- Assigns executor/DVNs based on OApp configurations.
- Calculates fees (gas, treasury cuts) and transfers payments.
- Message Emission:
- Constructs a
Packet
(nonce, GUID, sender/receiver, payload). - Emits event for off-chain relayers to propagate the message.
b. Verification & Execution
- Initial Verification:
- Anyone can initialize a pending message via
Endpoint.init_verify
.
- DVN Verification:
- DVNs call
verify()
after confirming on-chain data (e.g., block headers).
- Verification Commitment
- After enough DVNs have verified x-chain message,
Uln.commit_verification
can be called permissionlessly to register x-chain message intoEndpoint
.
- Final Execution:
- Executor calls
Executor.execute
, which invokes the OApp'slzReceive
. - 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
- OApp Setup: Register with Endpoint, configure libraries/peers.
- OFT Deployment: Choose native/adapter mode, set shared decimals.
- 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
Basic Process
OApp Register
- OApp calls
endpoint.register_oapp
to register OApp and its delegate. OApp delegate has authority to set OApp’s configuration inEndpoint
program.
- OApp delegate calls
endpoint.init_nonce
to initialize x-chain route, wherenonce
related accounts are initialized.
- 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.
- OApp delegate calls
endpoint.init_receive_library
to initialize receive library which will be used to verify x-chain messages sent from remote chains.
- OApp delegate calls
endpoint.init_config
to initializesend_config
andreceive_config
(control executor and dvn setting) in sender library program.
- OApp delegate calls
endpoint.set_config
to setsend_config
andreceive_config
in receiver library program.
Send x-chain Message
- 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
- 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.
- DVNs calls receiver library program’s
init_verify
andverify
instructions on remote chain to register x-chain messge with block confirmations.
- 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 callsEndpoint.verify
instruction to register the x-chain message’s nonce and payload hash, waiting for execution.
- executor calls
Executor.execute
to execute x-chain message where Executor calls OApp directly. And OApp callsEndpoint.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. - The logic behind replay preventation is that
Endpoint.Nonce.inbound_nonce
records the largest message nonce whose previous nonces have all be verified. EverytimeEndpoing.verify
is called, it tries to update theinbound_nonce
.- to call
Endpoint.init_verify
to initializePayloadHash
account,Endpoint
program requires x-chain message nonce should be larger than recordedinbound_nonce
. - when
Endpoing.verify
is called, it tries to update theinbound_nonce
. - when
oft.lzReceive
is called, it callsEndpoint.clear
to deletePayloadHash
account which records payload’s hash, and it requires x-chain message nonce should be smaller or equal thaninbound_nonce
recorded. - so as long as OApp successfully calls
Endpoint.clear
, which means the message nonce is smaller thaninbound_nonce
. Then the correspondingPayloadHash
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, ¶ms) }
InitOft::apply
- creates
OftConfig
pda which represents this OApp. ld2sd_rate
: conversion rate between local decimals and shared decimalstoken_mint
: token mint addresstoken_program
: token program address of the token mintendpoint_program
: endpoint program addressbump
: bump of thisOftConfig
pdaadmin
: admin of this OAppext
: 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 theOftConfig
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, ¶ms) }
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, ¶ms) }
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(), ¶ms.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, ¶ms) }
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(), ¶ms.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(¶ms.send)?; ctx.accounts.enforced_options.send = params.send.clone(); oapp::options::assert_type_3(¶ms.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, ¶ms) }
/// --- 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, ¶ms) }
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, ¶ms) } /// --- 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
- Calculate Remote Chain Received Token Amount
- if token’s
TransferFeeConfig
extension enabled, transfer fee will be charged. - decimals difference between local chain and remote chainincurs dust.
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
). - Minimal Bridge Amount Check
check amount can be received on remote chain is bigger than minimal amount specified in
params
- Re-calculate Bridge Amount
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
.- Consume Bridge Rate Limitation
consume bridge rate limitation if there is rate limit
- Burn/Lock token
- For Adapter OFT, it transfers token into escrow account
- For native OFT, it burns token
- Send token
calls
oapp::endpoint_cpi::send
to emit event to send token to remote chainNote:
peer
andoft_config
andenforced_options
should be initialized first
- 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 bridgeamount_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(), ¶ms.dst_eid.to_be_bytes() ], bump = peer.bump )] pub peer: Account<'info, Peer>, #[account( seeds = [ ENFORCED_OPTIONS_SEED, &oft_config.key().to_bytes(), ¶ms.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(), ¶ms.compose_msg, ), options: ctx .accounts .enforced_options .combine_options(¶ms.compose_msg, ¶ms.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(), ¶ms.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, ¶ms.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(), ¶ms.dst_eid.to_be_bytes(), ¶ms.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, ¶ms) } /// --- 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, ¶ms) }
/// --- 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(), ¶ms.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, ¶ms) }
/// --- 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, ¶ms.sender.to_bytes(), ¶ms.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, ¶ms.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, ¶ms.sender.to_bytes(), ¶ms.dst_eid.to_be_bytes(), ¶ms.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, ¶ms) } /// --- 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
- Increate Outbound Nonce
- Increase the outbound nonce of this x-chain message route
- Calculate Guid
- Outbound nonce
- endpoint id
- sender
- destination endpoint id
- receiver address
Guid is calculated based on
- Construct Packet
- Check Send Library Validity
- Get the send library(pda) address based on OApp’s config and default config
- Check the the send library is derived from passed-in
send_library_program
.
- Call
send_library_program
(Ultra Light Node Prorgram) to assign job to workers to send x-chain message
- Emit Event
Note:
- sender’s
send_library_config
pda should be initialized before by callingendpoint.init_send_library
default_send_library_config
is initialized by endpoint admin ba callingendpoint.init_default_send_library
, which means supported destination chain list should be registered previously.
send_library_info
(MessageLibInfo
) is registered by endpoint program admin previously.
nonce
account should be initialized before usingendpoint.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(), ¶ms.dst_eid.to_be_bytes()], bump = send_library_config.bump )] pub send_library_config: Account<'info, SendLibraryConfig>, #[account( seeds = [SEND_LIBRARY_CONFIG_SEED, ¶ms.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(), ¶ms.dst_eid.to_be_bytes(), ¶ms.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, ¶ms) }
use crate::*; #[derive(Accounts)] #[instruction(params: InitVerifyParams)] pub struct InitVerify<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( seeds = [ NONCE_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.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, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..], ¶ms.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, ¶ms) }
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, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = receive_library_config.bump )] pub receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( seeds = [RECEIVE_LIBRARY_CONFIG_SEED, ¶ms.src_eid.to_be_bytes()], bump = default_receive_library_config.bump )] pub default_receive_library_config: Account<'info, ReceiveLibraryConfig>, #[account( mut, seeds = [ NONCE_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..] ], bump = nonce.bump )] pub nonce: Account<'info, Nonce>, #[account( mut, seeds = [ PENDING_NONCE_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..] ], bump = pending_inbound_nonce.bump )] pub pending_inbound_nonce: Account<'info, PendingInboundNonce>, #[account( mut, seeds = [ PAYLOAD_HASH_SEED, ¶ms.receiver.to_bytes(), ¶ms.src_eid.to_be_bytes(), ¶ms.sender[..], ¶ms.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, ¶ms) } /// --- 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:
- Assign job to workers
- Quote and Charge Treasury Fee
- 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, ¶ms.packet.dst_eid.to_be_bytes(), ¶ms.packet.sender.to_bytes()], bump )] pub send_config: AccountInfo<'info>, #[account( seeds = [SEND_CONFIG_SEED, ¶ms.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, ¶ms.packet, ¶ms.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(¶ms.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:
- Get OApp’s send config
- Decode Options
- Quote Executor Fee and Send Fee to Executor
- 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 thecustom_config
is not initialized, it will return config with default value.
- aggregate
custom_config
anddefault_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, ¶ms) } /// --- 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(¶ms.sender)?; if config.msglibs.len() > 0 { require!( config.msglibs.binary_search(¶ms.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’slzReceive
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 inEndpoint
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, ¶ms) }
/// --- 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(¶ms.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, ¶ms) } /// -- 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(¶ms.sender)?; if config.msglibs.len() > 0 { require!( config.msglibs.binary_search(¶ms.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)?) }