Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0047ca9
Refactor: cleanup before updating FinalizeSettle
fedgiac Jul 6, 2026
b8b3804
Parse pushes in `FinalizeSettle`
fedgiac Jul 6, 2026
3e07aaa
Validate pushes to users in BeginSettle
fedgiac Jul 6, 2026
f6ad585
Push funds to users in `FinalizeSettle`
fedgiac Jul 6, 2026
49fbd56
Merge branch 'main' into refactor-test-simplification-helper-extracti…
kaze-cow Jul 14, 2026
dc509c3
Merge branch 'refactor-test-simplification-helper-extraction-renaming…
kaze-cow Jul 14, 2026
c84fc3f
Merge branch 'push-funds-to-user-parsing' into push-funds-to-user-pro…
kaze-cow Jul 14, 2026
9337a58
Merge branch 'main' into refactor-test-simplification-helper-extracti…
fedgiac Jul 14, 2026
606693d
Merge branch 'push-funds-to-user-processing-in-begin-settle' into pus…
kaze-cow Jul 14, 2026
d01ae1a
Merge branch 'refactor-test-simplification-helper-extraction-renaming…
fedgiac Jul 14, 2026
aad2a64
Consistency: index alignment and underscore separator
fedgiac Jul 14, 2026
903fa88
Test that two accounts popped trigger an error
fedgiac Jul 14, 2026
9f67290
Merge branch 'push-funds-to-user-parsing' into push-funds-to-user-pro…
fedgiac Jul 14, 2026
514e20b
Explicitly link mints to intents even if technically not needed
fedgiac Jul 14, 2026
e71d23e
Clearer proptest name
fedgiac Jul 14, 2026
75130af
Remove unnecessary count checks
fedgiac Jul 14, 2026
eb98478
Clearer comment
fedgiac Jul 14, 2026
8fe14a4
Simplify `push_destinations`
fedgiac Jul 14, 2026
a81feaa
Don't send transaction in helpers, just build it
fedgiac Jul 14, 2026
8c82ddc
Simplify comment
fedgiac Jul 14, 2026
8799e9d
Clarify that the helper isn't that generic
fedgiac Jul 14, 2026
006ebb9
Merge branch 'push-funds-to-user-processing-in-begin-settle' into pus…
fedgiac Jul 14, 2026
dc54836
tests refactoring/simplification to one fixture
kaze-cow Jul 15, 2026
da7f43c
Adjustments for readability
fedgiac Jul 15, 2026
7e2f5e0
Clarifying comment about the need for `mint`
fedgiac Jul 15, 2026
841def6
order -> orders
fedgiac Jul 15, 2026
c48a9f1
Reuse array length
fedgiac Jul 15, 2026
777ffec
Merge branch 'main' into push-funds-to-user-parsing
fedgiac Jul 15, 2026
5a5d282
Merge branch 'main' into push-funds-to-user-parsing
fedgiac Jul 15, 2026
af7e4c5
Merge branch 'push-funds-to-user-parsing' into push-funds-to-user-pro…
fedgiac Jul 15, 2026
bd4072a
Merge branch 'push-funds-to-user-processing-in-begin-settle' into pus…
fedgiac Jul 15, 2026
e29f7c8
Merge branch 'main' into push-funds-to-user-processing-in-begin-settle
fedgiac Jul 15, 2026
c495133
Merge branch 'push-funds-to-user-processing-in-begin-settle' into pus…
fedgiac Jul 15, 2026
c00acd1
Merge branch 'main' into push-funds-to-user-processing-in-finalize-se…
fedgiac Jul 16, 2026
572f55e
Fix endianness
fedgiac Jul 16, 2026
c14c364
Simplification: collect seeds plus bump in helper
fedgiac Jul 16, 2026
17f3cb8
Create signer with helper function
fedgiac Jul 16, 2026
0b496e1
Rename generic function to clarify it reverts on begin
fedgiac Jul 16, 2026
3933d96
Move tests from Finalize that fail in Begin to Begin
fedgiac Jul 16, 2026
3942889
Fix fmt
fedgiac Jul 16, 2026
1c2c02c
Rename test with misleading name: rejects_push_from_non_buffer_source…
fedgiac Jul 16, 2026
915dabc
Move buffer validation to dedicated function
fedgiac Jul 16, 2026
43147d2
Add tests for checking remaining fixed accounts
fedgiac Jul 16, 2026
db268c7
Unnecessary comments
fedgiac Jul 17, 2026
6c8213c
Merge branch 'main' into push-funds-to-user-processing-in-finalize-se…
fedgiac Jul 17, 2026
68dfbe0
Merge branch 'main' into push-funds-to-user-processing-in-finalize-se…
fedgiac Jul 17, 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
11 changes: 9 additions & 2 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,18 @@ pub enum SettlementError {
/// to the order's buy token account; its destination differs from the
/// `buy_token_account` in the order's intent.
PushDestinationMismatch = 21,
/// `FinalizeSettle`: a push doesn't draw funds from the canonical buffer
/// for its destination's mint.
PushSourceNotBuffer = 22,
/// `FinalizeSettle`: a push's destination isn't a valid SPL token account
/// (wrong data length or not owned by the token program), so its mint can't
/// be read to derive the buffer.
InvalidBuyTokenAccount = 23,
/// `ReclaimOrder` was called before the order's `valid_to` has elapsed.
OrderNotExpired = 22,
OrderNotExpired = 24,
/// `ReclaimOrder`'s `reclaim_recipient` account doesn't match the
/// `created_by` address recorded in the order.
ReclaimRecipientMismatch = 23,
ReclaimRecipientMismatch = 25,
}

impl From<SettlementError> for u32 {
Expand Down
66 changes: 66 additions & 0 deletions interface/src/pda/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
//! settlement state PDA (see [`crate::pda::state`]), the single authority
//! controlling every buffer.

use solana_account_view::AccountView;
use solana_address::Address;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

use crate::pda::SETTLEMENT_SEED;
use crate::SettlementError;

/// Trailing seed identifying the buffer PDAs.
pub const BUFFER_SEED: &[u8] = b"buffer";
Expand All @@ -26,11 +30,36 @@ pub fn buffer_pda_seeds(mint: &[u8; 32]) -> [&[u8]; 3] {
[SETTLEMENT_SEED, mint, BUFFER_SEED]
}

/// Canonical seeds for re-deriving the buffer PDA for `mint` with `bump`.
pub fn buffer_pda_signer_seeds<'a>(mint: &'a [u8; 32], bump: &'a [u8; 1]) -> [&'a [u8]; 4] {
let [s0, s1, s2] = buffer_pda_seeds(mint);
[s0, s1, s2, bump]
}

/// Derive the canonical buffer PDA address (and bump) for the token `mint`.
pub fn find_buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&buffer_pda_seeds(mint.as_array()), program_id)
}

/// Confirm `buffer` matches the derived buffer PDA for `mint` and `bump`.
#[must_use = "ignoring the output means ignoring the validation result"]
pub fn validate_buffer_pda(
program_id: &Address,
buffer: &AccountView,
mint: &Address,
bump: u8,
) -> Result<(), ProgramError> {
let derived = Address::create_program_address(
&buffer_pda_signer_seeds(&mint.to_bytes(), &[bump]),
program_id,
)
.map_err(|_| SettlementError::PushSourceNotBuffer)?;
if buffer.address() != &derived {
return Err(SettlementError::PushSourceNotBuffer.into());
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -45,6 +74,43 @@ mod tests {
);
}

#[test]
fn accepts_a_valid_pda() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (pda, bump) = find_buffer_pda(&program_id, &mint);

let buffer = crate::instruction::fixtures::fake_account(pda);
validate_buffer_pda(&program_id, &buffer, &mint, bump)
.expect("the canonical buffer PDA must be accepted");
}

#[test]
fn rejects_an_invalid_address() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (_, bump) = find_buffer_pda(&program_id, &mint);

// An account sitting at some other address is not the buffer.
let buffer = crate::instruction::fixtures::fake_account(Pubkey::new_unique());
let err = validate_buffer_pda(&program_id, &buffer, &mint, bump)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::PushSourceNotBuffer.into());
}

#[test]
fn rejects_a_wrong_bump() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (pda, bump) = find_buffer_pda(&program_id, &mint);

// The address is canonical but the carried bump doesn't derive it.
let buffer = crate::instruction::fixtures::fake_account(pda);
let err = validate_buffer_pda(&program_id, &buffer, &mint, bump ^ 1)
.expect_err("a wrong bump must be rejected");
assert_eq!(err, SettlementError::PushSourceNotBuffer.into());
}

mod proptest {
use ::proptest::prelude::*;

Expand Down
7 changes: 7 additions & 0 deletions interface/src/pda/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pub fn state_pda_seeds<'a>() -> [&'a [u8]; 1] {
[SETTLEMENT_SEED]
}

/// Canonical seeds for signing as the settlement state PDA with `bump`. The
/// on-chain settlement handlers use this to construct the CPI signer.
pub fn state_pda_signer_seeds(bump: &[u8; 1]) -> [&[u8]; 2] {
let [seed] = state_pda_seeds();
[seed, bump]
}

/// Derive the canonical settlement state PDA address (and bump).
pub fn find_state_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&state_pda_seeds(), program_id)
Expand Down
52 changes: 17 additions & 35 deletions programs/settlement/src/settle/begin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
use std::ops::Deref;

use pinocchio::{
cpi::{Seed, Signer},
error::ProgramError,
cpi::Signer,
sysvars::{
clock::Clock,
instructions::{Instructions, IntrospectedInstruction},
Expand All @@ -16,17 +15,15 @@ use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount};
use settlement_interface::{
data::order::OrderAccount,
instruction::{
create_buffer::SPL_TOKEN_PROGRAM_ID,
settle::{BeginSettleInput, SettledOrder, FINALIZE_FIXED_ACCOUNTS},
InstructionInputParsing,
},
pda::state::state_pda_seeds,
recover_discriminator, Pubkey, SettlementError, SettlementInstruction,
};

use crate::processor::is_cpi_call;

use super::validate_counterpart;
use super::{validate_counterpart, validate_token_account, with_state_pda_signer};

pub fn process_begin_settle(
program_id: &Address,
Expand Down Expand Up @@ -65,15 +62,17 @@ pub fn process_begin_settle(

let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?;

settle_orders(
program_id,
input.token_program_account,
input.state_pda_account,
input.orders.iter(),
&finalize_ix,
)?;
validate_token_account(input.token_program_account)?;

Ok(())
with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| {
settle_orders(
program_id,
input.state_pda_account,
state_pda_signer,
input.orders.iter(),
&finalize_ix,
)
})
}

/// The destination address of each push carried by the paired `FinalizeSettle`,
Expand Down Expand Up @@ -144,10 +143,9 @@ fn validate_no_nested_settlement<T: Deref<Target = [u8]>>(
Ok(())
}

/// Validate each order against its push, and pull user funds. This requires:
/// - the legacy SPL Token program;
/// - the canonical state PDA, which signs each transfer as the user's delegate;
/// - orders strictly increasing by address, rejecting duplicates.
/// Validate each order against its push, and pull user funds, signing the pulls
/// as the canonical state PDA (the user's delegate). Orders must be strictly
/// increasing by address, which rejects duplicates.
///
/// Each order is paid by exactly one push. The orders and the finalize's pushes
/// are both laid out sorted by order PDA, so order `i` is paid by push `i`, and
Expand All @@ -160,27 +158,11 @@ fn validate_no_nested_settlement<T: Deref<Target = [u8]>>(
#[must_use = "ignoring the output may lead to an unintended on-chain state"]
fn settle_orders<'a>(
program_id: &Address,
token_program_account: &AccountView,
state_pda_account: &AccountView,
state_pda_signer: &Signer,
orders: impl IntoIterator<Item = SettledOrder<'a>>,
finalize_ix: &IntrospectedInstruction,
) -> ProgramResult {
if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}

// Funds are pulled with the state PDA's delegation, so it must be the signer.
let seeds = state_pda_seeds();
let (state_pda, state_bump) = Address::find_program_address(&seeds, program_id);
if state_pda_account.address() != &state_pda {
return Err(SettlementError::StateAccountMismatch.into());
}

let [seed] = seeds;
let state_bump = [state_bump];
let signer_seeds = [seed, &state_bump].map(Seed::from);
let state_pda_signer = Signer::from(&signer_seeds);

// Orders must be passed strictly increasing by address; this rejects
// duplicates (settling the same order twice) without a separate scan.
let mut previous: Option<&Address> = None;
Expand Down Expand Up @@ -208,7 +190,7 @@ fn settle_orders<'a>(
push_destination,
now,
state_pda_account,
&state_pda_signer,
state_pda_signer,
)?;
}

Expand Down
65 changes: 58 additions & 7 deletions programs/settlement/src/settle/finalize.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
//! `FinalizeSettle` instruction handler.

use pinocchio::{sysvars::instructions::Instructions, AccountView, Address, ProgramResult};
use pinocchio::{
cpi::Signer, sysvars::instructions::Instructions, AccountView, Address, ProgramResult,
};
use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount};
use settlement_interface::{
instruction::{settle::FinalizeSettleInput, InstructionInputParsing},
instruction::{
settle::{FinalizeSettleInput, Pushes},
InstructionInputParsing,
},
pda::buffer::validate_buffer_pda,
SettlementError, SettlementInstruction,
};

use crate::processor::is_cpi_call;

use super::validate_counterpart;
use super::{validate_counterpart, validate_token_account, with_state_pda_signer};

pub fn process_finalize_settle(
program_id: &Address,
Expand All @@ -31,9 +38,53 @@ pub fn process_finalize_settle(
current_index,
input.begin_ix_index,
SettlementInstruction::BeginSettle,
)
)?;

// `BeginSettle` (which the counterpart check above guarantees ran) already
// validated the push count and destinations. `push_funds` adds the only
// remaining check: each push draws from the buffer for its mint.

validate_token_account(input.token_program_account)?;

with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| {
push_funds(
program_id,
input.state_pda_account,
state_pda_signer,
input.pushes,
)
})
}

/// Push each order's proceeds out of the settlement's buffers, signing each
/// transfer as the canonical state PDA (the buffers' SPL authority). Each push's
/// source must be the derived buffer for its destination's mint; pairing the
/// destination to an order is `BeginSettle`'s job.
#[must_use = "ignoring the output may lead to an unintended on-chain state"]
fn push_funds<'a>(
program_id: &Address,
state_pda_account: &AccountView,
state_pda_signer: &Signer,
pushes: Pushes<'a>,
) -> ProgramResult {
for push in pushes.iter() {
// Read the destination's mint; the borrow ends with this block, before
// the transfer reuses the account.
let mint = {
let destination = TokenAccount::from_account_view(push.destination)
.map_err(|_| SettlementError::InvalidBuyTokenAccount)?;
*destination.mint()
};
validate_buffer_pda(program_id, push.source_buffer, &mint, push.bump)?;

Transfer::new(
push.source_buffer,
push.destination,
state_pda_account,
u64::from_le_bytes(*push.amount),
)
.invoke_signed(core::slice::from_ref(state_pda_signer))?;
}

// Some checks are carried out by `BeginSettle` and we don't repeat them
// under the assumption that the counterpart exists and, since it's a
// `BeginSettle`, it performs the checks.
Ok(())
}
44 changes: 41 additions & 3 deletions programs/settlement/src/settle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

use std::ops::Deref;

use pinocchio::{sysvars::instructions::Instructions, Address, ProgramResult};
use pinocchio::{
cpi::{Seed, Signer},
error::ProgramError,
sysvars::instructions::Instructions,
AccountView, Address, ProgramResult,
};
use settlement_interface::{
instruction::settle::recover_counterpart, recover_discriminator, SettlementError,
SettlementInstruction,
instruction::{create_buffer::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart},
pda::state::{state_pda_seeds, state_pda_signer_seeds},
recover_discriminator, SettlementError, SettlementInstruction,
};

mod begin;
Expand Down Expand Up @@ -42,3 +48,35 @@ fn validate_counterpart<T: Deref<Target = [u8]>>(
}
Ok(())
}

/// Validate that `token_program_account` is the legacy SPL Token program, which
/// every settlement transfer is issued against.
#[must_use = "ignoring the output may lead to an unintended on-chain state"]
fn validate_token_account(token_program_account: &AccountView) -> ProgramResult {
if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
Comment on lines +52 to +60

@tilacog tilacog Jul 22, 2026

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.

Nothing critical, but I this looks like a good spot to call pinocchio_token::state::Account::from_account_view, which also validates the account data.


/// Validate that `state_pda_account` is the canonical state PDA and run `f` with
/// a signer for it. Both settlement transfers move funds under the state PDA's
/// authority, so it must sign each of them.
///
/// The signer only borrows its seed buffers, which are local to this frame;
/// running `f` here rather than returning the signer keeps them alive for as
/// long as `f` needs it.
fn with_state_pda_signer(
program_id: &Address,
state_pda_account: &AccountView,
f: impl FnOnce(&Signer) -> ProgramResult,
) -> ProgramResult {
let (state_pda, state_bump) = Address::find_program_address(&state_pda_seeds(), program_id);
if state_pda_account.address() != &state_pda {
return Err(SettlementError::StateAccountMismatch.into());
}

let state_bump = [state_bump];
let signer_seeds = state_pda_signer_seeds(&state_bump).map(Seed::from);
f(&Signer::from(&signer_seeds))
}
Loading