diff --git a/solana/programs/matching-engine/src/composite/mod.rs b/solana/programs/matching-engine/src/composite/mod.rs index 7d847490..9a62b0f0 100644 --- a/solana/programs/matching-engine/src/composite/mod.rs +++ b/solana/programs/matching-engine/src/composite/mod.rs @@ -320,7 +320,7 @@ pub struct ActiveAuction<'info> { ], bump = auction.info.as_ref().unwrap().custody_token_bump, )] - pub custody_token: Account<'info, anchor_spl::token::TokenAccount>, + pub custody_token: Box>, #[account( constraint = { @@ -332,7 +332,7 @@ pub struct ActiveAuction<'info> { true }, )] - pub config: Account<'info, crate::state::AuctionConfig>, + pub config: Box>, /// CHECK: Mutable. Must have the same key in auction data. #[account( @@ -398,9 +398,11 @@ pub struct ExecuteOrder<'info> { )] pub initial_offer_token: UncheckedAccount<'info>, - /// CHECK: Must be the owner of initial offer token account. If the initial offer token account - /// does not exist anymore, we will attempt to perform this check. - #[account(mut)] + /// CHECK: Must be the payer of the initial auction (see [Auction::prepared_by]). + #[account( + mut, + address = active_auction.prepared_by, + )] pub initial_participant: UncheckedAccount<'info>, } @@ -563,7 +565,7 @@ impl<'info> VaaDigest for ClosePreparedOrderResponse<'info> { #[derive(Accounts)] pub struct ReserveFastFillSequence<'info> { #[account(mut)] - payer: Signer<'info>, + pub payer: Signer<'info>, pub fast_order_path: FastOrderPath<'info>, @@ -650,7 +652,7 @@ pub struct ReserveFastFillSequence<'info> { } }, )] - pub auction: Account<'info, Auction>, + pub auction: Box>, system_program: Program<'info, System>, } diff --git a/solana/programs/matching-engine/src/error.rs b/solana/programs/matching-engine/src/error.rs index df814e19..b67e4023 100644 --- a/solana/programs/matching-engine/src/error.rs +++ b/solana/programs/matching-engine/src/error.rs @@ -81,6 +81,8 @@ pub enum MatchingEngineError { FastFillNotRedeemed = 0x435, ReservedSequenceMismatch = 0x438, AuctionAlreadySettled = 0x43a, + InvalidBaseFeeToken = 0x43c, + BaseFeeTokenRequired = 0x43e, CannotCloseAuctionYet = 0x500, AuctionHistoryNotFull = 0x502, diff --git a/solana/programs/matching-engine/src/events/auction_settled.rs b/solana/programs/matching-engine/src/events/auction_settled.rs index eb3c045a..bda0fd33 100644 --- a/solana/programs/matching-engine/src/events/auction_settled.rs +++ b/solana/programs/matching-engine/src/events/auction_settled.rs @@ -18,8 +18,9 @@ pub struct AuctionSettled { pub best_offer_token: Option, /// Depending on whether there was an active auction, this field will have the pubkey of the - /// executor token (if there was an auction) or fee recipient token (if there was no auction). - pub executor_token: Option, + /// base fee token account (if there was an auction) or fee recipient token (if there was no + /// auction). + pub base_fee_token: Option, /// This value will only be some if there was no active auction. pub with_execute: Option, diff --git a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/cctp.rs b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/cctp.rs index 9a502507..5494dbe4 100644 --- a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/cctp.rs +++ b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/cctp.rs @@ -79,12 +79,11 @@ pub fn handle_execute_fast_order_cctp( let super::PreparedOrderExecution { user_amount: amount, fill, - beneficiary, - } = super::prepare_order_execution(super::PrepareFastExecution { - execute_order: &mut ctx.accounts.execute_order, - custodian: &ctx.accounts.custodian, - token_program: &ctx.accounts.token_program, - })?; + } = super::handle_execute_fast_order( + &mut ctx.accounts.execute_order, + &ctx.accounts.custodian, + &ctx.accounts.token_program, + )?; let active_auction = &ctx.accounts.execute_order.active_auction; let auction_custody_token = &active_auction.custody_token; @@ -188,7 +187,11 @@ pub fn handle_execute_fast_order_cctp( token_program.to_account_info(), token::CloseAccount { account: auction_custody_token.to_account_info(), - destination: beneficiary.unwrap_or_else(|| payer.to_account_info()), + destination: ctx + .accounts + .execute_order + .initial_participant + .to_account_info(), authority: custodian.to_account_info(), }, &[Custodian::SIGNER_SEEDS], diff --git a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/local.rs b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/local.rs index eb736d58..d7f0fd77 100644 --- a/solana/programs/matching-engine/src/processor/auction/execute_fast_order/local.rs +++ b/solana/programs/matching-engine/src/processor/auction/execute_fast_order/local.rs @@ -98,12 +98,11 @@ pub fn execute_fast_order_local(ctx: Context) -> Result<( let super::PreparedOrderExecution { user_amount: amount, fill, - beneficiary, - } = super::prepare_order_execution(super::PrepareFastExecution { - execute_order: &mut ctx.accounts.execute_order, - custodian, - token_program, - })?; + } = super::handle_execute_fast_order( + &mut ctx.accounts.execute_order, + &ctx.accounts.custodian, + &ctx.accounts.token_program, + )?; let fast_fill = FastFill::new( fill, @@ -140,7 +139,11 @@ pub fn execute_fast_order_local(ctx: Context) -> Result<( token_program.to_account_info(), token::CloseAccount { account: auction_custody_token.to_account_info(), - destination: beneficiary.unwrap_or_else(|| ctx.accounts.payer.to_account_info()), + destination: ctx + .accounts + .execute_order + .initial_participant + .to_account_info(), authority: custodian.to_account_info(), }, &[Custodian::SIGNER_SEEDS], 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 0931b176..b7c12bf9 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 @@ -17,27 +17,16 @@ use common::messages::{ Fill, }; -struct PrepareFastExecution<'ctx, 'info> { - execute_order: &'ctx mut ExecuteOrder<'info>, - custodian: &'ctx CheckedCustodian<'info>, - token_program: &'ctx Program<'info, token::Token>, -} - -struct PreparedOrderExecution<'info> { +struct PreparedOrderExecution { pub user_amount: u64, pub fill: Fill, - pub beneficiary: Option>, } -fn prepare_order_execution<'info>( - accounts: PrepareFastExecution<'_, 'info>, -) -> Result> { - let PrepareFastExecution { - execute_order, - custodian, - token_program, - } = accounts; - +fn handle_execute_fast_order<'info>( + execute_order: &mut ExecuteOrder<'info>, + custodian: &CheckedCustodian<'info>, + token_program: &Program<'info, token::Token>, +) -> Result { let auction = &mut execute_order.active_auction.auction; let fast_vaa = &execute_order.fast_vaa; let custody_token = &execute_order.active_auction.custody_token; @@ -45,14 +34,13 @@ fn prepare_order_execution<'info>( let executor_token = &execute_order.executor_token; let best_offer_token = &execute_order.active_auction.best_offer_token; let initial_offer_token = &execute_order.initial_offer_token; - let initial_participant = &execute_order.initial_participant; let vaa = fast_vaa.load_unchecked(); let order = LiquidityLayerMessage::try_from(vaa.payload()) .unwrap() .to_fast_market_order_unchecked(); - let (user_amount, new_status, beneficiary) = { + let (user_amount, new_status) = { let auction_info = auction.info.as_ref().unwrap(); let current_slot = Clock::get().unwrap().slot; @@ -107,32 +95,19 @@ fn prepare_order_execution<'info>( deposit_and_fee = deposit_and_fee.saturating_sub(penalty); } - let mut beneficiary = None; - // 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. // - // Deserialize to token account to find owner. We check that this is a legitimate token - // account. - if let Some(token_data) = - utils::checked_deserialize_token_account(initial_offer_token, &custody_token.mint) + // We check that this is a legitimate token account. + if utils::checked_deserialize_token_account(initial_offer_token, &common::USDC_MINT) + .is_some() { - // Before setting the beneficiary to the initial participant, we need to make sure that - // he is the owner of this token account. - require_keys_eq!( - token_data.owner, - initial_participant.key(), - ErrorCode::ConstraintTokenOwner - ); - - beneficiary.replace(initial_participant.to_account_info()); - if best_offer_token.key() != initial_offer_token.key() { // Pay the auction initiator their fee. token::transfer( CpiContext::new_with_signer( token_program.to_account_info(), - anchor_spl::token::Transfer { + token::Transfer { from: custody_token.to_account_info(), to: initial_offer_token.to_account_info(), authority: auction.to_account_info(), @@ -165,7 +140,7 @@ fn prepare_order_execution<'info>( token::transfer( CpiContext::new_with_signer( token_program.to_account_info(), - anchor_spl::token::Transfer { + token::Transfer { from: custody_token.to_account_info(), to: best_offer_token.to_account_info(), authority: auction.to_account_info(), @@ -178,13 +153,13 @@ fn prepare_order_execution<'info>( // 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, &custody_token.mint) + if utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) .is_some() { token::transfer( CpiContext::new_with_signer( token_program.to_account_info(), - anchor_spl::token::Transfer { + token::Transfer { from: custody_token.to_account_info(), to: best_offer_token.to_account_info(), authority: auction.to_account_info(), @@ -203,7 +178,7 @@ fn prepare_order_execution<'info>( token::transfer( CpiContext::new_with_signer( token_program.to_account_info(), - anchor_spl::token::Transfer { + token::Transfer { from: custody_token.to_account_info(), to: executor_token.to_account_info(), authority: auction.to_account_info(), @@ -246,7 +221,6 @@ fn prepare_order_execution<'info>( slot: current_slot, execute_penalty: if penalized { penalty.into() } else { None }, }, - beneficiary, ) }; @@ -264,6 +238,5 @@ fn prepare_order_execution<'info>( .try_into() .map_err(|_| MatchingEngineError::RedeemerMessageTooLarge)?, }, - beneficiary, }) } diff --git a/solana/programs/matching-engine/src/processor/auction/history/add_entry.rs b/solana/programs/matching-engine/src/processor/auction/history/add_entry.rs index d49ac2a0..502dd097 100644 --- a/solana/programs/matching-engine/src/processor/auction/history/add_entry.rs +++ b/solana/programs/matching-engine/src/processor/auction/history/add_entry.rs @@ -8,7 +8,6 @@ use crate::{ }, }; use anchor_lang::{prelude::*, system_program}; -use anchor_spl::token; #[derive(Accounts)] pub struct AddAuctionHistoryEntry<'info> { @@ -62,21 +61,13 @@ pub struct AddAuctionHistoryEntry<'info> { )] auction: Account<'info, Auction>, - /// CHECK: This account will either be the owner of the fee recipient token account (if there - /// was no auction) or the owner of the initial offer token account. - #[account(mut)] - beneficiary: UncheckedAccount<'info>, - + /// CHECK: This account is whoever originally created the auction account (see + /// [Auction::prepared_by]. #[account( - token::authority = beneficiary, - address = { - match &auction.info { - Some(info) => info.initial_offer_token, - None => custodian.fee_recipient_token, - } - } + mut, + address = auction.prepared_by, )] - beneficiary_token: Account<'info, token::TokenAccount>, + beneficiary: UncheckedAccount<'info>, system_program: Program<'info, system_program::System>, } diff --git a/solana/programs/matching-engine/src/processor/auction/offer/improve.rs b/solana/programs/matching-engine/src/processor/auction/offer/improve.rs index 41458afa..743da66e 100644 --- a/solana/programs/matching-engine/src/processor/auction/offer/improve.rs +++ b/solana/programs/matching-engine/src/processor/auction/offer/improve.rs @@ -79,7 +79,7 @@ pub fn improve_offer(ctx: Context, offer_price: u64) -> Result<()> // If the best offer token happens to be closed, we will just keep the funds in the // auction custody account. The executor token account will collect these funds when the // order is executed. - if utils::checked_deserialize_token_account(best_offer_token, &custody_token.mint) + if utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) .is_some() { token::transfer( 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 2f04aaf4..190123e9 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 @@ -145,6 +145,7 @@ pub fn place_initial_offer_cctp( vaa_timestamp: fast_vaa.timestamp(), target_protocol: ctx.accounts.fast_order_path.to_endpoint.protocol, status: AuctionStatus::Active, + prepared_by: ctx.accounts.payer.key(), info: AuctionInfo { config_id: config.id, custody_token_bump: ctx.bumps.auction_custody_token, diff --git a/solana/programs/matching-engine/src/processor/auction/prepare_order_response/cctp.rs b/solana/programs/matching-engine/src/processor/auction/prepare_order_response/cctp.rs index 88778eba..838c5a9c 100644 --- a/solana/programs/matching-engine/src/processor/auction/prepare_order_response/cctp.rs +++ b/solana/programs/matching-engine/src/processor/auction/prepare_order_response/cctp.rs @@ -97,6 +97,23 @@ pub struct PrepareOrderResponseCctp<'info> { )] prepared_custody_token: Box>, + /// This token account will be the one that collects the base fee only if an auction's order + /// was executed late. Otherwise, the protocol's fee recipient token account will be used for + /// non-existent auctions and the best offer token account will be used for orders executed on + /// time. + #[account( + token::mint = usdc, + constraint = { + require!( + base_fee_token.key() != prepared_custody_token.key(), + MatchingEngineError::InvalidBaseFeeToken + ); + + true + } + )] + base_fee_token: Box>, + usdc: Usdc<'info>, cctp: CctpReceiveMessage<'info>, @@ -216,6 +233,7 @@ fn handle_prepare_order_response_cctp( }, info: PreparedOrderResponseInfo { prepared_by: ctx.accounts.payer.key(), + base_fee_token: ctx.accounts.base_fee_token.key(), source_chain: finalized_vaa.emitter_chain(), base_fee: order_response.base_fee(), fast_vaa_timestamp: fast_vaa.timestamp(), diff --git a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs index 21eb2395..00803bc0 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs @@ -2,35 +2,37 @@ use crate::{ error::MatchingEngineError, events::SettledTokenAccountInfo, state::{Auction, AuctionStatus, PreparedOrderResponse}, + utils, }; use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::get_associated_token_address, - token::{self, TokenAccount}, -}; +use anchor_spl::token::{self, TokenAccount}; #[derive(Accounts)] pub struct SettleAuctionComplete<'info> { - /// CHECK: To prevent squatters from preparing order responses on behalf of the auction winner, - /// we will always reward the owner of the executor token account with the lamports from the - /// prepared order response and its custody token account when we close these accounts. This - /// means we disregard the `prepared_by` field in the prepared order response. - #[account(mut)] - executor: UncheckedAccount<'info>, + /// CHECK: Must equal prepared_order_response.prepared_by, who paid the rent to post the + /// finalized VAA. + #[account( + mut, + address = prepared_order_response.prepared_by, + )] + beneficiary: UncheckedAccount<'info>, + /// This token account will receive the base fee only if there was a penalty when executing the + /// order. If it does not exist when there is a penalty, this instruction handler will revert. + /// + /// CHECK: This account must be the same as the base fee token in the prepared order response. #[account( mut, - token::mint = common::USDC_MINT, - token::authority = executor, + address = prepared_order_response.base_fee_token, )] - executor_token: Account<'info, TokenAccount>, + base_fee_token: UncheckedAccount<'info>, /// Destination token account, which the redeemer may not own. But because the redeemer is a /// signer and is the one encoded in the Deposit Fill message, he may have the tokens be sent /// to any account he chooses (this one). /// /// CHECK: This token account may exist. If it doesn't and there is a penalty, we will send all - /// of the tokens to the executor token account. + /// of the tokens to the base fee token account. #[account( mut, address = auction.info.as_ref().unwrap().best_offer_token, @@ -39,14 +41,14 @@ pub struct SettleAuctionComplete<'info> { #[account( mut, - close = executor, + close = beneficiary, seeds = [ PreparedOrderResponse::SEED_PREFIX, prepared_order_response.seeds.fast_vaa_hash.as_ref() ], bump = prepared_order_response.seeds.bump, )] - prepared_order_response: Account<'info, PreparedOrderResponse>, + prepared_order_response: Box>, /// CHECK: Seeds must be \["prepared-custody"\, prepared_order_response.key()]. #[account( @@ -57,7 +59,7 @@ pub struct SettleAuctionComplete<'info> { ], bump, )] - prepared_custody_token: Account<'info, TokenAccount>, + prepared_custody_token: Box>, #[account( mut, @@ -67,7 +69,7 @@ pub struct SettleAuctionComplete<'info> { ], bump = auction.bump, )] - auction: Account<'info, Auction>, + auction: Box>, token_program: Program<'info, token::Token>, } @@ -100,8 +102,8 @@ fn handle_settle_auction_complete( &[prepared_order_response.seeds.bump], ]; - let executor = &ctx.accounts.executor; - let executor_token = &ctx.accounts.executor_token; + let beneficiary = &ctx.accounts.beneficiary; + let base_fee_token = &ctx.accounts.base_fee_token; let best_offer_token = &ctx.accounts.best_offer_token; let token_program = &ctx.accounts.token_program; let prepared_custody_token = &ctx.accounts.prepared_custody_token; @@ -113,103 +115,84 @@ fn handle_settle_auction_complete( amount: u64, } - let (executor_result, best_offer_result) = match execute_penalty { + let (base_fee_result, best_offer_result) = match execute_penalty { + // When there is no penalty, we will give everything to the best offer token account. None => { - // If there is no penalty, we require that the executor token and best offer token be - // equal. The winning offer should not be penalized for calling this instruction when he - // has executed the order within the grace period. - // - // By requiring that these pubkeys are equal, we enforce that the owner of the best - // offer token gets rewarded the lamports from the prepared order response and its - // custody account. - require_keys_eq!( - executor_token.key(), - best_offer_token.key(), - MatchingEngineError::ExecutorTokenMismatch - ); - // If the token account happens to not exist anymore, we will revert. - match TokenAccount::try_deserialize(&mut &best_offer_token.data.borrow()[..]) { - Ok(best_offer) => ( - None, // executor_result - TokenAccountResult { - balance_before: best_offer.amount, - amount: repayment, - } - .into(), - ), - Err(err) => return Err(err), - } + let best_offer_token_data = + utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) + .ok_or_else(|| MatchingEngineError::BestOfferTokenRequired)?; + + ( + None, // base_fee_result + TokenAccountResult { + balance_before: best_offer_token_data.amount, + amount: repayment, + } + .into(), + ) } + // Otherwise, determine how the repayment should be divvied up. _ => { - // If there is a penalty, we want to return the lamports back to the person who paid to - // create the prepared order response and custody token accounts. - // - // The executor's intention here would be to collect the base fee to cover the cost to - // post the finalized VAA. - require_keys_eq!( - executor.key(), - prepared_order_response.prepared_by, - MatchingEngineError::ExecutorNotPreparedBy - ); - - // If the token account happens to not exist anymore, we will give everything to the - // executor. - match TokenAccount::try_deserialize(&mut &best_offer_token.data.borrow()[..]) { - Ok(best_offer) => { - if executor_token.key() == best_offer_token.key() { + match ( + utils::checked_deserialize_token_account(base_fee_token, &common::USDC_MINT), + utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT), + ) { + (Some(base_fee_token_data), Some(best_offer_token_data)) => { + if base_fee_token.key() == best_offer_token.key() { ( - None, // executor_result + None, // base_fee_result TokenAccountResult { - balance_before: best_offer.amount, + balance_before: best_offer_token_data.amount, amount: repayment, } .into(), ) } else { - // Because the auction participant was penalized for executing the order - // late, he will be deducted the base fee. This base fee will be sent to the - // executor token account if it is not the same as the best offer token - // account. - - // We require that the executor token account be an ATA. - require_keys_eq!( - executor_token.key(), - get_associated_token_address( - &executor_token.owner, - &executor_token.mint - ), - ErrorCode::AccountNotAssociatedTokenAccount - ); - ( TokenAccountResult { - balance_before: executor_token.amount, + balance_before: base_fee_token_data.amount, amount: base_fee, } .into(), TokenAccountResult { - balance_before: best_offer.amount, + balance_before: best_offer_token_data.amount, amount: repayment.saturating_sub(base_fee), } .into(), ) } } - Err(_) => ( + // If the best offer token account does not exist, we will give everything to the + // base fee token account. + (Some(base_fee_token_data), None) => ( TokenAccountResult { - balance_before: executor_token.amount, + balance_before: base_fee_token_data.amount, amount: repayment, } .into(), None, // best_offer_result ), + // If the base fee token account does not exist, we will give everything to the best + // offer token account. + (None, Some(best_offer_data)) => { + ( + None, // base_fee_result + TokenAccountResult { + balance_before: best_offer_data.amount, + amount: repayment, + } + .into(), + ) + } + // Otherwise revert. + _ => return err!(MatchingEngineError::BestOfferTokenRequired), } } }; - // Transfer executor his bounty if there are any. - let settled_executor_result = match executor_result { + // Transfer base fee token his bounty if there are any. + let settled_base_fee_result = match base_fee_result { Some(TokenAccountResult { balance_before, amount, @@ -219,7 +202,7 @@ fn handle_settle_auction_complete( token_program.to_account_info(), token::Transfer { from: prepared_custody_token.to_account_info(), - to: executor_token.to_account_info(), + to: base_fee_token.to_account_info(), authority: prepared_order_response.to_account_info(), }, &[prepared_order_response_signer_seeds], @@ -228,7 +211,7 @@ fn handle_settle_auction_complete( )?; SettledTokenAccountInfo { - key: executor_token.key(), + key: base_fee_token.key(), balance_after: balance_before.saturating_add(amount), } .into() @@ -267,7 +250,7 @@ fn handle_settle_auction_complete( emit!(crate::events::AuctionSettled { auction: ctx.accounts.auction.key(), best_offer_token: settled_best_offer_result, - executor_token: settled_executor_result, + base_fee_token: settled_base_fee_result, with_execute: Default::default(), }); @@ -276,7 +259,7 @@ fn handle_settle_auction_complete( token_program.to_account_info(), token::CloseAccount { account: prepared_custody_token.to_account_info(), - destination: executor.to_account_info(), + destination: beneficiary.to_account_info(), authority: prepared_order_response.to_account_info(), }, &[prepared_order_response_signer_seeds], diff --git a/solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs b/solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs index 7abf9a3a..bb26d7a4 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs @@ -84,7 +84,7 @@ fn settle_none_and_prepare_fill(accounts: SettleNoneAndPrepareFill<'_, '_>) -> R emit!(crate::events::AuctionSettled { auction: auction.key(), best_offer_token: Default::default(), - executor_token: crate::events::SettledTokenAccountInfo { + base_fee_token: crate::events::SettledTokenAccountInfo { key: fee_recipient_token.key(), balance_after: fee_recipient_token.amount.saturating_add(fee) } diff --git a/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/active_auction.rs b/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/active_auction.rs index 2e156056..7f9b7ead 100644 --- a/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/active_auction.rs +++ b/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/active_auction.rs @@ -1,6 +1,5 @@ use crate::{composite::*, error::MatchingEngineError, state::AuctionConfig}; use anchor_lang::prelude::*; -use anchor_spl::token; #[derive(Accounts)] pub struct ReserveFastFillSequenceActiveAuction<'info> { @@ -27,56 +26,12 @@ pub struct ReserveFastFillSequenceActiveAuction<'info> { } )] auction_config: Account<'info, AuctionConfig>, - - /// Best offer token account, whose owner will be the beneficiary of the reserved fast fill - /// sequence account when it is closed. - /// - /// CHECK: This account may not exist. If it does, it should equal the best offer token pubkey - /// in the auction account. - #[account( - constraint = { - // We know from the auction constraint that the auction is active, so the auction info - // is safe to unwrap. - let info = reserve_sequence.auction.info.as_ref().unwrap(); - - // Best offer token must equal the one in the auction account. - // - // NOTE: Unwrapping the auction info is safe because we know this is an active auction. - require_keys_eq!( - best_offer_token.key(), - info.best_offer_token, - MatchingEngineError::BestOfferTokenMismatch - ); - - true - } - )] - best_offer_token: UncheckedAccount<'info>, - - /// CHECK: If the best offer token does not exist anymore, this executor will be the beneficiary - /// of the reserved fast fill sequence account when it is closed. Otherwise, this account must - /// equal the best offer token account's owner. - executor: UncheckedAccount<'info>, } pub fn reserve_fast_fill_sequence_active_auction( ctx: Context, ) -> Result<()> { - let best_offer_token = &ctx.accounts.best_offer_token; - let beneficiary = ctx.accounts.executor.key(); - - // If the token account does exist, we will constrain that the executor is the best offer token. - if let Ok(token) = - token::TokenAccount::try_deserialize(&mut &best_offer_token.data.borrow()[..]) - { - require_keys_eq!( - *best_offer_token.owner, - token::ID, - ErrorCode::ConstraintTokenTokenProgram - ); - require_keys_eq!(token.owner, beneficiary, ErrorCode::ConstraintTokenOwner); - } - + let beneficiary = ctx.accounts.reserve_sequence.payer.key(); let fast_vaa_hash = ctx.accounts.reserve_sequence.auction.vaa_hash; super::set_reserved_sequence_data( diff --git a/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/mod.rs b/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/mod.rs index 94bdf408..f8f9fbd6 100644 --- a/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/mod.rs +++ b/solana/programs/matching-engine/src/processor/fast_fill/reserve_sequence/mod.rs @@ -25,8 +25,12 @@ fn set_reserved_sequence_data( // If the fast fill sequencer was just created, we need to set it with data. if sequencer.seeds == Default::default() { - msg!("Sequencer created"); + msg!("Create sequencer"); + msg!( + "account_data: {:?}", + &reserve_sequence.fast_order_path.fast_vaa.vaa.data.borrow()[..8] + ); let vaa = reserve_sequence.fast_order_path.fast_vaa.load_unchecked(); let sender = LiquidityLayerMessage::try_from(vaa.payload()) .unwrap() diff --git a/solana/programs/matching-engine/src/state/auction.rs b/solana/programs/matching-engine/src/state/auction.rs index b3f077a1..3dcb9598 100644 --- a/solana/programs/matching-engine/src/state/auction.rs +++ b/solana/programs/matching-engine/src/state/auction.rs @@ -149,6 +149,9 @@ pub struct Auction { /// Auction status. pub status: AuctionStatus, + /// The fee payer when placing the initial offer. + pub prepared_by: Pubkey, + /// Optional auction info. This field will be `None`` if there is no auction. pub info: Option, } diff --git a/solana/programs/matching-engine/src/state/prepared_order_response.rs b/solana/programs/matching-engine/src/state/prepared_order_response.rs index aae409f1..90a5a26b 100644 --- a/solana/programs/matching-engine/src/state/prepared_order_response.rs +++ b/solana/programs/matching-engine/src/state/prepared_order_response.rs @@ -11,6 +11,7 @@ pub struct PreparedOrderResponseSeeds { #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct PreparedOrderResponseInfo { pub prepared_by: Pubkey, + pub base_fee_token: Pubkey, pub fast_vaa_timestamp: u32, pub source_chain: u16, @@ -52,6 +53,7 @@ impl PreparedOrderResponse { vaa_timestamp: self.fast_vaa_timestamp, target_protocol: self.to_endpoint.protocol, status: Default::default(), + prepared_by: self.prepared_by, info: Default::default(), } } diff --git a/solana/programs/matching-engine/src/utils/mod.rs b/solana/programs/matching-engine/src/utils/mod.rs index f9f5309b..95a8a397 100644 --- a/solana/programs/matching-engine/src/utils/mod.rs +++ b/solana/programs/matching-engine/src/utils/mod.rs @@ -45,7 +45,7 @@ pub fn require_local_endpoint(endpoint: &RouterEndpoint) -> Result { pub fn checked_deserialize_token_account( acc_info: &AccountInfo, expected_mint: &Pubkey, -) -> Option { +) -> Option> { if acc_info.owner != &token::ID { None } else { @@ -54,5 +54,6 @@ pub fn checked_deserialize_token_account( token::TokenAccount::try_deserialize(&mut &data[..]) .ok() .filter(|token_data| &token_data.mint == expected_mint && !token_data.is_frozen()) + .map(Box::new) } } diff --git a/solana/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index 4c3ec0b1..b2ed77ac 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -65,13 +65,10 @@ { "name": "beneficiary", "docs": [ - "was no auction) or the owner of the initial offer token account." + "[Auction::prepared_by]." ], "writable": true }, - { - "name": "beneficiary_token" - }, { "name": "system_program" } @@ -696,9 +693,6 @@ }, { "name": "initial_participant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -891,9 +885,6 @@ }, { "name": "initial_participant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -1378,6 +1369,15 @@ "name": "prepared_custody_token", "writable": true }, + { + "name": "base_fee_token", + "docs": [ + "This token account will be the one that collects the base fee only if an auction's order", + "was executed late. Otherwise, the protocol's fee recipient token account will be used for", + "non-existent auctions and the best offer token account will be used for orders executed on", + "time." + ] + }, { "name": "usdc", "accounts": [ @@ -1649,22 +1649,6 @@ }, { "name": "auction_config" - }, - { - "name": "best_offer_token", - "docs": [ - "Best offer token account, whose owner will be the beneficiary of the reserved fast fill", - "sequence account when it is closed.", - "", - "in the auction account." - ] - }, - { - "name": "executor", - "docs": [ - "of the reserved fast fill sequence account when it is closed. Otherwise, this account must", - "equal the best offer token account's owner." - ] } ], "args": [] @@ -1859,16 +1843,19 @@ ], "accounts": [ { - "name": "executor", + "name": "beneficiary", "docs": [ - "we will always reward the owner of the executor token account with the lamports from the", - "prepared order response and its custody token account when we close these accounts. This", - "means we disregard the `prepared_by` field in the prepared order response." + "finalized VAA." ], "writable": true }, { - "name": "executor_token", + "name": "base_fee_token", + "docs": [ + "This token account will receive the base fee only if there was a penalty when executing the", + "order. If it does not exist when there is a penalty, this instruction handler will revert.", + "" + ], "writable": true }, { @@ -1878,7 +1865,7 @@ "signer and is the one encoded in the Deposit Fill message, he may have the tokens be sent", "to any account he chooses (this one).", "", - "of the tokens to the executor token account." + "of the tokens to the base fee token account." ], "writable": true }, @@ -3059,6 +3046,14 @@ "code": 7082, "name": "AuctionAlreadySettled" }, + { + "code": 7084, + "name": "InvalidBaseFeeToken" + }, + { + "code": 7086, + "name": "BaseFeeTokenRequired" + }, { "code": 7280, "name": "CannotCloseAuctionYet" @@ -3159,6 +3154,13 @@ } } }, + { + "name": "prepared_by", + "docs": [ + "The fee payer when placing the initial offer." + ], + "type": "pubkey" + }, { "name": "info", "docs": [ @@ -3494,10 +3496,11 @@ } }, { - "name": "executor_token", + "name": "base_fee_token", "docs": [ "Depending on whether there was an active auction, this field will have the pubkey of the", - "executor token (if there was an auction) or fee recipient token (if there was no auction)." + "base fee token account (if there was an auction) or fee recipient token (if there was no", + "auction)." ], "type": { "option": { @@ -4123,6 +4126,10 @@ "name": "prepared_by", "type": "pubkey" }, + { + "name": "base_fee_token", + "type": "pubkey" + }, { "name": "fast_vaa_timestamp", "type": "u32" diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index ebf59b2b..055f7e14 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -71,13 +71,10 @@ export type MatchingEngine = { { "name": "beneficiary", "docs": [ - "was no auction) or the owner of the initial offer token account." + "[Auction::prepared_by]." ], "writable": true }, - { - "name": "beneficiaryToken" - }, { "name": "systemProgram" } @@ -702,9 +699,6 @@ export type MatchingEngine = { }, { "name": "initialParticipant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -897,9 +891,6 @@ export type MatchingEngine = { }, { "name": "initialParticipant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -1384,6 +1375,15 @@ export type MatchingEngine = { "name": "preparedCustodyToken", "writable": true }, + { + "name": "baseFeeToken", + "docs": [ + "This token account will be the one that collects the base fee only if an auction's order", + "was executed late. Otherwise, the protocol's fee recipient token account will be used for", + "non-existent auctions and the best offer token account will be used for orders executed on", + "time." + ] + }, { "name": "usdc", "accounts": [ @@ -1655,22 +1655,6 @@ export type MatchingEngine = { }, { "name": "auctionConfig" - }, - { - "name": "bestOfferToken", - "docs": [ - "Best offer token account, whose owner will be the beneficiary of the reserved fast fill", - "sequence account when it is closed.", - "", - "in the auction account." - ] - }, - { - "name": "executor", - "docs": [ - "of the reserved fast fill sequence account when it is closed. Otherwise, this account must", - "equal the best offer token account's owner." - ] } ], "args": [] @@ -1865,16 +1849,19 @@ export type MatchingEngine = { ], "accounts": [ { - "name": "executor", + "name": "beneficiary", "docs": [ - "we will always reward the owner of the executor token account with the lamports from the", - "prepared order response and its custody token account when we close these accounts. This", - "means we disregard the `prepared_by` field in the prepared order response." + "finalized VAA." ], "writable": true }, { - "name": "executorToken", + "name": "baseFeeToken", + "docs": [ + "This token account will receive the base fee only if there was a penalty when executing the", + "order. If it does not exist when there is a penalty, this instruction handler will revert.", + "" + ], "writable": true }, { @@ -1884,7 +1871,7 @@ export type MatchingEngine = { "signer and is the one encoded in the Deposit Fill message, he may have the tokens be sent", "to any account he chooses (this one).", "", - "of the tokens to the executor token account." + "of the tokens to the base fee token account." ], "writable": true }, @@ -3065,6 +3052,14 @@ export type MatchingEngine = { "code": 7082, "name": "auctionAlreadySettled" }, + { + "code": 7084, + "name": "invalidBaseFeeToken" + }, + { + "code": 7086, + "name": "baseFeeTokenRequired" + }, { "code": 7280, "name": "cannotCloseAuctionYet" @@ -3165,6 +3160,13 @@ export type MatchingEngine = { } } }, + { + "name": "preparedBy", + "docs": [ + "The fee payer when placing the initial offer." + ], + "type": "pubkey" + }, { "name": "info", "docs": [ @@ -3500,10 +3502,11 @@ export type MatchingEngine = { } }, { - "name": "executorToken", + "name": "baseFeeToken", "docs": [ "Depending on whether there was an active auction, this field will have the pubkey of the", - "executor token (if there was an auction) or fee recipient token (if there was no auction)." + "base fee token account (if there was an auction) or fee recipient token (if there was no", + "auction)." ], "type": { "option": { @@ -4129,6 +4132,10 @@ export type MatchingEngine = { "name": "preparedBy", "type": "pubkey" }, + { + "name": "baseFeeToken", + "type": "pubkey" + }, { "name": "fastVaaTimestamp", "type": "u32" diff --git a/solana/ts/src/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index dd25dd7a..5b51455f 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -147,7 +147,7 @@ export type SettledTokenAccountInfo = { export type AuctionSettled = { auction: PublicKey; bestOfferToken: SettledTokenAccountInfo | null; - executorToken: SettledTokenAccountInfo | null; + baseFeeToken: SettledTokenAccountInfo | null; withExecute: MessageProtocol | null; }; @@ -1472,11 +1472,15 @@ export class MatchingEngineProgram { payer: PublicKey; fastVaa: PublicKey; finalizedVaa: PublicKey; + baseFeeToken?: PublicKey; }, args: CctpMessageArgs, ): Promise { const { payer, fastVaa, finalizedVaa } = accounts; + let { baseFeeToken } = accounts; + baseFeeToken ??= await splToken.getAssociatedTokenAddress(this.mint, payer); + const fastVaaAcct = await VaaAccount.fetch(this.program.provider.connection, fastVaa); const fromEndpoint = this.routerEndpointAddress(fastVaaAcct.emitterInfo().chain); @@ -1516,6 +1520,7 @@ export class MatchingEngineProgram { finalizedVaa: this.liquidityLayerVaaComposite(finalizedVaa), preparedOrderResponse, preparedCustodyToken: this.preparedCustodyTokenAddress(preparedOrderResponse), + baseFeeToken, usdc: this.usdcComposite(), cctp: { mintRecipient: this.cctpMintRecipientComposite(), @@ -1555,7 +1560,7 @@ export class MatchingEngineProgram { let { executor, fastVaa, finalizedVaa, auction, bestOfferToken } = accounts; const prepareOrderResponseIx = await this.prepareOrderResponseCctpIx( - { payer: executor, fastVaa, finalizedVaa }, + { payer: executor, fastVaa, finalizedVaa, baseFeeToken: bestOfferToken }, args, ); const fastVaaAccount = await VaaAccount.fetch(this.program.provider.connection, fastVaa); @@ -1574,10 +1579,11 @@ export class MatchingEngineProgram { } const settleAuctionCompletedIx = await this.settleAuctionCompleteIx({ - executor, + beneficiary: executor, auction, preparedOrderResponse, bestOfferToken, + baseFeeToken: bestOfferToken, }); const preparedTx: PreparedTransaction = { @@ -1595,23 +1601,24 @@ export class MatchingEngineProgram { } async settleAuctionCompleteIx(accounts: { - executor: PublicKey; preparedOrderResponse: PublicKey; auction?: PublicKey; + beneficiary?: PublicKey; + baseFeeToken?: PublicKey; bestOfferToken?: PublicKey; - executorToken?: PublicKey; }) { - const { executor, preparedOrderResponse } = accounts; + const { preparedOrderResponse } = accounts; - let { auction, bestOfferToken, executorToken } = accounts; - executorToken ??= splToken.getAssociatedTokenAddressSync(this.mint, executor); + let { auction, beneficiary, baseFeeToken, bestOfferToken } = accounts; - if (auction === undefined) { - const { seeds } = await this.fetchPreparedOrderResponse({ + if (auction === undefined || beneficiary === undefined || baseFeeToken === undefined) { + const { seeds, info } = await this.fetchPreparedOrderResponse({ address: preparedOrderResponse, }); - auction = this.auctionAddress(seeds.fastVaaHash); + auction ??= this.auctionAddress(seeds.fastVaaHash); + beneficiary ??= info.preparedBy; + baseFeeToken ??= info.baseFeeToken; } if (bestOfferToken === undefined) { @@ -1626,8 +1633,8 @@ export class MatchingEngineProgram { return this.program.methods .settleAuctionComplete() .accounts({ - executor, - executorToken, + beneficiary, + baseFeeToken, preparedOrderResponse, preparedCustodyToken: this.preparedCustodyTokenAddress(preparedOrderResponse), auction, @@ -1650,8 +1657,10 @@ export class MatchingEngineProgram { confirmOptions?: ConfirmOptions, ): Promise { const { executor, fastVaa, finalizedVaa, auction } = accounts; + + const { feeRecipientToken } = await this.fetchCustodian(); const prepareOrderResponseIx = await this.prepareOrderResponseCctpIx( - { payer: executor, fastVaa, finalizedVaa }, + { payer: executor, fastVaa, finalizedVaa, baseFeeToken: feeRecipientToken }, args, ); const fastVaaAccount = await VaaAccount.fetch(this.program.provider.connection, fastVaa); @@ -1979,18 +1988,14 @@ export class MatchingEngineProgram { } let auctionInfo: AuctionInfo | undefined; - if (initialOfferToken === undefined) { - const { info } = await this.fetchAuction({ address: auction }); + if (initialOfferToken === undefined || initialParticipant === undefined) { + const { preparedBy, info } = await this.fetchAuction({ address: auction }); if (info === null) { throw new Error("no auction info found"); } auctionInfo = info; - initialOfferToken = info.initialOfferToken; - } - - if (initialParticipant === undefined) { - const token = await splToken.getAccount(connection, initialOfferToken); - initialParticipant = token.owner; + initialOfferToken ??= info.initialOfferToken; + initialParticipant ??= preparedBy; } const { @@ -2143,8 +2148,6 @@ export class MatchingEngineProgram { reservedSequence?: PublicKey; auction?: PublicKey; auctionConfig?: PublicKey; - bestOfferToken?: PublicKey; - executor?: PublicKey; }, opts: ReserveFastFillSequenceCompositeOpts = {}, ): Promise { @@ -2170,25 +2173,14 @@ export class MatchingEngineProgram { opts, ); - let { auctionConfig, bestOfferToken, executor } = accounts; + let { auctionConfig } = accounts; - if (bestOfferToken === undefined || auctionConfig === undefined) { + if (auctionConfig === undefined) { const { info } = await this.fetchAuction({ address: reserveSequence.auction }); if (info === null) { throw new Error("no auction info found"); } - auctionConfig ??= this.auctionConfigAddress(info.configId); - bestOfferToken ??= info.bestOfferToken; - } - - if (executor === undefined) { - const token = await splToken - .getAccount(this.program.provider.connection, bestOfferToken) - .catch((_) => null); - if (token === null) { - throw new Error("Executor must be provided because best offer token is not found"); - } - executor = token.owner; + auctionConfig = this.auctionConfigAddress(info.configId); } return this.program.methods @@ -2196,8 +2188,6 @@ export class MatchingEngineProgram { .accounts({ reserveSequence, auctionConfig, - bestOfferToken, - executor, }) .instruction(); } @@ -2305,18 +2295,14 @@ export class MatchingEngineProgram { } let auctionInfo: AuctionInfo | undefined; - if (initialOfferToken === undefined) { - const { info } = await this.fetchAuction({ address: auction }); + if (initialOfferToken === undefined || initialParticipant === undefined) { + const { preparedBy, info } = await this.fetchAuction({ address: auction }); if (info === null) { throw new Error("no auction info found"); } auctionInfo = info; initialOfferToken ??= auctionInfo.initialOfferToken; - } - - if (initialParticipant === undefined) { - const token = await splToken.getAccount(connection, initialOfferToken); - initialParticipant = token.owner; + initialParticipant ??= preparedBy; } const activeAuction = await this.activeAuctionComposite( diff --git a/solana/ts/src/matchingEngine/state/Auction.ts b/solana/ts/src/matchingEngine/state/Auction.ts index ff69a4ec..0cb2eb38 100644 --- a/solana/ts/src/matchingEngine/state/Auction.ts +++ b/solana/ts/src/matchingEngine/state/Auction.ts @@ -38,6 +38,7 @@ export class Auction { vaaTimestamp: number; targetProtocol: MessageProtocol; status: AuctionStatus; + preparedBy: PublicKey; info: AuctionInfo | null; constructor( @@ -46,6 +47,7 @@ export class Auction { vaaTimestamp: number, targetProtocol: MessageProtocol, status: AuctionStatus, + preparedBy: PublicKey, info: AuctionInfo | null, ) { this.bump = bump; @@ -53,6 +55,7 @@ export class Auction { this.vaaTimestamp = vaaTimestamp; this.targetProtocol = targetProtocol; this.status = status; + this.preparedBy = preparedBy; this.info = info; } diff --git a/solana/ts/src/matchingEngine/state/PreparedOrderResponse.ts b/solana/ts/src/matchingEngine/state/PreparedOrderResponse.ts index 91749a47..40d20f45 100644 --- a/solana/ts/src/matchingEngine/state/PreparedOrderResponse.ts +++ b/solana/ts/src/matchingEngine/state/PreparedOrderResponse.ts @@ -10,6 +10,7 @@ export type PreparedOrderResponseSeeds = { export type PreparedOrderResponseInfo = { preparedBy: PublicKey; + baseFeeToken: PublicKey; fastVaaTimestamp: number; sourceChain: number; baseFee: BN; diff --git a/solana/ts/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 9c98b124..6a2a72cc 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -2051,7 +2051,7 @@ describe("Matching Engine", function () { newOfferAuthority, ); - const { bump, vaaHash, vaaTimestamp, targetProtocol, status, info } = + const { bump, vaaHash, vaaTimestamp, targetProtocol, status, preparedBy, info } = auctionDataBefore; const { configId, @@ -2071,7 +2071,7 @@ describe("Matching Engine", function () { const auctionDataAfter = await engine.fetchAuction({ address: auction }); expect(auctionDataAfter).to.eql( - new Auction(bump, vaaHash, vaaTimestamp, targetProtocol, status, { + new Auction(bump, vaaHash, vaaTimestamp, targetProtocol, status, preparedBy, { configId, custodyTokenBump, vaaSequence, @@ -2543,7 +2543,7 @@ describe("Matching Engine", function () { const ix = await engine.executeFastOrderCctpIx({ payer: liquidator.publicKey, fastVaa, - initialParticipant: payer.publicKey, + initialParticipant: auctionDataBefore.preparedBy, }); const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ @@ -3177,7 +3177,7 @@ describe("Matching Engine", function () { } = balancesBefore; let { custodyToken: custodyTokenBalance } = balancesBefore; - const { bump, vaaHash, vaaTimestamp, info } = auctionDataBefore; + const { bump, vaaHash, vaaTimestamp, preparedBy, info } = auctionDataBefore; const auctionDataAfter = await engine.fetchAuction({ address: auction }); @@ -3250,6 +3250,7 @@ describe("Matching Engine", function () { executePenalty: uint64ToBN(penalty), }, }, + preparedBy, info, ), ); @@ -3319,6 +3320,7 @@ describe("Matching Engine", function () { executePenalty: null, }, }, + preparedBy, info, ), ); @@ -3576,7 +3578,7 @@ describe("Matching Engine", function () { await settleAuctionCompleteForTest( { - executor: payer.publicKey, + beneficiary: payer.publicKey, preparedOrderResponse: result!.preparedOrderResponse, auction: engine.auctionAddress(fastVaaAccount.digest()), bestOfferToken: splToken.getAssociatedTokenAddressSync( @@ -3594,7 +3596,7 @@ describe("Matching Engine", function () { it("Cannot Settle Active Auction", async function () { await settleAuctionCompleteForTest( { - executor: payer.publicKey, + beneficiary: payer.publicKey, }, { prepareSigners: [payer], @@ -3604,15 +3606,15 @@ describe("Matching Engine", function () { ); }); - it("Cannot Settle Completed Auction with No Penalty (Executor != Best Offer)", async function () { + it("Cannot Settle Completed (Beneficiary != Prepared By)", async function () { await settleAuctionCompleteForTest( { - executor: payer.publicKey, + beneficiary: playerOne.publicKey, }, { - prepareSigners: [payer], - executeWithinGracePeriod: true, - errorMsg: "Error Code: ExecutorTokenMismatch", + executeWithinGracePeriod: false, + executorIsPreparer: false, + errorMsg: "beneficiary. Error Code: ConstraintAddress", }, ); }); @@ -3620,7 +3622,7 @@ describe("Matching Engine", function () { it("Settle Completed without Penalty", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { prepareSigners: [playerOne], @@ -3632,7 +3634,7 @@ describe("Matching Engine", function () { it("Settle Completed With Order Response Prepared Before Active Auction", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { prepareSigners: [playerOne], @@ -3642,23 +3644,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Settle Completed with Penalty (Executor != Prepared By)", async function () { + it("Settle Completed with Penalty (Base Fee Token == Best Offer Token)", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, - }, - { - executeWithinGracePeriod: false, - executorIsPreparer: false, - errorMsg: "Error Code: ExecutorNotPreparedBy", - }, - ); - }); - - it("Settle Completed with Penalty (Executor == Best Offer)", async function () { - await settleAuctionCompleteForTest( - { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { prepareSigners: [playerOne], @@ -3667,48 +3656,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Settle Completed with Penalty (Executor is not ATA)", async function () { - const executorTokenSigner = Keypair.generate(); - const executorToken = executorTokenSigner.publicKey; - - await expectIxOk( - connection, - [ - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: executorToken, - lamports: await connection.getMinimumBalanceForRentExemption( - splToken.ACCOUNT_SIZE, - ), - space: splToken.ACCOUNT_SIZE, - programId: splToken.TOKEN_PROGRAM_ID, - }), - splToken.createInitializeAccount3Instruction( - executorToken, - engine.mint, - playerTwo.publicKey, - ), - ], - [payer, executorTokenSigner], - ); - - await settleAuctionCompleteForTest( - { - executor: playerTwo.publicKey, - executorToken, - }, - { - prepareSigners: [playerTwo], - executeWithinGracePeriod: false, - errorMsg: "Error Code: AccountNotAssociatedTokenAccount", - }, - ); - }); - - it("Settle Completed with Penalty (Executor != Best Offer)", async function () { + it("Settle Completed with Penalty (Base Fee Token != Best Offer Token)", async function () { await settleAuctionCompleteForTest( { - executor: playerTwo.publicKey, + beneficiary: playerTwo.publicKey, }, { prepareSigners: [playerTwo], @@ -3892,35 +3843,20 @@ describe("Matching Engine", function () { ); }); - it("Cannot Add Entry from Settled Complete Auction with Beneficiary Token != Initial Offer Token", async function () { + it("Cannot Add Entry from Settled Complete Auction with Beneficiary != Auction's Preparer", async function () { await addAuctionHistoryEntryForTest( { payer: payer.publicKey, history: engine.auctionHistoryAddress(0), - beneficiary: payer.publicKey, + beneficiary: Keypair.generate().publicKey, }, { settlementType: "complete", - errorMsg: "beneficiary_token. Error Code: ConstraintAddress", + errorMsg: "beneficiary. Error Code: ConstraintAddress", }, ); }); - it("Cannot Add Entry from Settled Complete Auction with Beneficiary != Initial Offer Token Owner", async function () { - await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - beneficiary: payer.publicKey, - beneficiaryToken: splToken.getAssociatedTokenAddressSync( - USDC_MINT_ADDRESS, - playerOne.publicKey, - ), - }, - { settlementType: "complete", errorMsg: "Error Code: ConstraintTokenOwner" }, - ); - }); - it("Add Entry from Settled Complete Auction After Expiration Time", async function () { await addAuctionHistoryEntryForTest( { @@ -3947,33 +3883,20 @@ describe("Matching Engine", function () { ); }); - it("Cannot Close Auction Account from Settled Auction None with Beneficiary Token != Fee Recipient Token", async function () { + it("Cannot Close Auction Account from Settled Auction None with Beneficiary != Auction's Preparer", async function () { await addAuctionHistoryEntryForTest( { payer: payer.publicKey, history: engine.auctionHistoryAddress(0), - beneficiary: payer.publicKey, + beneficiary: Keypair.generate().publicKey, }, { settlementType: "none", - errorMsg: "beneficiary_token. Error Code: ConstraintAddress", + errorMsg: "beneficiary. Error Code: ConstraintAddress", }, ); }); - it("Cannot Close Auction Account from Settled Auction None with Beneficiary != Fee Recipient", async function () { - const { feeRecipientToken } = await engine.fetchCustodian(); - await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - beneficiary: payer.publicKey, - beneficiaryToken: feeRecipientToken, - }, - { settlementType: "none", errorMsg: "Error Code: ConstraintTokenOwner" }, - ); - }); - it("Close Auction Account from Settled Auction None", async function () { await addAuctionHistoryEntryForTest( { @@ -4172,7 +4095,6 @@ describe("Matching Engine", function () { auction?: PublicKey; history: PublicKey; beneficiary?: PublicKey; - beneficiaryToken?: PublicKey; }, opts: ForTestOpts & ObserveCctpOrderVaasOpts & @@ -4199,14 +4121,20 @@ describe("Matching Engine", function () { const vaaTimestamp = timestamp - 7200 + timeToWait; if (settlementType == "complete") { const result = await settleAuctionCompleteForTest( - { executor: playerOne.publicKey }, + { beneficiary: playerOne.publicKey }, { vaaTimestamp, prepareSigners: [playerOne] }, ); return result!.auction; } else if (settlementType == "none") { const result = await settleAuctionNoneCctpForTest( - { payer: payer.publicKey }, - { vaaTimestamp }, + { + payer: playerOne.publicKey, + baseFeeToken: splToken.getAssociatedTokenAddressSync( + USDC_MINT_ADDRESS, + playerOne.publicKey, + ), + }, + { vaaTimestamp, signers: [playerOne] }, ); return result!.auction; } else { @@ -4220,47 +4148,19 @@ describe("Matching Engine", function () { await waitUntilTimestamp(connection, current + timeToWait); } - const { beneficiary, beneficiaryToken } = await (async () => { - if (accounts.beneficiary !== undefined) { - return { - beneficiary: accounts.beneficiary, - beneficiaryToken: - accounts.beneficiaryToken ?? - splToken.getAssociatedTokenAddressSync( - USDC_MINT_ADDRESS, - accounts.beneficiary, - ), - }; - } else { - const { info } = await engine.fetchAuction({ address: auction }); - const beneficiaryToken = await (async () => { - if (info === null) { - const custodian = await engine.fetchCustodian(); - return custodian.feeRecipientToken; - } else { - return info!.initialOfferToken; - } - })(); - const { owner } = await splToken.getAccount(connection, beneficiaryToken); - return { - beneficiary: owner, - beneficiaryToken: accounts.beneficiaryToken ?? beneficiaryToken, - }; - } - })(); - - const { vaaHash, vaaTimestamp, info } = await engine.fetchAuction({ + const { vaaHash, vaaTimestamp, info, preparedBy } = await engine.fetchAuction({ address: auction, }); expect(info === null).equals(settlementType === "none"); + const beneficiary = accounts.beneficiary ?? preparedBy; + const ix = await engine.program.methods .addAuctionHistoryEntry() .accounts({ ...accounts, auction, beneficiary, - beneficiaryToken, custodian: engine.checkedCustodianComposite(), systemProgram: SystemProgram.programId, }) @@ -4459,6 +4359,7 @@ describe("Matching Engine", function () { fast.vaaAccount.timestamp(), { cctp: { domain: destinationDomain! } }, { active: {} }, + accounts.payer, { configId: auctionConfigId, custodyTokenBump, @@ -4546,6 +4447,7 @@ describe("Matching Engine", function () { payer: PublicKey; fastVaa?: PublicKey; finalizedVaa?: PublicKey; + baseFeeToken?: PublicKey; }, opts: ForTestOpts & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts = {}, ): Promise Token Router", function () { fast.vaaAccount.timestamp(), { local: { programId: tokenRouter.ID } }, { active: {} }, + accounts.payer, { configId: auctionConfigId, custodyTokenBump, @@ -778,6 +779,7 @@ describe("Matching Engine <> Token Router", function () { payer: PublicKey; fastVaa?: PublicKey; finalizedVaa?: PublicKey; + baseFeeToken?: PublicKey; }, opts: ForTestOpts & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts = {}, ): Promise Token Router", function () { toChainId(fastMarketOrder!.targetChain), ); + const baseFeeToken = + accounts.baseFeeToken ?? + splToken.getAssociatedTokenAddressSync(USDC_MINT_ADDRESS, payer.publicKey); + const { baseFee } = deposit!.message.payload! as SlowOrderResponse; expect(preparedOrderResponseData).to.eql( new matchingEngineSdk.PreparedOrderResponse( @@ -980,6 +986,7 @@ describe("Matching Engine <> Token Router", function () { }, { preparedBy: accounts.payer, + baseFeeToken, fastVaaTimestamp: fastVaaAccount.timestamp(), sourceChain: fastVaaAccount.emitterInfo().chain, baseFee: uint64ToBN(baseFee), @@ -1090,7 +1097,7 @@ describe("Matching Engine <> Token Router", function () { const { success, result } = await invokeReserveFastFillSequence( ix, fastVaaAccount, - playerOne.publicKey, + payer.publicKey, testOpts, );