Skip to content

Commit 66b75f4

Browse files
committed
Check order amounts
1 parent b535496 commit 66b75f4

7 files changed

Lines changed: 730 additions & 68 deletions

File tree

DESIGN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ created_by: Pubkey
126126
intent: OrderIntent
127127
```
128128

129-
There are also other parameters, like `snapshot`, used internally during the settlement.
129+
`amount_withdrawn` and `amount_received` are cumulative across all of the order's settlements. They cap how much of the order can still be traded, so it can't be filled beyond its sell and buy amounts.
130130

131131
An order PDA can only exist and hold data if the order has been [authenticated](#authenticating-an-order). If this account exists and is not cancelled, filled, or expired, then the order can be traded.
132132

@@ -255,9 +255,9 @@ Differences with Ethereum:
255255

256256
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.
257257

258-
- `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`.
258+
- `BeginSettle`: Pulls funds from each order’s sell token account to the solver-specified destination accounts, using the settlement state PDA’s token delegation. Validates each order's limit price and that its cumulative fill stays within the order's sell and buy amounts (fully filling a fill-or-kill order), and updates the order's `amount_withdrawn`/`amount_received`. Carries an explicit `finalize_ix_index` pointing to its paired `FinalizeSettle`.
259259
- (arbitrary interactions): Any instruction from the solver. This could be a token transfer, an AMM swap, or anything else.
260-
- `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`.
260+
- `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. Carries an explicit `begin_ix_index` pointing to its paired `BeginSettle`.
261261

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

client/src/instructions.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ mod tests {
237237
.iter()
238238
.map(|meta| fake_account_from_array(meta.pubkey.to_bytes()))
239239
.collect();
240-
let parsed = BeginSettleInput::parse(&ix.data, &mut accounts)
240+
let mut parsed = BeginSettleInput::parse(&ix.data, &mut accounts)
241241
.map_err(|e| TestCaseError::fail(format!("parse failed: {e:?}")))?;
242242

243243
prop_assert_eq!(parsed.finalize_ix_index, finalize_ix_index);
@@ -246,13 +246,18 @@ mod tests {
246246
&INSTRUCTIONS_SYSVAR_ID,
247247
);
248248

249-
let parsed_orders: Vec<_> = parsed.orders.iter().collect();
250-
prop_assert_eq!(parsed_orders.len(), expected.len());
251-
for (order, (order_pda, sell_token, bump)) in parsed_orders.iter().zip(&expected) {
249+
// Each order borrows the iterator, so only one is live at a time:
250+
// compare it against `expected` as it's yielded, without collecting.
251+
let mut orders = parsed.orders.iter_mut();
252+
for (order_pda, sell_token, bump) in &expected {
253+
let order = orders
254+
.next()
255+
.ok_or_else(|| TestCaseError::fail("fewer parsed orders than expected"))?;
252256
prop_assert_eq!(order.order_pda.address(), order_pda);
253257
prop_assert_eq!(order.sell_token_account.address(), sell_token);
254258
prop_assert_eq!(order.bump, *bump);
255259
}
260+
prop_assert!(orders.next().is_none());
256261
}
257262

258263
// `FinalizeSettle` derives each order's source buffer from its mint and

interface/src/instruction/settle/begin.rs

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct Pull {
3636
/// `[discriminator=0][finalize_ix_index: u16 LE][n: u8][bump×n][transfer_count×n]
3737
/// [amount: u64 LE ×T]`.
3838
/// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program
39-
/// (R)]` followed, per order, by `[order_pda (R), sell_token_account (W),
39+
/// (R)]` followed, per order, by `[order_pda (W), sell_token_account (W),
4040
/// destination (W)...]`.
4141
///
4242
/// The program requires the order PDAs to be strictly increasing by address.
@@ -98,8 +98,9 @@ impl From<BeginSettle<'_>> for Instruction {
9898
AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
9999
];
100100
for &i in &order {
101-
// Read-only account for the order.
102-
accounts.push(AccountMeta::new_readonly(order_pdas[i], false));
101+
// Writable account for the order: `BeginSettle` updates its filled
102+
// amounts (`amount_withdrawn`/`amount_received`).
103+
accounts.push(AccountMeta::new(order_pdas[i], false));
103104
// Writable accounts settling the order: its sell token account and the
104105
// recipient of each transfer.
105106
accounts.push(AccountMeta::new(sell_token_accounts[i], false));
@@ -119,7 +120,7 @@ impl From<BeginSettle<'_>> for Instruction {
119120
/// A single settled order, resulted from parsing `BeginSettle`, together with
120121
/// the funds to pull from its sell token account.
121122
pub struct SettledOrder<'a> {
122-
pub order_pda: &'a AccountView,
123+
pub order_pda: &'a mut AccountView,
123124
pub sell_token_account: &'a AccountView,
124125
pub bump: u8,
125126
/// Destination accounts for this order's transfers.
@@ -137,7 +138,7 @@ pub struct SettledOrders<'a> {
137138
/// - each order_accounts is a series of accounts:
138139
/// `order_pda_N, sell_token_account_N, destination_N_1, destination_N_2, ..., destination_N_M`
139140
/// - and M is `counts[N]`
140-
order_accounts: &'a [AccountView],
141+
order_accounts: &'a mut [AccountView],
141142
bumps: &'a [u8],
142143
/// One transfer count per order, parallel to `bumps`.
143144
counts: &'a [u8],
@@ -147,41 +148,47 @@ pub struct SettledOrders<'a> {
147148
}
148149

149150
impl<'a> SettledOrders<'a> {
150-
/// Returns an iterator yielding one [`SettledOrder`] per step.
151+
/// Returns an iterator yielding one [`SettledOrder`] per step. Each order's
152+
/// `order_pda` is handed out mutably (so `BeginSettle` can update its filled
153+
/// amounts), so the items borrow the iterator and must be processed one at a
154+
/// time rather than collected.
151155
#[allow(
152156
clippy::arithmetic_side_effects,
153157
reason = "offsets are bounded by tx limits"
154158
)]
155-
pub fn iter(&self) -> impl Iterator<Item = SettledOrder<'a>> + '_ {
156-
let order_count = self.bumps.len();
159+
pub fn iter_mut(&mut self) -> impl Iterator<Item = SettledOrder<'_>> + '_ {
160+
let bumps = self.bumps;
161+
let counts = self.counts;
162+
let amounts = self.amounts;
163+
// Cursor over the remaining order accounts; each step splits one order's
164+
// `[order_pda, sell_token_account, destinations..count]` off the front.
165+
let mut rest: &mut [AccountView] = self.order_accounts;
157166
let mut i = 0usize;
158-
let mut account_offset = 0usize;
159167
let mut amount_offset = 0usize;
160168
std::iter::from_fn(move || {
161-
if i >= order_count {
169+
if i >= bumps.len() {
162170
return None;
163171
}
164-
let bump = self.bumps[i];
165-
let count = usize::from(self.counts[i]);
172+
let bump = bumps[i];
173+
let count = usize::from(counts[i]);
166174
i += 1;
167175

168-
let order_pda = &self.order_accounts[account_offset];
169-
let sell_token_account = &self.order_accounts[account_offset + 1];
170-
let dest_start = account_offset + 2;
171-
let dest_end = dest_start + count;
172-
let destinations = &self.order_accounts[dest_start..dest_end];
173-
account_offset = dest_end;
176+
let taken = core::mem::take(&mut rest);
177+
let (order_pda, tail) = taken.split_first_mut()?;
178+
let (sell_token_account, tail) = tail.split_first_mut()?;
179+
let (destinations, remainder) = tail.split_at_mut(count);
180+
rest = remainder;
174181

175182
let amount_end = amount_offset + count;
176-
let amounts = &self.amounts[amount_offset..amount_end];
183+
let order_amounts = &amounts[amount_offset..amount_end];
177184
amount_offset = amount_end;
178185

179186
Some(SettledOrder {
180187
order_pda,
181188
sell_token_account,
182189
bump,
183190
destinations,
184-
amounts,
191+
amounts: order_amounts,
185192
})
186193
})
187194
}
@@ -371,16 +378,21 @@ mod tests {
371378
];
372379
let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
373380
assert_eq!(actual, expected);
374-
// The fixed accounts and the order PDAs are read-only; only the sell
375-
// token accounts are writable, following the sorted order.
381+
// The fixed accounts are read-only; each order PDA (updated with its
382+
// filled amounts) and sell token account are writable, in sorted order.
376383
let writable: Vec<Pubkey> = accounts
377384
.iter()
378385
.filter(|account| account.is_writable)
379386
.map(|account| account.pubkey)
380387
.collect();
381388
assert_eq!(
382389
writable,
383-
vec![low_sell_token_account, high_sell_token_account],
390+
vec![
391+
low_order_pda,
392+
low_sell_token_account,
393+
high_order_pda,
394+
high_sell_token_account,
395+
],
384396
);
385397
assert!(accounts.iter().all(|account| !account.is_signer));
386398
}
@@ -453,14 +465,17 @@ mod tests {
453465
];
454466
let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
455467
assert_eq!(actual, expected);
456-
// The fixed accounts and the order PDAs are read-only; sell and
457-
// destination accounts are writable for the transfer.
468+
// The fixed accounts are read-only; each order PDA (updated with its
469+
// filled amounts), sell, and destination accounts are writable.
458470
let writable: Vec<Pubkey> = accounts
459471
.iter()
460472
.filter(|account| account.is_writable)
461473
.map(|account| account.pubkey)
462474
.collect();
463-
assert_eq!(writable, vec![sell_a, dest_a0, dest_a1, sell_b, dest_b0]);
475+
assert_eq!(
476+
writable,
477+
vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0],
478+
);
464479
assert!(accounts.iter().all(|account| !account.is_signer));
465480
}
466481

@@ -483,13 +498,13 @@ mod tests {
483498
let BeginSettleInput {
484499
finalize_ix_index,
485500
instructions_sysvar_account,
486-
orders,
501+
mut orders,
487502
token_program_account,
488503
state_pda_account,
489504
} = BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
490505
assert_eq!(finalize_ix_index, 0x1337);
491506
assert_eq!(instructions_sysvar_account.address(), &sysvar);
492-
assert_eq!(orders.iter().count(), 0);
507+
assert_eq!(orders.iter_mut().count(), 0);
493508
assert_eq!(token_program_account.address(), &token_program);
494509
assert_eq!(state_pda_account.address(), &state);
495510
}
@@ -544,7 +559,7 @@ mod tests {
544559
let BeginSettleInput {
545560
finalize_ix_index,
546561
instructions_sysvar_account,
547-
orders,
562+
mut orders,
548563
state_pda_account,
549564
token_program_account,
550565
} = BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
@@ -553,7 +568,7 @@ mod tests {
553568
assert_eq!(token_program_account.address(), &token_program);
554569
assert_eq!(state_pda_account.address(), &state);
555570

556-
let mut orders = orders.iter();
571+
let mut orders = orders.iter_mut();
557572
let order = orders.next().expect("one settled order");
558573
assert_eq!(order.order_pda.address(), &order_pda);
559574
assert_eq!(order.sell_token_account.address(), &sell_token);
@@ -590,10 +605,10 @@ mod tests {
590605
0x3344u64.to_le_bytes(),
591606
];
592607

593-
let BeginSettleInput { orders, .. } =
608+
let BeginSettleInput { mut orders, .. } =
594609
BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
595610

596-
let mut orders = orders.iter();
611+
let mut orders = orders.iter_mut();
597612
let order = orders.next().expect("one settled order");
598613
assert_eq!(order.order_pda.address(), &order_pda);
599614
assert_eq!(order.sell_token_account.address(), &sell_token);
@@ -643,16 +658,20 @@ mod tests {
643658
[0u8; ORDER_COUNT],
644659
];
645660

646-
let parsed = BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
647-
let orders: Vec<_> = parsed.orders.iter().collect();
661+
let mut parsed =
662+
BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
648663

649-
assert_eq!(orders.len(), ORDER_COUNT);
650-
for (order, (order_pda, sell_token, bump)) in orders.iter().zip(&expected) {
664+
// Each order borrows the iterator, so only one is live at a time:
665+
// compare it against `expected` as it's yielded, without collecting.
666+
let mut orders = parsed.orders.iter_mut();
667+
for (order_pda, sell_token, bump) in &expected {
668+
let order = orders.next().expect("an order per expected entry");
651669
assert_eq!(order.order_pda.address(), order_pda);
652670
assert_eq!(order.sell_token_account.address(), sell_token);
653671
assert_eq!(order.bump, *bump);
654672
assert_eq!(order.destinations.len(), 0);
655673
}
674+
assert!(orders.next().is_none());
656675
}
657676

658677
#[test]

interface/src/instruction/settle/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
99
mod begin;
1010
mod finalize;
1111

12-
pub use begin::{BeginSettle, BeginSettleInput, Pull, SettledOrder};
12+
pub use begin::{BeginSettle, BeginSettleInput, Pull, SettledOrder, SettledOrders};
1313
pub use finalize::{
1414
finalize_push_amounts, FinalizeSettle, FinalizeSettleInput, Push, Pushes,
1515
FINALIZE_FIXED_ACCOUNTS,

interface/src/lib.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,24 @@ pub enum SettlementError {
160160
LimitPriceViolated = 24,
161161
/// `BeginSettle`: an order's pull amounts sum to more than `u64::MAX`.
162162
PullAmountOverflow = 25,
163+
/// `BeginSettle`: filling this order would consume more tokens than the
164+
/// maximum the user is willing to trade on this intent.
165+
/// Sell: `amount_in > sell_amount`; buy: `amount_out > buy_amount`.
166+
FillExceedsOrderAmount = 26,
167+
/// `BeginSettle`: a non-`partially_fillable` order isn't filled completely
168+
/// Sell: `amount_in != sell_amount`; buy: total `amount_out != buy_amount`.
169+
OrderNotFullyFilled = 27,
170+
/// `BeginSettle`: the order's cumulative `amount_withdrawn` would exceed
171+
/// `u64::MAX` once this settlement's pulls are added.
172+
AmountWithdrawnOverflow = 28,
173+
/// `BeginSettle`: the order's cumulative `amount_received` would exceed
174+
/// `u64::MAX` once this settlement's push is added.
175+
AmountReceivedOverflow = 29,
163176
/// `ReclaimOrder` was called before the order's `valid_to` has elapsed.
164-
OrderNotExpired = 26,
177+
OrderNotExpired = 30,
165178
/// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the
166179
/// `created_by` address recorded in the order.
167-
ReclaimRecipientMismatch = 27,
180+
ReclaimRecipientMismatch = 31,
168181
}
169182

170183
impl From<SettlementError> for u32 {

0 commit comments

Comments
 (0)