Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,8 @@ Differences with Ethereum:
A settlement transaction is split into multiple instructions. All settlement operations occur between a `BeginSettle` and a `FinalizeSettle` instruction with the exception of arbitrary interactions, which can take place at any point of a transaction. Except for that, the order of instructions in the transaction is arbitrary.

- `BeginSettle`: Snapshots each order's receiver token account, spender token account, and withdrawal balances. Pulls funds from each order’s sell token account to the solver-specified destination accounts, using the settlement state PDA’s token delegation. Carries an explicit `finalize_ix_index` pointing to its paired `FinalizeSettle`.
- `Push`: It references a unique SPL transfer token instruction between `BeginSettle` and `FinalizeSettle` that sends the proceeds of an order to its buy token account.
- (arbitrary interactions): Any instruction from the solver. This could be a token transfer, an AMM swap, or anything else.
- `FinalizeSettle`: Reads balances again, computes deltas against the snapshots, validates clearing/limit prices, updates `amount_received` and order status, revokes solver approvals. Carries an explicit `begin_ix_index` pointing to its paired `BeginSettle`.
- `FinalizeSettle`: Pushes the proceeds of each order from the settlement’s buffer accounts to the order’s buy token account, using the settlement state PDA’s authority over the buffers. Reads balances again, computes deltas against the snapshots, validates clearing/limit prices, updates `amount_received` and order status, revokes solver approvals. Carries an explicit `begin_ix_index` pointing to its paired `BeginSettle`.

Additionally, a settlement transaction will include the batch number as part of the instruction bytes of `BeginSettle`.

Expand Down
151 changes: 148 additions & 3 deletions client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use settlement_interface::{

// Reexport the instruction builders that don't change from the interface.
// We want the client to provide all instruction builders.
pub use settlement_interface::instruction::settle::{FinalizeSettle, Pull};
pub use settlement_interface::instruction::settle::Pull;

/// An order ready to be settled, together with the funds to pull from it:
/// `intent` identifies the order and `pulls` lists the [`Pull`]s to make from
Expand Down Expand Up @@ -57,6 +57,66 @@ impl From<BeginSettle<'_>> for Instruction {
}
}

/// A settled order whose proceeds are pushed to it: `intent` identifies the
/// order (its `buy_token_account` is the push destination), `mint` selects the
/// canonical source buffer, and `amount` is the quantity to push.
pub struct FinalizedIntent<'a> {
pub intent: &'a OrderIntent,
pub mint: Pubkey,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was surprised that this argument is needed, since the OrderIntent should contain buy_token_account which itself contains the data needed to access the mint. I'm guessing the reason we can't/don't want to exclude the mint from here is because pulling the would require an async operation to pull the buy_token_account data?

pub amount: u64,
}

/// Builder for a `FinalizeSettle` instruction pushing each order's proceeds to
/// its buy token account.
///
/// The destination is the order intent's `buy_token_account` and the source is
/// the canonical buffer PDA for `mint` (see [`find_buffer_pda`]). The orders are
/// sorted by their canonical order PDA (the same key [`BeginSettle`] orders its
/// settled-order list by) so the two instructions present the orders in the
/// same order and their lists line up.
pub struct FinalizeSettle<'a> {
pub program_id: Pubkey,
pub begin_ix_index: u16,
pub orders: &'a [FinalizedIntent<'a>],
}

impl From<FinalizeSettle<'_>> for Instruction {
fn from(builder: FinalizeSettle<'_>) -> Self {
// Sort the orders by their canonical order PDA, the key `BeginSettle`
// lays its settled orders out by, so the two instruction lists align.
// For BeginSettle, sorting can take place in the interface. But the
// order PDAs don't appear in the actual FinalizeSettle instruction, so
// the sorting can only happen here.
let mut order: Vec<usize> = (0..builder.orders.len()).collect();
order.sort_by_key(|&i| {
find_order_pda(&builder.program_id, &builder.orders[i].intent.uid()).0
});

let mut source_buffers: Vec<Pubkey> = Vec::with_capacity(builder.orders.len());
let mut destinations = Vec::with_capacity(builder.orders.len());
let mut bumps = Vec::with_capacity(builder.orders.len());
let mut amounts = Vec::with_capacity(builder.orders.len());
for &i in &order {
let (buffer_pda, bump) = find_buffer_pda(&builder.program_id, &builder.orders[i].mint);
source_buffers.push(buffer_pda);
destinations.push(builder.orders[i].intent.buy_token_account);
bumps.push(bump);
amounts.push(builder.orders[i].amount);
}
let (state_pda, _bump) = find_state_pda(&builder.program_id);
settlement_interface::instruction::settle::FinalizeSettle {
program_id: builder.program_id,
state_pda,
begin_ix_index: builder.begin_ix_index,
source_buffers: &source_buffers,
destinations: &destinations,
bumps: &bumps,
amounts: &amounts,
}
.into()
}
}

pub struct CreateOrder<'a> {
pub program_id: Pubkey,
pub owner: Pubkey,
Expand Down Expand Up @@ -127,14 +187,16 @@ mod tests {
data::intent::fixtures::arb_order_intent,
instruction::{
fixtures::fake_account_from_array,
settle::{BeginSettleInput, INSTRUCTIONS_SYSVAR_ID},
settle::{
BeginSettleInput, FinalizeSettleInput, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID,
},
InstructionInputParsing,
},
pda::order::find_order_pda,
};

proptest! {
// `begin_settle` derives each order's PDA from its intent and forwards to
// `BeginSettle` derives each order's PDA from its intent and forwards to
// the interface builder so that the on-chain parser recovers exactly
// those orders.
#[test]
Expand Down Expand Up @@ -188,5 +250,88 @@ mod tests {
prop_assert_eq!(order.bump, *bump);
}
}

// `FinalizeSettle` derives each order's source buffer from its mint and
// destination from the intent, sorting by canonical order PDA like
// `BeginSettle` so the on-chain parser recovers exactly those pushes in
// that order.
#[test]
fn finalize_settle_derives_buffers_from_mints(
begin_ix_index in any::<u16>(),
cases in prop::collection::vec(
(arb_order_intent(), any::<[u8; 32]>(), any::<u64>()),
1..=5,
),
) {
let program_id = Pubkey::new_unique();
let orders: Vec<FinalizedIntent> = cases
.iter()
.map(|(intent, mint, amount)| FinalizedIntent {
intent,
mint: Pubkey::new_from_array(*mint),
amount: *amount,
})
.collect();
let ix = Instruction::from(FinalizeSettle {
program_id,
begin_ix_index,
orders: &orders,
});

// Expected pushes: each order's buffer PDA (and its canonical bump),
// buy token account, and amount, sorted by the order's canonical PDA
// (the builder's order).
struct ExpectedPush {
order_pda: Pubkey,
buffer: Pubkey,
bump: u8,
destination: Pubkey,
amount: u64,
}
let mut expected: Vec<ExpectedPush> = orders
.iter()
.map(|order| {
let (order_pda, _bump) = find_order_pda(&program_id, &order.intent.uid());
let (buffer, bump) = find_buffer_pda(&program_id, &order.mint);
ExpectedPush {
order_pda,
buffer,
bump,
destination: order.intent.buy_token_account,
amount: order.amount,
}
})
.collect();
expected.sort_by_key(|push| push.order_pda);

let mut accounts: Vec<_> = ix
.accounts
.iter()
.map(|meta| fake_account_from_array(meta.pubkey.to_bytes()))
.collect();
let parsed = FinalizeSettleInput::parse(&ix.data, &mut accounts)
.map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?;

prop_assert_eq!(parsed.begin_ix_index, begin_ix_index);
prop_assert_eq!(
parsed.instructions_sysvar_account.address(),
&INSTRUCTIONS_SYSVAR_ID,
);
let (state_pda, _bump) = find_state_pda(&program_id);
prop_assert_eq!(parsed.state_pda_account.address(), &state_pda);
prop_assert_eq!(
parsed.token_program_account.address(),
&SPL_TOKEN_PROGRAM_ID,
);

let parsed_pushes: Vec<_> = parsed.pushes.iter().collect();
prop_assert_eq!(parsed_pushes.len(), expected.len());
for (push, expected) in parsed_pushes.iter().zip(&expected) {
prop_assert_eq!(push.source_buffer.address(), &expected.buffer);
prop_assert_eq!(push.destination.address(), &expected.destination);
prop_assert_eq!(push.bump, expected.bump);
prop_assert_eq!(u64::from_be_bytes(*push.amount), expected.amount);
}
}
}
}
Loading