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 pub clock: Sysvar<'info, Clock>,
42
43 pub rent: Sysvar<'info, Rent>,
45
46 pub ntt_manager: UncheckedAccount<'info>,
48
49 pub ntt_config: UncheckedAccount<'info>,
51
52 #[account(mut)]
54 pub ntt_inbox_rate_limit: UncheckedAccount<'info>,
55
56 pub ntt_session_authority: UncheckedAccount<'info>,
58
59 pub ntt_token_authority: UncheckedAccount<'info>,
61
62 #[account(mut)]
64 pub wormhole_message: UncheckedAccount<'info>,
65
66 pub transceiver: UncheckedAccount<'info>,
68
69 pub emitter: UncheckedAccount<'info>,
71
72 #[account(mut)]
74 pub wormhole_bridge: UncheckedAccount<'info>,
75
76 #[account(mut)]
78 pub wormhole_fee_collector: UncheckedAccount<'info>,
79
80 #[account(mut)]
82 pub wormhole_sequence: UncheckedAccount<'info>,
83
84 pub wormhole_program: UncheckedAccount<'info>,
86
87 #[account(address = cpi::ntt_with_executor::NTT_WITH_EXECUTOR_PROGRAM_ID)]
89 pub ntt_with_executor_program: UncheckedAccount<'info>,
90
91 #[account(address = cpi::ntt_with_executor::EXECUTOR_PROGRAM_ID)]
93 pub executor_program: UncheckedAccount<'info>,
94
95 pub ntt_peer: UncheckedAccount<'info>,
97
98 #[account(mut)]
100 pub ntt_outbox_item: Signer<'info>,
101
102 #[account(mut)]
104 pub ntt_outbox_rate_limit: UncheckedAccount<'info>,
105
106 #[account(mut)]
108 pub ntt_custody: UncheckedAccount<'info>,
109
110 #[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 #[account(address = instructions::ID)]
122 pub sysvar_instructions: UncheckedAccount<'info>,
123
124 #[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 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
211impl<'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
429fn parse_recipient_address(address_str: &str) -> Result<[u8; 32]> {
432 if let Ok(pubkey) = address_str.parse::<Pubkey>() {
434 return Ok(pubkey.to_bytes());
435 }
436
437 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 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
454fn 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 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; fn 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#[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 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
566fn 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 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 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}