Skip to content

Allow ReclaimOrder of completed orders before expiry - #122

Merged
kaze-cow merged 28 commits into
mainfrom
kaze/sc-288-reclaim-completed-orders
Aug 28, 2026
Merged

Allow ReclaimOrder of completed orders before expiry#122
kaze-cow merged 28 commits into
mainfrom
kaze/sc-288-reclaim-completed-orders

Conversation

@kaze-cow

@kaze-cow kaze-cow commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Allow ReclaimOrder before expiry when the order was created on-chain and

  1. the order has been cancelled, OR
  2. the order has been fully filled

Motivation

We recently decided we are primarily going to support an on-chain order
placement scheme. This means that an order creation transaction can't
effectively be replayed at least with current cases, so we can safely reclaim
the account once the order has completed its lifecycle (either cancelled or
fully settled).

Implementation

A flag, created_on_chain, is introduced to record how the order originated, which determines whether early reclamation is possible.

  • CreateOrder, as an on-chain creation path, rejects an intent that doesn't declare created_on_chain
  • ReclaimOrder accepts an unexpired order that carries the flag and is either
    cancelled or fully filled.
  • ReclaimOrder's OrderNotExpired becomes OrderNotReclaimable, since expiry is no longer the only route.
  • Deciding "fully filled" means knowing which side of the order is the exact one, which BeginSettle already computed inline. Moved to order::fill_progress and backs a new helper OrderAccount::is_fully_filled.

created_on_chain could have gone on OrderAccount instead of OrderIntent,
but putting it in the intent means the owner's signature commits to the
authentication scheme. It also makes better use of the flags byte we already have.

The new flag was added to the least significant bit instead of the most significant as it makes it easier to tell, as a human, at a glance looking at the flags byte, whether or not the order was created on-chain or not.

It is confirmed that an order reclaimed within a settlement (between BeginSettle and FinalizeSettle) succeeds as long as it has been fully settled.

This is a BREAKING CHANGE for the OrderIntent because it shifts the bits. It also changes the behavior of CreateOrder significantly such that its no longer possible to place an order that would have previously worked with the same bits.

Testing Plan

  • interface: is_fully_filled_tracks_the_exact_side_only covers both order
    kinds at, below and past the full amount; the intent codec tests extend to the
    third flag bit.
  • programs/settlement unit tests: early_reclaim_conditions covers the
    created_on_chain × cancelled × fill matrix, and
    process_create_order_rejects_intent_not_created_on_chain covers the new
    creation check.
  • Integration tests reclaim a fully filled order, a cancelled order and a
    partially filled one before expiry, and check that an off-chain order that is
    both cancelled and fully filled still has to wait for expiry. They stage
    those preconditions by writing the order PDA directly, since no instruction
    sequence reaches them.
  • integration test to confirm what happens if the order is reclaimed within a settlement bbd8830

kaze-cow and others added 2 commits August 26, 2026 17:04
`kind` and `partially_fillable` each occupied a whole byte of the encoded
intent, and each needed its own range check on decode. Fold both into one
flags byte instead: `partially_fillable` takes the most significant defined
bit, `kind` the next one down, and every remaining bit is reserved and must
be zero.

`OrderIntent` keeps the idiomatic representation — an `OrderKind` enum and a
`bool` — so callers are unaffected. Only the wire format and the validation
change: a single mask check now rejects any byte carrying an undefined bit,
which keeps the encoding injective and so keeps order UIDs unique.

`EncodedOrderIntent::SIZE` goes 150 -> 149 and `EncodedOrderAccount::SIZE`
201 -> 200.

This is a BREAKING CHANGE to the `OrderIntent` encoding: it repurposes
existing bytes, so it moves every order PDA and changes every order UID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An order PDA could only be reclaimed once its `valid_to` had elapsed, because
a reclaimed order can be recreated and reclaiming early would let a completed
order be replayed. That reasoning only holds for orders authenticated by an
off-chain signature, which anyone holding the signature can recreate. An order
created on-chain needs its owner's signature to come back, so once it has
finished its lifecycle its account is safe to close.

Add a `created_on_chain` flag to the intent, taking the least significant bit
of the flags byte, and let `ReclaimOrder` accept an order that carries it and
is either cancelled or fully filled. `CreateOrder` rejects an intent that
doesn't declare the flag, so it always records the flow that actually created
the order. `OrderNotExpired` becomes `OrderNotReclaimable`, since expiry is no
longer the only route.

Deciding "fully filled" needs the exact side of the order, which `BeginSettle`
already computes: that logic moves to `order::fill_progress` and backs the new
`OrderAccount::is_fully_filled`.

This is a BREAKING CHANGE to the `OrderIntent` encoding: `created_on_chain`
shifts the other flag bits up, so it moves every order PDA and changes every
order UID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kaze-cow
kaze-cow requested a review from a team as a code owner August 26, 2026 08:09
@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown

SC-288

kaze-cow added a commit that referenced this pull request Aug 26, 2026
# Description

Fold the encoded `OrderIntent`'s `kind` and `partially_fillable` into a
single
flags byte.

Split out of the reclaim work in #116's original scope, as suggested in

#116 (review).
The follow-up that introduces the `created_on_chain` flag and the
early-reclaim
behaviour is stacked on top of this branch in #122 .

## Motivation

`kind` and `partially_fillable` each took a whole byte of the wire
format and
each needed its own range check on decode. Both carry a single bit of
information, so a flags byte replaces two validated bytes with one, and
leaves
room for the flags we know are coming.

# Implementation

`partially_fillable` takes the most significant defined bit, `kind` the
next one
down (`Sell` = 0, `Buy` = 1). Every remaining bit is reserved and must
be zero.

`OrderIntent` keeps the idiomatic representation — an `OrderKind` enum
and a
`bool` — so callers are unaffected. Only the encoding and its validation
change.

`EncodedOrderIntent::SIZE` goes 150 → 149 and
`EncodedOrderAccount::SIZE`
201 → 200.

This is a BREAKING CHANGE for the `OrderIntent` encoding because it
repurposes
existing bytes, and therefore it modifies the location of the order PDA
and
changes the hash of all orders.

# Testing Plan

The existing codec tests carry over to the new representation, plus:

* `every_flag_owns_a_distinct_bit` pins that each flag occupies one bit,
that no
two flags share a bit, that they run from the most significant defined
bit
  down, and that together they are exactly `FLAGS_MASK`.
* `decode_accepts_defined_flag_bits_only` walks all 256 flag bytes and
checks
  each one is either rejected or decodes to the expected `kind` and
  `partially_fillable`.
* The proptest `rejects_reserved_flag_bits` is the property-based
counterpart,
and `bytes_roundtrip` pins that any accepted byte string re-encodes to
itself.
* `uid_digest_regression` and `encoding_regression` are updated for the
new
  encoding.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from kaze/sc-288-on-chain-orders-should-be-reclaimable-when-order-is to main August 26, 2026 12:29
Comment thread interface/src/data/intent.rs
Comment thread interface/src/data/intent.rs
Comment thread interface/src/data/intent.rs Outdated
Comment thread interface/src/data/intent.rs Outdated
Comment thread interface/src/data/order.rs Outdated
Comment thread interface/src/data/order.rs
Comment thread interface/src/data/order.rs Outdated
Comment thread interface/src/data/order.rs Outdated
Comment thread programs/settlement/src/reclaim_order.rs Outdated
Comment thread programs/settlement/src/reclaim_order.rs
Comment thread bench-report.json
Comment thread DESIGN.md
Comment thread DESIGN.md Outdated
kaze-cow and others added 6 commits August 28, 2026 23:26
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
…protocol/solana-programs into kaze/sc-288-reclaim-completed-orders
Comment thread programs/settlement/tests/common/settlement.rs
Comment thread programs/settlement/tests/common/settlement.rs Outdated
Comment thread programs/settlement/tests/reclaim_order.rs Outdated
Comment thread programs/settlement/tests/reclaim_order.rs Outdated
Comment thread programs/settlement/tests/reclaim_order.rs
Comment thread programs/settlement/tests/reclaim_order.rs
Comment thread programs/settlement/tests/reclaim_order.rs
rename and explicitly confirm that valid_to is as expected
Comment thread programs/settlement/tests/common/settlement.rs

@fedgiac fedgiac left a comment

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.

Wonderful work!

@kaze-cow
kaze-cow merged commit 73032ad into main Aug 28, 2026
14 checks passed
@kaze-cow
kaze-cow deleted the kaze/sc-288-reclaim-completed-orders branch August 28, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants