intent_transfer/intrachain/processor/
send_native.rs

1use crate::{
2    config::state::fee_config::{FeeConfig, FEE_CONFIG_SEED},
3    error::IntentTransferError,
4    fees::{PaidInstruction, VerifyAndCollectAccounts},
5    intrachain::message::Message,
6    nonce::{self, Nonce},
7    verify::{verify_and_update_nonce, verify_signer_matches_source},
8    INTENT_TRANSFER_SEED,
9};
10use anchor_lang::{
11    prelude::*,
12    solana_program::{
13        instruction::{AccountMeta, Instruction},
14        program,
15        sysvar::instructions,
16    },
17    system_program,
18};
19use anchor_spl::{
20    associated_token::AssociatedToken,
21    token::{spl_token::try_ui_amount_into_amount, Mint, Token, TokenAccount},
22};
23use chain_id::ChainId;
24use solana_intents::{Intent, SymbolOrMint};
25
26const FOGO_DECIMALS: u8 = 9;
27const SYSTEM_PROGRAM_INTENT_TRANSFER_DISCRIMINATOR: u32 = 4_000_001;
28
29#[derive(Accounts)]
30pub struct SendNative<'info> {
31    #[account(seeds = [chain_id::SEED], seeds::program = chain_id::ID, bump)]
32    pub chain_id: Account<'info, ChainId>,
33
34    /// CHECK: we check the address of this account
35    #[account(address = instructions::ID)]
36    pub sysvar_instructions: UncheckedAccount<'info>,
37
38    /// CHECK: this is just a signer for token program CPIs
39    #[account(seeds = [INTENT_TRANSFER_SEED], bump)]
40    pub intent_transfer_setter: UncheckedAccount<'info>,
41
42    /// CHECK: this is checked against the intent message
43    #[account(mut)]
44    pub source: UncheckedAccount<'info>,
45
46    /// CHECK: this is checked against the intent message
47    #[account(mut)]
48    pub destination: UncheckedAccount<'info>,
49
50    #[account(
51        init_if_needed,
52        payer = sponsor,
53        space = Nonce::DISCRIMINATOR.len() + Nonce::INIT_SPACE,
54        seeds = [nonce::INTENT_TRANSFER_NONCE_SEED, source.key().as_ref()],
55        bump
56    )]
57    pub nonce: Account<'info, Nonce>,
58
59    #[account(mut)]
60    pub sponsor: Signer<'info>,
61
62    #[account(mut, token::mint = fee_mint, token::authority = source)]
63    pub fee_source: Account<'info, TokenAccount>,
64
65    #[account(init_if_needed, payer = sponsor, associated_token::mint = fee_mint, associated_token::authority = sponsor)]
66    pub fee_destination: Account<'info, TokenAccount>,
67
68    pub fee_mint: Account<'info, Mint>,
69
70    pub fee_metadata: Option<UncheckedAccount<'info>>,
71
72    #[account(seeds = [FEE_CONFIG_SEED, fee_mint.key().as_ref()], bump)]
73    pub fee_config: Account<'info, FeeConfig>,
74
75    pub system_program: Program<'info, System>,
76    pub token_program: Program<'info, Token>,
77    pub associated_token_program: Program<'info, AssociatedToken>,
78}
79
80impl<'info> PaidInstruction<'info> for SendNative<'info> {
81    fn fee_amount(&self) -> u64 {
82        self.fee_config.intrachain_transfer_fee
83    }
84
85    fn verify_and_collect_accounts<'a>(&'a self) -> VerifyAndCollectAccounts<'a, 'info> {
86        let Self {
87            fee_source,
88            fee_destination,
89            fee_mint,
90            fee_metadata,
91            intent_transfer_setter,
92            token_program,
93            ..
94        } = self;
95        VerifyAndCollectAccounts {
96            fee_source,
97            fee_destination,
98            fee_mint,
99            fee_metadata,
100            intent_transfer_setter,
101            token_program,
102        }
103    }
104}
105
106impl<'info> SendNative<'info> {
107    pub fn verify_and_send(&mut self, signer_seeds: &[&[&[u8]]]) -> Result<()> {
108        let Self {
109            chain_id,
110            destination,
111            intent_transfer_setter,
112            sysvar_instructions,
113            nonce,
114            source,
115            ..
116        } = self;
117
118        let Intent {
119            message:
120                Message {
121                    amount,
122                    chain_id: expected_chain_id,
123                    recipient,
124                    symbol_or_mint,
125                    nonce: new_nonce,
126                    version: _,
127                    fee_amount,
128                    fee_symbol_or_mint,
129                },
130            signer,
131        } = Intent::load(sysvar_instructions.as_ref())
132            .map_err(Into::<IntentTransferError>::into)?;
133
134        if chain_id.chain_id != expected_chain_id {
135            return err!(IntentTransferError::ChainIdMismatch);
136        }
137
138        if symbol_or_mint != SymbolOrMint::Symbol(String::from("FOGO")) {
139            return err!(IntentTransferError::SymbolMismatch);
140        }
141
142        verify_signer_matches_source(signer, source.key())?;
143
144        require_keys_eq!(
145            recipient,
146            destination.key(),
147            IntentTransferError::RecipientMismatch
148        );
149
150        verify_and_update_nonce(nonce, new_nonce)?;
151
152        program::invoke_signed(
153            &Instruction {
154                program_id: system_program::ID,
155                accounts: vec![
156                    AccountMeta::new(source.key(), false),
157                    AccountMeta::new(destination.key(), false),
158                    AccountMeta::new_readonly(intent_transfer_setter.key(), true),
159                ],
160                data: SYSTEM_PROGRAM_INTENT_TRANSFER_DISCRIMINATOR
161                    .to_le_bytes()
162                    .into_iter()
163                    .chain(try_ui_amount_into_amount(amount, FOGO_DECIMALS)?.to_le_bytes())
164                    .collect(),
165            },
166            &[
167                source.to_account_info(),
168                destination.to_account_info(),
169                intent_transfer_setter.to_account_info(),
170            ],
171            signer_seeds,
172        )?;
173
174        self.verify_and_collect_fee(fee_amount, fee_symbol_or_mint, signer_seeds)
175    }
176}