Skip to content
8 changes: 8 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ struct OrderIntent {
// Either Buy or Sell
kind: OrderKind
partially_fillable: bool
// Receives the sell token account's rent when a settlement closes it.
sell_account_rent_recipient: Pubkey
// Usual app data field, it isn't directly used in the program.
app_data: [u8; 32]
}
Expand Down Expand Up @@ -188,6 +190,12 @@ Creating the order in advance is _not_ needed: if the order wasn’t created bef

Note that deleting the order PDA is _not_ enough to invalidate an order. In fact, if an order signature is available, the same order could always be created again until it expires.

### Sell Token Account clearing

Upon settlement, if an order whose `sell_token_account` is left with 0 funds *and* the settlement account's state account has been granted close authority, the sell token account will be automatically closed and the rent proceeds sent to `sell_account_rent_recipient`.

If the `sell_token_account` has not granted close authority or has any remaining funds, the account will not be closed and `sell_account_rent_recipient` is ignored.

### Order clearing

Allocating an order PDA requires paying rent.
Expand Down
56 changes: 28 additions & 28 deletions bench-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,55 +11,55 @@
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 4,
"settle/finalizes_with_no_pushes": 5,
"settle/pulls_from_multiple_orders": 15,
"settle/pulls_funds_to_destination": 10,
"settle/pulls_to_multiple_destinations": 11,
"settle/pushes_a_single_order": 9,
"settle/pushes_several_orders_from_different_buffers": 13,
"settle/pushes_several_orders_from_one_buffer": 12,
"settle/settles_a_single_order": 9,
"settle/settles_multiple_orders": 17
"settle/pulls_from_multiple_orders": 16,
"settle/pulls_funds_to_destination": 11,
"settle/pulls_to_multiple_destinations": 12,
"settle/pushes_a_single_order": 10,
"settle/pushes_several_orders_from_different_buffers": 14,
"settle/pushes_several_orders_from_one_buffer": 13,
"settle/settles_a_single_order": 10,
"settle/settles_multiple_orders": 18
},
"compute_units": {
"create_buffers/happy_path_creates_initialized_buffer_token_account": 10338,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21727,
"create_buffers/max_buffers_in_one_instruction": 176916,
"create_order/happy_path_creates_order_pda_with_expected_body": 7916,
"create_order/happy_path_creates_order_pda_with_expected_body": 4942,
"initialize/happy_path_initializes_state_pda_with_expected_data": 4525,
"reclaim_buffer/funded_buffer_is_skipped": 6376,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7523,
"reclaim_buffer/max_buffers_in_one_instruction": 136548,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18119,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2131,
"settle/finalizes_with_no_pushes": 7042,
"settle/pulls_from_multiple_orders": 19743,
"settle/pulls_funds_to_destination": 13413,
"settle/pulls_to_multiple_destinations": 14559,
"settle/pushes_a_single_order": 12263,
"settle/pushes_several_orders_from_different_buffers": 17444,
"settle/pushes_several_orders_from_one_buffer": 17445,
"settle/settles_a_single_order": 12281,
"settle/settles_multiple_orders": 22669
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2157,
"settle/finalizes_with_no_pushes": 7041,
"settle/pulls_from_multiple_orders": 19919,
"settle/pulls_funds_to_destination": 13501,
"settle/pulls_to_multiple_destinations": 14647,
"settle/pushes_a_single_order": 12354,
"settle/pushes_several_orders_from_different_buffers": 17623,
"settle/pushes_several_orders_from_one_buffer": 17624,
"settle/settles_a_single_order": 12372,
"settle/settles_multiple_orders": 22940
},
"transaction_bytes": {
"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": 389,
"create_order/happy_path_creates_order_pda_with_expected_body": 421,
"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,
"reclaim_buffer/max_buffers_in_one_instruction": 332,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 236,
"settle/finalizes_with_no_pushes": 289,
"settle/pulls_from_multiple_orders": 655,
"settle/pulls_funds_to_destination": 472,
"settle/pulls_to_multiple_destinations": 513,
"settle/pushes_a_single_order": 431,
"settle/pushes_several_orders_from_different_buffers": 573,
"settle/pushes_several_orders_from_one_buffer": 541,
"settle/settles_a_single_order": 431,
"settle/settles_multiple_orders": 715
"settle/pulls_from_multiple_orders": 689,
"settle/pulls_funds_to_destination": 505,
"settle/pulls_to_multiple_destinations": 546,
"settle/pushes_a_single_order": 464,
"settle/pushes_several_orders_from_different_buffers": 607,
"settle/pushes_several_orders_from_one_buffer": 575,
"settle/settles_a_single_order": 464,
"settle/settles_multiple_orders": 750
}
}
3 changes: 3 additions & 0 deletions client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ impl From<BeginSettle<'_>> for Instruction {
fn from(builder: BeginSettle<'_>) -> Self {
let mut order_pdas = Vec::with_capacity(builder.orders.len());
let mut sell_token_accounts = Vec::with_capacity(builder.orders.len());
let mut sell_account_rent_recipients = Vec::with_capacity(builder.orders.len());
let mut pull_lists: Vec<&[Pull]> = Vec::with_capacity(builder.orders.len());
for order in builder.orders {
let (order_pda, _bump) = find_order_pda(&builder.program_id, &order.intent.uid());
order_pdas.push(order_pda);
sell_token_accounts.push(order.intent.sell_token_account);
sell_account_rent_recipients.push(order.intent.sell_account_rent_recipient);
pull_lists.push(order.pulls);
}
let (state_pda, _bump) = find_state_pda(&builder.program_id);
Expand All @@ -52,6 +54,7 @@ impl From<BeginSettle<'_>> for Instruction {
auction_id: builder.auction_id,
order_pdas: &order_pdas,
sell_token_accounts: &sell_token_accounts,
sell_account_rent_recipients: &sell_account_rent_recipients,
pulls: &pull_lists,
}
.into()
Expand Down
57 changes: 47 additions & 10 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,20 @@ pub struct OrderIntent {
/// must consume the full sell amount (fill-or-kill).
pub partially_fillable: bool,

/// Account that receives the lamports reclaimed from
/// `sell_token_account` when a settlement closes it. Closing only
/// happens if the account is left empty by the settlement and its SPL
/// close authority is the settlement state PDA, which is the owner's
/// opt-in.
pub sell_account_rent_recipient: Pubkey,

/// Opaque 32 bytes set by the order creator. Not interpreted by the
/// settlement program; used off-chain for metadata such as the
/// frontend version, slippage hints, or attribution.
pub app_data: [u8; 32],
}

/// Canonical 150-byte representation of an [`OrderIntent`]. The wire format and
/// Canonical 182-byte representation of an [`OrderIntent`]. The wire format and
/// the order UID preimage.
///
/// Layout: one character per byte, cell widths proportional to field size,
Expand All @@ -93,12 +100,12 @@ pub struct OrderIntent {
/// ```text
/// partially_fillable ─────┐
/// kind ────┐│
/// ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────┬───────┬───┬┬┬───────────────────────────────┐
/// │ │ │ │sell_ │buy_ │val│││ │
/// │ owner │ buy_token_account │ sell_token_account │ │ │id_│││ app_data │
/// │ │ │ │amount │amount │to │││ │
/// └───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────┴───────┴───┴┴┴───────────────────────────────┘
/// 0 32 64 96 104 112 116 118 150
/// ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────┬───────┬───┬┬┬───────────────────────────────┬───────────────────────────────
/// │ │ │ │sell_ │buy_ │val│││ sell_account_ │
/// │ owner │ buy_token_account │ sell_token_account │ │ │id_│││ rent_recipient │ app_data │
/// │ │ │ │amount │amount │to │││ │
/// └───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────┴───────┴───┴┴┴───────────────────────────────┴───────────────────────────────
/// 0 32 64 96 104 112 116 118 150 182
/// 117
/// ```
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
Expand All @@ -114,9 +121,10 @@ impl EncodedOrderIntent {
const WIDTH_VALID_TO: usize = size_of::<u32>();
const WIDTH_KIND: usize = size_of::<OrderKind>();
const WIDTH_PARTIALLY_FILLABLE: usize = size_of::<bool>();
const WIDTH_SELL_ACCOUNT_RENT_RECIPIENT: usize = size_of::<Pubkey>();
const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();

pub const SIZE: usize = 150;
pub const SIZE: usize = 182;

/// Canonical hash of the bytes.
pub fn hash(&self) -> Hash {
Expand Down Expand Up @@ -163,6 +171,7 @@ impl From<&OrderIntent> for EncodedOrderIntent {
valid_to,
kind,
partially_fillable,
sell_account_rent_recipient,
app_data,
) = mut_array_refs![
&mut out,
Expand All @@ -174,6 +183,7 @@ impl From<&OrderIntent> for EncodedOrderIntent {
EncodedOrderIntent::WIDTH_VALID_TO,
EncodedOrderIntent::WIDTH_KIND,
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
EncodedOrderIntent::WIDTH_SELL_ACCOUNT_RENT_RECIPIENT,
EncodedOrderIntent::WIDTH_APP_DATA
];
*owner = intent.owner.to_bytes();
Expand All @@ -184,6 +194,7 @@ impl From<&OrderIntent> for EncodedOrderIntent {
*valid_to = intent.valid_to.to_le_bytes();
*kind = [intent.kind as u8];
*partially_fillable = [intent.partially_fillable as u8];
*sell_account_rent_recipient = intent.sell_account_rent_recipient.to_bytes();
*app_data = intent.app_data;
Self(out)
}
Expand All @@ -209,6 +220,7 @@ impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
valid_to,
kind,
partially_fillable,
sell_account_rent_recipient,
app_data,
) = array_refs![
bytes,
Expand All @@ -220,6 +232,7 @@ impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
EncodedOrderIntent::WIDTH_VALID_TO,
EncodedOrderIntent::WIDTH_KIND,
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
EncodedOrderIntent::WIDTH_SELL_ACCOUNT_RENT_RECIPIENT,
EncodedOrderIntent::WIDTH_APP_DATA
];

Expand All @@ -240,6 +253,7 @@ impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
[1] => true,
_ => return Err(ProgramError::InvalidInstructionData),
},
sell_account_rent_recipient: Pubkey::new_from_array(*sell_account_rent_recipient),
app_data: *app_data,
})
}
Expand Down Expand Up @@ -285,6 +299,7 @@ pub mod fixtures {
valid_to: 0xdead_beef,
kind,
partially_fillable,
sell_account_rent_recipient: Pubkey::new_from_array([0x55; 32]),
app_data: [0x44; 32],
}
}
Expand All @@ -306,9 +321,21 @@ pub mod fixtures {
arb_order_kind(),
any::<bool>(),
any::<[u8; 32]>(),
any::<[u8; 32]>(),
)
.prop_map(
|(owner, buy_tok, sell_tok, sell_amount, buy_amount, valid_to, kind, pf, app)| {
|(
owner,
buy_tok,
sell_tok,
sell_amount,
buy_amount,
valid_to,
kind,
pf,
rent_recipient,
app,
)| {
OrderIntent {
owner: Pubkey::new_from_array(owner),
buy_token_account: Pubkey::new_from_array(buy_tok),
Expand All @@ -318,6 +345,7 @@ pub mod fixtures {
valid_to,
kind,
partially_fillable: pf,
sell_account_rent_recipient: Pubkey::new_from_array(rent_recipient),
app_data: app,
}
},
Expand Down Expand Up @@ -377,6 +405,10 @@ mod tests {
EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
size_of_val(&intent.partially_fillable)
);
assert_eq!(
EncodedOrderIntent::WIDTH_SELL_ACCOUNT_RENT_RECIPIENT,
size_of_val(&intent.sell_account_rent_recipient)
);
assert_eq!(
EncodedOrderIntent::WIDTH_APP_DATA,
size_of_val(&intent.app_data)
Expand Down Expand Up @@ -458,7 +490,7 @@ mod tests {
#[test]
fn uid_digest_regression() {
let intent = sample_intent(OrderKind::Buy, true);
let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
let expected = hex!("7634777e7f671c95c082d21eb1e3d685d764d54f8716115c9baabdcb68ea5f61");
assert_eq!(intent.uid(), Hash::from(expected));
}

Expand Down Expand Up @@ -493,6 +525,11 @@ mod tests {
0x01,
// partially_fillable (true = 1)
0x01,
// sell_account_rent_recipient ([0x55; 32])
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
// app_data ([0x44; 32])
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
Expand Down
6 changes: 3 additions & 3 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 201-byte representation of an [`OrderAccount`]. The bytes
/// Canonical 233-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 ... 201
/// 0 1 2 3 11 19 51 ... 233
/// ```
#[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 = 201;
pub const SIZE: usize = 233;

/// Single-byte account discriminator. See [`crate::SettlementAccount`].
pub const DISCRIMINATOR: u8 = crate::SettlementAccount::OrderAccount.discriminator();
Expand Down
6 changes: 3 additions & 3 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, ..intent 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 182 intent bytes.
if instruction_data.len() != EncodedOrderIntent::SIZE {
return Err(ProgramError::InvalidInstructionData);
}
Expand Down Expand Up @@ -126,7 +126,7 @@ 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 182-byte intent payload for a valid sell order owned by
/// [`DEFAULT_OWNER`].
pub fn valid_intent_bytes() -> [u8; EncodedOrderIntent::SIZE] {
(&EncodedOrderIntent::from(&OrderIntent {
Expand Down
Loading