Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd787a3
Pack `OrderIntent` flags into a single byte
kaze-cow Aug 26, 2026
e1cee83
Allow `ReclaimOrder` of completed orders before expiry
kaze-cow Aug 26, 2026
a27cfa4
Merge remote-tracking branch 'origin/main' into kaze/sc-288-reclaim-c…
kaze-cow Aug 28, 2026
bbd8830
verify behavior of reclaim_order within settlement
kaze-cow Aug 28, 2026
56b4616
fix constant
kaze-cow Aug 28, 2026
33b9771
fix bench
kaze-cow Aug 28, 2026
bd1f186
Merge branch 'main' into kaze/sc-288-reclaim-completed-orders
kaze-cow Aug 28, 2026
3bda0dc
fix bench report again
kaze-cow Aug 28, 2026
cf035df
Merge branch 'kaze/sc-288-reclaim-completed-orders' of github.com:cow…
kaze-cow Aug 28, 2026
8a9ff48
Merge branch 'main' into kaze/sc-288-reclaim-completed-orders
kaze-cow Aug 28, 2026
eba176a
fix random issues I noticed while re-reviewing
kaze-cow Aug 28, 2026
16d594c
Merge branch 'kaze/sc-288-reclaim-completed-orders' of github.com:cow…
kaze-cow Aug 28, 2026
81e3f29
fix bench
kaze-cow Aug 28, 2026
e36f722
sort ascending
kaze-cow Aug 28, 2026
b2957df
fix out of order flags for consistency
kaze-cow Aug 28, 2026
6545178
improve comment explaining how fill_progress works
kaze-cow Aug 28, 2026
9233761
remove test sanity checking byte for offset over and over again
kaze-cow Aug 28, 2026
714437b
Update programs/settlement/src/reclaim_order.rs
kaze-cow Aug 28, 2026
72ca501
Update programs/settlement/src/reclaim_order.rs
kaze-cow Aug 28, 2026
f3c3b4c
update design info
kaze-cow Aug 28, 2026
41f7d57
update design paragraphs to be shorter/easier to understand
kaze-cow Aug 28, 2026
2cfd50b
Merge branch 'kaze/sc-288-reclaim-completed-orders' of github.com:cow…
kaze-cow Aug 28, 2026
c70614d
add a couple of additional test cases and improve the comments
kaze-cow Aug 28, 2026
0d9cc5b
assert_reclaim_while_unexpired fixes
kaze-cow Aug 28, 2026
dd199ca
fix it so that the test is not vacuous
kaze-cow Aug 28, 2026
ea0e79d
make finalize index technically correct
kaze-cow Aug 28, 2026
d363fcc
fix bench report
kaze-cow Aug 28, 2026
9897c28
return to previous comment situation
kaze-cow Aug 28, 2026
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
8 changes: 8 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ struct OrderIntent {
}

struct Flags {
// Indicates the path by which the order was created. Important for reclaim.
created_on_chain: bool
// Either Buy or Sell
kind: OrderKind
partially_fillable: bool
Expand Down Expand Up @@ -208,6 +210,8 @@ Allocating an order PDA requires paying rent.

If the order is expired, anyone can close the order account through the `ReclaimOrder` instruction. On account closure, the rent is sent to the order's `created_by` account, i.e., the original creator of the order.

If an order created on-chain (`created_on_chain`), it can safely be closed earlier. In this case, `ReclaimOrder` will additionally allow reclaiming of orders that are cancelled or completely filled.

This is useful for solvers who need to allocate the order for executing it, but the allocation itself would be orders of magnitude more expensive than the compute cost for executing an instruction. This is particularly relevant to make small orders economically viable.

## Authenticating an order
Expand Down Expand Up @@ -235,6 +239,8 @@ Raw Ed25519 signatures are supported by all native Solana accounts.

The data to be signed is encoded as an off-chain message and signed with raw Ed25519 signatures.

Orders that are created by this path must specify the flag `created_on_chain = false`.

Differences with Ethereum:

- Unlike ECDSA signatures in Ethereum, the owner account address cannot be recovered from the Ed25519 signature. This means that the address needs to be included as part of the signed data.
Expand All @@ -248,6 +254,8 @@ The order owner executes the `CreateOrder` instruction. The settlement program c

In this authentication flow, the user needs to pay for the rent in SOL necessary to create the PDA. Note that the rent may be significantly higher than the expected trading fee. The rent can be recovered by the user once the order has expired by [clearing the order](#order-clearing).

Orders that are created by this path must specify the flag `created_on_chain = true`.

This flow supports both standard ("on-curve") accounts and PDA signatures.

This flow is the only one allowing on-chain programs to trade through the settlement program.
Expand Down
36 changes: 24 additions & 12 deletions bench-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7,
"reclaim_buffer/max_buffers_in_one_instruction": 64,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 4,
"reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 4,
"reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 3,
"reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 3,
"reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 3,
"reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 3,
"settle/finalizes_with_no_pushes": 5,
"settle/pulls_from_multiple_orders": 15,
"settle/pulls_funds_to_destination": 10,
Expand All @@ -31,22 +35,26 @@
"create_buffers/happy_path_creates_initialized_buffer_token_account": 10345,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21743,
"create_buffers/max_buffers_in_one_instruction": 177040,
"create_order/happy_path_creates_order_pda_with_expected_body": 4978,
"create_order/happy_path_creates_order_pda_with_expected_body": 9485,
Comment thread
fedgiac marked this conversation as resolved.
"initialize/happy_path_initializes_state_pda_with_expected_data": 4529,
"reclaim_buffer/funded_buffer_is_skipped": 6335,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7483,
"reclaim_buffer/max_buffers_in_one_instruction": 136652,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18082,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183,
"reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202,
"reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2071,
"reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079,
"reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null,
"reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null,
"settle/finalizes_with_no_pushes": 7154,
"settle/pulls_from_multiple_orders": 20043,
"settle/pulls_funds_to_destination": 13632,
"settle/pulls_to_multiple_destinations": 14773,
"settle/pushes_a_single_order": 12487,
"settle/pushes_several_orders_from_different_buffers": 17751,
"settle/pushes_several_orders_from_one_buffer": 17750,
"settle/settles_a_single_order": 12505,
"settle/settles_multiple_orders": 23062,
"settle/pulls_from_multiple_orders": 20059,
"settle/pulls_funds_to_destination": 13640,
"settle/pulls_to_multiple_destinations": 14781,
"settle/pushes_a_single_order": 12495,
"settle/pushes_several_orders_from_different_buffers": 17767,
"settle/pushes_several_orders_from_one_buffer": 17766,
"settle/settles_a_single_order": 12513,
"settle/settles_multiple_orders": 23086,
"transfer_authority/manager_can_transfer_manager": 3174,
"transfer_authority/manager_can_transfer_reclaim_authority": 3176,
"transfer_authority/reclaim_authority_can_transfer_itself": 3180
Expand All @@ -63,7 +71,11 @@
"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,
"reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 236,
"reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 204,
"reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 204,
"reclaim_order/off_chain_order_is_reclaimable_only_once_expired": 204,
"reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": 204,
"settle/finalizes_with_no_pushes": 290,
"settle/pulls_from_multiple_orders": 656,
"settle/pulls_funds_to_destination": 473,
Expand Down
110 changes: 53 additions & 57 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! - [`OrderIntent`] is the idiomatic Rust representation.
//! - [`EncodedOrderIntent`] is its canonical byte representation: the only
//! thing sent on the wire and also the data encoding used to generate the
//! order UID.
//! order UID. There, `kind` and the intent's booleans share a single flags
//! byte.
//!
//! Conversion is asymmetric: [`EncodedOrderIntent`]`::from(OrderIntent)` is
//! infallible, but decoding raw bytes via [`OrderIntent`]`::try_from` returns
Expand Down Expand Up @@ -33,6 +34,12 @@ pub enum OrderKind {
/// Collection of [`OrderIntent`] fields that can be represented as a single bit.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub struct Flags {
/// How the order is authenticated: `true` if the owner creates it
/// themselves with a `CreateOrder` instruction they sign; `false` if it's
/// authenticated off-chain by an Ed25519 signature, which lets anyone
/// holding that signature create the order.
pub created_on_chain: bool,

/// Whether `sell_amount` or `buy_amount` is the exact figure; the
/// other side is treated as the limit (minimum to receive for `Sell`,
/// maximum to spend for `Buy`).
Expand All @@ -47,23 +54,27 @@ pub struct Flags {

impl Flags {
// The bit each field occupies
const PARTIALLY_FILLABLE: u8 = 1 << 1;
const KIND: u8 = 1 << 0;
const CREATED_ON_CHAIN: u8 = 1 << 0;
const KIND: u8 = 1 << 1;
const PARTIALLY_FILLABLE: u8 = 1 << 2;

/// Every bit the encoding defines; the others are reserved.
const DEFINED: u8 = Self::PARTIALLY_FILLABLE | Self::KIND;
const DEFINED: u8 = Self::CREATED_ON_CHAIN | Self::KIND | Self::PARTIALLY_FILLABLE;
}

impl From<Flags> for [u8; 1] {
/// The canonical flags byte. Reserved bits are left clear.
fn from(flags: Flags) -> Self {
let mut byte = 0;
if flags.partially_fillable {
byte |= Flags::PARTIALLY_FILLABLE;
if flags.created_on_chain {
byte |= Flags::CREATED_ON_CHAIN;
}
if flags.kind == OrderKind::Buy {
byte |= Flags::KIND;
}
if flags.partially_fillable {
byte |= Flags::PARTIALLY_FILLABLE;
}
[byte]
}
}
Expand All @@ -81,6 +92,7 @@ impl TryFrom<[u8; 1]> for Flags {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Flags {
created_on_chain: byte & Self::CREATED_ON_CHAIN != 0,
kind: if byte & Self::KIND == 0 {
OrderKind::Sell
} else {
Expand Down Expand Up @@ -350,10 +362,13 @@ pub mod fixtures {

/// Any valid [`Flags`].
pub fn arb_flags() -> impl Strategy<Value = Flags> {
(arb_order_kind(), any::<bool>()).prop_map(|(kind, partially_fillable)| Flags {
kind,
partially_fillable,
})
(any::<bool>(), arb_order_kind(), any::<bool>()).prop_map(
|(created_on_chain, kind, partially_fillable)| Flags {
created_on_chain,
kind,
partially_fillable,
},
)
}

/// Any flags byte the decoder accepts.
Expand Down Expand Up @@ -419,14 +434,18 @@ mod tests {
use super::fixtures::sample_intent;
use super::*;

// Every shape an `OrderIntent` can take on its validated axes: the `kind`
// enum and the `partially_fillable` flag bit.
// Every shape an `OrderIntent` can take on its validated axes: the
// `created_on_chain` flag bit, the `kind` enum, and the
// `partially_fillable` flag bit.
fn all_flag_shapes() -> impl Iterator<Item = OrderIntent> {
fixtures::ALL_ORDER_KINDS.into_iter().flat_map(|kind| {
[false, true].into_iter().map(move |partially_fillable| {
sample_intent(Flags {
kind,
partially_fillable,
[false, true].into_iter().flat_map(|created_on_chain| {
fixtures::ALL_ORDER_KINDS.into_iter().flat_map(move |kind| {
[false, true].into_iter().map(move |partially_fillable| {
sample_intent(Flags {
created_on_chain,
kind,
partially_fillable,
})
})
})
})
Expand Down Expand Up @@ -489,16 +508,17 @@ mod tests {
fn every_flag_owns_a_distinct_bit() {
let byte = |flags: Flags| <[u8; 1]>::from(flags)[0];
let cleared = Flags {
created_on_chain: false,
kind: OrderKind::Sell,
partially_fillable: false,
};
assert_eq!(byte(cleared), 0);

let set_one_by_one = [
(
Flags::PARTIALLY_FILLABLE,
Flags::CREATED_ON_CHAIN,
Flags {
partially_fillable: true,
created_on_chain: true,
..cleared
},
),
Expand All @@ -509,14 +529,21 @@ mod tests {
..cleared
},
),
(
Flags::PARTIALLY_FILLABLE,
Flags {
partially_fillable: true,
..cleared
},
),
];
let mut seen = 0u8;
for (bit, flags) in set_one_by_one {
assert_eq!(bit.count_ones(), 1, "a flag must occupy a single bit");
assert_eq!(seen & bit, 0, "two flags must not share a bit");
assert!(
seen == 0 || bit < seen,
"each flag must be less significant than the ones before it"
bit > seen,
"each flag must be more significant than the ones before it"
);
seen |= bit;
assert_eq!(byte(flags), bit);
Expand Down Expand Up @@ -548,39 +575,6 @@ mod tests {
}
}

#[test]
fn sanity_check_offsets() {
fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
lhs.iter().zip(rhs).position(|(l, r)| l != r)
}
let sell_false: EncodedOrderIntent = (&sample_intent(Flags {
kind: OrderKind::Sell,
partially_fillable: false,
}))
.into();
let sell_true: EncodedOrderIntent = (&sample_intent(Flags {
kind: OrderKind::Sell,
partially_fillable: true,
}))
.into();
let buy_true: EncodedOrderIntent = (&sample_intent(Flags {
kind: OrderKind::Buy,
partially_fillable: true,
}))
.into();

assert_eq!(
first_differing_byte(sell_false.as_slice(), sell_true.as_slice())
.expect("should have different flags byte"),
FLAGS_OFFSET
);
assert_eq!(
first_differing_byte(buy_true.as_slice(), sell_true.as_slice())
.expect("should have different flags byte"),
FLAGS_OFFSET
);
}

#[test]
fn decode_accepts_defined_flag_bits_only() {
let encoded = EncodedOrderIntent::from(&sample_intent(Default::default()));
Expand All @@ -606,18 +600,20 @@ mod tests {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
let intent = sample_intent(Flags {
created_on_chain: true,
kind: OrderKind::Buy,
partially_fillable: true,
});
assert_eq!(
hex(intent.uid().as_ref()),
"eddfac5ab968e8c8843c913f58f0ecb5061948a8558d8073dafe53f6f28d398a",
"de4096c6c100056f1e4636ea4fafefad40fc1d0b37692fe3ca1e0db3644b86bd",
);
}

#[test]
fn encoding_regression() {
let encoded = EncodedOrderIntent::from(&sample_intent(Flags {
created_on_chain: true,
kind: OrderKind::Buy,
partially_fillable: true,
}));
Expand Down Expand Up @@ -655,8 +651,8 @@ mod tests {
0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
// valid_to (0xdead_beef, LE u32)
0xef, 0xbe, 0xad, 0xde,
// flags (partially_fillable | kind (Buy = 1))
0b00000011,
// flags (created_on_chain | kind (Buy = 1) | partially_fillable)
0b00000111,
// app_data ([0x66; 32])
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
Expand Down
Loading