From ce23f0c6114041e4e6ed849193e70b744aae6834 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:02:07 +0900 Subject: [PATCH 1/9] Add single-byte account discriminators via a SettlementAccount enum Prefixes every account this program owns with real storage with a 1-byte discriminator, mirroring how SettlementInstruction already identifies instructions: - New SettlementAccount enum (interface/src/lib.rs), following the same num_enum::TryFromPrimitive pattern as SettlementInstruction. Discriminators start at 128 and increment per account type, kept distinct from the instruction discriminators (0-4). - OrderAccount: discriminator 128 (OrderAccount), account grows 199 -> 200 bytes. - Settlement state PDA: new interface::data::state module, discriminator 129 (SettlementState), account grows 0 -> 1 byte (written on Initialize). An Anchor-style IDL's `discriminator` field can be any byte length, not just Anchor's own 8-byte sha256("account:...") convention, so a single byte is enough for IDL-driven tooling (e.g. Solscan) to identify the account type while costing far less rent than an 8-byte discriminator. Also adds a hand-written Anchor-compatible IDL (programs/settlement/idl/ cow_settlement.json) describing the program's instructions/accounts/types. Since this is a native/Pinocchio program rather than an Anchor program, several spots can't be fully expressed in the IDL grammar (BeginSettle's dynamically-shaped tail, order_pda's hashed PDA seed) and are documented inline via `docs` fields instead. Builds on the little-endian encoding from the previous PR. Co-Authored-By: Claude Sonnet 5 --- interface/src/data/mod.rs | 1 + interface/src/data/order.rs | 55 +- interface/src/data/state.rs | 40 + interface/src/instruction/initialize.rs | 2 +- interface/src/lib.rs | 50 ++ programs/settlement/idl/cow_settlement.json | 864 ++++++++++++++++++++ programs/settlement/src/initialize.rs | 11 +- programs/settlement/tests/initialize.rs | 14 +- 8 files changed, 1019 insertions(+), 18 deletions(-) create mode 100644 interface/src/data/state.rs create mode 100644 programs/settlement/idl/cow_settlement.json diff --git a/interface/src/data/mod.rs b/interface/src/data/mod.rs index 47a4a0f..8365c12 100644 --- a/interface/src/data/mod.rs +++ b/interface/src/data/mod.rs @@ -4,3 +4,4 @@ pub mod intent; pub mod order; +pub mod state; diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 81c23a4..80442bf 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -51,7 +51,7 @@ pub struct OrderAccount { pub intent: OrderIntent, } -/// Canonical 199-byte representation of an [`OrderAccount`]. The bytes +/// Canonical 200-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, @@ -60,31 +60,40 @@ pub struct OrderAccount { /// [`EncodedOrderIntent`]; see that type's docs for its inner layout. /// /// ```text -/// ┌──── cancelled +/// ┌──── discriminator +/// │┌─── cancelled /// ┌┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐ /// ││amount_│amount_│ │ │ /// ││with- │re- │ created_by │ intent (EncodedOrderIntent) │ /// ││drawn │ceived │ │ │ /// └┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ -/// 0 1 9 17 49 ... 199 +/// 0 1 2 10 18 ... 200 /// ``` +/// +/// The first byte is an IDL-style account discriminator (see +/// [`EncodedOrderAccount::DISCRIMINATOR`]), letting IDL-driven tooling (e.g. +/// Solscan) identify the account type before decoding the rest. #[derive(Clone, Debug, Deref, Eq, PartialEq)] pub struct EncodedOrderAccount([u8; Self::SIZE]); impl EncodedOrderAccount { // Per-field widths, derived from the `OrderAccount` field types. + const W_DISCRIMINATOR: usize = 1; const W_CANCELLED: usize = size_of::(); const W_AMOUNT_WITHDRAWN: usize = size_of::(); const W_AMOUNT_RECEIVED: usize = size_of::(); const W_CREATED_BY: usize = size_of::(); const W_INTENT: usize = EncodedOrderIntent::SIZE; - pub const SIZE: usize = 199; + pub const SIZE: usize = 200; + + /// Single-byte account discriminator. See [`crate::SettlementAccount`]. + pub const DISCRIMINATOR: [u8; 1] = [crate::SettlementAccount::OrderAccount.discriminator()]; /// Decode the account body and compute the embedded intent's UID in one /// shot, mirroring [`EncodedOrderIntent::decode_and_hash`]. Decoding - /// validates the intent; returns [`ProgramError::InvalidAccountData`] on a - /// decode error. + /// validates the discriminator and the intent; returns + /// [`ProgramError::InvalidAccountData`] on a decode error. pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> { let order_account = OrderAccount::try_from(*bytes)?; // The order UID is the hash of the intent's canonical bytes. Decoding @@ -111,14 +120,23 @@ pub fn write_account( created_by: &Pubkey, encoded_intent: &[u8; EncodedOrderIntent::SIZE], ) { - let (cancelled_slot, amount_withdrawn_slot, amount_received_slot, created_by_slot, intent_slot) = mut_array_refs![ + let ( + discriminator_slot, + cancelled_slot, + amount_withdrawn_slot, + amount_received_slot, + created_by_slot, + intent_slot, + ) = mut_array_refs![ buffer, + EncodedOrderAccount::W_DISCRIMINATOR, EncodedOrderAccount::W_CANCELLED, EncodedOrderAccount::W_AMOUNT_WITHDRAWN, EncodedOrderAccount::W_AMOUNT_RECEIVED, EncodedOrderAccount::W_CREATED_BY, EncodedOrderAccount::W_INTENT ]; + *discriminator_slot = EncodedOrderAccount::DISCRIMINATOR; *cancelled_slot = [cancelled as u8]; *amount_withdrawn_slot = amount_withdrawn.to_le_bytes(); *amount_received_slot = amount_received.to_le_bytes(); @@ -151,8 +169,9 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { type Error = ProgramError; fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result { - let (cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![ + let (discriminator, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![ &bytes, + EncodedOrderAccount::W_DISCRIMINATOR, EncodedOrderAccount::W_CANCELLED, EncodedOrderAccount::W_AMOUNT_WITHDRAWN, EncodedOrderAccount::W_AMOUNT_RECEIVED, @@ -160,6 +179,10 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { EncodedOrderAccount::W_INTENT ]; + if *discriminator != EncodedOrderAccount::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + Ok(OrderAccount { cancelled: match cancelled { [0] => false, @@ -193,8 +216,8 @@ pub mod fixtures { }; // Hardcoded but verified in a sanity-check test. - pub const CANCELLED_OFFSET: usize = 0; - pub const INTENT_OFFSET: usize = 49; + pub const CANCELLED_OFFSET: usize = 1; + pub const INTENT_OFFSET: usize = 50; /// Hand-picked example order account wrapping [`sample_intent`]. pub fn sample_account(cancelled: bool) -> OrderAccount { @@ -321,6 +344,16 @@ mod tests { ); } + #[test] + fn decode_rejects_wrong_discriminator() { + let mut bytes: [u8; EncodedOrderAccount::SIZE] = + EncodedOrderAccount::from(sample_account(false)).into(); + bytes[0] ^= 0xff; + let err = + OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected"); + assert_eq!(err, ProgramError::InvalidAccountData); + } + #[test] fn decode_rejects_non_boolean_cancelled() { let mut bytes: [u8; EncodedOrderAccount::SIZE] = @@ -416,6 +449,8 @@ mod tests { kind in arb_order_kind(), partially_fillable in any::(), ) { + bytes[..EncodedOrderAccount::DISCRIMINATOR.len()] + .copy_from_slice(&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; diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs new file mode 100644 index 0000000..1c260dc --- /dev/null +++ b/interface/src/data/state.rs @@ -0,0 +1,40 @@ +//! Settlement state PDA body. +//! +//! The state PDA carries no fields of its own (see [`crate::pda::state`]); its +//! only content is the account discriminator, so that IDL-driven tooling can +//! identify the account type. + +use solana_program_error::ProgramError; + +use crate::SettlementAccount; + +/// Canonical size of the settlement state PDA's body: just the discriminator. +pub const SIZE: usize = 1; + +/// Single-byte account discriminator. See [`crate::SettlementAccount`]. +pub const DISCRIMINATOR: [u8; SIZE] = [SettlementAccount::SettlementState.discriminator()]; + +/// Validate that `bytes` carries the expected discriminator. +pub fn decode(bytes: &[u8; SIZE]) -> Result<(), ProgramError> { + if *bytes != DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_accepts_discriminator() { + assert_eq!(decode(&DISCRIMINATOR), Ok(())); + } + + #[test] + fn decode_rejects_wrong_discriminator() { + let mut bytes = DISCRIMINATOR; + bytes[0] ^= 0xff; + assert_eq!(decode(&bytes), Err(ProgramError::InvalidAccountData)); + } +} diff --git a/interface/src/instruction/initialize.rs b/interface/src/instruction/initialize.rs index ce37dfd..16d0fea 100644 --- a/interface/src/instruction/initialize.rs +++ b/interface/src/instruction/initialize.rs @@ -54,7 +54,7 @@ impl From for Instruction { /// Parsed inputs of an `Initialize` instruction. pub struct InitializeInput<'a> { pub payer: &'a AccountView, - pub state_pda: &'a AccountView, + pub state_pda: &'a mut AccountView, } impl<'a> InstructionInputParsing<'a> for InitializeInput<'a> { diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 732831d..c5590e4 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -34,6 +34,30 @@ impl SettlementInstruction { } } +/// Identifies the account type a given account's data belongs to, via the +/// single discriminator byte stored at its front. Starts at 128 to keep +/// account discriminators visually distinct from instruction discriminators. +#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] +#[repr(u8)] +#[num_enum(error_type( + name = ProgramError, + constructor = SettlementAccount::unknown_discriminator, +))] +pub enum SettlementAccount { + OrderAccount = 128, + SettlementState = 129, +} + +impl SettlementAccount { + pub const fn discriminator(self) -> u8 { + self as u8 + } + + fn unknown_discriminator(_: u8) -> ProgramError { + ProgramError::InvalidAccountData + } +} + /// Recover the discriminator from the first byte of the payload and the /// remaining bytes to parse. /// Returns `InvalidInstructionData` for an insufficient length or an @@ -175,4 +199,30 @@ mod tests { Ok(SettlementInstruction::BeginSettle) ); } + + #[test] + fn settlement_account_try_from_partitions_all_bytes() { + for i in u8::MIN..=u8::MAX { + match SettlementAccount::try_from(i) { + Ok(account) => assert_eq!(account as u8, i), + Err(err) => assert_eq!(err, ProgramError::InvalidAccountData), + } + } + } + + #[test] + fn settlement_account_try_from_matches_order_account() { + assert_eq!( + SettlementAccount::try_from(128), + Ok(SettlementAccount::OrderAccount) + ); + } + + #[test] + fn settlement_account_discriminators_are_distinct() { + assert_ne!( + SettlementAccount::OrderAccount.discriminator(), + SettlementAccount::SettlementState.discriminator(), + ); + } } diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json new file mode 100644 index 0000000..1d4237a --- /dev/null +++ b/programs/settlement/idl/cow_settlement.json @@ -0,0 +1,864 @@ +{ + "address": "MooohhPEAAHwAwEozL7JPEmnDvaahuUpccYN4Yb8ccK", + "metadata": { + "name": "cow_settlement", + "version": "0.1.0", + "spec": "0.1.0", + "description": "CoW Protocol settlement program. HAND-WRITTEN IDL: this program is a native/Pinocchio program, not built with the Anchor framework, so this file was authored manually to describe its wire format as closely as the Anchor IDL grammar allows. See the per-instruction/type docs for spots where the on-chain format cannot be fully expressed (non-standard 1-byte instruction discriminators, and BeginSettle's dynamically-shaped tail)." + }, + "instructions": [ + { + "name": "initialize", + "docs": [ + "Creates the singleton settlement state PDA. Succeeds only once.", + "Non-standard discriminator: this program uses a single instruction-selector byte (see SettlementInstruction in the Rust source), not Anchor's usual 8-byte sighash. The `discriminator` below reflects the real on-chain bytes." + ], + "discriminator": [3], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Funds the state PDA's rent and pays the transaction fee." + ] + }, + { + "name": "state_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + } + ] + }, + "docs": [ + "Canonical PDA seeded by the literal string \"settlement\"." + ] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "create_buffer", + "docs": [ + "Creates one or more per-token buffer PDAs (SPL token accounts) in a single instruction.", + "IDL MODELING NOTE: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (CreateBuffer rejects zero buffers). Anchor's IDL grammar has no 'repeated group' construct, so this IDL instead declares up to 15 indexed pairs (buffer_pda_0/mint_0 .. buffer_pda_14/mint_14), matching this deployment's largest expected batch. buffer_pda_0/mint_0 are unconditionally present; the rest are marked optional and are only present up to however many buffers the specific transaction actually creates. A transaction creating more than 15 buffers in one instruction (unlikely given transaction size/CU limits) would have trailing accounts this IDL doesn't name.", + "Each buffer_pda_i must be the canonical PDA for seeds [\"settlement\", mint_i, \"buffer\"]." + ], + "discriminator": [4], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "buffer_pda_0", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_0" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "docs": [ + "Guaranteed present: CreateBuffer rejects an instruction with zero buffers." + ] + }, + { + "name": "mint_0", + "docs": [ + "Guaranteed present: CreateBuffer rejects an instruction with zero buffers." + ] + }, + { + "name": "buffer_pda_1", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_1" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 2 buffers." + ] + }, + { + "name": "mint_1", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 2 buffers." + ] + }, + { + "name": "buffer_pda_2", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_2" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 3 buffers." + ] + }, + { + "name": "mint_2", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 3 buffers." + ] + }, + { + "name": "buffer_pda_3", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_3" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 4 buffers." + ] + }, + { + "name": "mint_3", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 4 buffers." + ] + }, + { + "name": "buffer_pda_4", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_4" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 5 buffers." + ] + }, + { + "name": "mint_4", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 5 buffers." + ] + }, + { + "name": "buffer_pda_5", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_5" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 6 buffers." + ] + }, + { + "name": "mint_5", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 6 buffers." + ] + }, + { + "name": "buffer_pda_6", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_6" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 7 buffers." + ] + }, + { + "name": "mint_6", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 7 buffers." + ] + }, + { + "name": "buffer_pda_7", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_7" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 8 buffers." + ] + }, + { + "name": "mint_7", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 8 buffers." + ] + }, + { + "name": "buffer_pda_8", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_8" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 9 buffers." + ] + }, + { + "name": "mint_8", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 9 buffers." + ] + }, + { + "name": "buffer_pda_9", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_9" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 10 buffers." + ] + }, + { + "name": "mint_9", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 10 buffers." + ] + }, + { + "name": "buffer_pda_10", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_10" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 11 buffers." + ] + }, + { + "name": "mint_10", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 11 buffers." + ] + }, + { + "name": "buffer_pda_11", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_11" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 12 buffers." + ] + }, + { + "name": "mint_11", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 12 buffers." + ] + }, + { + "name": "buffer_pda_12", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_12" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 13 buffers." + ] + }, + { + "name": "mint_12", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 13 buffers." + ] + }, + { + "name": "buffer_pda_13", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_13" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 14 buffers." + ] + }, + { + "name": "mint_13", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 14 buffers." + ] + }, + { + "name": "buffer_pda_14", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] + }, + { + "kind": "account", + "path": "mint_14" + }, + { + "kind": "const", + "value": [98, 117, 102, 102, 101, 114] + } + ] + }, + "optional": true, + "docs": [ + "Present only if the instruction creates at least 15 buffers." + ] + }, + { + "name": "mint_14", + "optional": true, + "docs": [ + "Present only if the instruction creates at least 15 buffers." + ] + } + ], + "args": [] + }, + { + "name": "create_order", + "docs": [ + "Allocates a per-order PDA and writes the initial OrderAccount body.", + "order_pda's canonical seeds are [\"settlement\", sha256(intent_bytes), \"order\"]. This is not expressible as a static `pda` entry because the middle seed is a hash of the entire `intent` argument, not a plain field/account reference, which is outside what the Anchor PDA-seed grammar (const / arg / account) can describe." + ], + "discriminator": [2], + "accounts": [ + { + "name": "owner", + "signer": true, + "docs": [ + "Must match intent.owner; authenticates the order." + ] + }, + { + "name": "created_by", + "writable": true, + "signer": true, + "docs": [ + "Funds the new order PDA's rent." + ] + }, + { + "name": "order_pda", + "writable": true, + "docs": [ + "See the seed-derivation note above; cannot be auto-derived by IDL-driven tooling." + ] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "intent", + "type": { + "defined": { + "name": "OrderIntent" + } + } + } + ] + }, + { + "name": "begin_settle", + "docs": [ + "Pulls funds for a batch of orders. Must be paired in the same transaction with a FinalizeSettle at `finalize_ix_index`.", + "IDL LIMITATION: only the fixed-size prefix (finalize_ix_index) is represented as a typed argument. After it, the real wire format is `[order_count: u8][bump; order_count][transfer_count; order_count][amount: u64 BE; sum(transfer_count)]` \u2014 a hand-packed layout with no Borsh length prefixes and a trailing array whose length depends on the sum of an earlier array. This has no representation in the Anchor/Borsh/IDL type grammar (no vec/array type can express 'shared count governs several sibling arrays' or 'length = sum of another field'), so it is intentionally left out of `args` rather than mislabeled as `bytes` (which would imply a Borsh Vec length prefix that isn't actually present and would make a generic decoder misparse it). Anchor/Solscan-style tooling will only decode the discriminator and finalize_ix_index for this instruction; the remainder needs bespoke client logic.", + "Per-order accounts follow the 3 shared accounts below as a repeated remaining-accounts group: [order_pda (readonly), sell_token_account (writable), destination (writable) x transfer_count], sorted by ascending order_pda address." + ], + "discriminator": [0], + "accounts": [ + { + "name": "instructions_sysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "state_pda", + "docs": [ + "Must be the canonical state PDA; signs each pull as the user's SPL delegate." + ] + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "finalize_ix_index", + "type": "u16", + "docs": [ + "Index of the paired FinalizeSettle in this transaction. Little-endian, matching standard Borsh/Anchor u16 decoding." + ] + } + ] + }, + { + "name": "finalize_settle", + "docs": [ + "Validates that a BeginSettle at `begin_ix_index` exists and points back at this instruction. Must not be called via CPI.", + "Non-standard discriminator, see the note on `initialize`." + ], + "discriminator": [1], + "accounts": [ + { + "name": "instructions_sysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + } + ], + "args": [ + { + "name": "begin_ix_index", + "type": "u16", + "docs": [ + "Index of the paired BeginSettle in this transaction. Little-endian, matching standard Borsh/Anchor u16 decoding." + ] + } + ] + } + ], + "accounts": [ + { + "name": "OrderAccount", + "discriminator": [128] + }, + { + "name": "SettlementState", + "discriminator": [129] + } + ], + "types": [ + { + "name": "OrderAccount", + "docs": [ + "Body of an order PDA, created by create_order. 200 bytes total: 1-byte discriminator + 199 bytes of fields below." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "cancelled", + "type": "bool" + }, + { + "name": "amount_withdrawn", + "type": "u64" + }, + { + "name": "amount_received", + "type": "u64" + }, + { + "name": "created_by", + "type": "pubkey" + }, + { + "name": "intent", + "type": { + "defined": { + "name": "OrderIntent" + } + } + } + ] + } + }, + { + "name": "SettlementState", + "docs": [ + "Body of the singleton settlement state PDA. Carries no fields beyond the 1-byte discriminator; its existence and address are what matter." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "OrderIntent", + "docs": [ + "Canonical 150-byte order intent. Also the exact bytes hashed (SHA-256) to produce the order UID used in the order PDA's seeds, and the exact wire format of create_order's `intent` argument. Field order and encoding here are load-bearing: they must match this program's Rust definition exactly." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "buy_token_account", + "type": "pubkey" + }, + { + "name": "sell_token_account", + "type": "pubkey" + }, + { + "name": "sell_amount", + "type": "u64" + }, + { + "name": "buy_amount", + "type": "u64" + }, + { + "name": "valid_to", + "type": "u32" + }, + { + "name": "kind", + "type": { + "defined": { + "name": "OrderKind" + } + } + }, + { + "name": "partially_fillable", + "type": "bool" + }, + { + "name": "app_data", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "OrderKind", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Sell" + }, + { + "name": "Buy" + } + ] + } + } + ], + "errors": [ + { + "code": 0, + "name": "FinalizeBeforeInitialize", + "msg": "The FinalizeSettle included as input to BeginSettle isn't before the actual BeginSettle index." + }, + { + "code": 1, + "name": "BeginFinalizePairOverlap", + "msg": "Another BeginSettle/FinalizeSettle of this program appears strictly between this pair's bounds, nesting or overlapping two settlements." + }, + { + "code": 2, + "name": "MissingCounterpartInstruction", + "msg": "The counterpart index points past the end of the transaction's instruction list, so no instruction sits there." + }, + { + "code": 3, + "name": "CounterpartIsExternal", + "msg": "The instruction at the counterpart index belongs to a different program." + }, + { + "code": 4, + "name": "InvalidCounterpartDiscriminator", + "msg": "The counterpart instruction's discriminator byte couldn't be recovered from its data." + }, + { + "code": 5, + "name": "InvalidCounterpartCounterpart", + "msg": "The counterpart instruction's own counterpart index couldn't be recovered from its data." + }, + { + "code": 6, + "name": "MismatchedCounterpartDiscriminator", + "msg": "The counterpart's discriminator isn't the expected BeginSettle/FinalizeSettle kind, or its counterpart index doesn't point back at this instruction." + }, + { + "code": 7, + "name": "OwnerMismatch", + "msg": "CreateOrder instruction wasn't signed by the created OrderIntent owner." + }, + { + "code": 8, + "name": "OrderNotCanonical", + "msg": "A BeginSettle order account doesn't sit at the canonical order PDA derived from the intent it stores and the supplied bump." + }, + { + "code": 9, + "name": "OrdersNotStrictlyIncreasing", + "msg": "BeginSettle's order accounts aren't passed strictly increasing by address." + }, + { + "code": 10, + "name": "SellTokenAccountMismatch", + "msg": "A BeginSettle sell token account doesn't match the sell_token_account recorded in the order's intent." + }, + { + "code": 11, + "name": "SellTokenAccountInvalid", + "msg": "A BeginSettle sell token account isn't a valid SPL token account (wrong data length or not owned by the token program)." + }, + { + "code": 12, + "name": "SellTokenOwnerMismatch", + "msg": "A BeginSettle sell token account's SPL owner isn't the order's intent owner." + }, + { + "code": 13, + "name": "AccountCountNotMatchingOrderCount", + "msg": "BeginSettle's order-account count doesn't match the structure its instruction data expects." + }, + { + "code": 14, + "name": "CalledViaCpi", + "msg": "BeginSettle or FinalizeSettle was invoked via CPI rather than as a top-level transaction instruction." + }, + { + "code": 15, + "name": "OrderCancelled", + "msg": "A BeginSettle order has been cancelled by its owner and can no longer be settled." + }, + { + "code": 16, + "name": "OrderExpired", + "msg": "A BeginSettle order's valid_to lies in the past: the order has expired and can no longer be settled." + }, + { + "code": 17, + "name": "TransferCountMismatch", + "msg": "The transfer counts in BeginSettle don't sum to the number of transfer amounts, so destinations and amounts can't be paired up exactly." + }, + { + "code": 18, + "name": "StateAccountMismatch", + "msg": "BeginSettle's state account isn't the canonical settlement state PDA, which must sign the pulls as the user's token delegate." + } + ] +} diff --git a/programs/settlement/src/initialize.rs b/programs/settlement/src/initialize.rs index 2ccf19e..653cd7a 100644 --- a/programs/settlement/src/initialize.rs +++ b/programs/settlement/src/initialize.rs @@ -1,7 +1,8 @@ //! `Initialize` instruction handler. -use pinocchio::{AccountView, Address, ProgramResult}; +use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; use settlement_interface::{ + data::state, instruction::{initialize::InitializeInput, InstructionInputParsing}, pda::state::state_pda_seeds, }; @@ -25,12 +26,18 @@ pub fn process_initialize( program_id, payer, pda: state_pda, - size: 0, + size: state::SIZE as u64, owner: program_id, seeds: state_pda_seeds(), } .create()?; + let mut buffer = state_pda.try_borrow_mut()?; + let buffer: &mut [u8; state::SIZE] = (&mut *buffer) + .try_into() + .map_err(|_| ProgramError::AccountDataTooSmall)?; + *buffer = state::DISCRIMINATOR; + Ok(()) } diff --git a/programs/settlement/tests/initialize.rs b/programs/settlement/tests/initialize.rs index 0f10753..cd91997 100644 --- a/programs/settlement/tests/initialize.rs +++ b/programs/settlement/tests/initialize.rs @@ -1,6 +1,6 @@ use settlement_client::instructions::Initialize; use settlement_client::settlement_interface::{ - instruction::initialize::Initialize as InitializeRaw, pda::state::find_state_pda, + data::state, instruction::initialize::Initialize as InitializeRaw, pda::state::find_state_pda, }; use solana_sdk::{ pubkey::Pubkey, @@ -10,7 +10,7 @@ use solana_sdk::{ mod common; #[test] -fn happy_path_initializes_empty_state_pda() { +fn happy_path_initializes_state_pda_with_discriminator() { let (mut svm, program_id, payer) = common::setup(); let (state_pda, _bump) = find_state_pda(&program_id); @@ -30,9 +30,13 @@ fn happy_path_initializes_empty_state_pda() { account.owner, program_id, "state PDA must be owned by the settlement program" ); - assert!(account.data.is_empty(), "state PDA must be empty"); + assert_eq!( + account.data, + state::DISCRIMINATOR, + "state PDA must hold only the Anchor-style discriminator" + ); - let rent = svm.minimum_balance_for_rent_exemption(0); + let rent = svm.minimum_balance_for_rent_exemption(state::SIZE); assert_eq!( account.lamports, rent, "state PDA must hold exactly the rent minimum: {} != {}", @@ -59,7 +63,7 @@ fn funding_payer_can_differ_from_fee_payer() { // The rent came out of the funder, not the fee payer: the funder paid no // transaction fee, so its balance dropped by exactly the PDA rent. - let rent = svm.minimum_balance_for_rent_exemption(0); + let rent = svm.minimum_balance_for_rent_exemption(state::SIZE); assert_eq!( common::lamports(&svm, &funder.pubkey()), funder_airdrop - rent, From 0e744185d89f16b87640f3ddc9e44a124b6026ce Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:04:53 +0900 Subject: [PATCH 2/9] fmt Co-Authored-By: Claude Sonnet 5 --- interface/src/data/order.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 80442bf..edea997 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -349,8 +349,7 @@ mod tests { let mut bytes: [u8; EncodedOrderAccount::SIZE] = EncodedOrderAccount::from(sample_account(false)).into(); bytes[0] ^= 0xff; - let err = - OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected"); + let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected"); assert_eq!(err, ProgramError::InvalidAccountData); } From c1812695f9443ab6b27bedf6f2127c60b7443c6a Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:08:49 +0900 Subject: [PATCH 3/9] Move the IDL to its own PR The hand-written IDL doesn't need to land together with the code changes it describes; splitting it out so it can be reviewed and iterated on separately (and marked draft while we figure out how to validate it). Co-Authored-By: Claude Sonnet 5 --- programs/settlement/idl/cow_settlement.json | 864 -------------------- 1 file changed, 864 deletions(-) delete mode 100644 programs/settlement/idl/cow_settlement.json diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json deleted file mode 100644 index 1d4237a..0000000 --- a/programs/settlement/idl/cow_settlement.json +++ /dev/null @@ -1,864 +0,0 @@ -{ - "address": "MooohhPEAAHwAwEozL7JPEmnDvaahuUpccYN4Yb8ccK", - "metadata": { - "name": "cow_settlement", - "version": "0.1.0", - "spec": "0.1.0", - "description": "CoW Protocol settlement program. HAND-WRITTEN IDL: this program is a native/Pinocchio program, not built with the Anchor framework, so this file was authored manually to describe its wire format as closely as the Anchor IDL grammar allows. See the per-instruction/type docs for spots where the on-chain format cannot be fully expressed (non-standard 1-byte instruction discriminators, and BeginSettle's dynamically-shaped tail)." - }, - "instructions": [ - { - "name": "initialize", - "docs": [ - "Creates the singleton settlement state PDA. Succeeds only once.", - "Non-standard discriminator: this program uses a single instruction-selector byte (see SettlementInstruction in the Rust source), not Anchor's usual 8-byte sighash. The `discriminator` below reflects the real on-chain bytes." - ], - "discriminator": [3], - "accounts": [ - { - "name": "payer", - "writable": true, - "signer": true, - "docs": [ - "Funds the state PDA's rent and pays the transaction fee." - ] - }, - { - "name": "state_pda", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - } - ] - }, - "docs": [ - "Canonical PDA seeded by the literal string \"settlement\"." - ] - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - } - ], - "args": [] - }, - { - "name": "create_buffer", - "docs": [ - "Creates one or more per-token buffer PDAs (SPL token accounts) in a single instruction.", - "IDL MODELING NOTE: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (CreateBuffer rejects zero buffers). Anchor's IDL grammar has no 'repeated group' construct, so this IDL instead declares up to 15 indexed pairs (buffer_pda_0/mint_0 .. buffer_pda_14/mint_14), matching this deployment's largest expected batch. buffer_pda_0/mint_0 are unconditionally present; the rest are marked optional and are only present up to however many buffers the specific transaction actually creates. A transaction creating more than 15 buffers in one instruction (unlikely given transaction size/CU limits) would have trailing accounts this IDL doesn't name.", - "Each buffer_pda_i must be the canonical PDA for seeds [\"settlement\", mint_i, \"buffer\"]." - ], - "discriminator": [4], - "accounts": [ - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "buffer_pda_0", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_0" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "docs": [ - "Guaranteed present: CreateBuffer rejects an instruction with zero buffers." - ] - }, - { - "name": "mint_0", - "docs": [ - "Guaranteed present: CreateBuffer rejects an instruction with zero buffers." - ] - }, - { - "name": "buffer_pda_1", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_1" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 2 buffers." - ] - }, - { - "name": "mint_1", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 2 buffers." - ] - }, - { - "name": "buffer_pda_2", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_2" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 3 buffers." - ] - }, - { - "name": "mint_2", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 3 buffers." - ] - }, - { - "name": "buffer_pda_3", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_3" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 4 buffers." - ] - }, - { - "name": "mint_3", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 4 buffers." - ] - }, - { - "name": "buffer_pda_4", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_4" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 5 buffers." - ] - }, - { - "name": "mint_4", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 5 buffers." - ] - }, - { - "name": "buffer_pda_5", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_5" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 6 buffers." - ] - }, - { - "name": "mint_5", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 6 buffers." - ] - }, - { - "name": "buffer_pda_6", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_6" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 7 buffers." - ] - }, - { - "name": "mint_6", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 7 buffers." - ] - }, - { - "name": "buffer_pda_7", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_7" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 8 buffers." - ] - }, - { - "name": "mint_7", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 8 buffers." - ] - }, - { - "name": "buffer_pda_8", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_8" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 9 buffers." - ] - }, - { - "name": "mint_8", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 9 buffers." - ] - }, - { - "name": "buffer_pda_9", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_9" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 10 buffers." - ] - }, - { - "name": "mint_9", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 10 buffers." - ] - }, - { - "name": "buffer_pda_10", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_10" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 11 buffers." - ] - }, - { - "name": "mint_10", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 11 buffers." - ] - }, - { - "name": "buffer_pda_11", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_11" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 12 buffers." - ] - }, - { - "name": "mint_11", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 12 buffers." - ] - }, - { - "name": "buffer_pda_12", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_12" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 13 buffers." - ] - }, - { - "name": "mint_12", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 13 buffers." - ] - }, - { - "name": "buffer_pda_13", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_13" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 14 buffers." - ] - }, - { - "name": "mint_13", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 14 buffers." - ] - }, - { - "name": "buffer_pda_14", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [115, 101, 116, 116, 108, 101, 109, 101, 110, 116] - }, - { - "kind": "account", - "path": "mint_14" - }, - { - "kind": "const", - "value": [98, 117, 102, 102, 101, 114] - } - ] - }, - "optional": true, - "docs": [ - "Present only if the instruction creates at least 15 buffers." - ] - }, - { - "name": "mint_14", - "optional": true, - "docs": [ - "Present only if the instruction creates at least 15 buffers." - ] - } - ], - "args": [] - }, - { - "name": "create_order", - "docs": [ - "Allocates a per-order PDA and writes the initial OrderAccount body.", - "order_pda's canonical seeds are [\"settlement\", sha256(intent_bytes), \"order\"]. This is not expressible as a static `pda` entry because the middle seed is a hash of the entire `intent` argument, not a plain field/account reference, which is outside what the Anchor PDA-seed grammar (const / arg / account) can describe." - ], - "discriminator": [2], - "accounts": [ - { - "name": "owner", - "signer": true, - "docs": [ - "Must match intent.owner; authenticates the order." - ] - }, - { - "name": "created_by", - "writable": true, - "signer": true, - "docs": [ - "Funds the new order PDA's rent." - ] - }, - { - "name": "order_pda", - "writable": true, - "docs": [ - "See the seed-derivation note above; cannot be auto-derived by IDL-driven tooling." - ] - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - } - ], - "args": [ - { - "name": "intent", - "type": { - "defined": { - "name": "OrderIntent" - } - } - } - ] - }, - { - "name": "begin_settle", - "docs": [ - "Pulls funds for a batch of orders. Must be paired in the same transaction with a FinalizeSettle at `finalize_ix_index`.", - "IDL LIMITATION: only the fixed-size prefix (finalize_ix_index) is represented as a typed argument. After it, the real wire format is `[order_count: u8][bump; order_count][transfer_count; order_count][amount: u64 BE; sum(transfer_count)]` \u2014 a hand-packed layout with no Borsh length prefixes and a trailing array whose length depends on the sum of an earlier array. This has no representation in the Anchor/Borsh/IDL type grammar (no vec/array type can express 'shared count governs several sibling arrays' or 'length = sum of another field'), so it is intentionally left out of `args` rather than mislabeled as `bytes` (which would imply a Borsh Vec length prefix that isn't actually present and would make a generic decoder misparse it). Anchor/Solscan-style tooling will only decode the discriminator and finalize_ix_index for this instruction; the remainder needs bespoke client logic.", - "Per-order accounts follow the 3 shared accounts below as a repeated remaining-accounts group: [order_pda (readonly), sell_token_account (writable), destination (writable) x transfer_count], sorted by ascending order_pda address." - ], - "discriminator": [0], - "accounts": [ - { - "name": "instructions_sysvar", - "address": "Sysvar1nstructions1111111111111111111111111" - }, - { - "name": "state_pda", - "docs": [ - "Must be the canonical state PDA; signs each pull as the user's SPL delegate." - ] - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - } - ], - "args": [ - { - "name": "finalize_ix_index", - "type": "u16", - "docs": [ - "Index of the paired FinalizeSettle in this transaction. Little-endian, matching standard Borsh/Anchor u16 decoding." - ] - } - ] - }, - { - "name": "finalize_settle", - "docs": [ - "Validates that a BeginSettle at `begin_ix_index` exists and points back at this instruction. Must not be called via CPI.", - "Non-standard discriminator, see the note on `initialize`." - ], - "discriminator": [1], - "accounts": [ - { - "name": "instructions_sysvar", - "address": "Sysvar1nstructions1111111111111111111111111" - } - ], - "args": [ - { - "name": "begin_ix_index", - "type": "u16", - "docs": [ - "Index of the paired BeginSettle in this transaction. Little-endian, matching standard Borsh/Anchor u16 decoding." - ] - } - ] - } - ], - "accounts": [ - { - "name": "OrderAccount", - "discriminator": [128] - }, - { - "name": "SettlementState", - "discriminator": [129] - } - ], - "types": [ - { - "name": "OrderAccount", - "docs": [ - "Body of an order PDA, created by create_order. 200 bytes total: 1-byte discriminator + 199 bytes of fields below." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "cancelled", - "type": "bool" - }, - { - "name": "amount_withdrawn", - "type": "u64" - }, - { - "name": "amount_received", - "type": "u64" - }, - { - "name": "created_by", - "type": "pubkey" - }, - { - "name": "intent", - "type": { - "defined": { - "name": "OrderIntent" - } - } - } - ] - } - }, - { - "name": "SettlementState", - "docs": [ - "Body of the singleton settlement state PDA. Carries no fields beyond the 1-byte discriminator; its existence and address are what matter." - ], - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "OrderIntent", - "docs": [ - "Canonical 150-byte order intent. Also the exact bytes hashed (SHA-256) to produce the order UID used in the order PDA's seeds, and the exact wire format of create_order's `intent` argument. Field order and encoding here are load-bearing: they must match this program's Rust definition exactly." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "owner", - "type": "pubkey" - }, - { - "name": "buy_token_account", - "type": "pubkey" - }, - { - "name": "sell_token_account", - "type": "pubkey" - }, - { - "name": "sell_amount", - "type": "u64" - }, - { - "name": "buy_amount", - "type": "u64" - }, - { - "name": "valid_to", - "type": "u32" - }, - { - "name": "kind", - "type": { - "defined": { - "name": "OrderKind" - } - } - }, - { - "name": "partially_fillable", - "type": "bool" - }, - { - "name": "app_data", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "OrderKind", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Sell" - }, - { - "name": "Buy" - } - ] - } - } - ], - "errors": [ - { - "code": 0, - "name": "FinalizeBeforeInitialize", - "msg": "The FinalizeSettle included as input to BeginSettle isn't before the actual BeginSettle index." - }, - { - "code": 1, - "name": "BeginFinalizePairOverlap", - "msg": "Another BeginSettle/FinalizeSettle of this program appears strictly between this pair's bounds, nesting or overlapping two settlements." - }, - { - "code": 2, - "name": "MissingCounterpartInstruction", - "msg": "The counterpart index points past the end of the transaction's instruction list, so no instruction sits there." - }, - { - "code": 3, - "name": "CounterpartIsExternal", - "msg": "The instruction at the counterpart index belongs to a different program." - }, - { - "code": 4, - "name": "InvalidCounterpartDiscriminator", - "msg": "The counterpart instruction's discriminator byte couldn't be recovered from its data." - }, - { - "code": 5, - "name": "InvalidCounterpartCounterpart", - "msg": "The counterpart instruction's own counterpart index couldn't be recovered from its data." - }, - { - "code": 6, - "name": "MismatchedCounterpartDiscriminator", - "msg": "The counterpart's discriminator isn't the expected BeginSettle/FinalizeSettle kind, or its counterpart index doesn't point back at this instruction." - }, - { - "code": 7, - "name": "OwnerMismatch", - "msg": "CreateOrder instruction wasn't signed by the created OrderIntent owner." - }, - { - "code": 8, - "name": "OrderNotCanonical", - "msg": "A BeginSettle order account doesn't sit at the canonical order PDA derived from the intent it stores and the supplied bump." - }, - { - "code": 9, - "name": "OrdersNotStrictlyIncreasing", - "msg": "BeginSettle's order accounts aren't passed strictly increasing by address." - }, - { - "code": 10, - "name": "SellTokenAccountMismatch", - "msg": "A BeginSettle sell token account doesn't match the sell_token_account recorded in the order's intent." - }, - { - "code": 11, - "name": "SellTokenAccountInvalid", - "msg": "A BeginSettle sell token account isn't a valid SPL token account (wrong data length or not owned by the token program)." - }, - { - "code": 12, - "name": "SellTokenOwnerMismatch", - "msg": "A BeginSettle sell token account's SPL owner isn't the order's intent owner." - }, - { - "code": 13, - "name": "AccountCountNotMatchingOrderCount", - "msg": "BeginSettle's order-account count doesn't match the structure its instruction data expects." - }, - { - "code": 14, - "name": "CalledViaCpi", - "msg": "BeginSettle or FinalizeSettle was invoked via CPI rather than as a top-level transaction instruction." - }, - { - "code": 15, - "name": "OrderCancelled", - "msg": "A BeginSettle order has been cancelled by its owner and can no longer be settled." - }, - { - "code": 16, - "name": "OrderExpired", - "msg": "A BeginSettle order's valid_to lies in the past: the order has expired and can no longer be settled." - }, - { - "code": 17, - "name": "TransferCountMismatch", - "msg": "The transfer counts in BeginSettle don't sum to the number of transfer amounts, so destinations and amounts can't be paired up exactly." - }, - { - "code": 18, - "name": "StateAccountMismatch", - "msg": "BeginSettle's state account isn't the canonical settlement state PDA, which must sign the pulls as the user's token delegate." - } - ] -} From 8f0d37c82cb0882fb0756c13437cf6bb4c0de8be Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:11:07 +0900 Subject: [PATCH 4/9] fix minor issues --- interface/src/data/order.rs | 16 ++++++++-------- interface/src/data/state.rs | 4 ---- interface/src/lib.rs | 8 -------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index edea997..2a46ab6 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -61,13 +61,13 @@ pub struct OrderAccount { /// /// ```text /// ┌──── discriminator -/// │┌─── cancelled -/// ┌┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐ -/// ││amount_│amount_│ │ │ -/// ││with- │re- │ created_by │ intent (EncodedOrderIntent) │ -/// ││drawn │ceived │ │ │ -/// └┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ -/// 0 1 2 10 18 ... 200 +/// │ ┌─── cancelled +/// ┌──┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐ +/// │ │amount_│amount_│ │ │ +/// │ │with- │re- │ created_by │ intent (EncodedOrderIntent) │ +/// │ │drawn │ceived │ │ │ +/// └──┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ +/// 0 2 10 18 50 ... 200 /// ``` /// /// The first byte is an IDL-style account discriminator (see @@ -78,7 +78,7 @@ pub struct EncodedOrderAccount([u8; Self::SIZE]); impl EncodedOrderAccount { // Per-field widths, derived from the `OrderAccount` field types. - const W_DISCRIMINATOR: usize = 1; + const W_DISCRIMINATOR: usize = size_of::(); const W_CANCELLED: usize = size_of::(); const W_AMOUNT_WITHDRAWN: usize = size_of::(); const W_AMOUNT_RECEIVED: usize = size_of::(); diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs index 1c260dc..558bf7f 100644 --- a/interface/src/data/state.rs +++ b/interface/src/data/state.rs @@ -1,8 +1,4 @@ //! Settlement state PDA body. -//! -//! The state PDA carries no fields of its own (see [`crate::pda::state`]); its -//! only content is the account discriminator, so that IDL-driven tooling can -//! identify the account type. use solana_program_error::ProgramError; diff --git a/interface/src/lib.rs b/interface/src/lib.rs index c5590e4..baccdc7 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -210,14 +210,6 @@ mod tests { } } - #[test] - fn settlement_account_try_from_matches_order_account() { - assert_eq!( - SettlementAccount::try_from(128), - Ok(SettlementAccount::OrderAccount) - ); - } - #[test] fn settlement_account_discriminators_are_distinct() { assert_ne!( From f6c80f2f9f9322d26ee80629e38d3970ab9af442 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:04:00 +0900 Subject: [PATCH 5/9] Apply suggestion from @fedgiac Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/data/order.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 2a46ab6..972d98b 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -69,10 +69,6 @@ pub struct OrderAccount { /// └──┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ /// 0 2 10 18 50 ... 200 /// ``` -/// -/// The first byte is an IDL-style account discriminator (see -/// [`EncodedOrderAccount::DISCRIMINATOR`]), letting IDL-driven tooling (e.g. -/// Solscan) identify the account type before decoding the rest. #[derive(Clone, Debug, Deref, Eq, PartialEq)] pub struct EncodedOrderAccount([u8; Self::SIZE]); From 0932f831d0715f0d3ee65f62cc4e787835f7bdf1 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:09:25 +0900 Subject: [PATCH 6/9] Update programs/settlement/tests/initialize.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/initialize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/settlement/tests/initialize.rs b/programs/settlement/tests/initialize.rs index cd91997..cc3cd25 100644 --- a/programs/settlement/tests/initialize.rs +++ b/programs/settlement/tests/initialize.rs @@ -33,7 +33,7 @@ fn happy_path_initializes_state_pda_with_discriminator() { assert_eq!( account.data, state::DISCRIMINATOR, - "state PDA must hold only the Anchor-style discriminator" + "state PDA must hold only the discriminator" ); let rent = svm.minimum_balance_for_rent_exemption(state::SIZE); From a3ff6f83bbb05314b268918b21ea0e566095b474 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:26:36 +0900 Subject: [PATCH 7/9] introduce DISCRIMINATOR_OFFSET --- interface/src/data/order.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 972d98b..773a77b 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -84,7 +84,7 @@ impl EncodedOrderAccount { pub const SIZE: usize = 200; /// Single-byte account discriminator. See [`crate::SettlementAccount`]. - pub const DISCRIMINATOR: [u8; 1] = [crate::SettlementAccount::OrderAccount.discriminator()]; + pub const DISCRIMINATOR: u8 = crate::SettlementAccount::OrderAccount.discriminator(); /// Decode the account body and compute the embedded intent's UID in one /// shot, mirroring [`EncodedOrderIntent::decode_and_hash`]. Decoding @@ -132,7 +132,7 @@ pub fn write_account( EncodedOrderAccount::W_CREATED_BY, EncodedOrderAccount::W_INTENT ]; - *discriminator_slot = EncodedOrderAccount::DISCRIMINATOR; + *discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR]; *cancelled_slot = [cancelled as u8]; *amount_withdrawn_slot = amount_withdrawn.to_le_bytes(); *amount_received_slot = amount_received.to_le_bytes(); @@ -175,7 +175,7 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { EncodedOrderAccount::W_INTENT ]; - if *discriminator != EncodedOrderAccount::DISCRIMINATOR { + if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] { return Err(ProgramError::InvalidAccountData); } @@ -212,6 +212,7 @@ pub mod fixtures { }; // Hardcoded but verified in a sanity-check test. + pub const DISCRIMINATOR_OFFSET: usize = 0; pub const CANCELLED_OFFSET: usize = 1; pub const INTENT_OFFSET: usize = 50; @@ -251,7 +252,7 @@ pub mod fixtures { mod tests { use core::mem::size_of; - use super::fixtures::{sample_account, CANCELLED_OFFSET, INTENT_OFFSET}; + 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}, @@ -444,8 +445,7 @@ mod tests { kind in arb_order_kind(), partially_fillable in any::(), ) { - bytes[..EncodedOrderAccount::DISCRIMINATOR.len()] - .copy_from_slice(&EncodedOrderAccount::DISCRIMINATOR); + 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; From 7d5b2d7d03feee51f6c57e6f4c1e8a56de7be133 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:36:10 +0900 Subject: [PATCH 8/9] add write_account function for state_pda --- interface/src/data/state.rs | 14 ++++++++++++++ programs/settlement/src/initialize.rs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs index 558bf7f..61a7f4a 100644 --- a/interface/src/data/state.rs +++ b/interface/src/data/state.rs @@ -18,6 +18,12 @@ pub fn decode(bytes: &[u8; SIZE]) -> Result<(), ProgramError> { Ok(()) } +/// Writes the canonical encoding of the settlement state PDA's body into +/// `buffer`. There are no fields beyond the discriminator. +pub fn write_account(buffer: &mut [u8; SIZE]) { + *buffer = DISCRIMINATOR; +} + #[cfg(test)] mod tests { use super::*; @@ -33,4 +39,12 @@ mod tests { bytes[0] ^= 0xff; assert_eq!(decode(&bytes), Err(ProgramError::InvalidAccountData)); } + + #[test] + fn write_account_produces_decodable_bytes() { + let mut buffer = [0u8; SIZE]; + write_account(&mut buffer); + assert_eq!(decode(&buffer), Ok(())); + assert_eq!(buffer, DISCRIMINATOR); + } } diff --git a/programs/settlement/src/initialize.rs b/programs/settlement/src/initialize.rs index 653cd7a..9adfd4f 100644 --- a/programs/settlement/src/initialize.rs +++ b/programs/settlement/src/initialize.rs @@ -36,7 +36,7 @@ pub fn process_initialize( let buffer: &mut [u8; state::SIZE] = (&mut *buffer) .try_into() .map_err(|_| ProgramError::AccountDataTooSmall)?; - *buffer = state::DISCRIMINATOR; + state::write_account(buffer); Ok(()) } From c896faf3b34d0162e3b25397c8a7b9f9566b8153 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:50:06 +0900 Subject: [PATCH 9/9] Update interface/src/data/order.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/data/order.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 773a77b..343afed 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -60,14 +60,14 @@ pub struct OrderAccount { /// [`EncodedOrderIntent`]; see that type's docs for its inner layout. /// /// ```text -/// ┌──── discriminator -/// │ ┌─── cancelled -/// ┌──┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐ -/// │ │amount_│amount_│ │ │ -/// │ │with- │re- │ created_by │ intent (EncodedOrderIntent) │ -/// │ │drawn │ceived │ │ │ -/// └──┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ -/// 0 2 10 18 50 ... 200 +/// ┌──── discriminator +/// │┌─── cancelled +/// ┌┬┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐ +/// ││|amount_│amount_│ │ │ +/// │││with- │re- │ created_by │ intent (EncodedOrderIntent) │ +/// │││drawn │ceived │ │ │ +/// └┴┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘ +/// 0 1 2 10 18 50 ... 200 /// ``` #[derive(Clone, Debug, Deref, Eq, PartialEq)] pub struct EncodedOrderAccount([u8; Self::SIZE]);