Skip to content
This repository was archived by the owner on Jun 16, 2025. It is now read-only.

Commit d5f4a1d

Browse files
authored
solana: init auction in reserve fast fill seq (#174)
Co-authored-by: A5 Pickle <[email protected]>
1 parent b43bf25 commit d5f4a1d

File tree

21 files changed

+261
-191
lines changed

21 files changed

+261
-191
lines changed

solana/programs/matching-engine/src/composite/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,53 @@ pub struct ReserveFastFillSequence<'info> {
602602
)]
603603
pub reserved: Box<Account<'info, ReservedFastFillSequence>>,
604604

605+
/// CHECK: This auction account may not exist. If it does not exist, the prepared order response
606+
/// must have been created by this point. Otherwise the auction account must reflect a completed
607+
/// auction.
608+
#[account(
609+
init_if_needed,
610+
payer = payer,
611+
space = if auction.data_is_empty() {
612+
8 + Auction::INIT_SPACE_NO_AUCTION
613+
} else {
614+
auction.data_len()
615+
},
616+
seeds = [
617+
Auction::SEED_PREFIX,
618+
fast_order_path.fast_vaa.load_unchecked().digest().as_ref(),
619+
],
620+
bump,
621+
constraint = match &auction.info {
622+
Some(info) => {
623+
// Verify that the auction is active.
624+
require_eq!(
625+
&auction.status,
626+
&AuctionStatus::Active,
627+
MatchingEngineError::AuctionNotActive
628+
);
629+
630+
// Out of paranoia, check that the auction is for a local fill.
631+
require!(
632+
matches!(auction.target_protocol, MessageProtocol::Local { .. }),
633+
MatchingEngineError::InvalidTargetRouter
634+
);
635+
636+
true
637+
},
638+
None => {
639+
// This check makes sure that the auction account did not exist before this
640+
// instruction was called.
641+
require!(
642+
auction.vaa_hash == [0; 32],
643+
MatchingEngineError::AuctionExists,
644+
);
645+
646+
true
647+
}
648+
},
649+
)]
650+
pub auction: Account<'info, Auction>,
651+
605652
system_program: Program<'info, System>,
606653
}
607654

solana/programs/matching-engine/src/error.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub enum MatchingEngineError {
7070
InvalidOfferToken = 0x424,
7171
FastFillTooLarge = 0x426,
7272
AuctionExists = 0x428,
73-
AccountNotAuction = 0x429,
73+
NoAuction = 0x429,
7474
BestOfferTokenMismatch = 0x42a,
7575
BestOfferTokenRequired = 0x42c,
7676
PreparedByMismatch = 0x42e,
@@ -80,6 +80,7 @@ pub enum MatchingEngineError {
8080
FastFillAlreadyRedeemed = 0x434,
8181
FastFillNotRedeemed = 0x435,
8282
ReservedSequenceMismatch = 0x438,
83+
AuctionAlreadySettled = 0x43a,
8384

8485
CannotCloseAuctionYet = 0x500,
8586
AuctionHistoryNotFull = 0x502,

solana/programs/matching-engine/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ pub mod matching_engine {
420420
/// This sequence number is warehoused in the `ReservedFastFillSequence` account and will be
421421
/// closed when the funds are finally settled.
422422
///
423+
/// NOTE: This instruction is expected to be in the same transaction as the one that executes
424+
/// the prepare order response instruction. If it is not, there is a risk that after preparing
425+
/// the order response that someone starts an auction. This scenario risks the preparer's rent
426+
/// that he paid because the winning auction participant can take his lamports when he calls
427+
/// settle auction complete. Although this is an unlikely scenario, it is possible if there is
428+
/// no deadline specified to start the auction and no participants use the fast VAA to start an
429+
/// auction until the finalized VAA exists (which guarantees that the funds have finalized on
430+
/// the source network).
431+
///
423432
/// # Arguments
424433
///
425434
/// * `ctx` - `ReserveFastFillSequenceNoAuction` context.

solana/programs/matching-engine/src/processor/auction/execute_fast_order/local.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct ExecuteFastOrderLocal<'info> {
5454
#[account(
5555
init,
5656
payer = payer,
57-
space = FastFill::checked_compute_size({
57+
space = FastFill::compute_size({
5858
let vaa = execute_order.fast_vaa.load_unchecked();
5959
6060
// We can unwrap and convert to FastMarketOrder unchecked because we validate the VAA
@@ -64,8 +64,7 @@ pub struct ExecuteFastOrderLocal<'info> {
6464
.to_fast_market_order_unchecked();
6565
6666
order.redeemer_message_len().into()
67-
})
68-
.ok_or(MatchingEngineError::FastFillTooLarge)?,
67+
}),
6968
seeds = [
7069
FastFill::SEED_PREFIX,
7170
&reserved_sequence.fast_fill_seeds.source_chain.to_be_bytes(),

solana/programs/matching-engine/src/processor/auction/settle/none/cctp.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ fn handle_settle_auction_none_cctp(
8484
ctx: Context<SettleAuctionNoneCctp>,
8585
destination_cctp_domain: u32,
8686
) -> Result<()> {
87+
let auction = &mut ctx.accounts.auction;
88+
89+
// First set data in the auction account.
90+
auction.set_inner(
91+
ctx.accounts
92+
.prepared
93+
.order_response
94+
.new_auction_placeholder(ctx.bumps.auction),
95+
);
96+
8797
let prepared_by = &ctx.accounts.prepared.by;
8898
let prepared_custody_token = &ctx.accounts.prepared.custody_token;
8999
let custodian = &ctx.accounts.custodian;
@@ -92,17 +102,14 @@ fn handle_settle_auction_none_cctp(
92102
let super::SettledNone {
93103
user_amount: amount,
94104
fill,
95-
} = super::settle_none_and_prepare_fill(
96-
super::SettleNoneAndPrepareFill {
97-
prepared_order_response: &mut ctx.accounts.prepared.order_response,
98-
prepared_custody_token,
99-
auction: &mut ctx.accounts.auction,
100-
fee_recipient_token: &ctx.accounts.fee_recipient_token,
101-
custodian,
102-
token_program,
103-
},
104-
ctx.bumps.auction,
105-
)?;
105+
} = super::settle_none_and_prepare_fill(super::SettleNoneAndPrepareFill {
106+
prepared_order_response: &mut ctx.accounts.prepared.order_response,
107+
prepared_custody_token,
108+
auction: &mut ctx.accounts.auction,
109+
fee_recipient_token: &ctx.accounts.fee_recipient_token,
110+
custodian,
111+
token_program,
112+
})?;
106113

107114
let EndpointInfo {
108115
chain: _,

solana/programs/matching-engine/src/processor/auction/settle/none/local.rs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
composite::*,
33
error::MatchingEngineError,
4-
state::{Auction, Custodian, FastFill, ReservedFastFillSequence},
4+
state::{Auction, AuctionStatus, Custodian, FastFill, ReservedFastFillSequence},
55
};
66
use anchor_lang::prelude::*;
77
use anchor_spl::token;
@@ -39,16 +39,25 @@ pub struct SettleAuctionNoneLocal<'info> {
3939
)]
4040
prepared: ClosePreparedOrderResponse<'info>,
4141

42-
/// There should be no account data here because an auction was never created.
42+
/// This account will have been created using the reserve fast fill sequence (no auction)
43+
/// instruction. We need to make sure that this account has not been used in an auction.
4344
#[account(
44-
init,
45-
payer = payer,
46-
space = 8 + Auction::INIT_SPACE_NO_AUCTION,
45+
mut,
4746
seeds = [
4847
Auction::SEED_PREFIX,
4948
prepared.order_response.seeds.fast_vaa_hash.as_ref(),
5049
],
5150
bump,
51+
constraint = {
52+
// Block this instruction if the auction status already has a meaningful value.
53+
require_eq!(
54+
&auction.status,
55+
&AuctionStatus::NotStarted,
56+
MatchingEngineError::AuctionExists
57+
);
58+
59+
true
60+
}
5261
)]
5362
auction: Box<Account<'info, Auction>>,
5463

@@ -81,7 +90,7 @@ pub struct SettleAuctionNoneLocal<'info> {
8190
#[account(
8291
init,
8392
payer = payer,
84-
space = FastFill::checked_compute_size(prepared.order_response.redeemer_message.len()).unwrap(),
93+
space = FastFill::compute_size(prepared.order_response.redeemer_message.len()),
8594
seeds = [
8695
FastFill::SEED_PREFIX,
8796
&reserved_sequence.fast_fill_seeds.source_chain.to_be_bytes(),
@@ -117,17 +126,14 @@ pub fn settle_auction_none_local(ctx: Context<SettleAuctionNoneLocal>) -> Result
117126
let super::SettledNone {
118127
user_amount: amount,
119128
fill,
120-
} = super::settle_none_and_prepare_fill(
121-
super::SettleNoneAndPrepareFill {
122-
prepared_order_response: &mut ctx.accounts.prepared.order_response,
123-
prepared_custody_token,
124-
auction: &mut ctx.accounts.auction,
125-
fee_recipient_token: &ctx.accounts.fee_recipient_token,
126-
custodian,
127-
token_program,
128-
},
129-
ctx.bumps.auction,
130-
)?;
129+
} = super::settle_none_and_prepare_fill(super::SettleNoneAndPrepareFill {
130+
prepared_order_response: &mut ctx.accounts.prepared.order_response,
131+
prepared_custody_token,
132+
auction: &mut ctx.accounts.auction,
133+
fee_recipient_token: &ctx.accounts.fee_recipient_token,
134+
custodian,
135+
token_program,
136+
})?;
131137

132138
let fast_fill = FastFill::new(
133139
fill,

solana/programs/matching-engine/src/processor/auction/settle/none/mod.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ struct SettledNone {
2626
fill: Fill,
2727
}
2828

29-
fn settle_none_and_prepare_fill(
30-
accounts: SettleNoneAndPrepareFill<'_, '_>,
31-
auction_bump_seed: u8,
32-
) -> Result<SettledNone> {
29+
fn settle_none_and_prepare_fill(accounts: SettleNoneAndPrepareFill<'_, '_>) -> Result<SettledNone> {
3330
let SettleNoneAndPrepareFill {
3431
prepared_order_response,
3532
prepared_custody_token,
@@ -78,20 +75,11 @@ fn settle_none_and_prepare_fill(
7875
custodian.key().into(),
7976
)?;
8077

81-
// This is a necessary security check. This will prevent a relayer from starting an auction with
82-
// the fast transfer VAA, even though the slow relayer already delivered the slow VAA. Not
83-
// setting this could lead to trapped funds (which would require an upgrade to fix).
84-
auction.set_inner(Auction {
85-
bump: auction_bump_seed,
86-
vaa_hash: prepared_order_response.seeds.fast_vaa_hash,
87-
vaa_timestamp: prepared_order_response.fast_vaa_timestamp,
88-
target_protocol: prepared_order_response.to_endpoint.protocol,
89-
status: AuctionStatus::Settled {
90-
fee,
91-
total_penalty: None,
92-
},
93-
info: None,
94-
});
78+
// Indicate that the auction has been settled.
79+
auction.status = AuctionStatus::Settled {
80+
fee,
81+
total_penalty: None,
82+
};
9583

9684
emit!(crate::events::AuctionSettled {
9785
auction: auction.key(),
Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,29 @@
1-
use crate::{
2-
composite::*,
3-
error::MatchingEngineError,
4-
state::{Auction, AuctionConfig, AuctionStatus, MessageProtocol},
5-
};
1+
use crate::{composite::*, error::MatchingEngineError, state::AuctionConfig};
62
use anchor_lang::prelude::*;
73
use anchor_spl::token;
84

95
#[derive(Accounts)]
106
pub struct ReserveFastFillSequenceActiveAuction<'info> {
117
reserve_sequence: ReserveFastFillSequence<'info>,
128

13-
/// CHECK: This auction account may not exist. If it does not exist, the prepared order response
14-
/// must have been created by this point. Otherwise the auction account must reflect a completed
15-
/// auction.
169
#[account(
17-
seeds = [
18-
Auction::SEED_PREFIX,
19-
reserve_sequence.fast_order_path.fast_vaa.load_unchecked().digest().as_ref(),
20-
],
21-
bump = auction.bump,
22-
constraint = {
23-
// Verify that the auction is active.
24-
require_eq!(
25-
&auction.status,
26-
&AuctionStatus::Active,
27-
MatchingEngineError::AuctionNotActive
28-
);
29-
30-
// Out of paranoia, check that the auction is for a local fill.
31-
require!(
32-
matches!(auction.target_protocol, MessageProtocol::Local { .. }),
33-
MatchingEngineError::InvalidTargetRouter
34-
);
35-
36-
true
37-
}
38-
)]
39-
auction: Account<'info, Auction>,
40-
41-
#[account(
42-
constraint = {
43-
// We know from the auction constraint that the auction is active, so the auction info
44-
// is safe to unwrap.
45-
let info = auction.info.as_ref().unwrap();
10+
constraint = match &reserve_sequence.auction.info {
11+
Some(info) => {
12+
// Verify that the auction period has expired.
13+
require_eq!(
14+
info.config_id,
15+
auction_config.id,
16+
MatchingEngineError::AuctionConfigMismatch
17+
);
18+
require!(
19+
!info.within_auction_duration(&auction_config),
20+
MatchingEngineError::AuctionPeriodNotExpired
21+
);
4622
47-
// Verify that the auction period has expired.
48-
require_eq!(
49-
info.config_id,
50-
auction_config.id,
51-
MatchingEngineError::AuctionConfigMismatch
52-
);
53-
require!(
54-
!info.within_auction_duration(&auction_config),
55-
MatchingEngineError::AuctionPeriodNotExpired
56-
);
57-
58-
true
23+
true
5924
25+
},
26+
_ => return err!(MatchingEngineError::NoAuction),
6027
}
6128
)]
6229
auction_config: Account<'info, AuctionConfig>,
@@ -70,7 +37,7 @@ pub struct ReserveFastFillSequenceActiveAuction<'info> {
7037
constraint = {
7138
// We know from the auction constraint that the auction is active, so the auction info
7239
// is safe to unwrap.
73-
let info = auction.info.as_ref().unwrap();
40+
let info = reserve_sequence.auction.info.as_ref().unwrap();
7441
7542
// Best offer token must equal the one in the auction account.
7643
//
@@ -110,10 +77,12 @@ pub fn reserve_fast_fill_sequence_active_auction(
11077
require_keys_eq!(token.owner, beneficiary, ErrorCode::ConstraintTokenOwner);
11178
}
11279

80+
let fast_vaa_hash = ctx.accounts.reserve_sequence.auction.vaa_hash;
81+
11382
super::set_reserved_sequence_data(
11483
&mut ctx.accounts.reserve_sequence,
11584
&ctx.bumps.reserve_sequence,
116-
ctx.accounts.auction.vaa_hash,
85+
fast_vaa_hash,
11786
beneficiary,
11887
)
11988
}

0 commit comments

Comments
 (0)