Support Token-2022 in BeginSettle and FinalizeSettle - #128
Open
kaze-cow wants to merge 4 commits into
Open
Conversation
`CreateBuffer` and `ReclaimBuffer` each took a `token_program` account and rejected anything that wasn't the legacy SPL Token program. They now accept Token-2022 as well and issue all of their CPIs against whichever of the two they were handed, so a buffer can be allocated, initialized and closed under either. Token-2022 encodes the instructions this program issues exactly as the legacy program does, so only the CPI target changes. Two things do differ: - Account data. A Token-2022 account carrying extensions is longer than the base layout, so the legacy reader (exact length, legacy owner) rejects it. `token::read_token_account` dispatches on the validated program and reads by value, which also drops the borrow before `ReclaimBuffer` closes the same account. - Buffer sizing. A buffer now gets the length its mint actually needs: a mint with no extension data keeps the base layout, and anything longer is priced by asking the token program via `GetAccountDataSize`, the way the associated-token-account program does. That keeps the answer authoritative at run time rather than freezing a mint-extension-to-account-extension table into the program. The program account is shared by the whole instruction, so the mints one instruction touches must all live under the same token program; splitting a mixed batch across two instructions is the caller's job. Adds `SettlementError::BufferSizeUnavailable` (35), reachable only defensively: a token program that fails the size query aborts the instruction on its own. `BeginSettle` and `FinalizeSettle` keep rejecting everything but the legacy program; Token-2022 for the settlement pair follows separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 28, 2026
`spl-token-interface` was a dependency of the interface crate for one thing: a 32-byte constant naming a program this code never calls directly. `spl-token-2022-interface` already carries that address in `inline_spl_token`, which exists precisely so a program that has to recognize both doesn't grow a second dependency for the one it only compares against. Both ids now come from there, and the legacy crate leaves the interface crate's dependency graph -- and with it the settlement program's. `instruction::settle` re-exported the same id straight from the legacy crate, so it moves to `token_program` too. That was the second import keeping the dependency alive, not a cosmetic change. The `.so` is byte-identical at 51,056 bytes: the constant was already inlined, so this narrows the dependency graph rather than the program. The program crate keeps `pinocchio-token`. Its state readers are not parameterized over the token program the way 0.7's instruction builders are -- `pinocchio_token_2022::state::Account` hardcodes an owner check against Token-2022 in all of its safe constructors -- so reading a legacy account without it means `from_bytes_unchecked` plus hand-rolled owner and length checks, in exchange for a crate `pinocchio-token-2022` depends on anyway. `test-cli` keeps the legacy crate as well. It talks to no other program, and the two crates' `native_mint` are different addresses, so swapping them there is a change to make deliberately rather than in passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`BeginSettle` and `FinalizeSettle` each took a `token_program` account and rejected anything that wasn't the legacy SPL Token program, so the buffers `CreateBuffer` can now open under Token-2022 had no way to be settled. They accept it too, and issue every transfer against whichever of the two they were handed, through the same `token::validate_token_program` gate the buffer instructions use. Token-2022 encodes `Transfer` exactly as the legacy program does, so only the CPI target changes. What differs is the account data: a Token-2022 account carrying extensions is longer than the base layout, and the legacy reader insists on an exact length. Both sides now read through `token::read_token_account`, which dispatches on the validated program — the sell account's owner in `BeginSettle`, the destination's mint in `FinalizeSettle`. That reader grows the `mint` and `owner` fields the settlement needs and the buffer instructions didn't, which costs `ReclaimBuffer` a little: its `max_buffers_in_one_instruction` goes 137,046 -> 138,392 CU for the wider read. The settle benchmarks rise 0.3-1.6% from the added dispatch. The `token_program` account is shared by the whole instruction, so every token one settlement touches must live under the same program; a mixed settlement still needs two instruction pairs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`BeginSettle` and `FinalizeSettle` took one `token_program` account, so every token an instruction touched had to live under the same program: settling a legacy SPL mint and a Token-2022 mint meant two instruction pairs. They now take one account per supported program, at fixed positions after the state PDA, and issue each transfer against the program that owns the account it moves. One pair can settle both, and the two sides of a single order need not agree — the pull follows the sell account's owner, the push the buy account's. A program the settlement doesn't touch is left out by putting the system program in its slot. The transfers still need their program named by the transaction, so the placeholder is how an instruction says this one isn't; in a transaction that already references the system program it costs an account index instead of another 32-byte address. Nothing new goes into the instruction data: the owner is the authority on which program an account belongs to, and the slots only decide whether the settlement can reach it. Resolving an account gives one of three answers: - owned by a carried program: its transfers CPI into that program; - owned by a supported program whose slot holds the placeholder: the new `SettlementError::TokenProgramNotProvided` (36), so a forgotten slot reads as itself rather than as a malformed account; - owned by neither: the existing `SellTokenAccountInvalid` / `InvalidBuyTokenAccount`, unchanged. The slots are positional. Each holds its own program or the placeholder; anything else, swapping the two included, is `IncorrectProgramId`. `FINALIZE_FIXED_ACCOUNTS` becomes 4, which `push_destinations` follows on its own. `CreateBuffer` and `ReclaimBuffer` keep their single `token_program` account: each works on one mint at a time, so there is nothing to mix. The settle benchmarks each gain one account, 34 transaction bytes, and 50-170 CU. The one- and two-unit drift on the unrelated create/reclaim lines is codegen, not behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kaze-cow
force-pushed
the
kaze/sc-153-token-2022-settle
branch
from
August 28, 2026 08:29
7af181d to
35bb3fe
Compare
kaze-cow
added a commit
that referenced
this pull request
Sep 4, 2026
The Token-2022 paths are worth little if they only hold for the handful of cases someone thought to write twice, so instead of a parallel suite this reruns the suite that already exists against the second program. `common::also_under_token_2022!(some_test)` sits in front of a test and generates `some_test_token_2022`, which runs the same body with Token-2022 as the thread-local active program. Nothing in the body changes: the token helpers build against the program that owns the account they are handed, and `payer_signed_tx` / `signed_tx` repoint the legacy program id in every instruction they assemble. 19 tests are covered this way, across buffer creation and reclamation -- error paths as much as happy ones. `CreateBuffer` and `ReclaimBuffer` name their token program as an account, so repointing reaches them. `common::buffer::ensure_buffer_exists` reads the program off the mint instead of taking it as a parameter, a buffer being a token account of its mint and so bound to the mint's own program; that also lets one test build buffers under both programs at once, which is what `ensure_buffer_exists_for` used to be for. Naming the test in front of it, rather than wrapping the body, keeps the indentation and makes a stale name a compile error instead of a test that quietly stopped being generated. A test that can only hold under one program goes without and says why -- the one pinned to the legacy native mint. Three tests are Token-2022-only, covering what has no legacy analogue -- a buffer for a mint with a `TransferFeeConfig`, which needs a `TransferFeeAmount` on every account holding it and so must be longer than the base layout: - `creates_buffer_sized_for_a_mint_with_extensions` - `recreating_an_extension_mint_buffer_is_idempotent` - `reclaims_a_buffer_sized_for_an_extension_mint` `bench-report.json` gains the CU, account, and transaction-byte readings for every generated test. It also picks up the `remove_solver` rows and a handful of single-digit CU corrections that were already owed from merging main, which the report on this branch's base had not been regenerated for. The settlement pair is deliberately left out. `BeginSettle` and `FinalizeSettle` name their token programs by value rather than by account, so rerunning their tests needs the slot handling from #128 and belongs on that branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kaze-cow
added a commit
that referenced
this pull request
Sep 7, 2026
Adds the most basic level of Token-2022 support which widens the accepted token programs and focuses on dealing with the edge cases of `CreateBuffers` and `ReclaimBuffers`. ## What changes `CreateBuffer` and `ReclaimBuffer` each take a `token_program` account and, until now, rejected anything that wasn't the legacy SPL Token program. They now accept Token-2022 too and issue all of their CPIs — `InitializeAccount3`, `CloseAccount` — against whichever of the two they were handed. Token2022 accounts may have dynamic length. Previously, a buffer could only hold 165 bytes (the length of SPL token account), but now it is now allocated at the length its mint actually needs. `GetAccountDataSize` is used to verify the length of the token required before creating it. For `CreateBuffers` creation of token accounts, We looked into the possibility of making the account longer than it needs to be and then shrinking it, but this doesn't work well because a extra CPI call is necessary to shrink the account back down to the correct size. But this requires a CPI call to `Reallocate` on the token program, and that is more expensive than just getting hte length. There is an early return on `token_account_len` which allows for skipping the `GetTokenAccountLength` CPI call. We originally did the early return if the Mint size was th ebase size (the logic being that a base size mint is either a canonical SPL token *OR* a token2022 mint with no required extensions), but since the vast majority of token2022 mints *DO* actually have extensions, there is very little benefit in having this broader check. So we decided to only do the early return for SPL tokens. ## Library Handling Changes `spl-token-2022-interface = "3"`, a library [published by anza-team](https://crates.io/crates/spl-token-2022-interface), and `pinocchio-token-2022 = "0.4"`, another library [published by anza-team](https://crates.io/crates/pinocchio-token-2022), are added for hopefully obvious reasons. ~~`spl-token-interface` was removed from the settlement program because it is no longer needed (later it will also be removed from test-cli, eliminating it as a direct dependency from the repo).~~ ended up being re-added after adding new tests because apparently a lot of mint creation utilities need to be recreated without it. `pinocchio-token` is bumped to `0.7` in order to gain access to the `invoke_with_unverified_program(token_program)` instruction builder function (prior to this release, there was no way to specify an alternative program). Its also the version that `pinnochio-token-2022` transitively depends on. Most of the functions in both `pinnochio-token` and `pinnochio-token-2022` are close to identical. For now most of the interfaces continue to use `pinnochio-token` because we never actually work with token 2022 tokens directly and the interfaces usually have slightly less dependencies (ex. not specifying the extension information). ## Out of Scope `BeginSettle` / `FinalizeSettle` require a different methodology to support simultaneous settlement from both token programs, so those follow in #128. `test-cli` is covered separately in #134 . The integration tests for both this PR and #128 are in #121, as we want to expand coverage with 2022 across as many tests as possible. At this time, only one token program can be supplied to the buffer functions. Two separate calls to `CreateBuffers` is required if it is necessary to create buffers for tokens on two separate prgorams. ## Compute cost `bench-report.json` is regenerated. The buffer instructions shift by roughly +0.2% to +0.4% from the added dispatch (`reclaim_buffer/max_buffers_in_one_instruction` 136,501 → 137,046 is the largest); the one- and two-unit drift on the unrelated settle and transfer-authority lines is codegen, not behaviour. ## Test Plan Verify the methodology. In particular, it would be good to verify the library dependency status as described above, because it is a bit awckward. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add support for Token2022. Both token programs can be used in the same settlement without issues.
Motivation
BeginSettleandFinalizeSettleeach took atoken_programaccount and rejected anything but the legacy SPL Token program. As settlements may need to pull or push from many different in the same settlement, we need a way to specify both.Methodology
Rather than including one token program per order, since there are only two possible token programs we are ever anticipating needing to support, we add the token2022 program to the list of fixed accounts for both
BeginSettleandFinalizeSettle.If either the token2022 program or the SPL token program is not required to complete the relevant settlement instruction, they can be excluded without incurring account cost by setting the slot to the system program.
Compute cost
bench-report.jsonis regenerated. The settle benchmarks each gain one account, 34 transaction bytes, and 50–170 CU on top of the Token-2022 dispatch above.Out of scope
Integration tests are expanded in #121
Test Plan
Verify methodology.
🤖 Generated with Claude Code