diff --git a/solana/programs/matching-engine/src/fallback/processor/execute_order.rs b/solana/programs/matching-engine/src/fallback/processor/execute_order.rs index 5d6d291a7..1c06ab408 100644 --- a/solana/programs/matching-engine/src/fallback/processor/execute_order.rs +++ b/solana/programs/matching-engine/src/fallback/processor/execute_order.rs @@ -9,8 +9,8 @@ use solana_program::{instruction::Instruction, program::invoke_signed_unchecked} use crate::{ error::MatchingEngineError, + processor::ExecuteOrderInternalAccounting, state::{Auction, AuctionStatus, Custodian, MessageProtocol}, - utils::{self, auction::DepositPenalty}, ID, }; @@ -346,193 +346,25 @@ pub(super) fn process(accounts: &[AccountInfo]) -> Result<()> { // TODO: Do we have to verify the CCTP message transmitter program is passed // in? - //////////////////////////////////////////////////////////////////////////// - // - // TODO: This execute order logic has been taken from the original execute - // order instructions. We plan on using a helper method instead of copy- - // pasting the same logic here. - // - //////////////////////////////////////////////////////////////////////////// - // Prepare the execute order (get the user amount, fill, and order executed event) - let current_slot = Clock::get().unwrap().slot; - - // We extend the grace period for locally executed orders. Reserving a sequence number for - // the fast fill will most likely require an additional transaction, so this buffer allows - // the best offer participant to perform his duty without the risk of getting slashed by - // another executor. - let additional_grace_period = match active_auction.target_protocol { - MessageProtocol::Local { .. } => { - crate::EXECUTE_FAST_ORDER_LOCAL_ADDITIONAL_GRACE_PERIOD.into() - } - _ => None, - }; - - let DepositPenalty { - penalty, - user_reward, - } = utils::auction::compute_deposit_penalty( - &auction_config, - active_auction_inner_info, - current_slot, - additional_grace_period, - ); - - let init_auction_fee = fast_market_order.init_auction_fee; - - let user_amount = active_auction_inner_info - .amount_in - .saturating_sub(active_auction_inner_info.offer_price) - .saturating_sub(init_auction_fee) - .saturating_add(user_reward); - - // Keep track of the remaining amount in the custody token account. Whatever remains will go - // to the executor. - - let custody_token = + let auction_custody = TokenAccount::try_deserialize(&mut &auction_custody_info.data.borrow()[..])?; - let mut remaining_custodied_amount = custody_token.amount.saturating_sub(user_amount); - - // Offer price + security deposit was checked in placing the initial offer. - let mut deposit_and_fee = active_auction_inner_info - .offer_price - .saturating_add(active_auction_inner_info.security_deposit) - .saturating_sub(user_reward); - let penalized = penalty > 0; - - if penalized && auction_best_offer_token_info.key != executor_token_info.key { - deposit_and_fee = deposit_and_fee.saturating_sub(penalty); + let (user_amount, new_status, _penalized) = ExecuteOrderInternalAccounting { + active_auction_key: active_auction_info.key, + active_auction: &active_auction, + auction_custody_key: auction_custody_info.key, + auction_custody: &auction_custody, + best_offer_token_info: auction_best_offer_token_info, + executor_token_key: executor_token_info.key, + initial_offer_token_info: auction_initial_offer_token_info, + custodian_key: custodian_info.key, + auction_config: &auction_config, } - - // Need these seeds in order to transfer tokens and then set authority of auction custody token account to the custodian - let auction_signer_seeds = &[ - Auction::SEED_PREFIX, - active_auction.vaa_hash.as_ref(), - &[active_auction.bump], - ]; - - // If the initial offer token account doesn't exist anymore, we have nowhere to send the - // init auction fee. The executor will get these funds instead. - // - // We check that this is a legitimate token account. - if utils::checked_deserialize_token_account( - auction_initial_offer_token_info, - &common::USDC_MINT, - ) - .is_some() - { - if auction_best_offer_token_info.key() != auction_initial_offer_token_info.key() { - // Pay the auction initiator their fee. - let transfer_ix = spl_token::instruction::transfer( - &spl_token::ID, - &auction_custody_info.key(), - &auction_initial_offer_token_info.key(), - &active_auction_info.key(), - &[], - init_auction_fee, - ) - .unwrap(); - - invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; - // Because the initial offer token was paid this fee, we account for it here. - remaining_custodied_amount = - remaining_custodied_amount.saturating_sub(init_auction_fee); - } else { - // Add it to the reimbursement. - deposit_and_fee = deposit_and_fee - .checked_add(init_auction_fee) - .ok_or_else(|| MatchingEngineError::U64Overflow)?; - } - } - - // Return the security deposit and the fee to the highest bidder. - if auction_best_offer_token_info.key == executor_token_info.key { - // If the best offer token is equal to the executor token, just send whatever remains in - // the custody token account. - // - // NOTE: This will revert if the best offer token does not exist. But this will present - // an opportunity for another executor to execute this order and take what the best - // offer token would have received. - let transfer_ix = spl_token::instruction::transfer( - &spl_token::ID, - &auction_custody_info.key(), - &auction_best_offer_token_info.key(), - &active_auction_info.key(), - &[], - deposit_and_fee, - ) - .unwrap(); - msg!( - "Sending deposit and fee amount {} to best offer token account", - deposit_and_fee - ); - invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; - } else { - // Otherwise, send the deposit and fee to the best offer token. If the best offer token - // doesn't exist at this point (which would be unusual), we will reserve these funds - // for the executor token. - if utils::checked_deserialize_token_account( - auction_best_offer_token_info, - &common::USDC_MINT, - ) - .is_some() - { - let transfer_ix = spl_token::instruction::transfer( - &spl_token::ID, - &auction_custody_info.key(), - &auction_best_offer_token_info.key(), - &active_auction_info.key(), - &[], - deposit_and_fee, - ) - .unwrap(); - msg!( - "Sending deposit and fee {} to best offer token account", - deposit_and_fee - ); - invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; - remaining_custodied_amount = remaining_custodied_amount.saturating_sub(deposit_and_fee); - } - - // And pay the executor whatever remains in the auction custody token account. - if remaining_custodied_amount > 0 { - let instruction = spl_token::instruction::transfer( - &spl_token::ID, - auction_custody_info.key, - executor_token_info.key, - &active_auction_info.key(), - &[], - remaining_custodied_amount, - ) - .unwrap(); - msg!( - "Sending remaining custodied amount {} to executor token account", - remaining_custodied_amount - ); - invoke_signed_unchecked(&instruction, accounts, &[auction_signer_seeds])?; - } - } - - // Set the authority of the custody token account to the custodian. He will take over from - // here. - let set_authority_ix = spl_token::instruction::set_authority( - &spl_token::ID, - auction_custody_info.key, - Some(custodian_info.key), - spl_token::instruction::AuthorityType::AccountOwner, - active_auction_info.key, - &[], - ) - .unwrap(); - - invoke_signed_unchecked(&set_authority_ix, accounts, &[auction_signer_seeds])?; + .into_calculate_and_transfer(fast_market_order.init_auction_fee, accounts)?; // Set the active auction status - active_auction.status = AuctionStatus::Completed { - slot: current_slot, - execute_penalty: if penalized { penalty.into() } else { None }, - }; + active_auction.status = new_status; let active_auction_info_data: &mut [u8] = &mut active_auction_info.data.borrow_mut(); let mut active_auction_cursor = std::io::Cursor::new(active_auction_info_data); @@ -549,12 +381,6 @@ pub(super) fn process(accounts: &[AccountInfo]) -> Result<()> { .unwrap(), }; - //////////////////////////////////////////////////////////////////////////// - // - // TODO: See above TODO. This is the end of the copy-pasted logic. - // - //////////////////////////////////////////////////////////////////////////// - // TODO: Write test that passes in random keypair for CCTP message account // to show that not having to check the PDA address is safe. let (_, new_cctp_message_bump) = Pubkey::find_program_address( diff --git a/solana/programs/matching-engine/src/fallback/processor/place_initial_offer.rs b/solana/programs/matching-engine/src/fallback/processor/place_initial_offer.rs index 30868f236..aa50eecc8 100644 --- a/solana/programs/matching-engine/src/fallback/processor/place_initial_offer.rs +++ b/solana/programs/matching-engine/src/fallback/processor/place_initial_offer.rs @@ -6,6 +6,7 @@ use solana_program::{instruction::Instruction, program::invoke_signed_unchecked} use crate::{ error::MatchingEngineError, + processor::calculate_security_deposit, state::{Auction, AuctionInfo, AuctionStatus, MessageProtocol}, ID, }; @@ -261,11 +262,10 @@ pub(super) fn process( // The total amount being transferred to the auction's custody token account // is the order's amount and auction participant's security deposit. - let security_deposit = fast_market_order.max_fee.saturating_add( - crate::utils::auction::compute_notional_security_deposit( - &auction_config, - fast_market_order.amount_in, - ), + let security_deposit = calculate_security_deposit( + fast_market_order.max_fee, + fast_market_order.amount_in, + &auction_config, ); let transfer_ix = spl_token::instruction::transfer( diff --git a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/mod.rs b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/mod.rs index 935596c07..0deb4dc87 100644 --- a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/mod.rs +++ b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/mod.rs @@ -3,16 +3,17 @@ pub use cctp::*; mod local; pub use local::*; +use solana_program::program::invoke_signed_unchecked; use crate::{ composite::*, error::MatchingEngineError, events::OrderExecuted, - state::{Auction, AuctionStatus, MessageProtocol}, + state::{Auction, AuctionConfig, AuctionInfo, AuctionStatus, MessageProtocol}, utils::{self, auction::DepositPenalty}, }; use anchor_lang::prelude::*; -use anchor_spl::token; +use anchor_spl::token::{self, spl_token, TokenAccount}; use common::messages::{ raw::{LiquidityLayerMessage, MessageToVec}, Fill, @@ -42,14 +43,85 @@ fn handle_execute_fast_order<'info>( .unwrap() .to_fast_market_order_unchecked(); - let (user_amount, new_status, order_executed_event) = { + let (user_amount, new_status, penalized) = ExecuteOrderInternalAccounting { + active_auction_key: &auction.key(), + active_auction: auction, + auction_custody_key: &custody_token.key(), + auction_custody: custody_token, + best_offer_token_info: &execute_order.active_auction.best_offer_token, + executor_token_key: &execute_order.executor_token.key(), + initial_offer_token_info: &execute_order.initial_offer_token, + custodian_key: &custodian.key(), + auction_config: config, + } + .into_calculate_and_transfer( + order.init_auction_fee(), + &[ + auction.to_account_info(), + custody_token.to_account_info(), + config.to_account_info(), + executor_token.to_account_info(), + best_offer_token.to_account_info(), + initial_offer_token.to_account_info(), + fast_vaa.to_account_info(), + token_program.to_account_info(), + ], + )?; + + let order_executed_event = + get_order_executed_event(auction, fast_vaa, auction.info.as_ref().unwrap(), penalized); + + // Set the auction status to completed. + auction.status = new_status; + + Ok(PreparedOrderExecution { + user_amount, + fill: Fill { + source_chain: vaa.emitter_chain(), + order_sender: order.sender(), + redeemer: order.redeemer(), + redeemer_message: order + .message_to_vec() + .try_into() + .map_err(|_| MatchingEngineError::RedeemerMessageTooLarge)?, + }, + order_executed_event, + }) +} + +pub struct ExecuteOrderInternalAccounting<'ix, 'info> { + pub active_auction_key: &'ix Pubkey, + pub active_auction: &'ix Auction, + pub auction_custody_key: &'ix Pubkey, + pub auction_custody: &'ix TokenAccount, + pub best_offer_token_info: &'ix AccountInfo<'info>, + pub executor_token_key: &'ix Pubkey, + pub initial_offer_token_info: &'ix AccountInfo<'info>, + pub custodian_key: &'ix Pubkey, + pub auction_config: &'ix AuctionConfig, +} + +impl<'ix, 'info> ExecuteOrderInternalAccounting<'ix, 'info> { + pub fn into_calculate_and_transfer( + self, + init_auction_fee: u64, + accounts: &[AccountInfo], + ) -> Result<(u64, AuctionStatus, bool)> { + let Self { + active_auction_key, + active_auction: auction, + auction_custody_key, + auction_custody: custody_token, + best_offer_token_info, + executor_token_key, + initial_offer_token_info, + custodian_key, + auction_config, + } = self; + let auction_info = auction.info.as_ref().unwrap(); let current_slot = Clock::get().unwrap().slot; - // We extend the grace period for locally executed orders. Reserving a sequence number for - // the fast fill will most likely require an additional transaction, so this buffer allows - // the best offer participant to perform his duty without the risk of getting slashed by - // another executor. let additional_grace_period = match auction.target_protocol { MessageProtocol::Local { .. } => { crate::EXECUTE_FAST_ORDER_LOCAL_ADDITIONAL_GRACE_PERIOD.into() @@ -61,25 +133,19 @@ fn handle_execute_fast_order<'info>( penalty, user_reward, } = utils::auction::compute_deposit_penalty( - config, + &auction_config.parameters, auction_info, current_slot, additional_grace_period, ); - let init_auction_fee = order.init_auction_fee(); - let user_amount = auction_info .amount_in .saturating_sub(auction_info.offer_price) .saturating_sub(init_auction_fee) .saturating_add(user_reward); - - // Keep track of the remaining amount in the custody token account. Whatever remains will go - // to the executor. let mut remaining_custodied_amount = custody_token.amount.saturating_sub(user_amount); - // Offer price + security deposit was checked in placing the initial offer. let mut deposit_and_fee = auction_info .offer_price .saturating_add(auction_info.security_deposit) @@ -93,32 +159,26 @@ fn handle_execute_fast_order<'info>( let penalized = penalty > 0; - if penalized && best_offer_token.key() != executor_token.key() { + if penalized && best_offer_token_info.key != executor_token_key { deposit_and_fee = deposit_and_fee.saturating_sub(penalty); } - // If the initial offer token account doesn't exist anymore, we have nowhere to send the - // init auction fee. The executor will get these funds instead. - // - // We check that this is a legitimate token account. - if utils::checked_deserialize_token_account(initial_offer_token, &common::USDC_MINT) + if utils::checked_deserialize_token_account(initial_offer_token_info, &common::USDC_MINT) .is_some() { - if best_offer_token.key() != initial_offer_token.key() { + if best_offer_token_info.key != initial_offer_token_info.key { // Pay the auction initiator their fee. - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: custody_token.to_account_info(), - to: initial_offer_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), + let transfer_ix = spl_token::instruction::transfer( + &spl_token::ID, + auction_custody_key, + initial_offer_token_info.key, + active_auction_key, + &[], init_auction_fee, - )?; + ) + .unwrap(); + invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; // Because the initial offer token was paid this fee, we account for it here. remaining_custodied_amount = remaining_custodied_amount.saturating_sub(init_auction_fee); @@ -131,112 +191,107 @@ fn handle_execute_fast_order<'info>( } // Return the security deposit and the fee to the highest bidder. - // - if best_offer_token.key() == executor_token.key() { + if best_offer_token_info.key == executor_token_key { // If the best offer token is equal to the executor token, just send whatever remains in // the custody token account. // // NOTE: This will revert if the best offer token does not exist. But this will present // an opportunity for another executor to execute this order and take what the best // offer token would have received. - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: custody_token.to_account_info(), - to: best_offer_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), - remaining_custodied_amount, - )?; + let transfer_ix = spl_token::instruction::transfer( + &spl_token::ID, + auction_custody_key, + best_offer_token_info.key, + active_auction_key, + &[], + deposit_and_fee, + ) + .unwrap(); + msg!( + "Sending deposit and fee amount {} to best offer token account", + deposit_and_fee + ); + invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; } else { // Otherwise, send the deposit and fee to the best offer token. If the best offer token // doesn't exist at this point (which would be unusual), we will reserve these funds // for the executor token. - if utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) + if utils::checked_deserialize_token_account(best_offer_token_info, &common::USDC_MINT) .is_some() { - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: custody_token.to_account_info(), - to: best_offer_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), + let transfer_ix = spl_token::instruction::transfer( + &spl_token::ID, + auction_custody_key, + best_offer_token_info.key, + active_auction_key, + &[], deposit_and_fee, - )?; - + ) + .unwrap(); + msg!( + "Sending deposit and fee {} to best offer token account", + deposit_and_fee + ); + invoke_signed_unchecked(&transfer_ix, accounts, &[auction_signer_seeds])?; remaining_custodied_amount = remaining_custodied_amount.saturating_sub(deposit_and_fee); } // And pay the executor whatever remains in the auction custody token account. if remaining_custodied_amount > 0 { - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: custody_token.to_account_info(), - to: executor_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), + let instruction = spl_token::instruction::transfer( + &spl_token::ID, + auction_custody_key, + executor_token_key, + active_auction_key, + &[], remaining_custodied_amount, - )?; + ) + .unwrap(); + msg!( + "Sending remaining custodied amount {} to executor token account", + remaining_custodied_amount + ); + invoke_signed_unchecked(&instruction, accounts, &[auction_signer_seeds])?; } } // Set the authority of the custody token account to the custodian. He will take over from // here. - token::set_authority( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::SetAuthority { - current_authority: auction.to_account_info(), - account_or_mint: custody_token.to_account_info(), - }, - &[auction_signer_seeds], - ), - token::spl_token::instruction::AuthorityType::AccountOwner, - custodian.key().into(), - )?; - - ( + let set_authority_ix = spl_token::instruction::set_authority( + &spl_token::ID, + auction_custody_key, + Some(custodian_key), + spl_token::instruction::AuthorityType::AccountOwner, + active_auction_key, + &[], + ) + .unwrap(); + + invoke_signed_unchecked(&set_authority_ix, accounts, &[auction_signer_seeds])?; + + Ok(( user_amount, AuctionStatus::Completed { slot: current_slot, execute_penalty: if penalized { penalty.into() } else { None }, }, - OrderExecuted { - fast_vaa_hash: auction.vaa_hash, - vaa: fast_vaa.key(), - source_chain: auction_info.source_chain, - target_protocol: auction.target_protocol, - penalized, - }, - ) - }; - - // Set the auction status to completed. - auction.status = new_status; + penalized, + )) + } +} - Ok(PreparedOrderExecution { - user_amount, - fill: Fill { - source_chain: vaa.emitter_chain(), - order_sender: order.sender(), - redeemer: order.redeemer(), - redeemer_message: order - .message_to_vec() - .try_into() - .map_err(|_| MatchingEngineError::RedeemerMessageTooLarge)?, - }, - order_executed_event, - }) +pub fn get_order_executed_event( + auction: &Auction, + fast_vaa: &AccountInfo<'_>, + auction_info: &AuctionInfo, + penalized: bool, +) -> OrderExecuted { + OrderExecuted { + fast_vaa_hash: auction.vaa_hash, + vaa: fast_vaa.key(), + source_chain: auction_info.source_chain, + target_protocol: auction.target_protocol, + penalized, + } } diff --git a/solana/programs/matching-engine/src/processor/auction/offer/place_initial/cctp.rs b/solana/programs/matching-engine/src/processor/auction/offer/place_initial/cctp.rs index 3bc3cae5b..0a4ba6bf8 100644 --- a/solana/programs/matching-engine/src/processor/auction/offer/place_initial/cctp.rs +++ b/solana/programs/matching-engine/src/processor/auction/offer/place_initial/cctp.rs @@ -1,7 +1,9 @@ use crate::{ composite::*, error::MatchingEngineError, - state::{Auction, AuctionConfig, AuctionInfo, AuctionStatus, MessageProtocol}, + state::{ + Auction, AuctionConfig, AuctionInfo, AuctionParameters, AuctionStatus, MessageProtocol, + }, utils, }; use anchor_lang::prelude::*; @@ -131,12 +133,7 @@ pub fn place_initial_offer_cctp( // Saturating to u64::MAX is safe here. If the amount really ends up being this large, the // checked addition below will catch it. let security_deposit = - order - .max_fee() - .saturating_add(utils::auction::compute_notional_security_deposit( - &ctx.accounts.auction_config, - amount_in, - )); + calculate_security_deposit(order.max_fee(), amount_in, &ctx.accounts.auction_config); // Set up the Auction account for this auction. let config = &ctx.accounts.auction_config; @@ -206,3 +203,14 @@ pub fn place_initial_offer_cctp( .ok_or_else(|| MatchingEngineError::U64Overflow)?, ) } + +pub fn calculate_security_deposit( + max_fee: u64, + amount_in: u64, + auction_parameters: &AuctionParameters, +) -> u64 { + max_fee.saturating_add(utils::auction::compute_notional_security_deposit( + auction_parameters, + amount_in, + )) +}