express_relay/
utils.rs

1use {
2    crate::{
3        error::ErrorCode,
4        state::*,
5        SubmitBid,
6        SubmitBidArgs,
7    },
8    anchor_lang::{
9        prelude::*,
10        solana_program::{
11            instruction::Instruction,
12            serialize_utils::read_u16,
13            sysvar::instructions::load_instruction_at_checked,
14        },
15        system_program::{
16            transfer,
17            Transfer,
18        },
19        Discriminator,
20    },
21};
22
23pub fn validate_fee_split(split: u64) -> Result<()> {
24    if split > FEE_SPLIT_PRECISION {
25        return err!(ErrorCode::FeeSplitLargerThanPrecision);
26    }
27    Ok(())
28}
29
30pub fn transfer_lamports(from: &AccountInfo, to: &AccountInfo, amount: u64) -> Result<()> {
31    **from.try_borrow_mut_lamports()? -= amount;
32    **to.try_borrow_mut_lamports()? += amount;
33    Ok(())
34}
35
36pub fn transfer_lamports_cpi<'info>(
37    from: &AccountInfo<'info>,
38    to: &AccountInfo<'info>,
39    amount: u64,
40    system_program: AccountInfo<'info>,
41) -> Result<()> {
42    let cpi_accounts = Transfer {
43        from: from.clone(),
44        to:   to.clone(),
45    };
46
47    transfer(CpiContext::new(system_program, cpi_accounts), amount)?;
48
49    Ok(())
50}
51
52pub fn check_fee_hits_min_rent(account: &AccountInfo, fee: u64) -> Result<()> {
53    let balance = account.lamports();
54    let rent = Rent::get()?.minimum_balance(account.data_len());
55    if balance
56        .checked_add(fee)
57        .ok_or(ProgramError::ArithmeticOverflow)?
58        < rent
59    {
60        return err!(ErrorCode::InsufficientRent);
61    }
62
63    Ok(())
64}
65
66pub struct PermissionInfo<'info> {
67    pub permission:             Pubkey,
68    pub router:                 Pubkey,
69    pub config_router:          AccountInfo<'info>,
70    pub express_relay_metadata: AccountInfo<'info>,
71}
72
73/// Performs instruction introspection to retrieve a vector of `SubmitBid` instructions in the current transaction.
74/// If `permission_info` is specified, only instructions with matching permission and router accounts are returned.
75pub fn get_matching_submit_bid_instructions(
76    sysvar_instructions: AccountInfo,
77    permission_info: Option<&PermissionInfo>,
78) -> Result<Vec<Instruction>> {
79    let num_instructions = read_u16(&mut 0, &sysvar_instructions.data.borrow())
80        .map_err(|_| ProgramError::InvalidInstructionData)?;
81    let mut matching_instructions = Vec::new();
82    for index in 0..num_instructions {
83        let ix = load_instruction_at_checked(index.into(), &sysvar_instructions)?;
84
85        if ix.program_id != crate::id() {
86            continue;
87        }
88        if &ix.data[0..8] != crate::instruction::SubmitBid::DISCRIMINATOR {
89            continue;
90        }
91
92        if let Some(permission_info) = permission_info {
93            if ix.accounts[2].pubkey != permission_info.permission {
94                continue;
95            }
96
97            if ix.accounts[3].pubkey != permission_info.router {
98                continue;
99            }
100
101            if ix.accounts[4].pubkey != permission_info.config_router.key() {
102                continue;
103            }
104
105            if ix.accounts[5].pubkey != permission_info.express_relay_metadata.key() {
106                continue;
107            }
108        }
109
110        matching_instructions.push(ix);
111    }
112
113    Ok(matching_instructions)
114}
115
116/// Extracts the bid paid from a `SubmitBid` instruction.
117pub fn extract_bid_from_submit_bid_ix(submit_bid_ix: &Instruction) -> Result<u64> {
118    let submit_bid_args = SubmitBidArgs::try_from_slice(
119        &submit_bid_ix.data[crate::instruction::SubmitBid::DISCRIMINATOR.len()..],
120    )
121    .map_err(|_| ProgramError::BorshIoError("Failed to deserialize SubmitBidArgs".to_string()))?;
122    Ok(submit_bid_args.bid_amount)
123}
124
125/// Computes the fee to pay the router based on the specified `bid_amount` and the `split_router`.
126fn perform_fee_split_router(bid_amount: u64, split_router: u64) -> Result<u64> {
127    let fee_router = bid_amount
128        .checked_mul(split_router)
129        .ok_or(ProgramError::ArithmeticOverflow)?
130        / FEE_SPLIT_PRECISION;
131    if fee_router > bid_amount {
132        // this error should never be reached due to fee split checks, but kept as a matter of defensive programming
133        return err!(ErrorCode::FeesHigherThanBid);
134    }
135    Ok(fee_router)
136}
137
138/// Performs fee splits on a bid amount.
139/// Returns amount to pay to router and amount to pay to relayer.
140pub fn perform_fee_splits(
141    bid_amount: u64,
142    split_router: u64,
143    split_relayer: u64,
144) -> Result<(u64, u64)> {
145    let fee_router = perform_fee_split_router(bid_amount, split_router)?;
146    // we inline the fee_relayer calculation because it is not straightforward and is only used here
147    // fee_relayer is computed as a proportion of the bid amount minus the fee paid to the router
148    let fee_relayer = bid_amount
149        .saturating_sub(fee_router)
150        .checked_mul(split_relayer)
151        .ok_or(ProgramError::ArithmeticOverflow)?
152        / FEE_SPLIT_PRECISION;
153
154    if fee_relayer
155        .checked_add(fee_router)
156        .ok_or(ProgramError::ArithmeticOverflow)?
157        > bid_amount
158    {
159        // this error should never be reached due to fee split checks, but kept as a matter of defensive programming
160        return err!(ErrorCode::FeesHigherThanBid);
161    }
162
163    Ok((fee_router, fee_relayer))
164}
165
166/// Performs instruction introspection on the current transaction to query SubmitBid instructions that match the specified permission and router.
167/// Returns the number of matching instructions and the total fees paid to the router.
168/// The `config_router` and `express_relay_metadata` accounts passed in `permission_info` are assumed to have already been validated. Note these are not validated in this function.
169pub fn inspect_permissions_in_tx(
170    sysvar_instructions: UncheckedAccount,
171    permission_info: PermissionInfo,
172) -> Result<(u16, u64)> {
173    let matching_ixs = get_matching_submit_bid_instructions(
174        sysvar_instructions.to_account_info(),
175        Some(&permission_info),
176    )?;
177    let n_ixs = matching_ixs.len() as u16;
178
179    let mut total_fees = 0u64;
180    let data_config_router = &mut &**permission_info.config_router.try_borrow_data()?;
181    let split_router = match ConfigRouter::try_deserialize(data_config_router) {
182        Ok(config_router) => config_router.split,
183        Err(_) => {
184            let data_express_relay_metadata =
185                &mut &**permission_info.express_relay_metadata.try_borrow_data()?;
186            let express_relay_metadata =
187                ExpressRelayMetadata::try_deserialize(data_express_relay_metadata)
188                    .map_err(|_| ProgramError::InvalidAccountData)?;
189            express_relay_metadata.split_router_default
190        }
191    };
192    for ix in matching_ixs {
193        let bid = extract_bid_from_submit_bid_ix(&ix)?;
194        total_fees = total_fees
195            .checked_add(perform_fee_split_router(bid, split_router)?)
196            .ok_or(ProgramError::ArithmeticOverflow)?;
197    }
198
199    Ok((n_ixs, total_fees))
200}
201
202pub fn handle_bid_payment(ctx: Context<SubmitBid>, bid_amount: u64) -> Result<()> {
203    let searcher = &ctx.accounts.searcher;
204    let rent_searcher = Rent::get()?.minimum_balance(searcher.to_account_info().data_len());
205    if bid_amount
206        .checked_add(rent_searcher)
207        .ok_or(ProgramError::ArithmeticOverflow)?
208        > searcher.lamports()
209    {
210        return err!(ErrorCode::InsufficientSearcherFunds);
211    }
212
213    let express_relay_metadata = &ctx.accounts.express_relay_metadata;
214    let split_relayer = express_relay_metadata.split_relayer;
215    let split_router_default = express_relay_metadata.split_router_default;
216
217    let config_router = &ctx.accounts.config_router;
218    let config_router_account_info = config_router.to_account_info();
219    // validate the router config account struct in program logic bc it may be uninitialized
220    // only validate if the account has data
221    let split_router: u64 = if config_router_account_info.data_len() > 0 {
222        let account_data = &mut &**config_router_account_info.try_borrow_data()?;
223        let config_router_data = ConfigRouter::try_deserialize(account_data)?;
224        config_router_data.split
225    } else {
226        split_router_default
227    };
228
229    let (fee_router, fee_relayer) = perform_fee_splits(bid_amount, split_router, split_relayer)?;
230
231    if fee_router > 0 {
232        check_fee_hits_min_rent(&ctx.accounts.router, fee_router)?;
233
234        transfer_lamports_cpi(
235            &searcher.to_account_info(),
236            &ctx.accounts.router.to_account_info(),
237            fee_router,
238            ctx.accounts.system_program.to_account_info(),
239        )?;
240    }
241    if fee_relayer > 0 {
242        check_fee_hits_min_rent(&ctx.accounts.fee_receiver_relayer, fee_relayer)?;
243
244        transfer_lamports_cpi(
245            &searcher.to_account_info(),
246            &ctx.accounts.fee_receiver_relayer.to_account_info(),
247            fee_relayer,
248            ctx.accounts.system_program.to_account_info(),
249        )?;
250    }
251
252    transfer_lamports_cpi(
253        &searcher.to_account_info(),
254        &express_relay_metadata.to_account_info(),
255        bid_amount
256            .saturating_sub(fee_router)
257            .saturating_sub(fee_relayer),
258        ctx.accounts.system_program.to_account_info(),
259    )?;
260
261    Ok(())
262}