diff --git a/audits/2024-06-07-sec3-composable-intents.pdf b/audits/2024-06-07-sec3-composable-intents.pdf new file mode 100644 index 000000000..690cc4eb5 Binary files /dev/null and b/audits/2024-06-07-sec3-composable-intents.pdf differ diff --git a/audits/2024-06-25-ottersec-composable-intents.pdf b/audits/2024-06-25-ottersec-composable-intents.pdf new file mode 100644 index 000000000..4c37da47d Binary files /dev/null and b/audits/2024-06-25-ottersec-composable-intents.pdf differ diff --git a/audits/2024-09-12-sec3-composable-intents-incremental.pdf b/audits/2024-09-12-sec3-composable-intents-incremental.pdf new file mode 100644 index 000000000..6eb1ee07d Binary files /dev/null and b/audits/2024-09-12-sec3-composable-intents-incremental.pdf differ diff --git a/deployment/config/mainnet/token-router.json b/deployment/config/mainnet/token-router.json index 0ff350ad9..0003b60b6 100644 --- a/deployment/config/mainnet/token-router.json +++ b/deployment/config/mainnet/token-router.json @@ -5,8 +5,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "18446744073709551615" }, @@ -16,8 +16,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, @@ -27,8 +27,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, @@ -38,8 +38,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, @@ -49,8 +49,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, @@ -60,8 +60,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, @@ -71,8 +71,8 @@ "fastTransferParameters": { "enabled": true, "maxAmount": "1000000000", - "baseFee": "1250000", - "initAuctionFee": "950000" + "baseFee": "10000", + "initAuctionFee": "60000" }, "cctpAllowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935" } diff --git a/deployment/helpers/utils.ts b/deployment/helpers/utils.ts index 829616ad2..f83481206 100644 --- a/deployment/helpers/utils.ts +++ b/deployment/helpers/utils.ts @@ -85,4 +85,6 @@ export function getVerifyCommand({ `; return command; -} \ No newline at end of file +} + +export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); diff --git a/deployment/scripts/solana/closeProposal.ts b/deployment/scripts/solana/closeProposal.ts new file mode 100644 index 000000000..56bf1289e --- /dev/null +++ b/deployment/scripts/solana/closeProposal.ts @@ -0,0 +1,45 @@ +import { + ComputeBudgetProgram, + Connection, + PublicKey, +} from "@solana/web3.js"; +import "dotenv/config"; +import { MatchingEngineProgram, ProgramId } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { solana, getLocalDependencyAddress, env } from "../../helpers"; +import { capitalize } from "../../helpers/utils"; +import { circle } from "@wormhole-foundation/sdk-base"; + +solana.runOnSolana("close-proposal", async (chain, signer, log) => { + const matchingEngineId = getLocalDependencyAddress("matchingEngineProxy", chain) as ProgramId; + const canonicalEnv = capitalize(env); + if (canonicalEnv !== "Mainnet" && canonicalEnv !== "Testnet") { + throw new Error(`Unsupported environment: ${env} must be Mainnet or Testnet`); + } + + const usdcMint = new PublicKey(circle.usdcContract(canonicalEnv, "Solana")); + const connection = new Connection(chain.rpc, solana.connectionCommitmentLevel); + const matchingEngine = new MatchingEngineProgram(connection, matchingEngineId, usdcMint); + + log('Matching Engine Program ID:', matchingEngineId.toString()); + + log("Proposal to be closed", await matchingEngine.fetchProposal()); + + if (solana.priorityMicrolamports === undefined || solana.priorityMicrolamports === 0) { + log(`(!) PRIORITY_MICROLAMPORTS is undefined or zero, your transaction may not land during congestion.`) + } + + const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: solana.priorityMicrolamports }); + const ownerOrAssistant = new PublicKey(await signer.getAddress()); + + const closeProposalIx = await matchingEngine.closeProposalIx({ + ownerOrAssistant, + }); + + try { + const closeTxSig = await solana.ledgerSignAndSend(connection, [closeProposalIx, priorityFee], []); + console.log(`Close Proposal Transaction ID: ${closeTxSig}`); + } catch (error) { + console.error('Failed to close proposal:', error); + } + +}); diff --git a/deployment/scripts/solana/fetchProposal.ts b/deployment/scripts/solana/fetchProposal.ts new file mode 100644 index 000000000..bdd3d3d33 --- /dev/null +++ b/deployment/scripts/solana/fetchProposal.ts @@ -0,0 +1,35 @@ +import { + ComputeBudgetProgram, + Connection, + PublicKey, +} from "@solana/web3.js"; +import "dotenv/config"; +import { MatchingEngineProgram } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { solana, getLocalDependencyAddress, env, capitalize } from "../../helpers"; +import { ProgramId } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { circle } from "@wormhole-foundation/sdk-base"; + +solana.runOnSolana("fetch-proposal", async (chain, signer, log) => { + const matchingEngineId = getLocalDependencyAddress("matchingEngineProxy", chain) as ProgramId; + + const canonicalEnv = capitalize(env); + + if (canonicalEnv !== "Mainnet" && canonicalEnv !== "Testnet") { + throw new Error(`Unsupported environment: ${env} must be Mainnet or Testnet.`); + } + + const usdcMint = new PublicKey(circle.usdcContract(canonicalEnv, "Solana")); + const connection = new Connection(chain.rpc, solana.connectionCommitmentLevel); + const matchingEngine = new MatchingEngineProgram(connection, matchingEngineId, usdcMint); + + log('Matching Engine Program ID:', matchingEngineId.toString()); + + const proposal = await matchingEngine.fetchProposal(); + log('Proposal:', proposal); + + if (proposal.slotEnactedAt !== null) { + log(`Proposal has already been enacted at slot ${proposal.slotEnactedAt.toNumber()}`); + } else { + log('Proposal has not been enacted yet. Update must be submitted after slot ' + proposal.slotEnactDelay.toNumber()); + } +}); diff --git a/deployment/scripts/solana/proposeAuctionParameters.ts b/deployment/scripts/solana/proposeAuctionParameters.ts new file mode 100644 index 000000000..7acdac9e7 --- /dev/null +++ b/deployment/scripts/solana/proposeAuctionParameters.ts @@ -0,0 +1,49 @@ +import { + ComputeBudgetProgram, + Connection, + PublicKey, +} from "@solana/web3.js"; +import "dotenv/config"; +import { MatchingEngineProgram, ProgramId } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { capitalize, env, getLocalDependencyAddress, getMatchingEngineAuctionParameters, solana } from "../../helpers"; +import { circle } from "@wormhole-foundation/sdk-base"; + +solana.runOnSolana("propose-auction-parameters", async (chain, signer, log) => { + const matchingEngineId = getLocalDependencyAddress("matchingEngineProxy", chain) as ProgramId; + const canonicalEnv = capitalize(env); + if (canonicalEnv !== "Mainnet" && canonicalEnv !== "Testnet") { + throw new Error(`Unsupported environment: ${env}. Must be Mainnet or Testnet.`); + } + + const usdcMint = new PublicKey(circle.usdcContract(canonicalEnv, "Solana")); + const connection = new Connection(chain.rpc, solana.connectionCommitmentLevel); + const matchingEngine = new MatchingEngineProgram(connection, matchingEngineId, usdcMint); + + log('Matching Engine Program ID:', matchingEngineId.toString()); + log('Current Matching Engine Auction parameters:', await matchingEngine.fetchAuctionParameters()); + log('\nTo-be-proposed Matching Engine Auction parameters:', getMatchingEngineAuctionParameters(chain)); + + if (solana.priorityMicrolamports === undefined || solana.priorityMicrolamports === 0) { + log(`(!) PRIORITY_MICROLAMPORTS is undefined or zero, your transaction may not land during congestion.`); + } + + const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: solana.priorityMicrolamports }); + const ownerOrAssistant = new PublicKey(await signer.getAddress()); + + const proposeIx = await matchingEngine.proposeAuctionParametersIx({ + ownerOrAssistant, + }, getMatchingEngineAuctionParameters(chain)); + + try { + const proposeTxSig = await solana.ledgerSignAndSend(connection, [proposeIx, priorityFee], []); + log(`Propose Transaction ID: ${proposeTxSig}.`) + + const proposal = await matchingEngine.fetchProposal(); + log(`The proposal has been published at slot ${proposal.slotProposedAt.toNumber()}.`); + log(`You must wait up to slot ${proposal.slotEnactDelay.toNumber()} to submit the auction parameters update.`); + } + catch (error) { + console.error('Failed to send transaction:', error); + } + +}); diff --git a/deployment/scripts/solana/updateAuctionParameters.ts b/deployment/scripts/solana/updateAuctionParameters.ts index 79ec4bced..e7cb5edbb 100644 --- a/deployment/scripts/solana/updateAuctionParameters.ts +++ b/deployment/scripts/solana/updateAuctionParameters.ts @@ -1,57 +1,43 @@ import { - ComputeBudgetProgram, - Connection, - PublicKey, + ComputeBudgetProgram, + Connection, + PublicKey, } from "@solana/web3.js"; import "dotenv/config"; -import { MatchingEngineProgram } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; -import { solana, getLocalDependencyAddress, env, getMatchingEngineAuctionParameters } from "../../helpers"; -import { ProgramId } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { MatchingEngineProgram, ProgramId } from "@wormhole-foundation/example-liquidity-layer-solana/matchingEngine"; +import { env, getLocalDependencyAddress, getMatchingEngineAuctionParameters, solana } from "../../helpers"; +import { capitalize } from "../../helpers/utils"; import { circle } from "@wormhole-foundation/sdk-base"; solana.runOnSolana("update-auction-parameters", async (chain, signer, log) => { - const matchingEngineId = getLocalDependencyAddress("matchingEngineProxy", chain) as ProgramId; - - const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); - const canonicalEnv = capitalize(env); - - if (canonicalEnv !== "Mainnet" && canonicalEnv !== "Testnet") { - throw new Error(`Unsupported environment: ${env} must be Mainnet or Testnet`); - } - - const usdcMint = new PublicKey(circle.usdcContract(canonicalEnv, "Solana")); - const connection = new Connection(chain.rpc, solana.connectionCommitmentLevel); - const matchingEngine = new MatchingEngineProgram(connection, matchingEngineId, usdcMint); - - log('Matching Engine Program ID:', matchingEngineId.toString()); - log('Current Matching Engine Auction parameters:', await matchingEngine.fetchAuctionParameters()); - log('\nTo-be-updated Matching Engine Auction parameters:', getMatchingEngineAuctionParameters(chain)); - - log('Proposing new Matching Engine Auction parameters...') - - const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: solana.priorityMicrolamports }); - const ownerOrAssistant = new PublicKey(await signer.getAddress()); - - const proposeInstructions = []; - const proposeIx = await matchingEngine.proposeAuctionParametersIx({ - ownerOrAssistant, - }, getMatchingEngineAuctionParameters(chain)); - - proposeInstructions.push(proposeIx, priorityFee); - const proposeTxSig = await solana.ledgerSignAndSend(connection, proposeInstructions, []); - - console.log(`Propose Transaction ID: ${proposeTxSig}, wait for confirmation...`); - - await connection.confirmTransaction(proposeTxSig, 'confirmed'); - - const updateInstructions = []; - const updateIx = await matchingEngine.updateAuctionParametersIx({ - owner: ownerOrAssistant, - }); - updateInstructions.push(updateIx, priorityFee); - const updateTxSig = await solana.ledgerSignAndSend(connection, updateInstructions, []); - - await connection.confirmTransaction(updateTxSig, 'confirmed'); - - console.log(`Update Transaction ID: ${updateTxSig}, wait for confirmation...`); + const matchingEngineId = getLocalDependencyAddress("matchingEngineProxy", chain) as ProgramId; + const canonicalEnv = capitalize(env); + if (canonicalEnv !== "Mainnet" && canonicalEnv !== "Testnet") { + throw new Error(`Unsupported environment: ${env} must be Mainnet or Testnet`); + } + + const usdcMint = new PublicKey(circle.usdcContract(canonicalEnv, "Solana")); + const connection = new Connection(chain.rpc, solana.connectionCommitmentLevel); + const matchingEngine = new MatchingEngineProgram(connection, matchingEngineId, usdcMint); + + log('Matching Engine Program ID:', matchingEngineId.toString()); + log('Current Matching Engine Auction parameters:', await matchingEngine.fetchAuctionParameters()); + log('\nTo-be-proposed Matching Engine Auction parameters:', getMatchingEngineAuctionParameters(chain)); + + if (solana.priorityMicrolamports === undefined || solana.priorityMicrolamports === 0) { + log(`(!) PRIORITY_MICROLAMPORTS is undefined or zero, your transaction may not land during congestion.`) + } + + const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: solana.priorityMicrolamports }); + + const ownerOrAssistant = new PublicKey(await signer.getAddress()); + const updateIx = await matchingEngine.updateAuctionParametersIx({ + owner: ownerOrAssistant, + }); + try { + const updateTxSig = await solana.ledgerSignAndSend(connection, [updateIx, priorityFee], []); + log(`Update Transaction ID: ${updateTxSig}`); + } catch (error) { + console.error('Failed to send transaction:', error); + } }); diff --git a/solana/programs/matching-engine/src/events/auction_closed.rs b/solana/programs/matching-engine/src/events/auction_closed.rs new file mode 100644 index 000000000..6f4dbd0c8 --- /dev/null +++ b/solana/programs/matching-engine/src/events/auction_closed.rs @@ -0,0 +1,9 @@ +use anchor_lang::prelude::*; + +use crate::state::Auction; + +#[event] +#[derive(Debug)] +pub struct AuctionClosed { + pub auction: Auction, +} diff --git a/solana/programs/matching-engine/src/events/mod.rs b/solana/programs/matching-engine/src/events/mod.rs index fd64b7582..8ec22aebc 100644 --- a/solana/programs/matching-engine/src/events/mod.rs +++ b/solana/programs/matching-engine/src/events/mod.rs @@ -1,3 +1,6 @@ +mod auction_closed; +pub use auction_closed::*; + mod auction_settled; pub use auction_settled::*; diff --git a/solana/programs/matching-engine/src/lib.rs b/solana/programs/matching-engine/src/lib.rs index 26a7263b0..58b353535 100644 --- a/solana/programs/matching-engine/src/lib.rs +++ b/solana/programs/matching-engine/src/lib.rs @@ -354,46 +354,6 @@ pub mod matching_engine { processor::settle_auction_none_local(ctx) } - /// This instruction is used to create the first `AuctionHistory` account, whose PDA is derived - /// using ID == 0. - /// - /// # Arguments - /// - /// * `ctx` - `CreateFirstAuctionHistory` context. - pub fn create_first_auction_history(ctx: Context) -> Result<()> { - processor::create_first_auction_history(ctx) - } - - /// This instruction is used to create a new `AuctionHistory` account. The PDA is derived using - /// its ID. A new history account can be created only when the current one is full (number of - /// entries equals the hard-coded max entries). - /// - /// # Arguments - /// - /// * `ctx` - `CreateNewAuctionHistory` context. - pub fn create_new_auction_history(ctx: Context) -> Result<()> { - processor::create_new_auction_history(ctx) - } - - /// This instruction is used to add a new entry to the `AuctionHistory` account if there is an - /// `Auction` with some info. Regardless of whether there is info in this account, the - /// instruction finishes its operation by closing this auction account. If the history account - /// is full, this instruction will revert and `create_new_auction_history`` will have to be - /// called to initialize another history account. - /// - /// This mechanism is important for auction participants. The initial offer participant will - /// pay lamports to create the `Auction` account. This instruction allows him to reclaim some - /// lamports by closing that account. And the protocol's fee recipient will be able to claim - /// lamports by closing the empty `Auction` account it creates when he calls any of the - /// `settle_auction_none_*` instructions. - /// - /// # Arguments - /// - /// * `ctx` - `AddAuctionHistoryEntry` context. - pub fn add_auction_history_entry(ctx: Context) -> Result<()> { - processor::add_auction_history_entry(ctx) - } - /// This instruction is used to reserve a sequence number for a fast fill. Fast fills are orders /// that have been fulfilled and are destined for Solana and are seeded by source chain, order /// sender and sequence number (similar to how Wormhole VAAs are identified by emitter chain, @@ -415,6 +375,7 @@ pub mod matching_engine { ) -> Result<()> { processor::reserve_fast_fill_sequence_active_auction(ctx) } + /// This instruction is used to reserve a sequence number for a fast fill. Fast fills are orders /// that have been fulfilled and are destined for Solana and are seeded by source chain, order /// sender and sequence number (similar to how Wormhole VAAs are identified by emitter chain, @@ -452,6 +413,72 @@ pub mod matching_engine { pub fn close_redeemed_fast_fill(ctx: Context) -> Result<()> { processor::close_redeemed_fast_fill(ctx) } + + /// This instruction is used to close an auction account after the auction has been settled and + /// the VAA's timestamp indicates the order has expired. This instruction can be called by + /// anyone to return the auction's preparer lamports from the rent required to keep this account + /// alive. The auction data will be serialized as Anchor event CPI instruction data. + /// + /// # Arguments + /// + /// * `ctx` - `CloseAuction` context. + pub fn close_auction(ctx: Context) -> Result<()> { + processor::close_auction(ctx) + } + + // Deprecated instructions. These instructions will revert with `ErrorCode::InstructionMissing`. + + /// DEPRECATED. This instruction does not exist anymore. + /// + /// This instruction is used to create the first `AuctionHistory` account, whose PDA is derived + /// using ID == 0. + /// + /// # Arguments + /// + /// * `ctx` - `CreateFirstAuctionHistory` context. + pub fn create_first_auction_history(_ctx: Context) -> Result<()> { + err!(ErrorCode::Deprecated) + } + + /// DEPRECATED. This instruction does not exist anymore. + /// + /// This instruction is used to create a new `AuctionHistory` account. The PDA is derived using + /// its ID. A new history account can be created only when the current one is full (number of + /// entries equals the hard-coded max entries). + /// + /// # Arguments + /// + /// * `ctx` - `CreateNewAuctionHistory` context. + pub fn create_new_auction_history(_ctx: Context) -> Result<()> { + err!(ErrorCode::Deprecated) + } + + /// DEPRECATED. This instruction does not exist anymore. + /// + /// This instruction is used to add a new entry to the `AuctionHistory` account if there is an + /// `Auction` with some info. Regardless of whether there is info in this account, the + /// instruction finishes its operation by closing this auction account. If the history account + /// is full, this instruction will revert and `create_new_auction_history`` will have to be + /// called to initialize another history account. + /// + /// This mechanism is important for auction participants. The initial offer participant will + /// pay lamports to create the `Auction` account. This instruction allows him to reclaim some + /// lamports by closing that account. And the protocol's fee recipient will be able to claim + /// lamports by closing the empty `Auction` account it creates when he calls any of the + /// `settle_auction_none_*` instructions. + /// + /// # Arguments + /// + /// * `ctx` - `AddAuctionHistoryEntry` context. + pub fn add_auction_history_entry(_ctx: Context) -> Result<()> { + err!(ErrorCode::Deprecated) + } +} + +#[derive(Accounts)] +pub struct DeprecatedInstruction<'info> { + /// CHECK: This account is here to avoid program macro compilation errors. + _dummy: UncheckedAccount<'info>, } #[cfg(test)] diff --git a/solana/programs/matching-engine/src/processor/auction/close.rs b/solana/programs/matching-engine/src/processor/auction/close.rs new file mode 100644 index 000000000..a49e9ef4f --- /dev/null +++ b/solana/programs/matching-engine/src/processor/auction/close.rs @@ -0,0 +1,48 @@ +use std::ops::Deref; + +use crate::{ + error::MatchingEngineError, + state::{Auction, AuctionStatus}, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +#[event_cpi] +pub struct CloseAuction<'info> { + #[account( + mut, + close = beneficiary, + constraint = { + require!( + matches!(auction.status, AuctionStatus::Settled {..}), + MatchingEngineError::AuctionNotSettled, + ); + + let expiration = + i64::from(auction.vaa_timestamp).saturating_add(crate::VAA_AUCTION_EXPIRATION_TIME); + require!( + Clock::get().unwrap().unix_timestamp >= expiration, + MatchingEngineError::CannotCloseAuctionYet, + ); + + true + } + )] + auction: Account<'info, Auction>, + + /// CHECK: This account is whoever originally created the auction account (see + /// [Auction::prepared_by]. + #[account( + mut, + address = auction.prepared_by, + )] + beneficiary: UncheckedAccount<'info>, +} + +pub fn close_auction(ctx: Context) -> Result<()> { + emit_cpi!(crate::events::AuctionClosed { + auction: ctx.accounts.auction.deref().clone(), + }); + + Ok(()) +} 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 deleted file mode 100644 index 502dd0975..000000000 --- a/solana/programs/matching-engine/src/processor/auction/history/add_entry.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::io::Write; - -use crate::{ - composite::*, - error::MatchingEngineError, - state::{ - Auction, AuctionEntry, AuctionHistory, AuctionHistoryInternal, AuctionInfo, AuctionStatus, - }, -}; -use anchor_lang::{prelude::*, system_program}; - -#[derive(Accounts)] -pub struct AddAuctionHistoryEntry<'info> { - #[account(mut)] - payer: Signer<'info>, - - custodian: CheckedCustodian<'info>, - - /// CHECK: Auction history account. This account is not deserialized via Anchor account context - /// because we will be writing to this account without using Anchor's [AccountsExit]. - #[account( - mut, - constraint = { - require_keys_eq!( - *history.owner, - crate::id(), - ErrorCode::ConstraintOwner, - ); - - let mut acc_data: &[_] = &history.try_borrow_data()?; - let history = AuctionHistoryInternal::try_deserialize(&mut acc_data)?; - - require!( - history.num_entries < AuctionHistory::MAX_ENTRIES, - MatchingEngineError::AuctionHistoryFull, - ); - - true - } - )] - history: UncheckedAccount<'info>, - - #[account( - mut, - close = beneficiary, - constraint = { - require!( - matches!(auction.status, AuctionStatus::Settled {..}), - MatchingEngineError::AuctionNotSettled, - ); - - let expiration = - i64::from(auction.vaa_timestamp).saturating_add(crate::VAA_AUCTION_EXPIRATION_TIME); - require!( - Clock::get().unwrap().unix_timestamp >= expiration, - MatchingEngineError::CannotCloseAuctionYet, - ); - - true - } - )] - auction: Account<'info, Auction>, - - /// CHECK: This account is whoever originally created the auction account (see - /// [Auction::prepared_by]. - #[account( - mut, - address = auction.prepared_by, - )] - beneficiary: UncheckedAccount<'info>, - - system_program: Program<'info, system_program::System>, -} - -pub fn add_auction_history_entry(ctx: Context) -> Result<()> { - match ctx.accounts.auction.info { - Some(info) => handle_add_auction_history_entry(ctx, info), - None => Ok(()), - } -} - -fn handle_add_auction_history_entry( - ctx: Context, - info: AuctionInfo, -) -> Result<()> { - let mut history = { - let mut acc_data: &[_] = &ctx.accounts.history.data.borrow(); - AuctionHistoryInternal::try_deserialize_unchecked(&mut acc_data).unwrap() - }; - - // This is safe because we already checked that this is less than MAX_ENTRIES. - history.num_entries = history.num_entries.saturating_add(1); - - // Update the history account with this new entry's vaa timestamp if it is less than the min or - // greater than the max. - let auction = &ctx.accounts.auction; - if auction.vaa_timestamp < history.min_timestamp.unwrap_or_else(|| u32::MAX) { - history.min_timestamp = auction.vaa_timestamp.into(); - } - if auction.vaa_timestamp > history.max_timestamp.unwrap_or_default() { - history.max_timestamp = auction.vaa_timestamp.into(); - } - - let mut encoded_entry = Vec::with_capacity(AuctionEntry::INIT_SPACE); - AuctionEntry { - vaa_hash: auction.vaa_hash, - vaa_timestamp: auction.vaa_timestamp, - info, - } - .serialize(&mut encoded_entry)?; - - // Transfer lamports to history account and realloc. - let write_index = { - let acc_info: &UncheckedAccount = &ctx.accounts.history; - - let index = acc_info.data_len(); - - // This operation should be safe because the size of an account should never be larger than - // u64 (usize in this case). - let new_len = index.saturating_add(encoded_entry.len()); - let lamport_diff = Rent::get() - .unwrap() - .minimum_balance(new_len) - .saturating_sub(acc_info.lamports()); - - // Transfer lamports - system_program::transfer( - CpiContext::new( - ctx.accounts.system_program.to_account_info(), - system_program::Transfer { - from: ctx.accounts.payer.to_account_info(), - to: acc_info.to_account_info(), - }, - ), - lamport_diff, - )?; - - // Realloc. - acc_info.realloc(new_len, false)?; - - index - }; - - // Serialize the header + num entries. This is safe because the underlying data structure for - // auction entries is a Vec, whose length is serialized as u32. - let acc_data: &mut [_] = &mut ctx.accounts.history.try_borrow_mut_data()?; - let mut cursor = std::io::Cursor::new(acc_data); - history.try_serialize(&mut cursor)?; - - // This cast is safe since we know the write index is within u64. - #[allow(clippy::as_conversions)] - cursor.set_position(write_index as u64); - - // Serialize entry data. - cursor.write_all(&encoded_entry).map_err(Into::into) -} diff --git a/solana/programs/matching-engine/src/processor/auction/history/create_first.rs b/solana/programs/matching-engine/src/processor/auction/history/create_first.rs deleted file mode 100644 index cee1e8ce0..000000000 --- a/solana/programs/matching-engine/src/processor/auction/history/create_first.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::state::AuctionHistory; -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct CreateFirstAuctionHistory<'info> { - #[account(mut)] - payer: Signer<'info>, - - #[account( - init, - payer = payer, - space = AuctionHistory::START, - seeds = [ - AuctionHistory::SEED_PREFIX, - &u64::default().to_be_bytes() - ], - bump, - )] - first_history: Account<'info, AuctionHistory>, - - system_program: Program<'info, System>, -} - -pub fn create_first_auction_history(ctx: Context) -> Result<()> { - ctx.accounts.first_history.set_inner(Default::default()); - - // Done. - Ok(()) -} diff --git a/solana/programs/matching-engine/src/processor/auction/history/create_new.rs b/solana/programs/matching-engine/src/processor/auction/history/create_new.rs deleted file mode 100644 index 6fa33a733..000000000 --- a/solana/programs/matching-engine/src/processor/auction/history/create_new.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::{ - error::MatchingEngineError, - state::{AuctionHistory, AuctionHistoryHeader, AuctionHistoryInternal}, -}; -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct CreateNewAuctionHistory<'info> { - #[account(mut)] - payer: Signer<'info>, - - #[account( - seeds = [ - AuctionHistory::SEED_PREFIX, - ¤t_history.id.to_be_bytes() - ], - bump, - constraint = { - require_eq!( - current_history.num_entries, - AuctionHistory::MAX_ENTRIES, - MatchingEngineError::AuctionHistoryNotFull, - ); - - true - } - )] - current_history: Account<'info, AuctionHistoryInternal>, - - #[account( - init, - payer = payer, - space = AuctionHistory::START, - seeds = [ - AuctionHistory::SEED_PREFIX, - ¤t_history - .id - .checked_add(1) - .map(|new_id| new_id.to_be_bytes()) - .ok_or_else(|| MatchingEngineError::U32Overflow)?, - ], - bump, - )] - new_history: Account<'info, AuctionHistory>, - - system_program: Program<'info, System>, -} - -pub fn create_new_auction_history(ctx: Context) -> Result<()> { - // NOTE: ID overflow was checked in the account context. - ctx.accounts.new_history.set_inner(AuctionHistory { - header: AuctionHistoryHeader::new(ctx.accounts.current_history.id.saturating_add(1)), - data: Default::default(), - }); - - // Done. - Ok(()) -} diff --git a/solana/programs/matching-engine/src/processor/auction/history/mod.rs b/solana/programs/matching-engine/src/processor/auction/history/mod.rs deleted file mode 100644 index 50bfb503c..000000000 --- a/solana/programs/matching-engine/src/processor/auction/history/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod add_entry; -pub use add_entry::*; - -mod create_first; -pub use create_first::*; - -mod create_new; -pub use create_new::*; diff --git a/solana/programs/matching-engine/src/processor/auction/mod.rs b/solana/programs/matching-engine/src/processor/auction/mod.rs index 6c9551fdc..bed8bfc7b 100644 --- a/solana/programs/matching-engine/src/processor/auction/mod.rs +++ b/solana/programs/matching-engine/src/processor/auction/mod.rs @@ -1,9 +1,9 @@ +mod close; +pub use close::*; + mod execute_fast_order; pub use execute_fast_order::*; -mod history; -pub(crate) use history::*; - mod offer; pub use offer::*; diff --git a/solana/ts/src/idl/json/matching_engine.json b/solana/ts/src/idl/json/matching_engine.json index 1900ecb71..dd46e1097 100644 --- a/solana/ts/src/idl/json/matching_engine.json +++ b/solana/ts/src/idl/json/matching_engine.json @@ -11,6 +11,8 @@ { "name": "add_auction_history_entry", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to add a new entry to the `AuctionHistory` account if there is an", "`Auction` with some info. Regardless of whether there is info in this account, the", "instruction finishes its operation by closing this auction account. If the history account", @@ -39,38 +41,7 @@ ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "custodian", - "accounts": [ - { - "name": "custodian" - } - ] - }, - { - "name": "history", - "docs": [ - "because we will be writing to this account without using Anchor's [AccountsExit]." - ], - "writable": true - }, - { - "name": "auction", - "writable": true - }, - { - "name": "beneficiary", - "docs": [ - "[Auction::prepared_by]." - ], - "writable": true - }, - { - "name": "system_program" + "name": "_dummy" } ], "args": [] @@ -268,6 +239,49 @@ ], "args": [] }, + { + "name": "close_auction", + "docs": [ + "This instruction is used to close an auction account after the auction has been settled and", + "the VAA's timestamp indicates the order has expired. This instruction can be called by", + "anyone to return the auction's preparer lamports from the rent required to keep this account", + "alive. The auction data will be serialized as Anchor event CPI instruction data.", + "", + "# Arguments", + "", + "* `ctx` - `CloseAuction` context." + ], + "discriminator": [ + 225, + 129, + 91, + 48, + 215, + 73, + 203, + 172 + ], + "accounts": [ + { + "name": "auction", + "writable": true + }, + { + "name": "beneficiary", + "docs": [ + "[Auction::prepared_by]." + ], + "writable": true + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [] + }, { "name": "close_proposal", "docs": [ @@ -487,6 +501,8 @@ { "name": "create_first_auction_history", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to create the first `AuctionHistory` account, whose PDA is derived", "using ID == 0.", "", @@ -506,16 +522,7 @@ ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "first_history", - "writable": true - }, - { - "name": "system_program" + "name": "_dummy" } ], "args": [] @@ -523,6 +530,8 @@ { "name": "create_new_auction_history", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to create a new `AuctionHistory` account. The PDA is derived using", "its ID. A new history account can be created only when the current one is full (number of", "entries equals the hard-coded max entries).", @@ -543,19 +552,7 @@ ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "current_history" - }, - { - "name": "new_history", - "writable": true - }, - { - "name": "system_program" + "name": "_dummy" } ], "args": [] @@ -2604,32 +2601,6 @@ 142 ] }, - { - "name": "AuctionHistory", - "discriminator": [ - 149, - 208, - 45, - 154, - 47, - 248, - 102, - 245 - ] - }, - { - "name": "AuctionHistoryInternal", - "discriminator": [ - 149, - 208, - 45, - 154, - 47, - 248, - 102, - 245 - ] - }, { "name": "Custodian", "discriminator": [ @@ -2736,6 +2707,19 @@ } ], "events": [ + { + "name": "AuctionClosed", + "discriminator": [ + 104, + 72, + 168, + 177, + 241, + 79, + 231, + 167 + ] + }, { "name": "AuctionSettled", "discriminator": [ @@ -3237,6 +3221,22 @@ ] } }, + { + "name": "AuctionClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "auction", + "type": { + "defined": { + "name": "Auction" + } + } + } + ] + } + }, { "name": "AuctionConfig", "type": { @@ -3279,105 +3279,6 @@ ] } }, - { - "name": "AuctionEntry", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vaa_hash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "vaa_timestamp", - "type": "u32" - }, - { - "name": "info", - "type": { - "defined": { - "name": "AuctionInfo" - } - } - } - ] - } - }, - { - "name": "AuctionHistory", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": { - "name": "AuctionHistoryHeader" - } - } - }, - { - "name": "data", - "type": { - "vec": { - "defined": { - "name": "AuctionEntry" - } - } - } - } - ] - } - }, - { - "name": "AuctionHistoryHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "id", - "type": "u64" - }, - { - "name": "min_timestamp", - "type": { - "option": "u32" - } - }, - { - "name": "max_timestamp", - "type": { - "option": "u32" - } - } - ] - } - }, - { - "name": "AuctionHistoryInternal", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": { - "name": "AuctionHistoryHeader" - } - } - }, - { - "name": "num_entries", - "type": "u32" - } - ] - } - }, { "name": "AuctionInfo", "type": { diff --git a/solana/ts/src/idl/ts/matching_engine.ts b/solana/ts/src/idl/ts/matching_engine.ts index 7ccf19c65..2d0af2e66 100644 --- a/solana/ts/src/idl/ts/matching_engine.ts +++ b/solana/ts/src/idl/ts/matching_engine.ts @@ -17,6 +17,8 @@ export type MatchingEngine = { { "name": "addAuctionHistoryEntry", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to add a new entry to the `AuctionHistory` account if there is an", "`Auction` with some info. Regardless of whether there is info in this account, the", "instruction finishes its operation by closing this auction account. If the history account", @@ -45,38 +47,7 @@ export type MatchingEngine = { ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "custodian", - "accounts": [ - { - "name": "custodian" - } - ] - }, - { - "name": "history", - "docs": [ - "because we will be writing to this account without using Anchor's [AccountsExit]." - ], - "writable": true - }, - { - "name": "auction", - "writable": true - }, - { - "name": "beneficiary", - "docs": [ - "[Auction::prepared_by]." - ], - "writable": true - }, - { - "name": "systemProgram" + "name": "dummy" } ], "args": [] @@ -274,6 +245,49 @@ export type MatchingEngine = { ], "args": [] }, + { + "name": "closeAuction", + "docs": [ + "This instruction is used to close an auction account after the auction has been settled and", + "the VAA's timestamp indicates the order has expired. This instruction can be called by", + "anyone to return the auction's preparer lamports from the rent required to keep this account", + "alive. The auction data will be serialized as Anchor event CPI instruction data.", + "", + "# Arguments", + "", + "* `ctx` - `CloseAuction` context." + ], + "discriminator": [ + 225, + 129, + 91, + 48, + 215, + 73, + 203, + 172 + ], + "accounts": [ + { + "name": "auction", + "writable": true + }, + { + "name": "beneficiary", + "docs": [ + "[Auction::prepared_by]." + ], + "writable": true + }, + { + "name": "eventAuthority" + }, + { + "name": "program" + } + ], + "args": [] + }, { "name": "closeProposal", "docs": [ @@ -493,6 +507,8 @@ export type MatchingEngine = { { "name": "createFirstAuctionHistory", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to create the first `AuctionHistory` account, whose PDA is derived", "using ID == 0.", "", @@ -512,16 +528,7 @@ export type MatchingEngine = { ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "firstHistory", - "writable": true - }, - { - "name": "systemProgram" + "name": "dummy" } ], "args": [] @@ -529,6 +536,8 @@ export type MatchingEngine = { { "name": "createNewAuctionHistory", "docs": [ + "DEPRECATED. This instruction does not exist anymore.", + "", "This instruction is used to create a new `AuctionHistory` account. The PDA is derived using", "its ID. A new history account can be created only when the current one is full (number of", "entries equals the hard-coded max entries).", @@ -549,19 +558,7 @@ export type MatchingEngine = { ], "accounts": [ { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "currentHistory" - }, - { - "name": "newHistory", - "writable": true - }, - { - "name": "systemProgram" + "name": "dummy" } ], "args": [] @@ -2610,32 +2607,6 @@ export type MatchingEngine = { 142 ] }, - { - "name": "auctionHistory", - "discriminator": [ - 149, - 208, - 45, - 154, - 47, - 248, - 102, - 245 - ] - }, - { - "name": "auctionHistoryInternal", - "discriminator": [ - 149, - 208, - 45, - 154, - 47, - 248, - 102, - 245 - ] - }, { "name": "custodian", "discriminator": [ @@ -2742,6 +2713,19 @@ export type MatchingEngine = { } ], "events": [ + { + "name": "auctionClosed", + "discriminator": [ + 104, + 72, + 168, + 177, + 241, + 79, + 231, + 167 + ] + }, { "name": "auctionSettled", "discriminator": [ @@ -3243,6 +3227,22 @@ export type MatchingEngine = { ] } }, + { + "name": "auctionClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "auction", + "type": { + "defined": { + "name": "auction" + } + } + } + ] + } + }, { "name": "auctionConfig", "type": { @@ -3285,105 +3285,6 @@ export type MatchingEngine = { ] } }, - { - "name": "auctionEntry", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vaaHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "vaaTimestamp", - "type": "u32" - }, - { - "name": "info", - "type": { - "defined": { - "name": "auctionInfo" - } - } - } - ] - } - }, - { - "name": "auctionHistory", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": { - "name": "auctionHistoryHeader" - } - } - }, - { - "name": "data", - "type": { - "vec": { - "defined": { - "name": "auctionEntry" - } - } - } - } - ] - } - }, - { - "name": "auctionHistoryHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "id", - "type": "u64" - }, - { - "name": "minTimestamp", - "type": { - "option": "u32" - } - }, - { - "name": "maxTimestamp", - "type": { - "option": "u32" - } - } - ] - } - }, - { - "name": "auctionHistoryInternal", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": { - "name": "auctionHistoryHeader" - } - } - }, - { - "name": "numEntries", - "type": "u32" - } - ] - } - }, { "name": "auctionInfo", "type": { diff --git a/solana/ts/src/matchingEngine/index.ts b/solana/ts/src/matchingEngine/index.ts index 877706d97..2da4eba3e 100644 --- a/solana/ts/src/matchingEngine/index.ts +++ b/solana/ts/src/matchingEngine/index.ts @@ -16,9 +16,8 @@ import { SystemProgram, TransactionInstruction, } from "@solana/web3.js"; +import { ChainId, isChainId, toChainId } from "@wormhole-foundation/sdk-base"; import { PreparedTransaction, PreparedTransactionOptions } from ".."; -import IDL from "../idl/json/matching_engine.json"; -import { MatchingEngine } from "../idl/ts/matching_engine"; import { MessageTransmitterProgram, TokenMessengerMinterProgram } from "../cctp"; import { LiquidityLayerMessage, @@ -31,6 +30,8 @@ import { uint64ToBigInt, writeUint64BE, } from "../common"; +import IDL from "../idl/json/matching_engine.json"; +import { MatchingEngine } from "../idl/ts/matching_engine"; import { UpgradeManagerProgram } from "../upgradeManager"; import { ArrayQueue, BPF_LOADER_UPGRADEABLE_PROGRAM_ID, programDataAddress } from "../utils"; import { VaaAccount } from "../wormhole"; @@ -55,8 +56,6 @@ import { ReservedFastFillSequence, RouterEndpoint, } from "./state"; -import { ChainId, toChainId, isChainId } from "@wormhole-foundation/sdk-base"; -import { decodeIdlAccount } from "anchor-0.29.0/dist/cjs/idl"; export const PROGRAM_IDS = [ "MatchingEngine11111111111111111111111111111", @@ -200,6 +199,10 @@ export type FastFillRedeemed = { fastFill: FastFillSeeds; }; +export type AuctionClosed = { + auction: Auction; +}; + export type MatchingEngineEvent = { auctionSettled?: AuctionSettled; auctionUpdated?: AuctionUpdated; @@ -209,6 +212,7 @@ export type MatchingEngineEvent = { localFastOrderFilled?: LocalFastOrderFilled; fastFillSequenceReserved?: FastFillSequenceReserved; fastFillRedeemed?: FastFillRedeemed; + auctionClosed?: AuctionClosed; }; export type FastOrderPathComposite = { @@ -2372,6 +2376,49 @@ export class MatchingEngineProgram { .instruction(); } + async closeAuctionIx(accounts: { + auction: PublicKey; + beneficiary?: PublicKey; + }): Promise { + const { auction } = accounts; + let { beneficiary } = accounts; + + if (beneficiary === undefined) { + const { preparedBy } = await this.fetchAuction({ address: auction }); + beneficiary = preparedBy; + } + + return this.program.methods + .closeAuction() + .accounts({ + auction, + beneficiary, + eventAuthority: this.eventAuthorityAddress(), + program: this.ID, + }) + .instruction(); + } + + async closeAuctionTx( + accounts: { auction: PublicKey; beneficiary: PublicKey }, + signers: Signer[], + opts: PreparedTransactionOptions, + confirmOptions?: ConfirmOptions, + ): Promise { + const closeAuctionIx = await this.closeAuctionIx(accounts); + + return { + ixs: [closeAuctionIx], + signers, + computeUnits: opts.computeUnits!, + feeMicroLamports: opts.feeMicroLamports, + nonceAccount: opts.nonceAccount, + addressLookupTableAccounts: opts.addressLookupTableAccounts, + txName: "closeAuction", + confirmOptions, + }; + } + async redeemFastFillAccounts(fastFill: PublicKey): Promise { const { seeds: { sourceChain }, diff --git a/solana/ts/tests/01__matchingEngine.ts b/solana/ts/tests/01__matchingEngine.ts index 6a2a72ccd..3c9775aef 100644 --- a/solana/ts/tests/01__matchingEngine.ts +++ b/solana/ts/tests/01__matchingEngine.ts @@ -1,4 +1,4 @@ -import { BN } from "@coral-xyz/anchor"; +import { BN, utils } from "@coral-xyz/anchor"; import * as splToken from "@solana/spl-token"; import { AddressLookupTableProgram, @@ -29,9 +29,11 @@ import { } from "../src/common"; import { Auction, + AuctionClosed, AuctionConfig, AuctionHistory, AuctionParameters, + CPI_EVENT_IX_SELECTOR, CctpMessageArgs, Custodian, MatchingEngineProgram, @@ -3776,39 +3778,8 @@ describe("Matching Engine", function () { }); }); - describe("Auction History", function () { - it("Cannot Create First Auction History with Incorrect PDA", async function () { - await createFirstAuctionHistoryForTest( - { - payer: payer.publicKey, - firstHistory: Keypair.generate().publicKey, - }, - { - errorMsg: "Error Code: ConstraintSeeds", - }, - ); - }); - - it("Create First Auction History", async function () { - await createFirstAuctionHistoryForTest({ - payer: payer.publicKey, - }); - }); - - it("Cannot Create First Auction History Again", async function () { - const auctionHistory = engine.auctionHistoryAddress(0); - - await createFirstAuctionHistoryForTest( - { - payer: payer.publicKey, - }, - { - errorMsg: `Allocate: account Address { address: ${auctionHistory.toString()}, base: None } already in use`, - }, - ); - }); - - it("Cannot Add Entry from Unsettled Auction", async function () { + describe("Close Auction", function () { + it("Cannot Close Unsettled Auction", async function () { const result = await placeInitialOfferCctpForTest( { payer: playerOne.publicKey, @@ -3816,10 +3787,9 @@ describe("Matching Engine", function () { { signers: [playerOne], finalized: false }, ); - await addAuctionHistoryEntryForTest( + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), auction: result!.auction, beneficiary: playerOne.publicKey, }, @@ -3829,11 +3799,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Add Entry from Settled Complete Auction Before Expiration Time", async function () { - await addAuctionHistoryEntryForTest( + it("Cannot Close Settled Complete Auction Before Expiration Time", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), }, { settlementType: "complete", @@ -3843,11 +3812,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Add Entry from Settled Complete Auction with Beneficiary != Auction's Preparer", async function () { - await addAuctionHistoryEntryForTest( + it("Cannot Close Settled Complete Auction with Beneficiary != Auction's Preparer", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), beneficiary: Keypair.generate().publicKey, }, { @@ -3857,11 +3825,10 @@ describe("Matching Engine", function () { ); }); - it("Add Entry from Settled Complete Auction After Expiration Time", async function () { - await addAuctionHistoryEntryForTest( + it("Close Settled Complete Auction After Expiration Time", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), }, { settlementType: "complete", @@ -3869,11 +3836,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Close Auction Account from Settled Auction None Before Expiration Time", async function () { - await addAuctionHistoryEntryForTest( + it("Cannot Close Settled Auction (info is None) Before Expiration Time", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), }, { settlementType: "none", @@ -3883,11 +3849,10 @@ describe("Matching Engine", function () { ); }); - it("Cannot Close Auction Account from Settled Auction None with Beneficiary != Auction's Preparer", async function () { - await addAuctionHistoryEntryForTest( + it("Cannot Close Settled Auction (info is None) with Beneficiary != Auction's Preparer", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), beneficiary: Keypair.generate().publicKey, }, { @@ -3897,203 +3862,19 @@ describe("Matching Engine", function () { ); }); - it("Close Auction Account from Settled Auction None", async function () { - await addAuctionHistoryEntryForTest( + it("Close Settled Auction (info is None)", async function () { + await closeAuctionForTest( { payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), }, { settlementType: "none" }, ); }); - it("Cannot Create New Auction History with Current History Not Full", async function () { - await createNewAuctionHistoryForTest( - { - payer: payer.publicKey, - currentHistory: engine.auctionHistoryAddress(0), - }, - { errorMsg: "Error Code: AuctionHistoryNotFull" }, - ); - }); - - it("Add Another Entry from Settled Complete Auction", async function () { - await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, - { - settlementType: "complete", - }, - ); - }); - - it("Cannot Add Another Entry from Settled Complete Auction To Full History", async function () { - await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(0), - }, - { - settlementType: "complete", - errorMsg: "Error Code: AuctionHistoryFull", - }, - ); - }); - - it("Create New Auction History", async function () { - await createNewAuctionHistoryForTest({ - payer: payer.publicKey, - currentHistory: engine.auctionHistoryAddress(0), - }); - }); - - it("Add Another Entry from Settled Complete Auction To New History", async function () { - await addAuctionHistoryEntryForTest( - { - payer: payer.publicKey, - history: engine.auctionHistoryAddress(1), - }, - { - settlementType: "complete", - }, - ); - }); - - async function createFirstAuctionHistoryForTest( - accounts: { payer: PublicKey; firstHistory?: PublicKey }, - opts: ForTestOpts = {}, - ) { - let [{ signers, errorMsg }] = setDefaultForTestOpts(opts); - - const ix = await engine.program.methods - .createFirstAuctionHistory() - .accounts(createFirstAuctionHistoryAccounts(accounts)) - .instruction(); - - if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); - } - - const auctionHistory = engine.auctionHistoryAddress(0); - { - const accInfo = await connection.getAccountInfo(auctionHistory); - expect(accInfo).is.null; - } - - await expectIxOk(connection, [ix], signers); - - const firstHistoryData = await engine.fetchAuctionHistory({ - address: auctionHistory, - }); - expect(firstHistoryData).to.eql( - new AuctionHistory( - { - id: uint64ToBN(0), - minTimestamp: null, - maxTimestamp: null, - }, - [], - ), - ); - - return { auctionHistory }; - } - - function createFirstAuctionHistoryAccounts(accounts: { - payer: PublicKey; - firstHistory?: PublicKey; - }) { - const { payer } = accounts; - let { firstHistory } = accounts; - firstHistory ??= engine.auctionHistoryAddress(0); - - return { - payer, - firstHistory, - systemProgram: SystemProgram.programId, - }; - } - - async function createNewAuctionHistoryForTest( - accounts: { payer: PublicKey; currentHistory: PublicKey; newHistory?: PublicKey }, - opts: ForTestOpts = {}, - ) { - let [{ signers, errorMsg }] = setDefaultForTestOpts(opts); - - const definedAccounts = await createNewAuctionHistoryAccounts(accounts); - - const ix = await engine.program.methods - .createNewAuctionHistory() - .accounts(definedAccounts) - .instruction(); - - if (errorMsg !== null) { - return expectIxErr(connection, [ix], signers, errorMsg); - } - - const { newHistory } = definedAccounts; - { - const accInfo = await connection.getAccountInfo(newHistory); - expect(accInfo).is.null; - } - - await expectIxOk(connection, [ix], signers); - - const [{ id }, numEntries] = await engine.fetchAuctionHistoryHeader({ - address: definedAccounts.currentHistory, - }); - expect(numEntries).equals(2); - - const newHistoryData = await engine.fetchAuctionHistory({ - address: newHistory, - }); - expect(newHistoryData).to.eql( - new AuctionHistory( - { - id: uint64ToBN(id.addn(1)), - minTimestamp: null, - maxTimestamp: null, - }, - [], - ), - ); - - return { newHistory }; - } - - async function createNewAuctionHistoryAccounts(accounts: { - payer: PublicKey; - currentHistory: PublicKey; - newHistory?: PublicKey; - }) { - const { payer, currentHistory } = accounts; - - const newHistory = await (async () => { - if (accounts.newHistory !== undefined) { - return accounts.newHistory; - } else { - const [header] = await engine.fetchAuctionHistoryHeader({ - address: currentHistory, - }); - return engine.auctionHistoryAddress(header.id.addn(1)); - } - })(); - - return { - payer, - currentHistory, - newHistory, - systemProgram: SystemProgram.programId, - }; - } - - async function addAuctionHistoryEntryForTest( + async function closeAuctionForTest( accounts: { payer: PublicKey; auction?: PublicKey; - history: PublicKey; beneficiary?: PublicKey; }, opts: ForTestOpts & @@ -4148,23 +3929,17 @@ describe("Matching Engine", function () { await waitUntilTimestamp(connection, current + timeToWait); } - const { vaaHash, vaaTimestamp, info, preparedBy } = await engine.fetchAuction({ + const auctionData = await engine.fetchAuction({ address: auction, }); - expect(info === null).equals(settlementType === "none"); + expect(auctionData.info === null).equals(settlementType === "none"); - const beneficiary = accounts.beneficiary ?? preparedBy; + const beneficiary = accounts.beneficiary ?? auctionData.preparedBy; - const ix = await engine.program.methods - .addAuctionHistoryEntry() - .accounts({ - ...accounts, - auction, - beneficiary, - custodian: engine.checkedCustodianComposite(), - systemProgram: SystemProgram.programId, - }) - .instruction(); + const ix = await engine.closeAuctionIx({ + auction, + beneficiary, + }); if (errorMsg !== null) { return expectIxErr(connection, [ix], signers, errorMsg); @@ -4175,53 +3950,45 @@ describe("Matching Engine", function () { const expectedLamports = await connection .getAccountInfo(auction) .then((info) => info!.lamports); - const historyDataBefore = await engine.fetchAuctionHistory({ - address: accounts.history, + + const commitment = "confirmed"; + const txSig = await expectIxOk(connection, [ix], signers, { + confirmOptions: { commitment }, }); - const { header } = historyDataBefore; - const minTimestamp = - header.minTimestamp === null - ? vaaTimestamp - : Math.min(vaaTimestamp, header.minTimestamp); - const maxTimestamp = - header.maxTimestamp === null - ? vaaTimestamp - : Math.max(vaaTimestamp, header.maxTimestamp); + const parsedTx = await connection.getParsedTransaction(txSig, { + commitment, + maxSupportedTransactionVersion: 0, + }); - const prevDataLen = await connection - .getAccountInfo(accounts.history) - .then((info) => info!.data.length); + if (parsedTx === null) { + throw new Error("parsedTx is null"); + } - await expectIxOk(connection, [ix], signers); + let evt: AuctionClosed | null = null; - const historyData = await engine.fetchAuctionHistory({ - address: accounts.history, - }); + for (const innerIx of parsedTx.meta?.innerInstructions!) { + for (const ix of innerIx.instructions) { + if (!ix.programId.equals(engine.ID) || !("data" in ix)) { + continue; + } - if (settlementType === "none") { - expect(historyData).to.eql(historyDataBefore); - } else { - const data = Array.from(historyDataBefore.data); - data.push({ - vaaHash, - vaaTimestamp, - info: info!, - }); - expect(historyData).to.eql( - new AuctionHistory({ id: header.id, minTimestamp, maxTimestamp }, data), - ); + const data = utils.bytes.bs58.decode(ix.data); + if (!data.subarray(0, 8).equals(CPI_EVENT_IX_SELECTOR)) { + continue; + } - { - const accInfo = await connection.getAccountInfo(accounts.history); + const decoded = engine.program.coder.events.decode( + utils.bytes.base64.encode(data.subarray(8)), + ); - let entrySize = 159; - if (info!.destinationAssetInfo === null) { - entrySize -= 9; + if (decoded !== null) { + evt = decoded.data; } - expect(accInfo!.data).has.length(prevDataLen + entrySize); } } + expect(evt).is.not.null; + expect(evt!.auction).to.eql(auctionData); { const accInfo = await connection.getAccountInfo(auction); @@ -4232,6 +3999,56 @@ describe("Matching Engine", function () { expect(beneficiaryBalanceAfter - beneficiaryBalanceBefore).equals(expectedLamports); } }); + + describe("DEPRECATED -- Auction History", function () { + it("Cannot Invoke `create_first_auction_history`", async function () { + await expectIxErr( + connection, + [ + await engine.program.methods + .createFirstAuctionHistory() + .accounts({ + dummy: Keypair.generate().publicKey, + }) + .instruction(), + ], + [payer], + "Error Code: Deprecated", + ); + }); + + it("Cannot Invoke `create_new_auction_history`", async function () { + await expectIxErr( + connection, + [ + await engine.program.methods + .createNewAuctionHistory() + .accounts({ + dummy: Keypair.generate().publicKey, + }) + .instruction(), + ], + [payer], + "Error Code: Deprecated", + ); + }); + + it("Cannot Invoke `add_auction_history_entry`", async function () { + await expectIxErr( + connection, + [ + await engine.program.methods + .addAuctionHistoryEntry() + .accounts({ + dummy: Keypair.generate().publicKey, + }) + .instruction(), + ], + [payer], + "Error Code: Deprecated", + ); + }); + }); }); async function placeInitialOfferCctpForTest(