Skip to main content

intent_transfer/bridge/processor/
bridge_ntt_tokens.rs

1use crate::{
2    bridge::{
3        be::{U16BE, U64BE},
4        cpi::{self, ntt_with_executor::RelayNttMessageArgs},
5        message::{convert_chain_id_to_wormhole, BridgeMessage, NttMessage, WormholeChainId},
6    },
7    config::state::{
8        fee_config::{FeeConfig, FEE_CONFIG_SEED},
9        ntt_config::{verify_ntt_manager, ExpectedNttConfig, EXPECTED_NTT_CONFIG_SEED},
10    },
11    error::IntentTransferError,
12    fees::{PaidInstruction, VerifyAndCollectAccounts},
13    nonce::Nonce,
14    verify::{verify_and_update_nonce, verify_signer_matches_source, verify_symbol_or_mint},
15    INTENT_TRANSFER_SEED,
16};
17use anchor_lang::{prelude::*, solana_program::sysvar::instructions};
18use anchor_spl::{
19    associated_token::AssociatedToken,
20    token::{
21        approve, close_account, spl_token::try_ui_amount_into_amount, transfer_checked, Approve,
22        CloseAccount, Mint, Token, TokenAccount, TransferChecked,
23    },
24};
25use borsh::{BorshDeserialize, BorshSerialize};
26use chain_id::ChainId;
27use solana_intents::Intent;
28
29const BRIDGE_NTT_INTERMEDIATE_SEED: &[u8] = b"bridge_ntt_intermediate";
30const BRIDGE_NTT_NONCE_SEED: &[u8] = b"bridge_ntt_nonce";
31
32#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
33pub struct BridgeNttTokensArgs {
34    pub signed_quote_bytes: [u8; 165],
35    pub pay_destination_ata_rent: bool,
36}
37
38#[derive(Accounts)]
39pub struct Ntt<'info> {
40    /// CHECK: Clock sysvar
41    pub clock: Sysvar<'info, Clock>,
42
43    /// CHECK: Rent sysvar
44    pub rent: Sysvar<'info, Rent>,
45
46    /// CHECK: checked in NTT manager program
47    pub ntt_manager: UncheckedAccount<'info>,
48
49    /// CHECK: checked in NTT manager program
50    pub ntt_config: UncheckedAccount<'info>,
51
52    /// CHECK: checked in NTT manager program
53    #[account(mut)]
54    pub ntt_inbox_rate_limit: UncheckedAccount<'info>,
55
56    /// CHECK: checked in NTT manager program
57    pub ntt_session_authority: UncheckedAccount<'info>,
58
59    /// CHECK: checked in NTT manager program
60    pub ntt_token_authority: UncheckedAccount<'info>,
61
62    /// CHECK: checked in NTT manager program
63    #[account(mut)]
64    pub wormhole_message: UncheckedAccount<'info>,
65
66    /// CHECK: checked in NTT manager program
67    pub transceiver: UncheckedAccount<'info>,
68
69    /// CHECK: checked in NTT manager program
70    pub emitter: UncheckedAccount<'info>,
71
72    /// CHECK: checked in NTT manager program
73    #[account(mut)]
74    pub wormhole_bridge: UncheckedAccount<'info>,
75
76    /// CHECK: checked in wormhole program
77    #[account(mut)]
78    pub wormhole_fee_collector: UncheckedAccount<'info>,
79
80    /// CHECK: checked in wormhole program
81    #[account(mut)]
82    pub wormhole_sequence: UncheckedAccount<'info>,
83
84    /// CHECK: address is checked in NTT manager program
85    pub wormhole_program: UncheckedAccount<'info>,
86
87    /// CHECK: address is checked
88    #[account(address = cpi::ntt_with_executor::NTT_WITH_EXECUTOR_PROGRAM_ID)]
89    pub ntt_with_executor_program: UncheckedAccount<'info>,
90
91    /// CHECK: address is checked
92    #[account(address = cpi::ntt_with_executor::EXECUTOR_PROGRAM_ID)]
93    pub executor_program: UncheckedAccount<'info>,
94
95    /// CHECK: check not important per https://github.com/wormholelabs-xyz/example-ntt-with-executor-svm/blob/10c51da84ee5deb9dee7b2afa69382ce90984eae/programs/example-ntt-with-executor-svm/src/lib.rs#L74-L76
96    pub ntt_peer: UncheckedAccount<'info>,
97
98    /// CHECK: check not important per https://github.com/wormholelabs-xyz/example-ntt-with-executor-svm/blob/10c51da84ee5deb9dee7b2afa69382ce90984eae/programs/example-ntt-with-executor-svm/src/lib.rs#L78-L80
99    #[account(mut)]
100    pub ntt_outbox_item: Signer<'info>,
101
102    /// CHECK: check not important per https://github.com/wormhole-foundation/native-token-transfers/blob/8bd672c5164c53d5a3f9403dc7ce3450da539450/solana/programs/example-native-token-transfers/src/queue/outbox.rs#L50
103    #[account(mut)]
104    pub ntt_outbox_rate_limit: UncheckedAccount<'info>,
105
106    /// CHECK: checked in NTT manager program
107    #[account(mut)]
108    pub ntt_custody: UncheckedAccount<'info>,
109
110    /// CHECK: checked in NTT with executor program
111    #[account(mut)]
112    pub payee_ntt_with_executor: UncheckedAccount<'info>,
113}
114
115#[derive(Accounts)]
116pub struct BridgeNttTokens<'info> {
117    #[account(seeds = [chain_id::SEED], seeds::program = chain_id::ID, bump)]
118    pub from_chain_id: Account<'info, ChainId>,
119
120    /// CHECK: we check the address of this account
121    #[account(address = instructions::ID)]
122    pub sysvar_instructions: UncheckedAccount<'info>,
123
124    /// CHECK: this is just a signer for token program CPIs
125    #[account(seeds = [INTENT_TRANSFER_SEED], bump)]
126    pub intent_transfer_setter: UncheckedAccount<'info>,
127
128    #[account(mut, token::mint = mint)]
129    pub source: Account<'info, TokenAccount>,
130
131    #[account(
132        init_if_needed,
133        payer = sponsor,
134        seeds = [BRIDGE_NTT_INTERMEDIATE_SEED, source.key().as_ref()],
135        bump,
136        token::mint = mint,
137        token::authority = intent_transfer_setter,
138    )]
139    pub intermediate_token_account: Account<'info, TokenAccount>,
140
141    #[account(mut)]
142    pub mint: Account<'info, Mint>,
143
144    pub metadata: Option<UncheckedAccount<'info>>,
145
146    #[account(
147        seeds = [EXPECTED_NTT_CONFIG_SEED, mint.key().as_ref()],
148        bump,
149    )]
150    pub expected_ntt_config: Account<'info, ExpectedNttConfig>,
151
152    #[account(
153        init_if_needed,
154        payer = sponsor,
155        space = Nonce::DISCRIMINATOR.len() + Nonce::INIT_SPACE,
156        seeds = [BRIDGE_NTT_NONCE_SEED, source.owner.key().as_ref()],
157        bump
158    )]
159    pub nonce: Account<'info, Nonce>,
160
161    #[account(mut)]
162    pub sponsor: Signer<'info>,
163
164    #[account(mut, token::mint = fee_mint, token::authority = source.owner )]
165    pub fee_source: Account<'info, TokenAccount>,
166
167    #[account(init_if_needed, payer = sponsor, associated_token::mint = fee_mint, associated_token::authority = sponsor)]
168    pub fee_destination: Account<'info, TokenAccount>,
169
170    pub fee_mint: Account<'info, Mint>,
171
172    pub fee_metadata: Option<UncheckedAccount<'info>>,
173
174    #[account(seeds = [FEE_CONFIG_SEED, fee_mint.key().as_ref()], bump)]
175    pub fee_config: Account<'info, FeeConfig>,
176
177    pub system_program: Program<'info, System>,
178    pub token_program: Program<'info, Token>,
179    pub associated_token_program: Program<'info, AssociatedToken>,
180
181    // NTT-specific accounts
182    pub ntt: Ntt<'info>,
183}
184
185impl<'info> PaidInstruction<'info> for BridgeNttTokens<'info> {
186    fn fee_amount(&self) -> u64 {
187        self.fee_config.bridge_transfer_fee
188    }
189
190    fn verify_and_collect_accounts<'a>(&'a self) -> VerifyAndCollectAccounts<'a, 'info> {
191        let Self {
192            fee_source,
193            fee_destination,
194            fee_mint,
195            fee_metadata,
196            intent_transfer_setter,
197            token_program,
198            ..
199        } = self;
200        VerifyAndCollectAccounts {
201            fee_source,
202            fee_destination,
203            fee_mint,
204            fee_metadata,
205            intent_transfer_setter,
206            token_program,
207        }
208    }
209}
210
211// TODO: implement slot staleness check for intent messages
212impl<'info> BridgeNttTokens<'info> {
213    pub fn verify_and_initiate_bridge(
214        &mut self,
215        signer_seeds: &[&[&[u8]]],
216        args: BridgeNttTokensArgs,
217    ) -> Result<()> {
218        let Intent { message, signer } =
219            Intent::<BridgeMessage>::load(self.sysvar_instructions.as_ref())
220                .map_err(Into::<IntentTransferError>::into)?;
221
222        match message {
223            BridgeMessage::Ntt(ntt_message) => {
224                self.process_ntt_bridge(ntt_message, signer, signer_seeds, args)
225            }
226        }
227    }
228
229    fn process_ntt_bridge(
230        &mut self,
231        ntt_message: NttMessage,
232        signer: Pubkey,
233        signer_seeds: &[&[&[u8]]],
234        args: BridgeNttTokensArgs,
235    ) -> Result<()> {
236        let Self {
237            from_chain_id,
238            intent_transfer_setter,
239            metadata,
240            mint,
241            source,
242            intermediate_token_account,
243            sysvar_instructions: _,
244            token_program,
245            expected_ntt_config,
246            nonce,
247            sponsor,
248            system_program,
249            ntt,
250            ..
251        } = self;
252
253        let Ntt {
254            clock,
255            rent,
256            ntt_manager,
257            ntt_config,
258            ntt_inbox_rate_limit,
259            ntt_session_authority,
260            ntt_token_authority,
261            wormhole_message,
262            transceiver,
263            emitter,
264            wormhole_bridge,
265            wormhole_fee_collector,
266            wormhole_sequence,
267            wormhole_program,
268            ntt_with_executor_program,
269            executor_program,
270            ntt_peer,
271            ntt_outbox_item,
272            ntt_outbox_rate_limit,
273            ntt_custody,
274            payee_ntt_with_executor,
275        } = ntt;
276
277        let NttMessage {
278            version: _,
279            from_chain_id: expected_chain_id,
280            symbol_or_mint,
281            amount: ui_amount,
282            to_chain_id,
283            recipient_address,
284            nonce: new_nonce,
285            fee_amount,
286            fee_symbol_or_mint,
287        } = ntt_message;
288
289        if from_chain_id.chain_id != expected_chain_id {
290            return err!(IntentTransferError::ChainIdMismatch);
291        }
292
293        verify_symbol_or_mint(&symbol_or_mint, metadata, mint)?;
294        verify_signer_matches_source(signer, source.owner)?;
295        verify_and_update_nonce(nonce, new_nonce)?;
296        verify_ntt_manager(ntt_manager.key(), expected_ntt_config)?;
297
298        let amount = try_ui_amount_into_amount(ui_amount, mint.decimals)?;
299
300        transfer_checked(
301            CpiContext::new_with_signer(
302                token_program.to_account_info(),
303                TransferChecked {
304                    authority: intent_transfer_setter.to_account_info(),
305                    from: source.to_account_info(),
306                    mint: mint.to_account_info(),
307                    to: intermediate_token_account.to_account_info(),
308                },
309                signer_seeds,
310            ),
311            amount,
312            mint.decimals,
313        )?;
314
315        let to_chain_id_wormhole = convert_chain_id_to_wormhole(&to_chain_id)
316            .ok_or(IntentTransferError::UnsupportedToChainId)?;
317
318        let transfer_args = cpi::ntt_manager::TransferArgs {
319            amount,
320            recipient_chain: cpi::ntt_manager::ChainId {
321                id: to_chain_id_wormhole.into(),
322            },
323            recipient_address: parse_recipient_address(&recipient_address)?,
324            should_queue: false,
325        };
326
327        approve(
328            CpiContext::new_with_signer(
329                token_program.to_account_info(),
330                Approve {
331                    to: intermediate_token_account.to_account_info(),
332                    delegate: ntt_session_authority.to_account_info(),
333                    authority: intent_transfer_setter.to_account_info(),
334                },
335                signer_seeds,
336            ),
337            amount,
338        )?;
339
340        cpi::ntt_manager::transfer_burn(
341            CpiContext::new(
342                ntt_manager.to_account_info(),
343                cpi::ntt_manager::TransferBurn {
344                    payer: sponsor.to_account_info(),
345                    config: ntt_config.to_account_info(),
346                    mint: mint.to_account_info(),
347                    from: intermediate_token_account.to_account_info(),
348                    token_program: token_program.to_account_info(),
349                    outbox_item: ntt_outbox_item.to_account_info(),
350                    outbox_rate_limit: ntt_outbox_rate_limit.to_account_info(),
351                    custody: ntt_custody.to_account_info(),
352                    system_program: system_program.to_account_info(),
353                    inbox_rate_limit: ntt_inbox_rate_limit.to_account_info(),
354                    peer: ntt_peer.to_account_info(),
355                    session_authority: ntt_session_authority.to_account_info(),
356                    token_authority: ntt_token_authority.to_account_info(),
357                },
358            ),
359            transfer_args,
360            ntt_manager.key(),
361        )?;
362
363        cpi::ntt_manager::release_wormhole_outbound(
364            CpiContext::new(
365                ntt_manager.to_account_info(),
366                cpi::ntt_manager::ReleaseWormholeOutbound {
367                    payer: sponsor.to_account_info(),
368                    config: ntt_config.to_account_info(),
369                    outbox_item: ntt_outbox_item.to_account_info(),
370                    transceiver: transceiver.to_account_info(),
371                    wormhole_message: wormhole_message.to_account_info(),
372                    emitter: emitter.to_account_info(),
373                    wormhole_bridge: wormhole_bridge.to_account_info(),
374                    wormhole_fee_collector: wormhole_fee_collector.to_account_info(),
375                    wormhole_sequence: wormhole_sequence.to_account_info(),
376                    wormhole_program: wormhole_program.to_account_info(),
377                    system_program: system_program.to_account_info(),
378                    clock: clock.to_account_info(),
379                    rent: rent.to_account_info(),
380                },
381            ),
382            cpi::ntt_manager::ReleaseOutboundArgs {
383                revert_on_delay: true,
384            },
385            ntt_manager.key(),
386        )?;
387
388        let BridgeNttTokensArgs {
389            signed_quote_bytes,
390            pay_destination_ata_rent,
391        } = args;
392
393        let relay_ntt_args = compute_relay_ntt_args(
394            to_chain_id_wormhole,
395            signed_quote_bytes.to_vec(),
396            pay_destination_ata_rent,
397        )?;
398
399        cpi::ntt_with_executor::relay_ntt_message(
400            CpiContext::new(
401                ntt_with_executor_program.to_account_info(),
402                cpi::ntt_with_executor::RelayNttMessage {
403                    payer: sponsor.to_account_info(),
404                    payee: payee_ntt_with_executor.to_account_info(),
405                    ntt_program_id: ntt_manager.to_account_info(),
406                    ntt_peer: ntt_peer.to_account_info(),
407                    ntt_message: ntt_outbox_item.to_account_info(),
408                    executor_program: executor_program.to_account_info(),
409                    system_program: system_program.to_account_info(),
410                },
411            ),
412            relay_ntt_args,
413        )?;
414
415        close_account(CpiContext::new_with_signer(
416            token_program.to_account_info(),
417            CloseAccount {
418                account: intermediate_token_account.to_account_info(),
419                destination: sponsor.to_account_info(),
420                authority: intent_transfer_setter.to_account_info(),
421            },
422            signer_seeds,
423        ))?;
424
425        self.verify_and_collect_fee(fee_amount, fee_symbol_or_mint, signer_seeds)
426    }
427}
428
429/// Parses a recipient address string into a 32-byte array.
430/// Supports Solana Pubkey (base58) and hex-encoded addresses (e.g., EVM, Sui).
431fn parse_recipient_address(address_str: &str) -> Result<[u8; 32]> {
432    // try to parse as Solana Pubkey first (base58 encoded, 32 bytes)
433    if let Ok(pubkey) = address_str.parse::<Pubkey>() {
434        return Ok(pubkey.to_bytes());
435    }
436
437    // fallback: try to parse as hex string
438    let hex_str = address_str.strip_prefix("0x").unwrap_or(address_str);
439
440    let bytes = hex::decode(hex_str).map_err(|_| IntentTransferError::InvalidRecipientAddress)?;
441
442    if bytes.len() > 32 {
443        return err!(IntentTransferError::InvalidRecipientAddress);
444    }
445
446    // left-pad with zeros to make it 32 bytes
447    let mut result = [0u8; 32];
448    let start_idx = 32 - bytes.len();
449    result
450        .get_mut(start_idx..)
451        .expect("We checked start_idx is within bounds")
452        .copy_from_slice(&bytes);
453
454    Ok(result)
455}
456
457/// Computes the relay ntt args to pass to the NTT with executor CPI.
458fn compute_relay_ntt_args(
459    to_chain_id_wormhole: WormholeChainId,
460    signed_quote_bytes: Vec<u8>,
461    pay_destination_ata_rent: bool,
462) -> Result<RelayNttMessageArgs> {
463    let (msg_value, gas_limit) = if to_chain_id_wormhole == WormholeChainId::Solana {
464        compute_msg_value_and_gas_limit_solana(pay_destination_ata_rent)
465    } else {
466        return Err(IntentTransferError::UnsupportedToChainId.into());
467    };
468
469    // constructed in line with the gas instruction format: https://github.com/wormholelabs-xyz/example-messaging-executor?tab=readme-ov-file#relay-instructions
470    let relay_instructions = [
471        [1u8].as_slice(),
472        gas_limit.to_be_bytes().as_slice(),
473        msg_value.to_be_bytes().as_slice(),
474    ]
475    .concat();
476
477    let signed_quote = SignedQuote::try_from_slice(&signed_quote_bytes)
478        .map_err(|_| IntentTransferError::InvalidNttSignedQuote)?;
479    let exec_amount =
480        compute_exec_amount(to_chain_id_wormhole, signed_quote, gas_limit, msg_value)?;
481
482    Ok(RelayNttMessageArgs {
483        recipient_chain: to_chain_id_wormhole.into(),
484        exec_amount,
485        signed_quote_bytes,
486        relay_instructions,
487    })
488}
489
490pub const LAMPORTS_PER_SIGNATURE: u128 = 5000;
491pub const RENT_TOKEN_ACCOUNT_SOLANA: u128 = 2_039_280; // equals 0.00203928 SOL
492
493/// Based on the logic encoded in https://github.com/wormhole-foundation/native-token-transfers/blob/20fc162f4a37391694dfb0e31afedf72549ea477/solana/ts/sdk/nttWithExecutor.ts#L304-L344.
494fn compute_msg_value_and_gas_limit_solana(pay_destination_ata_rent: bool) -> (u128, u128) {
495    let mut msg_value = 0u128;
496    msg_value = msg_value
497        .saturating_add(2 * LAMPORTS_PER_SIGNATURE + 7 * LAMPORTS_PER_SIGNATURE + 1_400_000);
498    msg_value = msg_value.saturating_add(2 * LAMPORTS_PER_SIGNATURE + 7 * LAMPORTS_PER_SIGNATURE);
499    msg_value = msg_value.saturating_add(5000 + 3_200_000);
500    msg_value = msg_value.saturating_add(5000 + 5_000_000);
501    msg_value = msg_value.saturating_add(5000);
502    if pay_destination_ata_rent {
503        msg_value = msg_value.saturating_add(RENT_TOKEN_ACCOUNT_SOLANA);
504    }
505
506    (msg_value, 250_000)
507}
508
509pub type H160 = [u8; 20];
510
511// Derived from the documentation: https://github.com/wormholelabs-xyz/example-messaging-executor?tab=readme-ov-file#off-chain-quote
512#[derive(BorshSerialize, BorshDeserialize)]
513pub struct SignedQuoteHeader {
514    pub prefix: [u8; 4],
515    pub quoter_address: H160,
516    pub payee_address: [u8; 32],
517    pub source_chain: U16BE,
518    pub destination_chain: U16BE,
519    pub expiry_time: U64BE,
520}
521
522#[derive(BorshSerialize, BorshDeserialize)]
523pub struct SignedQuote {
524    pub header: SignedQuoteHeader,
525    pub base_fee: U64BE,
526    pub destination_gas_price: U64BE,
527    pub source_price: U64BE,
528    pub destination_price: U64BE,
529    pub signature: [u8; 65],
530}
531
532impl SignedQuote {
533    pub fn try_get_message_body(&self) -> Result<[u8; 100]> {
534        let signed_quote_serialized = self.try_to_vec()?;
535        signed_quote_serialized
536            .get(..100)
537            .and_then(|slice| slice.try_into().ok())
538            .ok_or_else(|| IntentTransferError::InvalidNttSignedQuote.into())
539    }
540
541    // Extracts the signature components (the signature, the recovery index).
542    pub fn try_get_signature_components(&self) -> Result<(&[u8; 64], u8)> {
543        let signature = &self.signature;
544        let (sig_bytes, recovery_index_bytes) = signature.split_at(64);
545        let sig_array = sig_bytes
546            .try_into()
547            .map_err(|_| IntentTransferError::InvalidNttSignedQuote)?;
548        let recovery_index = recovery_index_bytes
549            .first()
550            .copied()
551            .ok_or(IntentTransferError::InvalidNttSignedQuote)?;
552        Ok((sig_array, recovery_index))
553    }
554}
555
556const DECIMALS_QUOTE: u32 = 10;
557const DECIMALS_MAX: u32 = 18;
558
559fn normalize(amount: u128, decimals_from: u32, decimals_to: u32) -> Result<u128> {
560    if decimals_from > decimals_to {
561        Ok(amount / 10u128.pow(decimals_from - decimals_to))
562    } else {
563        amount
564            .checked_mul(10u128.pow(decimals_to - decimals_from))
565            .ok_or(ProgramError::ArithmeticOverflow.into())
566    }
567}
568
569/// Computes the estimated cost of initiating the relay based on the quote details and the expected gas spend on the destination chain.
570/// Based on the logic in https://github.com/wormholelabs-xyz/example-executor-ci-test/blob/6bf0e7156bf81d54f3ded707e53815a2ff62555e/src/utils.ts#L98.
571/// Adjusted to eliminate unnecessary conversions and mitigate chances of arithmetic overflow. Note that this allows some overestimation relative to
572/// the original logic, which is acceptable since the exec_amount needs to be at least the amount computed by the original logic and since the delta
573/// is expected to be small in practice. This slight imprecision is worth the lowered overflow risk. See inline comments for analysis of the delta.
574fn compute_exec_amount(
575    to_chain_id: WormholeChainId,
576    quote: SignedQuote,
577    gas_limit: u128,
578    msg_value: u128,
579) -> Result<u64> {
580    let decimals_destination_native = to_chain_id.decimals_native();
581    let decimals_destination_gas = to_chain_id.decimals_gas_price();
582    let decimals_source_native = WormholeChainId::Fogo.decimals_native();
583
584    let base_fee = u128::from(quote.base_fee);
585    let source_price = u128::from(quote.source_price);
586    let destination_price = u128::from(quote.destination_price);
587    let destination_gas_price = u128::from(quote.destination_gas_price);
588
589    let amount_base = normalize(base_fee, DECIMALS_QUOTE, decimals_source_native)?;
590
591    // Both amount_msg_value and amount_gas computations use similar computation logic, so we can analyze the delta relative to
592    // the original logic together here. In the original logic, both computations involve a multiplication of the form:
593    //
594    // A = floor(X * SC / 10^27), where SC = floor(p_D * 10^18 / p_S), p_D is destination price, p_S is source price.
595    //
596    // This early flooring of SC introduces a delta relative to the current computation (which multiplies the numerator and denominator
597    // components first and only floors once at the end). The delta can be analyzed as follows:
598    //
599    // R := p_D / p_S
600    // SC = floor(R * 10^18) = 10^18 * R - f, where 0 <= f < 1
601    // X * SC / 10^27 = X * (10^18 * R - f) / 10^27 = X * R / 10^9 - X * f / 10^27
602    //
603    // Meanwhile, our current computation lacks the - X * f / 10^27 term, so the delta is:
604    //
605    // Delta = floor(X_{gas} * f / 10^27) + floor(X_{msg_value} * f / 10^27)
606    // = floor( (ell * q_D * 10^3) * f / 10^27 ) + floor ( (m * 10^9) * f / 10^27 )
607    //
608    // where ell = gas_limit, q_D = destination_gas_price, m = msg_value. Given that f < 1, we can upper bound the delta as:
609    //
610    // Delta < floor( (ell * q_D * 10^3) / 10^27 ) + floor ( (m * 10^9) / 10^27 )
611    //
612    // Given typical values for ell (250_000), q_D (< 100_000_000; this is an extreme upper bound for Solana), and m (11_744_280),
613    // we get
614    //
615    // Delta < floor( (250_000 * 100_000_000 * 10^3) / 10^27 ) + floor ( (11_744_280 * 10^9) / 10^27 ) = 0
616    //
617    // Thus, in practice, the delta is likely to be zero; even if it is non-zero, it is unlikely to exceed 1 or 2 lamports given realistic
618    // parameter values, since each of the terms in the inequality above is unlikely to exceed 1. Therefore, the current computation is a safe
619    // overestimation of the original logic. This logic should be revisited if significantly larger parameter values are expected in the future
620    // or if other chains with very high parameter values are supported.
621
622    // Note that for Fogo -> Solana, assuming gas_limit = 250_000, destination_gas_price = 10_000, decimals_destination_gas = 15,
623    // the amount_gas computation will overflow when dest_price (18 decimals) >= 136_112_946_768_375_385_349_842_973 = 1.36e26.
624    // This implies a UI price (9 decimals) of approximately $136.1 quadrillion per SOL. This is well above u64::MAX, so we are safe.
625    let gas_limit_cost = gas_limit
626        .checked_mul(destination_gas_price)
627        .ok_or(ProgramError::ArithmeticOverflow)?;
628    let gas_limit_cost_normalized =
629        normalize(gas_limit_cost, decimals_destination_gas, DECIMALS_MAX)?;
630    let amount_gas = normalize(
631        gas_limit_cost_normalized
632            .checked_mul(destination_price)
633            .ok_or(ProgramError::ArithmeticOverflow)?
634            .checked_div(source_price)
635            .ok_or(ProgramError::ArithmeticOverflow)?,
636        DECIMALS_MAX,
637        decimals_source_native,
638    )?;
639
640    // Note that for Fogo -> Solana, assuming msg_val = 11_744_280, decimals_destination_native = 9,
641    // the amount_msg_value computation will overflow when dest_price (18 decimals) >= 28_974_306_379_015_015_263_889 = 2.89e22.
642    // This implies a UI price (9 decimals) of approximately $28.9 trillion per SOL. This is well above u64::MAX, so we are safe.
643    // For context, we use the msg_val value of 11_744_280 because this appears in some historical NTT transactions.
644    // In practice, msg_val is expected to be lower, but we use this conservative bound to ensure overflow safety.
645    let msg_value_normalized = normalize(msg_value, decimals_destination_native, DECIMALS_MAX)?;
646    let amount_msg_value = normalize(
647        msg_value_normalized
648            .checked_mul(destination_price)
649            .ok_or(ProgramError::ArithmeticOverflow)?
650            .checked_div(source_price)
651            .ok_or(ProgramError::ArithmeticOverflow)?,
652        DECIMALS_MAX,
653        decimals_source_native,
654    )?;
655
656    let total_amount = amount_base
657        .checked_add(amount_gas)
658        .ok_or(ProgramError::ArithmeticOverflow)?
659        .checked_add(amount_msg_value)
660        .ok_or(ProgramError::ArithmeticOverflow)?;
661
662    Ok(u64::try_from(total_amount)?)
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    #[test]
670    fn test_compute_exec_amount_solana() {
671        let quote = SignedQuote {
672            header: SignedQuoteHeader {
673                prefix: *b"EQ01",
674                quoter_address: [0u8; 20],
675                payee_address: [0u8; 32],
676                source_chain: U16BE(0u16),
677                destination_chain: U16BE(1u16),
678                expiry_time: U64BE(0u64),
679            },
680            base_fee: U64BE(500_000_000),
681            destination_gas_price: U64BE(10_000),
682            source_price: U64BE(2_000_000_000),
683            destination_price: U64BE(1_531_800_000_000),
684            signature: [0u8; 65],
685        };
686
687        let gas_limit = 250_000u128;
688        let msg_value = 9_705_000u128;
689
690        let result = compute_exec_amount(WormholeChainId::Solana, quote, gas_limit, msg_value);
691
692        assert_eq!(result, Ok(7484974250));
693    }
694
695    #[test]
696    fn test_compute_exec_amount_solana_max_price() {
697        let quote = SignedQuote {
698            header: SignedQuoteHeader {
699                prefix: *b"EQ01",
700                quoter_address: [0u8; 20],
701                payee_address: [0u8; 32],
702                source_chain: U16BE(0u16),
703                destination_chain: U16BE(1u16),
704                expiry_time: U64BE(0u64),
705            },
706            base_fee: U64BE(500_000_000),
707            destination_gas_price: U64BE(10_000),
708            source_price: U64BE(2_000_000_000),
709            destination_price: U64BE(u64::MAX),
710            signature: [0u8; 65],
711        };
712
713        let gas_limit = 250_000u128;
714        let msg_value = 11_744_280u128;
715
716        let result = compute_exec_amount(WormholeChainId::Solana, quote, gas_limit, msg_value);
717
718        assert!(result.is_ok());
719    }
720}