Skip to content

Buy Native SOL - #151

Merged
kaze-cow merged 76 commits into
mainfrom
kaze/sc-275-buy-native-sol
Sep 24, 2026
Merged

kaze-cow merged 76 commits into
mainfrom
kaze/sc-275-buy-native-sol

Conversation

@kaze-cow

@kaze-cow kaze-cow commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #163, which adds OrderIntentAccessor and its optimizations. This PR adds buying native SOL and restructures the OrderIntent buy/sell fields into TokenAsset/Asset.

Description

Adds the ability for a trader to specify the system account rather than a token mint as the buy_mint, and in doing so, receive native SOL directly from the settlement program.

Motivation

Being able to buy and sell native tokens from the settlement program is known to improve. Its relatively simple to support this from the settlement program side.

Changes overview

The OrderIntent buy_mint field may be set to 11111..1111 (the system program). When this is the case, buy_token_account is treated as a regular account that receives native lamports rather than a token account that receives tokens.

Elsewhere in the solana ecosystem the system program seems to be treated for this very purpose.

Note that the settlement program itself doesn't actually need to be supplied as an input account since its only used as a data marker. Instead, the source_buffer is set to the state_pda.

Before an account can receive lamports, it must have been created. This requires the user to create the account if it doesn't exist already, the same as they would need to with a token account.

The source "buffer" for lamports is the settlement state pda. Its the most natural choice since it will already be loaded during execution and can be directly debited using lamport math. The solana runtime will prevent the withdrawal of more lamports than are required for rent exemption, so this prevents solvers from withdrawing too much.

To prevent issues with creating orders on the settlement program itself (which is expected to come soon), the move_lamports function will do nothing if the sender is the same as the receiver. So if the buy_token_account is set to state_pda it works as expected (funds remain in the state pda)

Default derive was removed from Asset to prevent "accidents" with using Default for some reason and just ending up with a 0 address. But I still wanted to be able to use Default in the tests, so it became a cfg.

This propogated up the chain, all the way to the client lib, so we are still using cfg test fixtures to prevent default from being used outside that case. So if you are seeing cfg sections added for default, that is why.

Gas Impact

Looking at the correct bench report, it seems very small. Unrelated orders use about ~20 more CU on settle, whereas just about every other function somehow saved 1 CU.

Out of scope

For fee collection, lamports cannot directly be taken from the state pda, so a separate instruction will likely need to be created.

Questions

Should we rename buy_token_account to simply buy_account? should we do the same for sell side?

@kaze-cow kaze-cow self-assigned this Sep 14, 2026
@linear-code

linear-code Bot commented Sep 14, 2026

Copy link
Copy Markdown

SC-275

@kaze-cow kaze-cow changed the title setup buy native sol Buy Native SOL Sep 14, 2026
Comment thread interface/src/pda/state.rs Outdated
Comment thread programs/settlement/src/processor/begin_settle.rs Outdated
@kaze-cow
kaze-cow marked this pull request as ready for review September 15, 2026 09:22
@kaze-cow
kaze-cow requested a review from a team as a code owner September 15, 2026 09:22
Comment thread interface/src/data/intent.rs Outdated
Comment thread DESIGN.md Outdated
Comment thread test-cli/src/cmd/settle.rs
@fedgiac

fedgiac commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Very interesting bug spotted when preparing the prerelease, see failing test here. It seems our understanding of accounts in an instruction is incorrect.

Claude summary of the bug
The working tree is clean again (instrumentation removed, clean .so rebuilt). Note: git diff shows HEAD currently contains the debug block in settle_native_sol.rs — so when you commit, include this revert so the
  instrumentation doesn't linger in history.

  Why it fails now

  It's a latent bug in FinalizeSettle's native-SOL payout, newly triggered by the release-prep changes (new declare_id + v0.5 seed) — not caused by the cancel work.

  The bug. In a settlement that mixes a token payout and a native-SOL payout, the state PDA appears twice in the finalize: once as the fixed state_pda_account, and once as the native push's source_buffer. push_funds processes
  pushes in order:

  1. native push first: move_lamports directly debits the state PDA (−1,000,000) and credits the destination (+1,000,000) — the logs show state_pda 5_789_232 → 4_789_232, dest 0 → 1_000_000. Balanced so far.
  2. token push next: an SPL Transfer CPI that passes the state PDA (the other handle, state_pda_account) as the authority.

  When the CPI returns, the runtime writes the state PDA's lamports back from the handle it was given for the CPI — which never saw the −1M — so the debit is undone, while the destination keeps its +1M. Net result: 1,000,000
  lamports created from nothing → UnbalancedInstruction ("sum of account balances before and after instruction do not match").

  Why only now, and only this test. push_funds runs pushes in order-PDA-sorted order. That order depends on the program id and the version-stamped SETTLEMENT_SEED — both of which just changed. At the old id/v0.4, the token
  push sorted first, so the native move_lamports ran last, with no CPI after it to clobber the debit (which is why pure-native settlements and the old mixed case pass). At the new id/v0.5, the native push sorts first, so it's
  now followed by the token CPI → the clobber. happy_path_with_token_payout is the only test that mixes a CPI push and a native push in one finalize, so it's the only one affected.

  Fix options

  The robust fix is to make the direct lamport moves never precede a CPI on the state PDA. Either:

  - Two passes in push_funds: do all SPL (CPI) pushes first, then all native move_lamports last. Matches the ordering that already works, order-independent. Simplest and safest.
  - Or use the same state_pda_account handle for the native move instead of the separate push.source_buffer handle, so there's no stale second view for the CPI to write back.

  I'd recommend the two-pass approach, plus a regression test that pins the native-before-token ordering (e.g. a mixed settlement asserted to settle regardless of order-PDA sort) so this can't silently reappear on a future
  address/version change.

@fedgiac

fedgiac commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Suggested test (sorry if it's already there, it's more of a reminder for my review): make sure that if filling a buy order would leave less than the needed rent in the settle PDA.

@kaze-cow

kaze-cow commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Very interesting bug spotted when preparing the prerelease,

I see it happens because the order that the order pdas are supplied changes. Would be nice to have proptests which would cover something like this!

Ok probably easiest way to test that this issue is sure to appear is by increasing the number of different orders tested with, so will add that.

And for the fix itself, I tried following the second suggestion, but . The first suggestion works pretty simply, but it impacts CU a bit more. Maybe I am over optimizing, but ended up splitting the lamport math into two separate

This would still create a arithmetic issue for the unlikely case where the token account being paid out on buy-sol order is the same account being paid on a buy SOL order (ex. the token account's native balance is funded). I think this edge case is minescule

Comment thread interface/src/data/intent.rs
Comment thread interface/src/data/intent.rs Outdated
Comment thread interface/src/data/intent.rs
Comment thread programs/settlement/tests/begin_settle_orders.rs Outdated
Comment thread bench-report.json Outdated
Comment thread programs/settlement/src/processor/begin_settle.rs Outdated
Comment thread programs/settlement/src/processor/finalize_settle.rs
Comment thread programs/settlement/src/processor/finalize_settle.rs
Comment thread programs/settlement/src/processor/utils/lamports.rs
kaze-cow and others added 10 commits September 24, 2026 18:10
Folding resolve_buy into resolve passed the self-order use_buffer flag to
the buy side too, so a self order's treasury became the buy-mint buffer
and the push moved the proceeds from that buffer back into itself. The
buy side never draws from a buffer, so always resolve it with
use_buffer = false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kaze-cow
kaze-cow requested a review from fedgiac September 24, 2026 11:54

@fedgiac fedgiac left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed everything, to the best of my knowledge the comments below are everything that needs to be addressed before merging, and all of them are easy.
One exception:
#151 (comment)
Example code here.

Comment thread interface/src/data/intent.rs
Comment thread interface/src/data/intent.rs Outdated
Comment thread interface/src/data/intent.rs Outdated
Comment thread programs/settlement/tests/settle_solver_auth.rs
Comment thread DESIGN.md Outdated
Comment thread DESIGN.md Outdated
Comment thread DESIGN.md
Comment thread DESIGN.md
Comment thread DESIGN.md Outdated
kaze-cow and others added 8 commits September 24, 2026 23:28
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
…olana-programs into kaze/sc-275-buy-native-sol
…ill-mint-and-account' into kaze/sc-275-buy-native-sol
@kaze-cow
kaze-cow requested a review from fedgiac September 24, 2026 15:50

@fedgiac fedgiac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, great work!

@kaze-cow
kaze-cow merged commit 0f13b4e into main Sep 24, 2026
18 of 19 checks passed
@kaze-cow
kaze-cow deleted the kaze/sc-275-buy-native-sol branch September 24, 2026 15:53
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