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[start_idx..].copy_from_slice(&bytes);
450
451    Ok(result)
452}
453
454/// Computes the relay ntt args to pass to the NTT with executor CPI.
455fn compute_relay_ntt_args(
456    to_chain_id_wormhole: WormholeChainId,
457    signed_quote_bytes: Vec<u8>,
458    pay_destination_ata_rent: bool,
459) -> Result<RelayNttMessageArgs> {
460    let (msg_value, gas_limit) = if to_chain_id_wormhole == WormholeChainId::Solana {
461        compute_msg_value_and_gas_limit_solana(pay_destination_ata_rent)
462    } else {
463        return Err(IntentTransferError::UnsupportedToChainId.into());
464    };
465
466    // constructed in line with the gas instruction format: https://github.com/wormholelabs-xyz/example-messaging-executor?tab=readme-ov-file#relay-instructions
467    let relay_instructions = [
468        [1u8].as_slice(),
469        gas_limit.to_be_bytes().as_slice(),
470        msg_value.to_be_bytes().as_slice(),
471    ]
472    .concat();
473
474    let signed_quote = SignedQuote::try_from_slice(&signed_quote_bytes)
475        .map_err(|_| IntentTransferError::InvalidNttSignedQuote)?;
476    let exec_amount =
477        compute_exec_amount(to_chain_id_wormhole, signed_quote, gas_limit, msg_value)?;
478
479    Ok(RelayNttMessageArgs {
480        recipient_chain: to_chain_id_wormhole.into(),
481        exec_amount,
482        signed_quote_bytes,
483        relay_instructions,
484    })
485}
486
487pub const LAMPORTS_PER_SIGNATURE: u128 = 5000;
488pub const RENT_TOKEN_ACCOUNT_SOLANA: u128 = 2_039_280; // equals 0.00203928 SOL
489
490/// Based on the logic encoded in https://github.com/wormhole-foundation/native-token-transfers/blob/20fc162f4a37391694dfb0e31afedf72549ea477/solana/ts/sdk/nttWithExecutor.ts#L304-L344.
491fn compute_msg_value_and_gas_limit_solana(pay_destination_ata_rent: bool) -> (u128, u128) {
492    let mut msg_value = 0u128;
493    msg_value = msg_value
494        .saturating_add(2 * LAMPORTS_PER_SIGNATURE + 7 * LAMPORTS_PER_SIGNATURE + 1_400_000);
495    msg_value = msg_value.saturating_add(2 * LAMPORTS_PER_SIGNATURE + 7 * LAMPORTS_PER_SIGNATURE);
496    msg_value = msg_value.saturating_add(5000 + 3_200_000);
497    msg_value = msg_value.saturating_add(5000 + 5_000_000);
498    msg_value = msg_value.saturating_add(5000);
499    if pay_destination_ata_rent {
500        msg_value = msg_value.saturating_add(RENT_TOKEN_ACCOUNT_SOLANA);
501    }
502
503    (msg_value, 250_000)
504}
505
506pub type H160 = [u8; 20];
507
508// Derived from the documentation: https://github.com/wormholelabs-xyz/example-messaging-executor?tab=readme-ov-file#off-chain-quote
509#[derive(BorshSerialize, BorshDeserialize)]
510pub struct SignedQuoteHeader {
511    pub prefix: [u8; 4],
512    pub quoter_address: H160,
513    pub payee_address: [u8; 32],
514    pub source_chain: U16BE,
515    pub destination_chain: U16BE,
516    pub expiry_time: U64BE,
517}
518
519#[derive(BorshSerialize, BorshDeserialize)]
520pub struct SignedQuote {
521    pub header: SignedQuoteHeader,
522    pub base_fee: U64BE,
523    pub destination_gas_price: U64BE,
524    pub source_price: U64BE,
525    pub destination_price: U64BE,
526    pub signature: [u8; 65],
527}
528
529impl SignedQuote {
530    pub fn try_get_message_body(&self) -> Result<[u8; 100]> {
531        let signed_quote_serialized = self.try_to_vec()?;
532        signed_quote_serialized
533            .get(..100)
534            .and_then(|slice| slice.try_into().ok())
535            .ok_or_else(|| IntentTransferError::InvalidNttSignedQuote.into())
536    }
537
538    // Extracts the signature components (the signature, the recovery index).
539    pub fn try_get_signature_components(&self) -> Result<(&[u8; 64], u8)> {
540        let signature = &self.signature;
541        let (sig_bytes, recovery_index_bytes) = signature.split_at(64);
542        let sig_array = sig_bytes
543            .try_into()
544            .map_err(|_| IntentTransferError::InvalidNttSignedQuote)?;
545        let recovery_index = recovery_index_bytes
546            .first()
547            .copied()
548            .ok_or(IntentTransferError::InvalidNttSignedQuote)?;
549        Ok((sig_array, recovery_index))
550    }
551}
552
553const DECIMALS_QUOTE: u32 = 10;
554const DECIMALS_MAX: u32 = 18;
555
556fn normalize(amount: u128, decimals_from: u32, decimals_to: u32) -> Result<u128> {
557    if decimals_from > decimals_to {
558        Ok(amount / 10u128.pow(decimals_from - decimals_to))
559    } else {
560        amount
561            .checked_mul(10u128.pow(decimals_to - decimals_from))
562            .ok_or(ProgramError::ArithmeticOverflow.into())
563    }
564}
565
566/// Computes the estimated cost of initiating the relay based on the quote details and the expected gas spend on the destination chain.
567/// Based on the logic in https://github.com/wormholelabs-xyz/example-executor-ci-test/blob/6bf0e7156bf81d54f3ded707e53815a2ff62555e/src/utils.ts#L98.
568fn compute_exec_amount(
569    to_chain_id: WormholeChainId,
570    quote: SignedQuote,
571    gas_limit: u128,
572    msg_value: u128,
573) -> Result<u64> {
574    let decimals_destination_native = to_chain_id.decimals_native();
575    let decimals_destination_gas = to_chain_id.decimals_gas_price();
576    let decimals_source_native = WormholeChainId::Fogo.decimals_native();
577
578    let base_fee = u128::from(quote.base_fee);
579    let source_price = u128::from(quote.source_price);
580    let destination_price = u128::from(quote.destination_price);
581    let destination_gas_price = u128::from(quote.destination_gas_price);
582
583    let amount_base = normalize(base_fee, DECIMALS_QUOTE, decimals_source_native)?;
584
585    let src_price_normalized = normalize(source_price, DECIMALS_QUOTE, DECIMALS_MAX)?;
586    let dst_price_normalized = normalize(destination_price, DECIMALS_QUOTE, DECIMALS_MAX)?;
587    let scaled_conversion = dst_price_normalized
588        .checked_mul(10u128.pow(DECIMALS_MAX))
589        .ok_or(ProgramError::ArithmeticOverflow)?
590        .checked_div(src_price_normalized)
591        .ok_or(ProgramError::ArithmeticOverflow)?;
592
593    // Note that for Fogo -> Solana, assuming gas_limit = 250_000, destination_gas_price = 10_000,
594    // the amount_gas computation will overflow when dest_price / src_price >= 136112947.
595    let gas_limit_cost = gas_limit
596        .checked_mul(destination_gas_price)
597        .ok_or(ProgramError::ArithmeticOverflow)?;
598    let gas_limit_cost_normalized =
599        normalize(gas_limit_cost, decimals_destination_gas, DECIMALS_MAX)?;
600    let amount_gas = normalize(
601        gas_limit_cost_normalized
602            .checked_mul(scaled_conversion)
603            .ok_or(ProgramError::ArithmeticOverflow)?
604            .checked_div(10u128.pow(DECIMALS_MAX))
605            .ok_or(ProgramError::ArithmeticOverflow)?,
606        DECIMALS_MAX,
607        decimals_source_native,
608    )?;
609
610    // Note that for Fogo -> Solana, assuming msg_val = 11_744_280,
611    // the amount_msg_value computation will overflow when dest_price / src_price >= 28975.
612    let msg_value_normalized = normalize(msg_value, decimals_destination_native, DECIMALS_MAX)?;
613    let amount_msg_value = normalize(
614        msg_value_normalized
615            .checked_mul(scaled_conversion)
616            .ok_or(ProgramError::ArithmeticOverflow)?
617            .checked_div(10u128.pow(DECIMALS_MAX))
618            .ok_or(ProgramError::ArithmeticOverflow)?,
619        DECIMALS_MAX,
620        decimals_source_native,
621    )?;
622
623    let total_amount = amount_base
624        .checked_add(amount_gas)
625        .ok_or(ProgramError::ArithmeticOverflow)?
626        .checked_add(amount_msg_value)
627        .ok_or(ProgramError::ArithmeticOverflow)?;
628
629    Ok(u64::try_from(total_amount)?)
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    #[test]
637    fn test_compute_exec_amount_solana() {
638        let quote = SignedQuote {
639            header: SignedQuoteHeader {
640                prefix: *b"EQ01",
641                quoter_address: [0u8; 20],
642                payee_address: [0u8; 32],
643                source_chain: U16BE(0u16),
644                destination_chain: U16BE(1u16),
645                expiry_time: U64BE(0u64),
646            },
647            base_fee: U64BE(500_000_000),
648            destination_gas_price: U64BE(10_000),
649            source_price: U64BE(2_000_000_000),
650            destination_price: U64BE(1_531_800_000_000),
651            signature: [0u8; 65],
652        };
653
654        let gas_limit = 250_000u128;
655        let msg_value = 9_705_000u128;
656
657        let result = compute_exec_amount(WormholeChainId::Solana, quote, gas_limit, msg_value);
658
659        assert_eq!(result, Ok(7484974250));
660    }
661}