Allow ReclaimOrder of completed orders before expiry - #122
Merged
Conversation
`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
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
…protocol/solana-programs into kaze/sc-288-reclaim-completed-orders
fedgiac
reviewed
Aug 28, 2026
…protocol/solana-programs into kaze/sc-288-reclaim-completed-orders
fedgiac
reviewed
Aug 28, 2026
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
fedgiac
reviewed
Aug 28, 2026
fedgiac
reviewed
Aug 28, 2026
rename and explicitly confirm that valid_to is as expected
fedgiac
reviewed
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Allow
ReclaimOrderbefore expiry when the order was created on-chain andMotivation
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 declarecreated_on_chainReclaimOrderaccepts an unexpired order that carries the flag and is eithercancelled or fully filled.
ReclaimOrder'sOrderNotExpiredbecomesOrderNotReclaimable, since expiry is no longer the only route.BeginSettlealready computed inline. Moved toorder::fill_progressand backs a new helperOrderAccount::is_fully_filled.created_on_chaincould have gone onOrderAccountinstead ofOrderIntent,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
flagsbyte, 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
OrderIntentbecause it shifts the bits. It also changes the behavior ofCreateOrdersignificantly 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_onlycovers both orderkinds at, below and past the full amount; the intent codec tests extend to the
third flag bit.
programs/settlementunit tests:early_reclaim_conditionscovers thecreated_on_chain × cancelled × fillmatrix, andprocess_create_order_rejects_intent_not_created_on_chaincovers the newcreation check.
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.