Skip to content

Push proceeds to users in FinalizeSettle#62

Open
fedgiac wants to merge 1 commit into
push-funds-to-user-processing-in-begin-settlefrom
push-funds-to-user-processing-in-finalize-settle
Open

Push proceeds to users in FinalizeSettle#62
fedgiac wants to merge 1 commit into
push-funds-to-user-processing-in-begin-settlefrom
push-funds-to-user-processing-in-finalize-settle

Conversation

@fedgiac

@fedgiac fedgiac commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Push each order's proceeds out of the buffers in FinalizeSettle.

This is the final part of the settlement push flow. The previous PR taught BeginSettle to pair each settled order with exactly one push and check the push pays the order's buy token account, but nothing moved yet. Here FinalizeSettle actually performs the transfers.

What push_funds does

BeginSettle already guarantees, for the paired finalize, that the push count matches the order count and that each push's destination is the right buy token account, and the counterpart check guarantees that begin ran.
So the only things left for FinalizeSettle to check is that:

  • each push draws from the canonical buffer for its destination's mint;
  • the buy token is a valid token account.

And naturally, it needs to actually send funds.

How to test

finalize_settle_pushes.rs moves from asserting that pushes merely parse to asserting they actually pay: single-order, several orders sharing one buffer, and several orders drawing from different buffers, each checking both the destinations' credited balances and the buffers' debited balances. It also covers new edge cases.

@fedgiac fedgiac requested a review from a team as a code owner July 6, 2026 21:29
@fedgiac fedgiac mentioned this pull request Jul 6, 2026
Comment on lines +71 to +85
if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}

// The buffers' SPL authority is the state PDA, so it must sign each transfer.
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);

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.

these first lines appear to be nearly, if not exactly the same as what is in begin.rs parallel function pull_funds

Comment on lines +90 to +105
let mint = {
let destination = TokenAccount::from_account_view(push.destination)
.map_err(|_| SettlementError::InvalidBuyTokenAccount)?;
*destination.mint()
};
// Re-derive the buffer from the carried bump (one hash, not a full
// search). A buffer exists only at its canonical address, so a wrong
// bump yields an address the transfer can't draw from.
let derived = Address::create_program_address(
&buffer_pda_signer_seeds(mint.as_array(), &[push.bump]),
program_id,
)
.map_err(|_| SettlementError::PushSourceNotBuffer)?;
if push.source_buffer.address() != &derived {
return Err(SettlementError::PushSourceNotBuffer.into());
}

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.

In places like this where we need to rederive the canonical buffer, it would be really nice if that happens via a function like Buffer::load_from_pda function or similar which simultaneously reads the program data while also verifying canonical. You can see an example of me doing this in my own PR here.

Technically this would change the functionality a bit such that the canonical address check would happen by checking the mint reported in the actual buffer account, and not the mint reported in the buy_token_account, but this should actually still be fine.


#[test]
fn finalizes_with_single_push() {
fn pushes_a_single_order() {

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.

the single order test probably actually isnt necessary when there is already a multiple order test that supersedes it (at least, the tests are structured nearly the same with the exception of the single order part)

}

#[test]
fn rejects_push_to_wrong_destination() {

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.

this is a test technically for the begin function validation so it seems weird to see it here. I didn't see a duplicate test pre-existing on the begin side though.

// which `FinalizeSettle` rejects when it reads the destination's mint.
let orders = [FinalizedIntent {
intent: &intent,
mint: other_mint,

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.

technically this is sending from a different buffer rather than a non-buffer source. Of course this case should still fail, but the reasoning for why it fails should probably be ultimately different considering my previous comment suggesting to create a load_from_pda function.

}

#[test]
fn rejects_push_from_substituted_source() {

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.

this test seems very similar ot the previous non_buffer_source test. Probably we could get all the test validation we need from just validating like is done with this test.

// `FinalizeSettle` re-derives the buffer from the destination's mint and
// rejects the mismatch before touching the substituted account.
let source_index = 3;
finalize.accounts[source_index].pubkey = Pubkey::new_unique();

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.

maybe a good idea to make this test stronger by making this account look like an actual buffer in every way possible? for example copying the data of the original buffer account into this one somehow and transferring ownership to the settlement program (I am not sure if all of this is actually possible but seems interesting)

}

#[test]
fn rejects_fewer_pushes_than_orders() {

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.

i could have sworn I saw this test somewhere else but now I can't find it. in either case, this test again seems like it should ultimately actually go in the begin tests due to that being where the condition is.

}

#[test]
fn rejects_more_pushes_than_orders() {

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.

same here

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