Skip to content
11 changes: 9 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,21 @@ struct OrderIntent {
buy_amount: u64
// Unix timestamp
valid_to: u32
flags: Flags
// Usual app data field, it isn't directly used in the program.
app_data: [u8; 32]
}

struct Flags {
// Either Buy or Sell
kind: OrderKind
partially_fillable: bool
// Usual app data field, it isn't directly used in the program.
app_data: [u8; 32]
}
```

The fields grouped in `Flags` share a single byte in the encoded form, one bit
each, with the remaining bits reserved and required when decoding to be zero.

Differences with Ethereum:

- In Solana, the spender token account (and the owner) is part of the intent, while in Ethereum it is implied in the signature.
Expand Down
22 changes: 11 additions & 11 deletions bench-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@
"create_buffers/happy_path_creates_initialized_buffer_token_account": 10340,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21731,
"create_buffers/max_buffers_in_one_instruction": 176947,
"create_order/happy_path_creates_order_pda_with_expected_body": 3473,
"create_order/happy_path_creates_order_pda_with_expected_body": 4976,
"initialize/happy_path_initializes_state_pda_with_expected_data": 4526,
"reclaim_buffer/funded_buffer_is_skipped": 6299,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7447,
"reclaim_buffer/max_buffers_in_one_instruction": 136501,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18043,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2186,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183,
"settle/finalizes_with_no_pushes": 7062,
"settle/pulls_from_multiple_orders": 19927,
"settle/pulls_funds_to_destination": 13527,
"settle/pulls_to_multiple_destinations": 14678,
"settle/pushes_a_single_order": 12369,
"settle/pushes_several_orders_from_different_buffers": 17614,
"settle/pushes_several_orders_from_one_buffer": 17615,
"settle/settles_a_single_order": 12387,
"settle/settles_multiple_orders": 22903,
"settle/pulls_from_multiple_orders": 19921,
"settle/pulls_funds_to_destination": 13524,
"settle/pulls_to_multiple_destinations": 14675,
"settle/pushes_a_single_order": 12366,
"settle/pushes_several_orders_from_different_buffers": 17608,
"settle/pushes_several_orders_from_one_buffer": 17609,
"settle/settles_a_single_order": 12384,
"settle/settles_multiple_orders": 22894,
"transfer_authority/manager_can_transfer_manager": 3170,
"transfer_authority/manager_can_transfer_reclaim_authority": 3172,
"transfer_authority/reclaim_authority_can_transfer_itself": 3175
Expand All @@ -51,7 +51,7 @@
"create_buffers/happy_path_creates_initialized_buffer_token_account": 303,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 435,
"create_buffers/max_buffers_in_one_instruction": 331,
"create_order/happy_path_creates_order_pda_with_expected_body": 453,
"create_order/happy_path_creates_order_pda_with_expected_body": 452,
"initialize/happy_path_initializes_state_pda_with_expected_data": 301,
"reclaim_buffer/funded_buffer_is_skipped": 400,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 400,
Expand Down
4 changes: 2 additions & 2 deletions client/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mod tests {
BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize, InitializedIntent,
};
use cow_settlement_interface::{
data::intent::{fixtures::sample_intent, OrderKind},
data::intent::fixtures::sample_intent,
fixtures::pubkey_from_seed,
instruction::{
fixtures::fake_account_from_array, reclaim_buffer::ReclaimBuffer,
Expand All @@ -85,7 +85,7 @@ mod tests {
fn build(instruction: SettlementInstruction) -> Instruction {
let program_id = pubkey_from_seed("program id");
let payer = pubkey_from_seed("payer");
let intent = sample_intent(OrderKind::Sell, false);
let intent = sample_intent(Default::default());
match instruction {
SettlementInstruction::Initialize => Initialize {
program_id,
Expand Down
388 changes: 230 additions & 158 deletions interface/src/data/intent.rs

Large diffs are not rendered by default.

45 changes: 18 additions & 27 deletions interface/src/data/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl OrderAccount {
}
}

/// Canonical 265-byte representation of an [`OrderAccount`]. The bytes
/// Canonical 264-byte representation of an [`OrderAccount`]. The bytes
/// written to/read from the order PDA's data area.
///
/// Layout: one character per byte, cell widths proportional to field size,
Expand All @@ -105,7 +105,7 @@ impl OrderAccount {
/// ││││with- │re- │ created_by │ intent (EncodedOrderIntent) │
/// ││││drawn │ceived │ │ │
/// └┴┴┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘
/// 0 1 2 3 11 19 51 ... 265
/// 0 1 2 3 11 19 51 ... 264
/// ```
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
pub struct EncodedOrderAccount([u8; Self::SIZE]);
Expand All @@ -120,7 +120,7 @@ impl EncodedOrderAccount {
const W_CREATED_BY: usize = size_of::<Pubkey>();
const W_INTENT: usize = EncodedOrderIntent::SIZE;

pub const SIZE: usize = 265;
pub const SIZE: usize = 264;

/// Single-byte account discriminator. See [`SettlementAccount`].
pub const DISCRIMINATOR: u8 = SettlementAccount::OrderAccount.discriminator();
Expand Down Expand Up @@ -263,10 +263,7 @@ pub mod fixtures {
use proptest::prelude::*;

use super::{OrderAccount, Pubkey};
use crate::data::intent::{
fixtures::{arb_order_intent, sample_intent},
OrderKind,
};
use crate::data::intent::fixtures::{arb_order_intent, sample_intent};

// Hardcoded but verified in a sanity-check test.
pub const DISCRIMINATOR_OFFSET: usize = 0;
Expand All @@ -281,7 +278,7 @@ pub mod fixtures {
amount_withdrawn: 0x0112_2334_4556_6778,
amount_received: 0x899a_abbc_cdde_eff0,
created_by: Pubkey::new_from_array([0x43; 32]),
intent: sample_intent(OrderKind::Sell, false),
intent: sample_intent(Default::default()),
}
}

Expand Down Expand Up @@ -316,10 +313,7 @@ mod tests {

use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
use super::*;
use crate::data::intent::{
fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET},
OrderKind,
};
use crate::data::intent::fixtures::{sample_intent, FLAGS_OFFSET};

// Pin each width to the size of the `OrderAccount` field it encodes. The
// widths summing to `SIZE` is enforced separately, at compile time, by the
Expand Down Expand Up @@ -390,9 +384,9 @@ mod tests {
(&EncodedOrderIntent::from(&sample_account_base.intent)).into();
// Hack: xoring each byte makes sure all bytes are different.
// In general, it isn't guaranteed that the result encodes to a
// valid intent, but in this case we know it because the only bytes
// that may fail decoding are `kind` and `partially_fillable`, both
// of which stay valid if flipped with `^0x01`.
// valid intent, but in this case we know it because the only byte
// that may fail decoding is the flags byte, and `^0x01` only flips
// its `kind` bit, never a reserved one.
let bitwise_different_encoded_intent: [u8; EncodedOrderIntent::SIZE] =
encoded_intent.map(|b| b ^ 0x01);
sample_account_base.intent =
Expand Down Expand Up @@ -430,13 +424,12 @@ mod tests {
fn decode_propagates_invalid_intent() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
// Corrupt the `kind` byte inside the intent slot: the intent
// decoder rejects it and the order-account decode surfaces that
// failure as `InvalidAccountData`.
let kind_offset = INTENT_OFFSET + KIND_OFFSET;
bytes[kind_offset] = 0x02;
// Set a reserved bit of the flags byte inside the intent slot: the
// intent decoder rejects it and the order-account decode surfaces
// that failure as `InvalidAccountData`.
bytes[INTENT_OFFSET + FLAGS_OFFSET] = 0xff;
let err = OrderAccount::try_from(bytes)
.expect_err("an invalid intent kind byte must propagate as a decode failure");
.expect_err("an invalid intent flags byte must propagate as a decode failure");
assert_eq!(err, ProgramError::InvalidAccountData);
}

Expand Down Expand Up @@ -567,7 +560,7 @@ mod tests {
let cancelled = true;
let amount_withdrawn = 1337;
let amount_received = 31337;
let intent = sample_intent(OrderKind::Sell, false);
let intent = sample_intent(Default::default());
let created_by = Pubkey::new_from_array([0x42u8; 32]);

let mut buffer = [0u8; EncodedOrderAccount::SIZE];
Expand Down Expand Up @@ -598,7 +591,7 @@ mod tests {
use ::proptest::{prelude::*, test_runner::TestCaseError};

use super::*;
use crate::data::{intent::fixtures::arb_order_kind, order::fixtures::arb_order_account};
use crate::data::{intent::fixtures::arb_flags_byte, order::fixtures::arb_order_account};

proptest! {
// For any `OrderAccount`, encode then decode returns the same
Expand All @@ -618,13 +611,11 @@ mod tests {
fn bytes_roundtrip(
mut bytes in any::<[u8; EncodedOrderAccount::SIZE]>(),
cancelled in any::<bool>(),
kind in arb_order_kind(),
partially_fillable in any::<bool>(),
flags in arb_flags_byte(),
) {
bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
bytes[CANCELLED_OFFSET] = cancelled as u8;
bytes[INTENT_OFFSET + KIND_OFFSET] = kind as u8;
bytes[INTENT_OFFSET + PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
bytes[INTENT_OFFSET + FLAGS_OFFSET] = flags;
let account = OrderAccount::try_from(bytes)
.map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
prop_assert_eq!(*EncodedOrderAccount::from(account), bytes);
Expand Down
12 changes: 5 additions & 7 deletions interface/src/instruction/create_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use crate::{data::intent::EncodedOrderIntent, SettlementInstruction};
/// instruction reverts with `AccountAlreadyInitialized`. Recreating the same
/// order is only possible after its PDA has been closed.
///
/// Wire format: `[discriminator=2, ..150 intent bytes]`, 151 bytes.
/// Wire format: `[discriminator=2, ..149 intent bytes]`, 150 bytes.
/// Required accounts:
/// `[owner (S), created_by (W,S), order_pda (W), system_program (R)]`.
/// The system program needs to be available but doesn't need to be at that
Expand Down Expand Up @@ -84,7 +84,7 @@ impl<'a, A> InstructionInputParsing<'a, A> for CreateOrderInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CreateOrder;

fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
// Body (discriminator already stripped): exactly the 150 intent bytes.
// Body (discriminator already stripped): exactly the 149 intent bytes.
if instruction_data.len() != EncodedOrderIntent::SIZE {
return Err(ProgramError::InvalidInstructionData);
}
Expand Down Expand Up @@ -115,9 +115,7 @@ pub mod fixtures {
use solana_address::Address;

use super::{CreateOrder, Instruction};
use crate::data::intent::{
fixtures::sample_intent, EncodedOrderIntent, OrderIntent, OrderKind,
};
use crate::data::intent::{fixtures::sample_intent, EncodedOrderIntent, OrderIntent};

/// Owner baked into [`valid_intent_bytes`]' sample intent.
pub const DEFAULT_OWNER: Address = Address::new_from_array([0x11; 32]);
Expand All @@ -126,12 +124,12 @@ pub mod fixtures {
/// and the system program.
pub const NUM_ACCOUNTS: usize = 4;

/// Canonical 150-byte intent payload for a valid sell order owned by
/// Canonical 149-byte intent payload for a valid sell order owned by
/// [`DEFAULT_OWNER`].
pub fn valid_intent_bytes() -> [u8; EncodedOrderIntent::SIZE] {
(&EncodedOrderIntent::from(&OrderIntent {
owner: DEFAULT_OWNER,
..sample_intent(OrderKind::Sell, true)
..sample_intent(Default::default())
}))
.into()
}
Expand Down
12 changes: 9 additions & 3 deletions programs/settlement/src/create_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub fn process_create_order(

#[cfg(test)]
mod tests {
use cow_settlement_interface::data::intent::{OrderIntent, OrderKind};
use cow_settlement_interface::data::intent::{Flags, OrderIntent, OrderKind};
use cow_settlement_interface::instruction::create_order::fixtures::{
default_order_data, valid_intent_bytes, DEFAULT_OWNER, NUM_ACCOUNTS,
};
Expand Down Expand Up @@ -107,11 +107,17 @@ mod tests {
fn process_create_order_rejects_invalid_encoded_intent() {
let intent: OrderIntent = (&valid_intent_bytes()).try_into().expect("should be valid");
let intent_bytes_buy = EncodedOrderIntent::from(&OrderIntent {
kind: OrderKind::Buy,
flags: Flags {
kind: OrderKind::Buy,
..intent.flags
},
..intent
});
let intent_bytes_sell = EncodedOrderIntent::from(&OrderIntent {
kind: OrderKind::Sell,
flags: Flags {
kind: OrderKind::Sell,
..intent.flags
},
..intent
});
fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
Expand Down
10 changes: 7 additions & 3 deletions programs/settlement/src/settle/begin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,11 @@ fn validated_final_amounts(
.checked_add(amount_out)
.ok_or(SettlementError::AmountReceivedOverflow)?;

let (filled, order_amount) = match intent.kind {
let (filled, order_amount) = match intent.flags.kind {
OrderKind::Sell => (amount_withdrawn, intent.sell_amount),
OrderKind::Buy => (amount_received, intent.buy_amount),
};
if filled != order_amount && !intent.partially_fillable {
if filled != order_amount && !intent.flags.partially_fillable {
return Err(SettlementError::OrderNotExactlyFilled);
} else if filled > order_amount {
return Err(SettlementError::FillExceedsOrderAmount);
Expand All @@ -408,6 +408,7 @@ fn validated_final_amounts(
mod tests {
use super::*;
use cow_settlement_interface::data::intent::fixtures::{arb_order_intent, sample_intent};
use cow_settlement_interface::data::intent::Flags;
use cow_settlement_interface::instruction::fixtures::fake_account;
use cow_settlement_interface::instruction::settle::fixtures::arb_pushes;
use cow_settlement_interface::instruction::settle::{FinalizeSettle, FinalizeSettleInput};
Expand Down Expand Up @@ -435,7 +436,10 @@ mod tests {
OrderIntent {
sell_amount: self.sell,
buy_amount: self.buy,
..sample_intent(self.kind, self.partially_fillable)
..sample_intent(Flags {
kind: self.kind,
partially_fillable: self.partially_fillable,
})
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions programs/settlement/tests/common/order.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! On-chain order construction shared by the settlement integration tests.

use cow_settlement_client::cow_settlement_interface::data::intent::{OrderIntent, OrderKind};
use cow_settlement_client::cow_settlement_interface::data::intent::{
Flags, OrderIntent, OrderKind,
};
use cow_settlement_client::instructions::CreateOrder;
use litesvm::LiteSVM;
use solana_sdk::{
Expand All @@ -24,8 +26,10 @@ pub fn sample_intent(owner: Pubkey, salt: u8) -> OrderIntent {
sell_amount: 1_000_000,
buy_amount: 2_000_000,
valid_to: 0xdead_beef,
kind: OrderKind::Sell,
partially_fillable: true,
flags: Flags {
kind: OrderKind::Sell,
partially_fillable: true,
},
app_data: [salt; 32],
}
}
Expand Down Expand Up @@ -125,13 +129,13 @@ impl<'a> OrderBuilder<'a> {

/// Set the order's kind (`Sell` or `Buy`). Defaults to `Sell`.
pub fn kind(mut self, kind: OrderKind) -> Self {
self.intent.kind = kind;
self.intent.flags.kind = kind;
self
}

/// Set whether the order may be filled partially. Defaults to `true`.
pub fn partially_fillable(mut self, partially_fillable: bool) -> Self {
self.intent.partially_fillable = partially_fillable;
self.intent.flags.partially_fillable = partially_fillable;
self
}

Expand Down
4 changes: 2 additions & 2 deletions programs/settlement/tests/create_order.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use cow_settlement_client::cow_settlement_interface::{
data::{
intent::{fixtures, EncodedOrderIntent, OrderIntent, OrderKind},
intent::{fixtures, EncodedOrderIntent, OrderIntent},
order::{EncodedOrderAccount, OrderAccount},
},
instruction::create_order::CreateOrder,
Expand All @@ -24,7 +24,7 @@ mod common;
fn sample_intent(owner: Pubkey) -> OrderIntent {
OrderIntent {
owner,
..fixtures::sample_intent(OrderKind::Sell, true)
..fixtures::sample_intent(Default::default())
}
}

Expand Down
4 changes: 2 additions & 2 deletions programs/settlement/tests/reclaim_order.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use cow_settlement_client::cow_settlement_interface::{
data::intent::{fixtures::sample_intent, EncodedOrderIntent, OrderIntent, OrderKind},
data::intent::{fixtures::sample_intent, EncodedOrderIntent, OrderIntent},
instruction::{create_order::CreateOrder, reclaim_order::ReclaimOrder},
pda::order::find_order_pda,
SettlementError,
Expand All @@ -23,7 +23,7 @@ fn reclaim_sample_intent(owner: Pubkey) -> OrderIntent {
OrderIntent {
owner,
valid_to: VALID_TO,
..sample_intent(OrderKind::Sell, true)
..sample_intent(Default::default())
}
}

Expand Down
Loading