From 432e97b8b563814df99672e8d61d8c42b52fc5d2 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 9 Aug 2024 16:22:01 -0500 Subject: [PATCH 01/10] solana: add `Auction::prepared_by` --- solana/programs/matching-engine/src/composite/mod.rs | 2 +- .../processor/auction/offer/place_initial/cctp.rs | 1 + .../src/processor/fast_fill/reserve_sequence/mod.rs | 6 +++++- solana/programs/matching-engine/src/state/auction.rs | 3 +++ .../src/state/prepared_order_response.rs | 1 + solana/ts/src/idl/json/matching_engine.json | 7 +++++++ solana/ts/src/idl/ts/matching_engine.ts | 7 +++++++ solana/ts/src/matchingEngine/state/Auction.ts | 3 +++ solana/ts/tests/01__matchingEngine.ts | 12 ++++++++---- solana/ts/tests/04__interaction.ts | 1 + 10 files changed, 37 insertions(+), 6 deletions(-) diff --git a/solana/programs/matching-engine/src/composite/mod.rs b/solana/programs/matching-engine/src/composite/mod.rs index 7d847490..fc214727 100644 --- a/solana/programs/matching-engine/src/composite/mod.rs +++ b/solana/programs/matching-engine/src/composite/mod.rs @@ -650,7 +650,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/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/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..d0cad2d9 100644 --- a/solana/programs/matching-engine/src/state/prepared_order_response.rs +++ b/solana/programs/matching-engine/src/state/prepared_order_response.rs @@ -52,6 +52,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/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index 4c3ec0b1..63aacaf7 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -3159,6 +3159,13 @@ } } }, + { + "name": "prepared_by", + "docs": [ + "The fee payer when placing the initial offer." + ], + "type": "pubkey" + }, { "name": "info", "docs": [ diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index ebf59b2b..d996ed11 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -3165,6 +3165,13 @@ export type MatchingEngine = { } } }, + { + "name": "preparedBy", + "docs": [ + "The fee payer when placing the initial offer." + ], + "type": "pubkey" + }, { "name": "info", "docs": [ 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/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 9c98b124..4fad8514 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, @@ -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, ), ); @@ -4459,6 +4461,7 @@ describe("Matching Engine", function () { fast.vaaAccount.timestamp(), { cctp: { domain: destinationDomain! } }, { active: {} }, + accounts.payer, { configId: auctionConfigId, custodyTokenBump, @@ -5051,7 +5054,7 @@ describe("Matching Engine", function () { const fastVaaHash = fastVaaAccount.digest(); const auction = engine.auctionAddress(fastVaaHash); const auctionData = await engine.fetchAuction({ address: auction }); - const { bump, info } = auctionData; + const { bump, preparedBy, info } = auctionData; expect(info).is.null; expect(auctionData).to.eql( @@ -5066,6 +5069,7 @@ describe("Matching Engine", function () { totalPenalty: null, }, }, + preparedBy, null, ), ); diff --git a/solana/ts/tests/04__interaction.ts b/solana/ts/tests/04__interaction.ts index 6c235f45..6397920d 100644 --- a/solana/ts/tests/04__interaction.ts +++ b/solana/ts/tests/04__interaction.ts @@ -736,6 +736,7 @@ describe("Matching Engine <> Token Router", function () { fast.vaaAccount.timestamp(), { local: { programId: tokenRouter.ID } }, { active: {} }, + accounts.payer, { configId: auctionConfigId, custodyTokenBump, From 40b51702aa361dc3da6d168c19468a0b209d1091 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 9 Aug 2024 16:45:07 -0500 Subject: [PATCH 02/10] solana: replace beneficiary logic to use prepared_by --- .../matching-engine/src/composite/mod.rs | 9 +- .../auction/execute_fast_order/cctp.rs | 17 ++-- .../auction/execute_fast_order/local.rs | 17 ++-- .../auction/execute_fast_order/mod.rs | 98 +++++++------------ solana/ts/src/matchingEngine/index.ts | 22 ++--- solana/ts/tests/01__matchingEngine.ts | 2 +- 6 files changed, 69 insertions(+), 96 deletions(-) diff --git a/solana/programs/matching-engine/src/composite/mod.rs b/solana/programs/matching-engine/src/composite/mod.rs index fc214727..26f9c288 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( @@ -400,7 +400,10 @@ pub struct ExecuteOrder<'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)] + #[account( + mut, + address = active_auction.prepared_by, + )] pub initial_participant: UncheckedAccount<'info>, } 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..bc957b82 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,50 +95,36 @@ 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, &custody_token.mint) + .is_some() + && best_offer_token.key() != initial_offer_token.key() { - // 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 { - from: custody_token.to_account_info(), - to: initial_offer_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), - init_auction_fee, - )?; + // 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], + ), + init_auction_fee, + )?; - // 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)?; - } + // 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. @@ -165,7 +139,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(), @@ -184,7 +158,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(), @@ -203,7 +177,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 +220,6 @@ fn prepare_order_execution<'info>( slot: current_slot, execute_penalty: if penalized { penalty.into() } else { None }, }, - beneficiary, ) }; @@ -264,6 +237,5 @@ fn prepare_order_execution<'info>( .try_into() .map_err(|_| MatchingEngineError::RedeemerMessageTooLarge)?, }, - beneficiary, }) } diff --git a/solana/ts/src/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index dd25dd7a..daf4f761 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -1979,18 +1979,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 { @@ -2305,18 +2301,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/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 4fad8514..960230b6 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -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({ From d445c139899f8af6fb7b7837d0d11c581af3949e Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 9 Aug 2024 17:08:01 -0500 Subject: [PATCH 03/10] solana: settle complete executor must always be prepared_by --- .../src/processor/auction/settle/complete.rs | 51 +++++++------------ solana/ts/tests/01__matchingEngine.ts | 15 +----- 2 files changed, 18 insertions(+), 48 deletions(-) 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..5c0b1d70 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs @@ -11,19 +11,19 @@ use anchor_spl::{ #[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)] + /// 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, + )] executor: UncheckedAccount<'info>, #[account( mut, token::mint = common::USDC_MINT, - token::authority = executor, )] - executor_token: Account<'info, TokenAccount>, + executor_token: Box>, /// 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 @@ -46,7 +46,7 @@ pub struct SettleAuctionComplete<'info> { ], 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 +57,7 @@ pub struct SettleAuctionComplete<'info> { ], bump, )] - prepared_custody_token: Account<'info, TokenAccount>, + prepared_custody_token: Box>, #[account( mut, @@ -67,7 +67,7 @@ pub struct SettleAuctionComplete<'info> { ], bump = auction.bump, )] - auction: Account<'info, Auction>, + auction: Box>, token_program: Program<'info, token::Token>, } @@ -115,19 +115,6 @@ fn handle_settle_auction_complete( let (executor_result, best_offer_result) = match execute_penalty { 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) => ( @@ -142,17 +129,6 @@ fn handle_settle_auction_complete( } } _ => { - // 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()[..]) { @@ -182,6 +158,13 @@ fn handle_settle_auction_complete( ErrorCode::AccountNotAssociatedTokenAccount ); + // And enforce that the owner of this ATA is the executor. + require_keys_eq!( + executor.key(), + executor_token.owner, + ErrorCode::ConstraintTokenOwner, + ); + ( TokenAccountResult { balance_before: executor_token.amount, diff --git a/solana/ts/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 960230b6..cf5fb49f 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -3606,19 +3606,6 @@ describe("Matching Engine", function () { ); }); - it("Cannot Settle Completed Auction with No Penalty (Executor != Best Offer)", async function () { - await settleAuctionCompleteForTest( - { - executor: payer.publicKey, - }, - { - prepareSigners: [payer], - executeWithinGracePeriod: true, - errorMsg: "Error Code: ExecutorTokenMismatch", - }, - ); - }); - it("Settle Completed without Penalty", async function () { await settleAuctionCompleteForTest( { @@ -3652,7 +3639,7 @@ describe("Matching Engine", function () { { executeWithinGracePeriod: false, executorIsPreparer: false, - errorMsg: "Error Code: ExecutorNotPreparedBy", + errorMsg: "executor. Error Code: ConstraintAddress", }, ); }); From 2746d8078beb0d3f381edd0de8392ad275ce70ff Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 12 Aug 2024 08:54:18 -0500 Subject: [PATCH 04/10] solana: fix idl --- solana/ts/src/idl/json/matching_engine.json | 4 +--- solana/ts/src/idl/ts/matching_engine.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/solana/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index 63aacaf7..740c9760 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -1861,9 +1861,7 @@ { "name": "executor", "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 }, diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index d996ed11..45baf7ba 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -1867,9 +1867,7 @@ export type MatchingEngine = { { "name": "executor", "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 }, From 8e451aa9861d8a2d05abb44209e900992738f192 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 12 Aug 2024 16:38:18 -0500 Subject: [PATCH 05/10] solana: fix beneficiary for reserved sequence --- .../matching-engine/src/composite/mod.rs | 2 +- .../auction/execute_fast_order/mod.rs | 49 ++++++++++--------- .../src/processor/auction/offer/improve.rs | 2 +- .../src/processor/auction/settle/complete.rs | 30 ++++++------ .../reserve_sequence/active_auction.rs | 47 +----------------- solana/ts/tests/04__interaction.ts | 2 +- 6 files changed, 45 insertions(+), 87 deletions(-) diff --git a/solana/programs/matching-engine/src/composite/mod.rs b/solana/programs/matching-engine/src/composite/mod.rs index 26f9c288..b6e926ca 100644 --- a/solana/programs/matching-engine/src/composite/mod.rs +++ b/solana/programs/matching-engine/src/composite/mod.rs @@ -566,7 +566,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>, 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 bc957b82..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 @@ -99,32 +99,33 @@ fn handle_execute_fast_order<'info>( // 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, &custody_token.mint) + if utils::checked_deserialize_token_account(initial_offer_token, &common::USDC_MINT) .is_some() - && 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(), - token::Transfer { - from: custody_token.to_account_info(), - to: initial_offer_token.to_account_info(), - authority: auction.to_account_info(), - }, - &[auction_signer_seeds], - ), - init_auction_fee, - )?; + 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(), + token::Transfer { + from: custody_token.to_account_info(), + to: initial_offer_token.to_account_info(), + authority: auction.to_account_info(), + }, + &[auction_signer_seeds], + ), + init_auction_fee, + )?; - // 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)?; + // 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. @@ -152,7 +153,7 @@ fn handle_execute_fast_order<'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( 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/settle/complete.rs b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs index 5c0b1d70..b0276bef 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs @@ -2,6 +2,7 @@ use crate::{ error::MatchingEngineError, events::SettledTokenAccountInfo, state::{Auction, AuctionStatus, PreparedOrderResponse}, + utils, }; use anchor_lang::prelude::*; use anchor_spl::{ @@ -116,23 +117,24 @@ fn handle_settle_auction_complete( let (executor_result, best_offer_result) = match execute_penalty { None => { // 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 = + utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) + .ok_or_else(|| MatchingEngineError::BestOfferTokenRequired)?; + + ( + None, // executor_result + TokenAccountResult { + balance_before: best_offer.amount, + amount: repayment, + } + .into(), + ) } _ => { // 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) => { + match utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) { + Some(best_offer) => { if executor_token.key() == best_offer_token.key() { ( None, // executor_result @@ -179,7 +181,7 @@ fn handle_settle_auction_complete( ) } } - Err(_) => ( + None => ( TokenAccountResult { balance_before: executor_token.amount, amount: repayment, 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/ts/tests/04__interaction.ts b/solana/ts/tests/04__interaction.ts index 6397920d..e1ec2753 100644 --- a/solana/ts/tests/04__interaction.ts +++ b/solana/ts/tests/04__interaction.ts @@ -1091,7 +1091,7 @@ describe("Matching Engine <> Token Router", function () { const { success, result } = await invokeReserveFastFillSequence( ix, fastVaaAccount, - playerOne.publicKey, + payer.publicKey, testOpts, ); From 2f075125c21bb7c7a918340fa36cddbac5df904c Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 12 Aug 2024 16:51:06 -0500 Subject: [PATCH 06/10] solana: fix idl and sdk --- solana/ts/src/idl/json/matching_engine.json | 16 ---------------- solana/ts/src/idl/ts/matching_engine.ts | 16 ---------------- solana/ts/src/matchingEngine/index.ts | 21 +++------------------ 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/solana/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index 740c9760..d6159ab1 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -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": [] diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index 45baf7ba..e603e072 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -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": [] diff --git a/solana/ts/src/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index daf4f761..5764ddcc 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -2139,8 +2139,6 @@ export class MatchingEngineProgram { reservedSequence?: PublicKey; auction?: PublicKey; auctionConfig?: PublicKey; - bestOfferToken?: PublicKey; - executor?: PublicKey; }, opts: ReserveFastFillSequenceCompositeOpts = {}, ): Promise { @@ -2166,25 +2164,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 @@ -2192,8 +2179,6 @@ export class MatchingEngineProgram { .accounts({ reserveSequence, auctionConfig, - bestOfferToken, - executor, }) .instruction(); } From c0d0308ddf3e32ad1e3912c52922dd74fd05d2ad Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 14 Aug 2024 09:21:26 -0500 Subject: [PATCH 07/10] solana: fix comment --- solana/programs/matching-engine/src/composite/mod.rs | 3 +-- solana/ts/src/idl/json/matching_engine.json | 6 ------ solana/ts/src/idl/ts/matching_engine.ts | 6 ------ 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/solana/programs/matching-engine/src/composite/mod.rs b/solana/programs/matching-engine/src/composite/mod.rs index b6e926ca..9a62b0f0 100644 --- a/solana/programs/matching-engine/src/composite/mod.rs +++ b/solana/programs/matching-engine/src/composite/mod.rs @@ -398,8 +398,7 @@ 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. + /// CHECK: Must be the payer of the initial auction (see [Auction::prepared_by]). #[account( mut, address = active_auction.prepared_by, diff --git a/solana/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index d6159ab1..b913f336 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -696,9 +696,6 @@ }, { "name": "initial_participant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -891,9 +888,6 @@ }, { "name": "initial_participant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index e603e072..abf9df3a 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -702,9 +702,6 @@ export type MatchingEngine = { }, { "name": "initialParticipant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] @@ -897,9 +894,6 @@ export type MatchingEngine = { }, { "name": "initialParticipant", - "docs": [ - "does not exist anymore, we will attempt to perform this check." - ], "writable": true } ] From c6bd633745634f4be688aacf67c307c6a027b8c4 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 14 Aug 2024 09:56:14 -0500 Subject: [PATCH 08/10] solana: fix beneficiary in add history entry --- .../processor/auction/history/add_entry.rs | 19 ++--- solana/ts/src/idl/json/matching_engine.json | 5 +- solana/ts/src/idl/ts/matching_engine.ts | 5 +- solana/ts/tests/01__matchingEngine.ts | 82 +++---------------- 4 files changed, 20 insertions(+), 91 deletions(-) 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/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index b913f336..a94b37b8 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" } diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index abf9df3a..75ce8915 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" } diff --git a/solana/ts/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index cf5fb49f..57461d0f 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -3881,35 +3881,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( { @@ -3936,30 +3921,17 @@ 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", - }, - ); - }); - - 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, + errorMsg: "beneficiary. Error Code: ConstraintAddress", }, - { settlementType: "none", errorMsg: "Error Code: ConstraintTokenOwner" }, ); }); @@ -4161,7 +4133,6 @@ describe("Matching Engine", function () { auction?: PublicKey; history: PublicKey; beneficiary?: PublicKey; - beneficiaryToken?: PublicKey; }, opts: ForTestOpts & ObserveCctpOrderVaasOpts & @@ -4194,8 +4165,8 @@ describe("Matching Engine", function () { return result!.auction; } else if (settlementType == "none") { const result = await settleAuctionNoneCctpForTest( - { payer: payer.publicKey }, - { vaaTimestamp }, + { payer: playerOne.publicKey }, + { vaaTimestamp, signers: [playerOne] }, ); return result!.auction; } else { @@ -4209,47 +4180,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, }) @@ -4964,11 +4907,12 @@ describe("Matching Engine", function () { } else { const result = await prepareOrderResponseCctpForTest( { - payer: payer.publicKey, + payer: accounts.payer, }, { ...excludedForTestOpts, placeInitialOffer: false, + signers, }, ); expect(typeof result == "object" && "preparedOrderResponse" in result).is.true; From c9ac52c7d458292c9f4fd67a96e668a5b7b7ffee Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 16 Aug 2024 09:59:54 -0500 Subject: [PATCH 09/10] solana: add base fee token --- solana/programs/matching-engine/src/error.rs | 2 + .../src/events/auction_settled.rs | 5 +- .../auction/prepare_order_response/cctp.rs | 18 ++ .../src/processor/auction/settle/complete.rs | 67 +++----- .../src/processor/auction/settle/none/mod.rs | 2 +- .../src/state/prepared_order_response.rs | 1 + .../programs/matching-engine/src/utils/mod.rs | 3 +- solana/ts/src/idl/json/matching_engine.json | 37 ++++- solana/ts/src/idl/ts/matching_engine.ts | 37 ++++- solana/ts/src/matchingEngine/index.ts | 37 +++-- .../state/PreparedOrderResponse.ts | 1 + solana/ts/tests/01__matchingEngine.ts | 157 ++++++++---------- solana/ts/tests/04__interaction.ts | 6 + 13 files changed, 215 insertions(+), 158 deletions(-) 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/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 b0276bef..883e805d 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs @@ -5,10 +5,7 @@ use crate::{ 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> { @@ -18,20 +15,24 @@ pub struct SettleAuctionComplete<'info> { mut, address = prepared_order_response.prepared_by, )] - executor: UncheckedAccount<'info>, + 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, + address = prepared_order_response.base_fee_token, )] - executor_token: Box>, + 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, @@ -40,7 +41,7 @@ pub struct SettleAuctionComplete<'info> { #[account( mut, - close = executor, + close = beneficiary, seeds = [ PreparedOrderResponse::SEED_PREFIX, prepared_order_response.seeds.fast_vaa_hash.as_ref() @@ -101,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; @@ -131,11 +132,15 @@ fn handle_settle_auction_complete( ) } _ => { + let base_fee_token_data = + utils::checked_deserialize_token_account(base_fee_token, &common::USDC_MINT) + .ok_or_else(|| MatchingEngineError::BaseFeeTokenRequired)?; + // If the token account happens to not exist anymore, we will give everything to the - // executor. + // base fee token account. match utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) { Some(best_offer) => { - if executor_token.key() == best_offer_token.key() { + if base_fee_token.key() == best_offer_token.key() { ( None, // executor_result TokenAccountResult { @@ -145,31 +150,9 @@ fn handle_settle_auction_complete( .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 - ); - - // And enforce that the owner of this ATA is the executor. - require_keys_eq!( - executor.key(), - executor_token.owner, - ErrorCode::ConstraintTokenOwner, - ); - ( TokenAccountResult { - balance_before: executor_token.amount, + balance_before: base_fee_token_data.amount, amount: base_fee, } .into(), @@ -183,7 +166,7 @@ fn handle_settle_auction_complete( } None => ( TokenAccountResult { - balance_before: executor_token.amount, + balance_before: base_fee_token_data.amount, amount: repayment, } .into(), @@ -193,7 +176,7 @@ fn handle_settle_auction_complete( } }; - // Transfer executor his bounty if there are any. + // Transfer base fee token his bounty if there are any. let settled_executor_result = match executor_result { Some(TokenAccountResult { balance_before, @@ -204,7 +187,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], @@ -213,7 +196,7 @@ fn handle_settle_auction_complete( )?; SettledTokenAccountInfo { - key: executor_token.key(), + key: base_fee_token.key(), balance_after: balance_before.saturating_add(amount), } .into() @@ -252,7 +235,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_executor_result, with_execute: Default::default(), }); @@ -261,7 +244,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/state/prepared_order_response.rs b/solana/programs/matching-engine/src/state/prepared_order_response.rs index d0cad2d9..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, 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 a94b37b8..b2ed77ac 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -1369,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": [ @@ -1834,14 +1843,19 @@ ], "accounts": [ { - "name": "executor", + "name": "beneficiary", "docs": [ "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 }, { @@ -1851,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 }, @@ -3032,6 +3046,14 @@ "code": 7082, "name": "AuctionAlreadySettled" }, + { + "code": 7084, + "name": "InvalidBaseFeeToken" + }, + { + "code": 7086, + "name": "BaseFeeTokenRequired" + }, { "code": 7280, "name": "CannotCloseAuctionYet" @@ -3474,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": { @@ -4103,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 75ce8915..055f7e14 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -1375,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": [ @@ -1840,14 +1849,19 @@ export type MatchingEngine = { ], "accounts": [ { - "name": "executor", + "name": "beneficiary", "docs": [ "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 }, { @@ -1857,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 }, @@ -3038,6 +3052,14 @@ export type MatchingEngine = { "code": 7082, "name": "auctionAlreadySettled" }, + { + "code": 7084, + "name": "invalidBaseFeeToken" + }, + { + "code": 7086, + "name": "baseFeeTokenRequired" + }, { "code": 7280, "name": "cannotCloseAuctionYet" @@ -3480,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": { @@ -4109,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 5764ddcc..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); 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 57461d0f..6a2a72cc 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -3578,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( @@ -3596,7 +3596,7 @@ describe("Matching Engine", function () { it("Cannot Settle Active Auction", async function () { await settleAuctionCompleteForTest( { - executor: payer.publicKey, + beneficiary: payer.publicKey, }, { prepareSigners: [payer], @@ -3606,98 +3606,60 @@ describe("Matching Engine", function () { ); }); - it("Settle Completed without Penalty", async function () { + it("Cannot Settle Completed (Beneficiary != Prepared By)", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { - prepareSigners: [playerOne], - executeWithinGracePeriod: true, + executeWithinGracePeriod: false, + executorIsPreparer: false, + errorMsg: "beneficiary. Error Code: ConstraintAddress", }, ); }); - it("Settle Completed With Order Response Prepared Before Active Auction", async function () { + it("Settle Completed without Penalty", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { prepareSigners: [playerOne], executeWithinGracePeriod: true, - prepareAfterExecuteOrder: false, - }, - ); - }); - - it("Cannot Settle Completed with Penalty (Executor != Prepared By)", async function () { - await settleAuctionCompleteForTest( - { - executor: playerOne.publicKey, - }, - { - executeWithinGracePeriod: false, - executorIsPreparer: false, - errorMsg: "executor. Error Code: ConstraintAddress", }, ); }); - it("Settle Completed with Penalty (Executor == Best Offer)", async function () { + it("Settle Completed With Order Response Prepared Before Active Auction", async function () { await settleAuctionCompleteForTest( { - executor: playerOne.publicKey, + beneficiary: playerOne.publicKey, }, { prepareSigners: [playerOne], - executeWithinGracePeriod: false, + executeWithinGracePeriod: true, + prepareAfterExecuteOrder: false, }, ); }); - 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], - ); - + it("Settle Completed with Penalty (Base Fee Token == Best Offer Token)", async function () { await settleAuctionCompleteForTest( { - executor: playerTwo.publicKey, - executorToken, + beneficiary: playerOne.publicKey, }, { - prepareSigners: [playerTwo], + prepareSigners: [playerOne], 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], @@ -4159,13 +4121,19 @@ 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: playerOne.publicKey }, + { + payer: playerOne.publicKey, + baseFeeToken: splToken.getAssociatedTokenAddressSync( + USDC_MINT_ADDRESS, + playerOne.publicKey, + ), + }, { vaaTimestamp, signers: [playerOne] }, ); return result!.auction; @@ -4479,6 +4447,7 @@ describe("Matching Engine", function () { payer: PublicKey; fastVaa?: PublicKey; finalizedVaa?: PublicKey; + baseFeeToken?: PublicKey; }, opts: ForTestOpts & ObserveCctpOrderVaasOpts & PrepareOrderResponseForTestOptionalOpts = {}, ): Promise 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( @@ -981,6 +986,7 @@ describe("Matching Engine <> Token Router", function () { }, { preparedBy: accounts.payer, + baseFeeToken, fastVaaTimestamp: fastVaaAccount.timestamp(), sourceChain: fastVaaAccount.emitterInfo().chain, baseFee: uint64ToBN(baseFee), From 544b9306f92a5987c87be61b3487ffe1ef05c99e Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Tue, 20 Aug 2024 15:10:52 -0600 Subject: [PATCH 10/10] solana: fix base fee token handling --- .../src/processor/auction/settle/complete.rs | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) 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 883e805d..00803bc0 100644 --- a/solana/programs/matching-engine/src/processor/auction/settle/complete.rs +++ b/solana/programs/matching-engine/src/processor/auction/settle/complete.rs @@ -115,36 +115,35 @@ 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 the token account happens to not exist anymore, we will revert. - let best_offer = + let best_offer_token_data = utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) .ok_or_else(|| MatchingEngineError::BestOfferTokenRequired)?; ( - None, // executor_result + None, // base_fee_result TokenAccountResult { - balance_before: best_offer.amount, + balance_before: best_offer_token_data.amount, amount: repayment, } .into(), ) } + // Otherwise, determine how the repayment should be divvied up. _ => { - let base_fee_token_data = - utils::checked_deserialize_token_account(base_fee_token, &common::USDC_MINT) - .ok_or_else(|| MatchingEngineError::BaseFeeTokenRequired)?; - - // If the token account happens to not exist anymore, we will give everything to the - // base fee token account. - match utils::checked_deserialize_token_account(best_offer_token, &common::USDC_MINT) { - Some(best_offer) => { + 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(), @@ -157,14 +156,16 @@ fn handle_settle_auction_complete( } .into(), TokenAccountResult { - balance_before: best_offer.amount, + balance_before: best_offer_token_data.amount, amount: repayment.saturating_sub(base_fee), } .into(), ) } } - None => ( + // 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: base_fee_token_data.amount, amount: repayment, @@ -172,12 +173,26 @@ fn handle_settle_auction_complete( .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 base fee token his bounty if there are any. - let settled_executor_result = match executor_result { + let settled_base_fee_result = match base_fee_result { Some(TokenAccountResult { balance_before, amount, @@ -235,7 +250,7 @@ fn handle_settle_auction_complete( emit!(crate::events::AuctionSettled { auction: ctx.accounts.auction.key(), best_offer_token: settled_best_offer_result, - base_fee_token: settled_executor_result, + base_fee_token: settled_base_fee_result, with_execute: Default::default(), });