Instruction handlers
This layer declares one account struct and one handler per instruction, then hands the work to a runner or a venue binding.
Every fact on this page comes from programs/sickle/src/instructions/ and the
files it imports, read on 2026-09-08. Line numbers are path:line in that
tree. The instruction pages under docs/dev/instructions/ document each
instruction one by one. This page documents the layer.
Files
The directory holds one file per instruction, one root module, eight domain modules, eight production support modules, and three test-only support modules. The support code is counted separately because it carries no instruction.
| Count | Value |
|---|---|
| Handler files | 65 |
| Root module | 1 |
| Domain modules | 8 |
| Production support | 8 |
| Test-only support | 3 |
| Lines, all Rust files | 15,864 |
mod.rs declares every module and re-exports every item with pub use
(instructions/mod.rs:1-113). lib.rs then names each account struct
directly, for example Context<OrcaOpenPosition> (lib.rs:266-272).
Dispatch styles
Each handler body uses one of four dispatch styles.
| Style | What the handler body does | Where the venue CPI is built |
|---|---|---|
direct | Writes program state, or builds and invokes CPIs itself. No runner. | The handler file, or a helper in the same file. |
runner | Calls one venue::*::run_* function and emits the returned events. | The adapter for the account struct, in venue/adapters/. |
runner + impl | Calls one venue::*::run_* function. The runner trait is implemented for its account struct by a macro in a sibling module, not in the handler file. | instructions/meteora/yield_adapter.rs. Only the Meteora keeper handlers do this. |
intent | Calls helpers from intent.rs and, for the finalizers, token transfers. No venue CPI. | Not applicable. |
Groups
The 12 groups follow dependency-graph.md section 5. They group by runner and binding, not by caller. The caller classes, which partition the instruction set, are the five instruction rows in the glossary; the Sickle group here includes sweep_token, which the glossary counts as an owner instruction.
| Group | Files | Dispatch |
|---|---|---|
| Admin | initialize_global_config, migrate_global_config, transfer_admin, accept_admin, set_fee, set_fee_collector, set_guardian, pause, unpause, set_protocol_program, set_global_automator, revoke_global_automator | direct |
| Sickle | initialize_sickle, set_manager, set_custom_automator, create_sickle_ata, sweep_token | direct |
| Settings | set_position_settings, unset_position_settings | direct |
| Orca owner | orca_open_position, orca_increase_liquidity, orca_decrease_liquidity, orca_collect_fees, orca_collect_rewards, orca_close_position, orca_rebalance_position | runner |
| Orca keeper | harvest_for, compound_for, rebalance_for, exit_for | runner |
| Raydium owner | raydium_open_position, raydium_increase_liquidity, raydium_decrease_liquidity, raydium_collect_fees, raydium_close_position, raydium_rebalance_position | runner |
| Raydium keeper | raydium_harvest_for, raydium_compound_for, raydium_rebalance_for, raydium_exit_for | runner |
| Meteora owner | meteora_open_position, meteora_increase_position_length, meteora_add_liquidity, meteora_remove_liquidity, meteora_claim_fees, meteora_claim_reward, meteora_close_position | direct |
| Meteora keeper | meteora_harvest_for, meteora_compound_for, meteora_rebalance_for, meteora_exit_for, meteora_intent_open_position, meteora_intent_increase_position_length, meteora_intent_add_liquidity | runner + impl, direct |
| Swap | jupiter_swap | direct |
| Authorization (CLMM) | initialize_intent_domain, set_intent_session, preauthorize_intent, cancel_intent_authorization, expire_intent_authorization, finalize_intent | intent |
| Authorization (DLMM) | preauthorize_dlmm_intent, cancel_dlmm_intent_authorization, expire_dlmm_intent_authorization, finalize_dlmm_intent | intent |
Every file
The Emits column names the event mechanism. cpi+log means the Anchor
emit_cpi! and emit! macro pair. reusable means the allocation-reusing
emitter in events.rs. none means the handler emits nothing.
| Path | Lines | Group | Dispatch | Binding | Emits | Purpose |
|---|---|---|---|---|---|---|
instructions/admin/accept_admin.rs | 53 | Admin | direct | none | none | The pending admin accepts the admin role. |
instructions/intent/cancel_dlmm_intent_authorization.rs | 173 | Authorization (DLMM) | intent | intent | cpi+log | The owner closes a Meteora authorization before use. |
instructions/intent/cancel_intent_authorization.rs | 171 | Authorization (CLMM) | intent | intent | cpi+log | The owner closes a CLMM authorization before use. |
instructions/orca/compound_for.rs | 244 | Orca keeper | runner | orca | cpi+log | A keeper claims Orca yield as compound credit. |
instructions/sickle/create_sickle_ata.rs | 138 | Sickle | direct | token | none | Anyone creates a vault for one mint. |
instructions/orca/exit_for.rs | 281 | Orca keeper | runner | orca | cpi+log | A keeper closes an Orca position under an authorization. |
instructions/intent/expire_dlmm_intent_authorization.rs | 106 | Authorization (DLMM) | intent | intent | cpi+log | Anyone closes an expired Meteora authorization. |
instructions/intent/expire_intent_authorization.rs | 108 | Authorization (CLMM) | intent | intent | cpi+log | Anyone closes an expired CLMM authorization. |
instructions/intent/finalize_dlmm_intent.rs | 135 | Authorization (DLMM) | intent | intent | cpi+log | Proves and closes a Meteora action. |
instructions/intent/finalize_intent.rs | 628 | Authorization (CLMM) | intent | intent | cpi+log | Proves and closes a CLMM action. Holds the shared finalizer. |
instructions/orca/harvest_for.rs | 200 | Orca keeper | runner | orca | cpi+log | A keeper collects Orca fees and rewards. |
instructions/admin/initialize_global_config.rs | 78 | Admin | direct | none | none | The upgrade authority creates the global config. |
instructions/intent/initialize_intent_domain.rs | 64 | Authorization (CLMM) | intent | intent | none | The upgrade authority creates the intent domain. |
instructions/sickle/initialize_sickle.rs | 48 | Sickle | direct | none | cpi+log | A wallet creates its Sickle. |
instructions/jupiter/swap.rs | 615 | Swap | direct | jupiter | cpi+log | The owner, or an action, swaps between two vaults. |
instructions/meteora/add_liquidity.rs | 267 | Meteora owner | direct | meteora | cpi+log | The owner adds liquidity to a Meteora position. |
instructions/meteora/claim_fees.rs | 318 | Meteora owner | direct | meteora | cpi+log | The owner claims Meteora fees for a bin range. |
instructions/meteora/claim_reward.rs | 289 | Meteora owner | direct | meteora | cpi+log | The owner claims one Meteora reward slot. |
instructions/meteora/close_position.rs | 149 | Meteora owner | direct | meteora | cpi+log | The owner closes an empty Meteora position. |
instructions/meteora/compound_for.rs | 194 | Meteora keeper | runner + impl | meteora | cpi+log | A keeper claims Meteora yield as compound credit. |
instructions/meteora/exit_for.rs | 622 | Meteora keeper | direct | meteora | reusable | A keeper closes a Meteora position under an authorization. |
instructions/meteora/harvest_for.rs | 194 | Meteora keeper | runner + impl | meteora | cpi+log | A keeper collects Meteora yield. |
instructions/meteora/increase_position_length.rs | 149 | Meteora owner | direct | meteora | cpi+log | The owner widens a Meteora position. |
instructions/meteora/intent_add_liquidity.rs | 340 | Meteora keeper | direct | meteora | cpi+log | An action adds Meteora liquidity within signed limits. |
instructions/meteora/intent_increase_position_length.rs | 196 | Meteora keeper | direct | meteora | cpi+log | An action widens a Meteora position within signed limits. |
instructions/meteora/intent_open_position.rs | 279 | Meteora keeper | direct | meteora | cpi+log | An action opens a Meteora position within signed limits. |
instructions/meteora/keeper_flow.rs | 264 | Meteora shared | none | meteora | none | What the Meteora keeper flows share. It names no account struct, which is what keeps the module graph acyclic. |
instructions/meteora/open_position.rs | 147 | Meteora owner | direct | meteora | cpi+log | The owner opens an empty Meteora position. |
instructions/meteora/rebalance_for.rs | 847 | Meteora keeper | direct | meteora | reusable | A keeper moves a Meteora position to a new bin range. |
instructions/meteora/remove_liquidity.rs | 274 | Meteora owner | direct | meteora | cpi+log | The owner removes Meteora liquidity for a bin range. |
instructions/admin/migrate_global_config.rs | 147 | Admin | direct | none | none | The admin grows a legacy global config. |
instructions/meteora/yield_adapter.rs | 427 | Meteora shared | none | meteora | none | The one ClaimAllYieldAdapter impl, applied to all four Meteora keeper structs. |
instructions/orca/close_position.rs | 137 | Orca owner | runner | orca | cpi+log | The owner closes an empty Orca position. |
instructions/orca/collect_fees.rs | 153 | Orca owner | runner | orca | cpi+log | The owner collects Orca fees. |
instructions/orca/collect_rewards.rs | 85 | Orca owner | runner | orca | cpi+log | The owner collects Orca rewards. |
instructions/orca/decrease_liquidity.rs | 149 | Orca owner | runner | orca | cpi+log | The owner removes Orca liquidity. |
instructions/orca/increase_liquidity.rs | 184 | Orca owner | runner | orca | cpi+log | The owner, or an action, adds Orca liquidity. |
instructions/orca/open_position.rs | 99 | Orca owner | runner | orca | cpi+log | The owner opens an empty Orca position. |
instructions/orca/rebalance_position.rs | 242 | Orca owner | runner | orca | cpi+log | The owner moves an Orca position. |
instructions/orca/rebalance_accounts.rs | 108 | Orca shared | none | orca | none | Six constraint-closed account composites shared by owner and keeper rebalance. |
instructions/orca/yield_accounts.rs | 77 | Orca shared | none | orca | none | Five constraint-closed account composites shared by collect, harvest, and compound. |
instructions/admin/pause.rs | 60 | Admin | direct | none | none | The admin or the guardian pauses the program. |
instructions/intent/preauthorize_dlmm_intent.rs | 136 | Authorization (DLMM) | intent | intent | cpi+log | Stores a signed Meteora authorization. |
instructions/intent/preauthorize_intent.rs | 150 | Authorization (CLMM) | intent | intent | cpi+log | Stores a signed CLMM authorization. |
instructions/raydium/close_position.rs | 131 | Raydium owner | runner | raydium | cpi+log | The owner closes an empty Raydium position. |
instructions/raydium/collect_fees.rs | 155 | Raydium owner | runner | raydium | cpi+log | The owner collects Raydium fees and rewards. |
instructions/raydium/compound_for.rs | 211 | Raydium keeper | runner | raydium | cpi+log | A keeper claims Raydium yield as compound credit. |
instructions/raydium/decrease_liquidity.rs | 154 | Raydium owner | runner | raydium | cpi+log | The owner removes Raydium liquidity. Holds the reward-group parsers. |
instructions/raydium/exit_for.rs | 270 | Raydium keeper | runner | raydium | reusable | A keeper closes a Raydium position under an authorization. |
instructions/raydium/harvest_for.rs | 172 | Raydium keeper | runner | raydium | cpi+log | A keeper collects Raydium fees and rewards. |
instructions/raydium/increase_liquidity.rs | 180 | Raydium owner | runner | raydium | cpi+log | The owner, or an action, adds Raydium liquidity. |
instructions/raydium/open_position.rs | 165 | Raydium owner | runner | raydium | cpi+log | The owner opens an empty Raydium position. Holds the bitmap check. |
instructions/raydium/rebalance_for.rs | 262 | Raydium keeper | runner | raydium | reusable | A keeper moves a Raydium position under an authorization. |
instructions/raydium/rebalance_position.rs | 235 | Raydium owner | runner | raydium | cpi+log | The owner moves a Raydium position. Holds the shared Raydium helpers. |
instructions/raydium/rebalance_accounts.rs | 111 | Raydium shared | none | raydium | none | Seven constraint-closed account composites shared by owner and keeper rebalance. |
instructions/raydium/yield_accounts.rs | 95 | Raydium shared | none | raydium | none | Five constraint-closed account composites shared by collect, harvest, and compound. |
instructions/orca/rebalance_for.rs | 307 | Orca keeper | runner | orca | cpi+log | A keeper moves an Orca position under an authorization. |
instructions/admin/revoke_global_automator.rs | 116 | Admin | direct | none | none | The admin removes a global keeper. |
instructions/sickle/set_custom_automator.rs | 109 | Sickle | direct | none | none | The owner names a custom keeper. |
instructions/admin/set_fee.rs | 60 | Admin | direct | none | none | The admin sets one fee. |
instructions/admin/set_fee_collector.rs | 50 | Admin | direct | none | none | The admin sets the fee collector. |
instructions/admin/set_global_automator.rs | 160 | Admin | direct | none | none | The admin adds a global keeper. |
instructions/admin/set_guardian.rs | 54 | Admin | direct | none | none | The admin sets the guardian. |
instructions/intent/set_intent_session.rs | 141 | Authorization (CLMM) | intent | intent | cpi+log | The owner sets or revokes a session key. |
instructions/sickle/set_manager.rs | 59 | Sickle | direct | none | none | The owner sets the manager. |
instructions/position/set_position_settings.rs | 299 | Settings | direct | meteora | cpi+log | The owner writes position settings. |
instructions/admin/set_protocol_program.rs | 87 | Admin | direct | none | none | The admin sets one allowlist slot. |
instructions/sickle/sweep_token.rs | 205 | Sickle | direct | token | cpi+log | The owner moves a vault balance to the owner. |
instructions/admin/transfer_admin.rs | 51 | Admin | direct | none | none | The admin names a pending admin. |
instructions/admin/unpause.rs | 51 | Admin | direct | none | none | The admin unpauses the program. |
instructions/position/unset_position_settings.rs | 173 | Settings | direct | none | cpi+log | The owner closes position settings. |
instructions/intent/preauthorize.rs | 63 | support | - | intent | none | Shared transaction-A preauthorization checks. |
instructions/jupiter/cpi.rs | 282 | support | - | jupiter | none | Jupiter instruction and custody helpers. |
instructions/raydium/account_order_tests.rs | 621 | test support | - | raydium | none | Source-level account-order tests. |
instructions/orca/account_order_tests.rs | 569 | test support | - | orca | none | Source-level account-order tests. |
instructions/test_utils.rs | 277 | test support | - | none | none | Host entrypoint-dispatch fixture for direct handler tests. |
instructions/mod.rs | 113 | - | - | - | - | Module list and re-exports. |
Responsibilities
- Declare the account struct for one instruction, with Anchor constraints and a
CHECKcomment on every unchecked account. - Convert the instruction arguments into the request struct of a runner, or into venue CPI arguments.
- Run the caller check that the account struct cannot express, when the runner does not run it.
- Run the pause check for the handler classes that keep it outside the runner.
- Call the runner, the local helper, or the venue binding.
- Emit the events the runner returns, or the events the handler measured itself.
- Host shared flow helpers that both the adapter layer and the runner layer import.
- Move large local frames into
#[inline(never)]helpers.
Public items
The table lists every pub item and every pub(crate) or pub(super) item. The first column is the file. Each handler file has one account struct and one handler function with the same name in snake case. Only the extra items are named.
| File | Account struct | Handler | Other items |
|---|---|---|---|
instructions/admin/accept_admin.rs | AcceptAdmin | accept_admin | none |
instructions/intent/cancel_dlmm_intent_authorization.rs | CancelDlmmIntentAuthorization | cancel_dlmm_intent_authorization | none |
instructions/intent/cancel_intent_authorization.rs | CancelIntentAuthorization | cancel_intent_authorization | none |
instructions/orca/compound_for.rs | CompoundFor | compound_for | none |
instructions/sickle/create_sickle_ata.rs | CreateSickleAta | create_sickle_ata | none |
instructions/orca/exit_for.rs | ExitFor | exit_for | none |
instructions/intent/expire_dlmm_intent_authorization.rs | ExpireDlmmIntentAuthorization | expire_dlmm_intent_authorization | none |
instructions/intent/expire_intent_authorization.rs | ExpireIntentAuthorization | expire_intent_authorization | none |
instructions/intent/finalize_dlmm_intent.rs | FinalizeDlmmIntent | finalize_dlmm_intent | none |
instructions/intent/finalize_intent.rs | FinalizeIntent | finalize_intent | IntentFinalizationReceipt (struct), finalize_action_intent (fn, pub(crate)) shared with the DLMM finalizer |
instructions/orca/harvest_for.rs | HarvestFor | harvest_for | none |
instructions/admin/initialize_global_config.rs | InitializeGlobalConfig | initialize_global_config | none |
instructions/intent/initialize_intent_domain.rs | InitializeIntentDomain | initialize_intent_domain | none |
instructions/sickle/initialize_sickle.rs | InitializeSickle | initialize_sickle | none |
instructions/jupiter/swap.rs | JupiterSwap | jupiter_swap | none |
instructions/meteora/add_liquidity.rs | MeteoraAddLiquidity | meteora_add_liquidity | execute_meteora_add_liquidity (fn, pub(crate)) shared with the intent add |
instructions/meteora/claim_fees.rs | MeteoraClaimFees | meteora_claim_fees | none |
instructions/meteora/claim_reward.rs | MeteoraClaimReward | meteora_claim_reward | none |
instructions/meteora/close_position.rs | MeteoraClosePosition | meteora_close_position | none |
instructions/meteora/compound_for.rs | MeteoraCompoundFor | meteora_compound_for | none |
instructions/meteora/exit_for.rs | MeteoraExitFor | meteora_exit_for | private begin_exit_intent, remove_all_liquidity, charge_exit_principal_fees, close_position, emit_exit_events |
instructions/meteora/harvest_for.rs | MeteoraHarvestFor | meteora_harvest_for | none |
instructions/meteora/increase_position_length.rs | MeteoraIncreasePositionLength | meteora_increase_position_length | none |
instructions/meteora/intent_add_liquidity.rs | MeteoraIntentAddLiquidity | meteora_intent_add_liquidity | signed_distribution_matches (fn, pub(crate)) |
instructions/meteora/intent_increase_position_length.rs | MeteoraIntentIncreasePositionLength | meteora_intent_increase_position_length | none |
instructions/meteora/intent_open_position.rs | MeteoraIntentOpenPosition | meteora_intent_open_position | none |
instructions/meteora/open_position.rs | MeteoraOpenPosition | meteora_open_position | none |
instructions/meteora/rebalance_for.rs | MeteoraRebalanceFor | meteora_rebalance_for | private begin_rebalance_intent, remove_all_liquidity, close_old_position, charge_rebalance_principal_fees, emit_rebalance_events |
instructions/meteora/remove_liquidity.rs | MeteoraRemoveLiquidity | meteora_remove_liquidity | none |
instructions/meteora/keeper_flow.rs | none | none | MeteoraHarvestReward, PreparedMeteoraHarvest (structs), reload_pool_vaults_after_principal_fee (fn) |
instructions/meteora/yield_adapter.rs | none | none | impl_meteora_yield_adapter! (macro) that implements the yield trait for all 4 Meteora keeper structs (:424-427) |
instructions/admin/migrate_global_config.rs | MigrateGlobalConfig | migrate_global_config | none |
instructions/orca/close_position.rs | OrcaClosePosition | orca_close_position | none |
instructions/orca/collect_fees.rs | OrcaCollectFees | orca_collect_fees | none |
instructions/orca/collect_rewards.rs | OrcaCollectRewards | orca_collect_rewards | none |
instructions/orca/decrease_liquidity.rs | OrcaDecreaseLiquidity | orca_decrease_liquidity | none |
instructions/orca/increase_liquidity.rs | OrcaIncreaseLiquidity | orca_increase_liquidity | none |
instructions/orca/open_position.rs | OrcaOpenPosition | orca_open_position | none |
instructions/orca/rebalance_position.rs | OrcaRebalancePosition | orca_rebalance_position | DrainOutcome moved with the legs that return it; see venue/adapters/orca_flows.rs |
instructions/orca/rebalance_accounts.rs | none | none | OrcaRebalanceConfig, OrcaRebalanceOldPosition, OrcaRebalanceNewPosition, OrcaRebalanceMints, OrcaRebalanceVaults, OrcaRebalanceTickArrays (account composites) |
instructions/orca/yield_accounts.rs | none | none | OrcaYieldConfig, OrcaYieldPositionAccounts, OrcaYieldMints, OrcaYieldPrograms, OrcaYieldRewardPrograms (account composites) |
instructions/admin/pause.rs | Pause | pause | none |
instructions/intent/preauthorize_dlmm_intent.rs | PreauthorizeDlmmIntent | preauthorize_dlmm_intent | none |
instructions/intent/preauthorize_intent.rs | PreauthorizeIntent | preauthorize_intent | none |
instructions/raydium/close_position.rs | RaydiumClosePosition | raydium_close_position | none |
instructions/raydium/collect_fees.rs | RaydiumCollectFees | raydium_collect_fees | none |
instructions/raydium/compound_for.rs | RaydiumCompoundFor | raydium_compound_for | none |
instructions/raydium/decrease_liquidity.rs | RaydiumDecreaseLiquidity | raydium_decrease_liquidity | none |
instructions/raydium/exit_for.rs | RaydiumExitFor | raydium_exit_for | none |
instructions/raydium/harvest_for.rs | RaydiumHarvestFor | raydium_harvest_for | none |
instructions/raydium/increase_liquidity.rs | RaydiumIncreaseLiquidity | raydium_increase_liquidity | none |
instructions/raydium/open_position.rs | RaydiumOpenPosition | raydium_open_position | none |
instructions/raydium/rebalance_for.rs | RaydiumRebalanceFor | raydium_rebalance_for | none |
instructions/raydium/rebalance_position.rs | RaydiumRebalancePosition | raydium_rebalance_position | none |
instructions/raydium/rebalance_accounts.rs | none | none | RaydiumRebalanceConfig, RaydiumRebalancePool, RaydiumRebalanceNewPosition, RaydiumRebalanceTickArrays, RaydiumRebalanceVaults, RaydiumRebalanceFeeCollectors, RaydiumRebalancePrograms (account composites) |
instructions/raydium/yield_accounts.rs | none | none | RaydiumYieldConfig, RaydiumYieldPosition, RaydiumYieldTickArrays, RaydiumYieldVaults, RaydiumYieldPrograms (account composites) |
instructions/orca/rebalance_for.rs | RebalanceFor | rebalance_for | assert_token_2022_mint (fn) |
instructions/admin/revoke_global_automator.rs | RevokeGlobalAutomator | revoke_global_automator | none |
instructions/sickle/set_custom_automator.rs | SetCustomAutomator | set_custom_automator | none |
instructions/admin/set_fee.rs | SetFee | set_fee | none |
instructions/admin/set_fee_collector.rs | SetFeeCollector | set_fee_collector | none |
instructions/admin/set_global_automator.rs | SetGlobalAutomator | set_global_automator | none |
instructions/admin/set_guardian.rs | SetGuardian | set_guardian | none |
instructions/intent/set_intent_session.rs | SetIntentSession | set_intent_session | IntentSessionArgs (struct) |
instructions/sickle/set_manager.rs | SetManager | set_manager | none |
instructions/position/set_position_settings.rs | SetPositionSettings | set_position_settings | none |
instructions/admin/set_protocol_program.rs | SetProtocolProgram | set_protocol_program | none |
instructions/sickle/sweep_token.rs | SweepToken | sweep_token | none |
instructions/admin/transfer_admin.rs | TransferAdmin | transfer_admin | none |
instructions/admin/unpause.rs | Unpause | unpause | none |
instructions/position/unset_position_settings.rs | UnsetPositionSettings | unset_position_settings | none |
How a call flows through it
Account struct conventions
Every account struct uses the same building blocks. The table counts them across the handler files the census above counts.
| Construct | Occurrences | Files | Use |
|---|---|---|---|
#[event_cpi] | 49 | 49 | Every handler that emits. It adds the event authority and the program account to the struct. |
Signer<'info> | 82 | 63 | The payer, the authority, and fresh position mints. |
Account<'info, T> | 294 | 64 | Program-owned accounts: the global config, the Sickle, position settings, keeper records, intent accounts. |
InterfaceAccount<'info, T> | 127 | 33 | Token accounts and mints that may live on Token or Token-2022. |
UncheckedAccount<'info> | 424 | 46 | Venue-owned accounts and program IDs. Each one carries a /// CHECK comment. |
/// CHECK | 424 | 46 | Anchor requires the comment. The text says where the check runs. |
Program<'info, T> | 77 | 39 | Token, Token-2022, Associated Token, and System. |
Sysvar<'info, Rent> | 3 | 3 | Raydium open and rebalance need the rent sysvar. |
Option<...> | 77 | 24 | Optional accounts: the global keeper record, the intent accounts, the Raydium bitmap, the old settings candidate. |
Box<...> | 252 | 43 | Every deserialized account in a large struct, to keep try_accounts inside the SBF frame. |
seeds = [...] | 161 | 65 | Every program PDA is re-derived with bump = account.bump. |
address = ... | 62 | 36 | Fixed program IDs, the rent receiver as sickle.owner, the rent payer stored in an authorization. |
constraint = ... | 80 | 42 | The owner check, the position NFT balance, mint equality. |
has_one | 14 | 12 | Admin and owner links on small structs. |
token::authority | 96 | 31 | Every vault must be owned by the Sickle. Every fee-collector account must be owned by the fee collector. |
token::mint | 42 | 21 | Fee-collector accounts must match the vault mint. |
close = ... | 15 | 11 | Authorization and settings accounts close to a stored payer or to the owner. |
init / init_if_needed | 10 / 10 | 10 / 6 | New PDAs and vaults. |
ctx.remaining_accounts | 53 | 32 | Reward groups, hook accounts, intent vaults, Meteora bin arrays. |
The owner check has one spelling and two mechanisms. Eighteen constraints derive the Sickle from sickle.owner and authorize with constraint = sickle.is_owner(&authority.key()) (instructions/orca/close_position.rs:50, instructions/position/set_position_settings.rs:47, instructions/intent/set_intent_session.rs:43). Two structs derive it from the signer instead, so the seeds authorize and no constraint does the work: instructions/sickle/set_custom_automator.rs:24-29 and instructions/sickle/set_manager.rs:16-20. The first keeps a has_one = owner that cannot fail, because initialize_sickle is the only writer of owner and seeds with the same key; both structs say so.
Option<Account> follows two patterns. A read-only optional record is Option<Box<Account<'info, GlobalAutomator>>> (instructions/orca/harvest_for.rs:60, instructions/orca/exit_for.rs:84). An optional account the program creates is Option<Box<Account<'info, PositionSettings>>> with init (instructions/orca/rebalance_position.rs:163-174). The intent accounts on keeper structs are Option<UncheckedAccount> and the runner loads them (instructions/orca/rebalance_for.rs:214-235, loaded by the adapter at venue/adapters/orca.rs:669 through venue/adapters/mod.rs:99-117).
Remaining-account grammar
Handlers do not parse ctx.remaining_accounts. They pass the slice to the runner, and the adapter or a helper in a handler file consumes it in fixed order. The grammar per venue is on the venue adapters page. The handler declares only the lengths it cannot derive: hook_lens: [u8; 2] and reward_hook_lens: [u8; 3] for Orca (instructions/orca/harvest_for.rs:134-135), min_bin_id and max_bin_id for Meteora (instructions/meteora/harvest_for.rs:135-136).
Frame-size convention
A handler whose locals plus one call's arguments exceed the SBF frame corrupts a value at runtime. The convention moves such blocks into #[inline(never)] functions.
| File | #[inline(never)] functions |
|---|---|
venue/adapters/orca_flows.rs | reopen_orca_position (:214), resolve_pool_hooks (:359), close_drained_position (:411), drain_position_liquidity (:448), collect_position_fees (:548), charge_drain_fees (:643), withdraw_collect_charge_close_with_actions (:724), drain_accounts (:851), reopen_accounts (:860), invoke_orca_fee_collect (:881), collect_fees_and_charge (:940), take_orca_reward_groups (:1064), collect_orca_rewards_and_charge (:1153), assert_token_2022_mint (:1321) |
venue/adapters/raydium_flows.rs | charge_raydium_reward_fees (:216), charge_raydium_pool_fees (:322), collect_drain_charge_close_with_actions (:502), close_drained_raydium_position (:607), reopen_raydium_position (:692), drain_accounts (:763), reopen_accounts (:772), take_reward_groups (:870), take_reward_collectors (:922), reward_baselines (:952), invoke_raydium_decrease (:975), decrease_liquidity_and_charge (:1053) |
instructions/intent/finalize_intent.rs | sweep_exit_residuals (:224), finalize_action_intent (:342) |
instructions/meteora/exit_for.rs | charge_exit_principal_fees generated at :275, close_position (:278), emit_exit_events (:315) |
instructions/meteora/keeper_flow.rs | reload_pool_vaults_after_principal_fee (:23), credit_meteora_drain_ledger (:122), the remove_all_liquidity written by meteora_remove_all_liquidity! (:170) |
instructions/meteora/rebalance_for.rs | close_old_position (:352), charge_rebalance_principal_fees (:388), emit_rebalance_events (:395) |
instructions/meteora/yield_adapter.rs | prepare_reward_groups (:106), prepare_all_yield (:175), claim_all_yield (:249) |
The same convention removes Anchor constraints from the keeper rebalance structs. RebalanceFor.old_position_mint is UncheckedAccount and assert_token_2022_mint re-runs the mint check in its own frame (instructions/orca/rebalance_for.rs:239-252). OrcaRebalancePosition.old_position_mint keeps the typed InterfaceAccount<Mint> (instructions/orca/rebalance_position.rs:81-82). The account bundles OrcaDrainAccounts and OrcaReopenAccounts are built on the heap inside drain_accounts and reopen_accounts for the same reason (venue/adapters/orca_flows.rs:165-171, venue/adapters/orca_flows.rs:840-844).
Event emission convention
A handler stores each event in a local once and emits both copies. The self-CPI copy goes through emit_cpi!. The log copy goes through emit!. FeeCharged gets only the self-CPI copy (instructions/orca/collect_fees.rs:137-149). The runner returns the event set and never emits (venue/owner_rebalance.rs:122-123, venue/keeper_exit.rs:50).
| Emitter | Users | Why |
|---|---|---|
emit_cpi! + emit! | 45 handler files | Anchor default. |
ReusableEventEmitter | instructions/raydium/rebalance_for.rs, instructions/raydium/exit_for.rs, instructions/meteora/rebalance_for.rs, instructions/meteora/exit_for.rs | These flows emit many events after many CPIs. The reusable emitter allocates once (events.rs:53-63). |
| none | 16 files: the admin and Sickle-config handlers and instructions/intent/initialize_intent_domain.rs | The instruction data determines the state change (events.rs:26-30). |
The reusable emitter takes the event authority from the struct and calls emit_user for the pair or emit_cpi for the self-CPI only (instructions/raydium/exit_for.rs:253-268).
Walk-through: a thin handler
instructions/orca/open_position.rs has 99 lines. The account struct spans 56 lines (instructions/orca/open_position.rs:23-78), and the handler function has 20 lines (:80-99). It follows one call from the entry point to the event.
lib.rs:266-278receives the call and forwards the arguments toinstructions::orca::open_position::orca_open_position.instructions/orca/open_position.rs:23-78is the account struct.#[event_cpi]adds the event authority.global_configandsickleare PDAs withseedsandbump.whirlpool_program,whirlpool,position, andposition_token_accountare unchecked.metadata_update_authhasaddress = WP_NFT_UPDATE_AUTH(:76).instructions/orca/open_position.rs:86-95builds anOpenRequestwith the tick range and the metadata flag, then callsrun_open.venue/open_increase.rs:163callsvalidate_active_flow. That function runs the pause check and the allowlist check (:46-49).venue/open_increase.rs:164-167runs the owner check and returnsUnauthorizedon failure.venue/open_increase.rs:174calls the adapter'sprepare_open. The adapter checks the pool owner, the tick range, the position PDA, and the position token account address (venue/adapters/orca.rs:1191-1228).venue/open_increase.rs:175calls the adapter'sexecute_open. The adapter builds the Orca instruction and invokes it (venue/adapters/orca.rs:1231-1260), then reads the new position state (:1262-1284).venue/open_increase.rs:176callsvalidate_open_receipt. That function checks the receipt against the identity and requires an empty position (:102-151).venue/open_increase.rs:177-183builds thePositionOpenedevent.instructions/orca/open_position.rs:96-97emits the event twice.
Walk-through: a thick handler
instructions/orca/rebalance_position.rs has 242 lines. It is not the largest handler file: instructions/meteora/rebalance_for.rs has 847 lines and instructions/raydium/rebalance_position.rs has 235 lines. The handler function itself (:189-242) has 54 lines; its declaration is now :67-186, with six constraint-closed groups shared from instructions/orca/rebalance_accounts.rs:12-108. Both helpers this file used to hold have left it. The shared Orca remove-and-close helper that the owner rebalance, the keeper rebalance, and the keeper exit all call now sits beside the adapter that drives it (venue/adapters/orca_flows.rs:724-838), and the CLMM settings-transfer wrapper both venues' runners import was venue-agnostic and moved to settings.rs:144-200.
instructions/orca/rebalance_position.rs:67-186is the account struct. Its six constraint-closed groups come frominstructions/orca/rebalance_accounts.rs:12-108.receiveris checked by the runner (instructions/orca/rebalance_position.rs:137-141).old_position_settingsisOption<UncheckedAccount>(instructions/orca/rebalance_position.rs:143-159).new_position_settingsisOption<Box<Account<PositionSettings>>>withinit(instructions/orca/rebalance_position.rs:161-174).instructions/orca/rebalance_position.rs:202reads the bump of the new settings fromctx.bumps.instructions/orca/rebalance_position.rs:203-219builds anOwnerRebalanceRequestwith the range, the minimum tokens, and the Orca-specific request, then callsrun_owner_rebalance.venue/owner_rebalance.rs:438-442runs the pause check, the allowlist check, and the owner check.venue/owner_rebalance.rs:455checks the rent accounts.:459-489resolves the old settings candidate and:490-493requires the new settings account exactly when old settings exist.venue/owner_rebalance.rs:495calls the adapter'sprepare_owner_rebalance(venue/adapters/orca.rs:479-558). The adapter reads the old position throughread_old_position(venue/adapters/orca_flows.rs:52-60). It re-derives the new position throughassert_new_position_derivation(venue/adapters/orca_flows.rs:65-89). It builds the two account bundles throughdrain_accounts(venue/adapters/orca_flows.rs:851-856) andreopen_accounts(venue/adapters/orca_flows.rs:860-875).venue/owner_rebalance.rs:505calls the adapter'sexecute_drain_and_close. The adapter callswithdraw_collect_charge_close_with_actions(venue/adapters/orca_flows.rs:724-838), which is now a sequence of calls rather than the work itself. It checks the memo program and the token programs inline (venue/adapters/orca_flows.rs:748-754), then resolves the hook accounts once throughresolve_pool_hooks(venue/adapters/orca_flows.rs:359-402, called at:759). It removes all liquidity throughdrain_position_liquidity(venue/adapters/orca_flows.rs:448-540, called at:768) and collects the fees throughcollect_position_fees(venue/adapters/orca_flows.rs:548-610, called at:778). It charges the fees throughcharge_drain_fees(venue/adapters/orca_flows.rs:643-720, called at:798), collects the rewards by callingcollect_orca_rewards_and_chargedirectly (:815), and closes the position throughclose_drained_position(venue/adapters/orca_flows.rs:411-428, called at:825).venue/owner_rebalance.rs:506-507validates the receipt and requires the closed accounts to be empty.venue/owner_rebalance.rs:518callstransfer_position_settings(settings.rs:159-200). That wrapper checks the CLMM pool and tick width, then delegates the complete settings copy, timestamp, and cleared keeper history toPositionSettings::transfer_from(state.rs:335-351).venue/owner_rebalance.rs:542calls the adapter'sexecute_reopen. The adapter callsreopen_orca_position(venue/adapters/orca_flows.rs:214-251).venue/owner_rebalance.rs:543validates the reopened position.:558closes the old settings candidate to the receiver.instructions/orca/rebalance_position.rs:221-241emits the fee, reward, rebalance, open, and settings events.
Invariants the layer owns
The runners own most flow invariants. This layer owns the checks that run before a runner, and every check inside a direct handler.
- Only the owner calls an owner venue instruction. The account struct enforces it with
constraint = sickle.is_owner(...)on the close, decrease, collect, sweep, and Meteora owner structs (instructions/orca/close_position.rs:50,instructions/sickle/sweep_token.rs:26,instructions/meteora/add_liquidity.rs:39). The runner enforces it for open, increase, and owner rebalance (venue/open_increase.rs:164-167,venue/owner_rebalance.rs:439-442). The error isUnauthorized. - A keeper instruction requires an authorized keeper. Every runner calls
is_authorized_automatorwith the permission bit its operation names: harvest and compound throughauthorize_keeper_claim(venue/claim.rs:252-259), keeper rebalance and keeper exit in the runner body (venue/keeper_rebalance.rs:255,venue/keeper_exit.rs:266). The error isUnauthorized. - The pause check covers 38 of the 65 instructions 01-entry-points.md enumerates: every Orca, Raydium, and Meteora instruction that moves liquidity, fees, or rewards (35, the
meteora_intent_*legs included),jupiter_swap(instructions/jupiter/swap.rs:440), and both finalizers. The check runs asGlobalConfig::assert_not_paused, or asassert_not_paused_unless_owner_exitfor the owner's decrease, collect, close, and Meteora remove, claim, and close (state.rs:913-914). It sits in the handler, in the runner, or in a shared helper the handler calls: the six keeper claims reach it throughauthorize_keeper_claim(venue/claim.rs:246),finalize_dlmm_intentthroughfinalize_action_intent(instructions/intent/finalize_intent.rs:350), andmeteora_intent_add_liquiditythrough theexecute_meteora_add_liquidity!macro (instructions/meteora/add_liquidity.rs:104). The other 27 instructions have no pause check: the 12 admin instructions, the 4 Sickle instructions, the owner'ssweep_token(state.rs:892), the 2 settings instructions, and the 8 authorization instructions other than the two finalizers. The error isPaused. - A venue program must match its allowlist slot. The runner checks it for open, increase, owner rebalance, keeper rebalance, and keeper exit (
venue/open_increase.rs:48-49,venue/owner_rebalance.rs:438,venue/keeper_rebalance.rs:235,venue/keeper_exit.rs:249). The adapter checks it for decrease and close (venue/adapters/orca.rs:1456-1457,:1605-1606,venue/adapters/raydium.rs:1719-1720,venue/adapters/raydium_flows.rs:826). The Orca reward parser checks it again (venue/adapters/orca_flows.rs:1073). Every Meteora handler andjupiter_swapcheck it in the handler (instructions/meteora/open_position.rs:66,instructions/jupiter/swap.rs:440). - A fixed program ID must equal its constant. The memo program is checked by
address = MEMO_PROGRAM_IDon 22 structs, by the adapter onRaydiumRebalanceFor(venue/adapters/raydium.rs:660-664), and by the helper onRebalanceFor(venue/adapters/orca_flows.rs:748-752). The error isInvalidAccountOwner. - Rent returns to the owner. Close handlers pin
receiverwithaddress = sickle.owner @ InvalidRentReceiver(instructions/raydium/close_position.rs:63,instructions/meteora/close_position.rs:41); rebalance runners enforce the same invariant for their nested account structs (venue/owner_rebalance.rs:455,venue/keeper_rebalance.rs:230). Authorization rent returns to the stored payer (instructions/intent/finalize_intent.rs:57-62). - Every keeper claim vault is the Sickle's canonical associated token account for the pool mint. The venue layer re-derives both addresses and returns
RewardRecipientMismatch(venue/adapters/orca.rs:272-299for the two Orca claims,venue/adapters/raydium.rs:182-223for the two Raydium ones,instructions/meteora/yield_adapter.rs:83-102for the two Meteora ones). The owner collects are not covered: their destinations carry onlytoken::authority = sickle.
Check placement by handler class
| Handler class | Pause check | Owner or keeper check | Allowlist check |
|---|---|---|---|
| Orca and Raydium open, increase | runner venue/open_increase.rs:47 | runner :164-167, or the intent path | runner :48-49 |
Orca and Raydium decrease, collect, close, orca_collect_rewards | handler, owner-exit form (instructions/orca/close_position.rs:119, instructions/raydium/decrease_liquidity.rs:139, instructions/orca/collect_rewards.rs:58) | struct constraint | adapter or helper (venue/adapters/orca.rs:1456, venue/adapters/raydium_flows.rs:826) |
| Orca and Raydium owner rebalance | runner venue/owner_rebalance.rs:438 | runner :439-442 | runner :438 |
| Orca, Raydium, and Meteora harvest, compound | runner venue/claim.rs:246 | runner venue/claim.rs:252-270 | Orca: reward parser (venue/adapters/orca_flows.rs:1073). Raydium: validate_decrease_accounts (venue/adapters/raydium_flows.rs:826) |
| Orca and Raydium keeper rebalance | runner venue/keeper_rebalance.rs:235 | runner :255 | runner :235 |
| Orca and Raydium keeper exit | runner, owner-exit form venue/keeper_exit.rs:246 | runner :266 | runner :249 |
| Meteora owner | handler, owner-exit form for remove, claim, close (instructions/meteora/remove_liquidity.rs:116); plain form for open, resize, add (instructions/meteora/open_position.rs:63) | struct constraint (instructions/meteora/open_position.rs:37) | handler (instructions/meteora/open_position.rs:66) |
| Meteora keeper rebalance, exit | handler (instructions/meteora/rebalance_for.rs:505, instructions/meteora/exit_for.rs:412) | handler (instructions/meteora/rebalance_for.rs:510-517, instructions/meteora/exit_for.rs:417-424) | handler (instructions/meteora/rebalance_for.rs:508, instructions/meteora/exit_for.rs:415) |
jupiter_swap | handler instructions/jupiter/swap.rs:440 | request validator instructions/jupiter/swap.rs:72-100 | handler instructions/jupiter/swap.rs:441 |
| Admin, Sickle, Settings | none | struct constraint or has_one | not applicable |
| Authorization except the two finalizers (8) | none | upgrade authority, owner, session key, or anyone, per instruction (see layer 08) | not applicable |
finalize_intent, finalize_dlmm_intent | shared helper instructions/intent/finalize_intent.rs:342 | executor (instructions/intent/finalize_intent.rs:96, instructions/intent/finalize_dlmm_intent.rs:75) | not applicable |
meteora_intent_add_liquidity, meteora_intent_open_position, meteora_intent_increase_position_length | handler or shared macro (instructions/meteora/add_liquidity.rs:104, instructions/meteora/intent_open_position.rs:124, instructions/meteora/intent_increase_position_length.rs:64) | intent path | handler or shared macro |
Tests
| Test location | What it covers |
|---|---|
tests/sickle/core.ts | Admin, Sickle, settings, and sweep instructions on the legacy validator. |
tests/sickle/orca.ts, orca-lifecycle.ts | Orca owner and keeper instructions against a cloned Orca program. |
tests/sickle/raydium.ts | Raydium owner and keeper instructions against a cloned Raydium program. |
tests/sickle/intents.ts | The CLMM authorization instructions. |
tests/sickle/jupiter.ts | jupiter_swap. |
tests/surfpool-*.ts | Full lifecycles on a mainnet fork for Orca, Raydium, Meteora, Jupiter, and Token-2022 pools. |
programs/sickle/tests/clmm_conformance.rs | Compile-time proof that each Orca and Raydium account struct implements the runner traits it needs. |
instructions/*/account_order_tests.rs | Frozen flattened account order and meta flags for ten nested Orca and Raydium yield/rebalance instructions. |
| Rust unit tests inside handler files | 30 files carry #[cfg(test)] modules. Seventeen dispatch the generated instruction entrypoint directly. |
Ten admin and Sickle tests now build serialized program accounts, dispatch the
generated instruction through crate::entry, and read the persisted account
back through Anchor. The shared fixture is instructions/test_utils.rs:1-277;
the handler cases are instructions/admin/accept_admin.rs:27-53,
instructions/admin/pause.rs:32-60, instructions/admin/set_fee.rs:26-60,
instructions/admin/set_fee_collector.rs:25-50,
instructions/admin/set_guardian.rs:27-54,
instructions/admin/set_protocol_program.rs:34-87,
instructions/admin/transfer_admin.rs:25-51,
instructions/admin/unpause.rs:27-51,
instructions/sickle/set_custom_automator.rs:53-109, and
instructions/sickle/set_manager.rs:27-59. These are handler tests, not pure
helper tests: they include account deserialization, PDA and has_one
constraints, the handler call, and account write-back.
Two custody tests use the same entrypoint boundary. The existing-account path
of create_sickle_ata proves that an arbitrary payer may provision only the
canonical Sickle vault (instructions/sickle/create_sickle_ata.rs:47-138). A
zero-amount sweep_token proves that the early return does not bypass the
owner, vault ATA, vault authority, or owner-destination constraints
(instructions/sickle/sweep_token.rs:87-205).
The existing-account path of set_global_automator now reaches the generated
entrypoint too. It pins the admin link, all three permission/authority guards,
and the complete keeper write
(instructions/admin/set_global_automator.rs:63-160).
Both authorization-cancellation families now reach that same entrypoint
boundary. They pin an owner signer rather than the configured manager, the
stored rent receiver, the family-specific authorization PDA, and the account
discriminator (instructions/intent/cancel_intent_authorization.rs:61-171,
instructions/intent/cancel_dlmm_intent_authorization.rs:63-173). The host
tests stop at those rejection paths. The CLMM close runs in the validator suite,
but neither cancellation event is asserted there; the DLMM success path is
reached only by the funded acceptance suite
(tests/mainnet-meteora-acceptance.ts:214).
The global-keeper revocation and direct settings removal handlers now reach the
generated entrypoint as well. The host tests pin the admin or owner signer, the
stored authority link, the owner-only settings boundary, the rent receiver,
and both close-account PDAs before Anchor performs the close
(instructions/admin/revoke_global_automator.rs:39-116,
instructions/position/unset_position_settings.rs:72-173). They deliberately
stop at those rejection paths; the successful close lifecycles continue to run
in the validator suite (tests/sickle/core.ts:730,
tests/sickle/orca.ts:1417).
The two permissionless authorization-expiry handlers now test their pure
slot-boundary and family-specific event-building seams
(instructions/intent/expire_intent_authorization.rs:54-108,
instructions/intent/expire_dlmm_intent_authorization.rs:51-106). The live
Clock read, Anchor close, and paired event emissions remain covered only by the
validator suite; the host tests do not fake those runtime operations.
The file census is structural, not a count of untested behavior. Several host tests deliberately overlap validator coverage because they isolate one generated constraint or persisted field without validator startup or shared chain state; the runtime tests remain the proof of CPIs, closes, event emission, and full lifecycle behavior.
| Handler files | Existing runtime coverage | Additional value of the host entrypoint test |
|---|---|---|
accept_admin, transfer_admin | Role transfer and acceptance run at tests/sickle/core.ts:249-304. | Separates the signer/has_one boundary from the two-step lifecycle and checks the exact persisted fields. |
set_fee, set_fee_collector, set_guardian, pause, unpause, set_protocol_program | Admin success and rejection paths run at tests/sickle/core.ts:163-231, 322-485. | Gives each guard and one-field write its own mutation target without relying on validator account history. |
set_manager, set_custom_automator | Owner and manager/permission rejections run at tests/sickle/core.ts:537-610. | Pins the generated owner constraint and the exact write or clear in one in-memory dispatch. |
set_global_automator, revoke_global_automator | Create, update, reject, and revoke run at tests/sickle/core.ts:636-730. | Isolates the existing-account guards and close preconditions; account creation and close stay runtime-owned. |
create_sickle_ata, sweep_token | ATA creation and sweep custody run at tests/sickle/core.ts:765-1021. | Uses no-CPI paths to pin every Anchor custody constraint independently of token-program behavior. |
cancel_intent_authorization, cancel_dlmm_intent_authorization | CLMM cancellation runs at tests/sickle/intents.ts:789-828; DLMM cancellation runs at tests/mainnet-meteora-acceptance.ts:214. | Pins owner, signer, receiver, PDA, and discriminator rejection order before close/event CPI. |
unset_position_settings | Successful and rejected removals run at tests/sickle/orca.ts:1417,3069-3114. | Pins owner-only authority, receiver, and settings PDA before close/event CPI. |
Structural census: 35 handler files carry no #[cfg(test)] module. That
number is not a coverage gap and is no longer a closure metric. The remaining
coverage question is which handler constraints or state transitions survive
all existing host and validator tests; that behavior-level matrix has not been
enumerated. A further handler test must name such a surviving mutation or a
seam required by a planned refactor before it counts as audit progress.
First-time
set_global_automator initialization and the successful close lifecycles remain
validator-only because they respectively invoke System-program account creation
and Anchor close. The host fixture supplies Rent only for an already-initialized
init_if_needed account; it does not pretend to model either lifecycle
transition. The check-placement table above is not asserted by a test.
No test asserts that the 4 reusable-emitter handlers produce the same bytes as
the macro pair for the same event set, except the events.rs unit test on the
emitter itself.
Aggregate checkpoint, 2026-09-16. An independent structured review read the
complete on-chain range f78ad65..b5ecaa8 (42 files, +2,256/-1,199) and found
no P0-P2 issue. A separate endpoint differential compared the unchanged
Surfpool suites at GitHub Actions runs 35062744219 and 35128161313: Orca
passed 35 tests at both endpoints, Raydium 18, and Meteora 9. The normalized
62-test title list had SHA-256
85c96111b157abcb6ae4cedadbfceaed84234bb31ab9f7cf93d9e2c650d6e838 at
both endpoints. Reported Meteora compute units moved only by -154 (compound),
-132 (rebalance), and -52 (exit). This is an assertion-level differential,
not a byte-for-byte poststate comparison: the lifecycle suites do not emit a
durable instruction, event, balance, or account-state artifact to diff.
Simplification and audit notes
- L02-N005 — The source-location census is 35 of 65 handler files without a Rust unit-test module, but that is not a behavioral coverage measure. Seventeen handlers reach Anchor's generated entrypoint in host tests and thirteen other handler files carry helper- or shape-level tests; several deliberately overlap the validator coverage recorded in the Tests section because the host form isolates exact generated constraints and writes while the runtime form proves CPIs and lifecycle effects. The open gap is the absence of a constraint-by-handler matrix across both levels: no further
#[cfg(test)]module closes this note unless a mutation first demonstrates behavior that every existing test misses, or the test pins a seam required by a measured refactor. Invenue/adapters/,orca.rshas four unit tests,raydium.rshas none, andraydium_flows.rshas seven.
Resolved
The numbers are the ones these notes carried before this contract; the open list above is renumbered. Citations here describe the code as it was, and are not remapped when it moves.
L02-N001 — The Orca and Raydium harvest/compound pairs now share their constraint-closed account groups. Orca's
HarvestFor(instructions/orca/harvest_for.rs:44-128) andCompoundFor(instructions/orca/compound_for.rs:55-173) adopt five composites frominstructions/orca/yield_accounts.rs:17-77; the four groups also used by collect hold ten accounts, and the keeper-only reward-program group holds two more.authorityandglobal_automatorstay outside because compound alone makesauthoritywritable and the keeper PDA seeds depend on it. The frozen pre-refactor tables ininstructions/orca/account_order_tests.rs:1-335prove all flattened names, positions, signer and writable flags stayed fixed; reversing one shared mint group makes all three instruction tests fail. The exact-head Orca Surfpool lifecycle remains the generatedtry_accountsruntime gate. Resolved on 2026-09-17.L02-N002 —
OrcaCollectFees(instructions/orca/collect_fees.rs:36-96) and the two keeper claim structs now use the same config, position, mint, and program composites. Destination vaults and fee collectors remain inline because their built-in token constraints depend on outer fields; moving those constraints would change Anchor's enforcement or error precedence rather than merely extract duplication. The root builders and direct validator callers now spell the nested IDL shape, while recursively flattening the before/after IDLs yields identical names, order and meta flags for collect, harvest, and compound. Resolved with L02-N001 on 2026-09-17.L02-N003 — The owner/keeper rebalance pairs now share thirteen constraint-closed composites: six Orca groups with sixteen leaf accounts (
instructions/orca/rebalance_accounts.rs:12-108) and seven Raydium groups with twenty (instructions/raydium/rebalance_accounts.rs:15-111). Authority, keeper/intent slots, receiver, settings, and the owner/keeper type differences remain inline.system_programalso stays at the parent: Anchor's parent-levelinitcannot discover it inside a composite, and moving it would break the derive rather than deduplicate safely. The flat account-order tables frozen before the extraction (instructions/orca/account_order_tests.rs:1-569,instructions/raydium/account_order_tests.rs:1-621) pass unchanged, and recursively flattening the four generated IDL account trees yields the exact pre-refactor names, order, signer, writable, and optional flags. Reversing one shared group makes both instructions in that venue fail their focused order tests. Resolved on 2026-09-17.L02-N004 — Every function this note tracked is now below 150 lines without changing CPI or event order. The CLMM adapters and Jupiter handler were resolved in the preceding passes. Meteora rebalance is 123 lines (
instructions/meteora/rebalance_for.rs:508-630) and exit is 79 (instructions/meteora/exit_for.rs:398-476). Their two-sided principal-fee frame is generated from one macro (instructions/meteora/keeper_flow.rs:50-100), and both credit the common intent ledger through the same checked arithmetic (instructions/meteora/keeper_flow.rs:102-152) before their handler-specific event frames (instructions/meteora/rebalance_for.rs:403-506,instructions/meteora/exit_for.rs:306-396). The focused test (instructions/meteora/keeper_flow.rs:253-264) distinguishes principal from harvest fees and pins both subtraction failures plus addition overflow; sourcing the first deduction from the harvest fee fails with 149 instead of 145, and saturating the addition fails the overflow assertion.cargo build-sbf --tools-version v1.52remains the frame gate. Resolved on 2026-09-17.L02-R001 —
instructions/meteora/rebalance_for.rs:242-348andinstructions/meteora/exit_for.rs:177-261each hold a privatebegin_*_intentwith the same shape. Theirremove_all_liquiditypair was the other half of this note and is now one macro. Resolved in this pass.One note, two pairs, and they are opposite. Measured 2026-09-11 before anything was changed.
remove_all_liquiditywas real duplication: 77 lines each, byte-identical after substitutingMeteoraRebalanceForforMeteoraExitFor. Nothing held them together, so it is now onemeteora_remove_all_liquidity!inkeeper_flow.rsandexpand-check.shconfirms both expansions still match the hand-written code they replaced.begin_*_intentis not duplication: 58 lines of diff after normalising away the struct and action names. The two share a four-line preamble -- load the DLMM authorization, take its two views, verify the withdrawal floor -- and then check entirely different things. Rebalance pins the live active bin, re-derives the expected bin range, applies the anti-cycle progress rule and bounds the dust policy; exit matches the signed range and the exit policy. Merging them would produce a switch over two policies wearing one name, and the shared prefix is deliberately not hoisted either: pulling a common prefix out of two otherwise-different functions makes the difference harder to see, not easier.Decided no change for that half, with the reasoning written at both functions rather than only here. The next reader to notice two similarly named
begin_*functions will meet it where they are tempted, which is the conventionvenue::open_increase::validate_active_flowalready follows.What the note could not tell you, and the thing worth carrying: "the same shape" is not a measurement. Two functions can carry the same name shape, the same parameters and the same first four lines and still be a merge that costs a reader more than it saves. The question a redundancy note cannot answer for itself is what stops the copies diverging, and here the answer differed within one note.
L02-R002 —
instructions/meteora/yield_adapter.rs:418-421implements the yield trait forMeteoraCompoundFor,MeteoraRebalanceFor, andMeteoraExitForthrough a macro. The three sibling files import their own struct's implementation from the harvest file (instructions/meteora/exit_for.rs:28,instructions/meteora/rebalance_for.rs:31).instructions/meteora/harvest_for.rs:27-29imports the three sibling structs. The four files depend on each other in a cycle. Resolved in this pass.Not a four-file cycle, and not what the note says is imported.
instructions/meteora/compound_for.rsimports nothing from any sibling, so it was never in a cycle at all. The two real back-edges came fromexit_for.rsandrebalance_for.rs, and what they imported was a helper function and a type -- a trait implementation cannot be imported, only the trait can.The shape underneath was that
harvest_for.rsheld 434 lines of infrastructure for four files and, to invoke the macro on all four, had to import three sibling structs. The crate already had a convention for exactly this:impl_active_flow_policy!is defined invenue/adapters/mod.rs, exported withpub(crate) use, and invoked where the structs are (venue/adapters/mod.rs:108). This macro was the one exception.Split along that convention, into two modules that sit on opposite sides of the handler files.
keeper_flow.rsnames no account struct, so every handler may import it.yield_adapter.rsnames all four and is imported by none. The graph is a DAG; nothing in the directory now imports a sibling handler exceptintent_add_liquidity.rs, which is unrelated and acyclic.The four macro invocations deliberately stay together rather than moving next to their structs. That the harvest, compound, rebalance and exit structs claim yield identically is a property of this venue, and four invocations in four files would leave nothing that states it. Both module doc comments say which half they are and what would put the cycle back.
L02-R003 — The owner check has three spellings.
constraint = sickle.is_owner(&authority.key())atinstructions/orca/close_position.rs:50.constraint = sickle.owner == authority.key()atinstructions/position/set_position_settings.rs:47,instructions/position/unset_position_settings.rs:32,instructions/intent/set_intent_session.rs:43.has_one = owneratinstructions/sickle/set_custom_automator.rs:27. Resolved in this pass.Four spellings, not three, and the fourth is the one that matters: two structs authorize by seeds rather than by a check.
set_custom_automatorandset_managerderive the Sickle fromowner.key(), the signer, so a caller can only ever reach their own and no constraint is doing the work. Within that pair the first addshas_one = ownerand the second adds nothing, which is what made them look inconsistent.That
has_oneis unreachable.initialize_sickleis the only writer ofownerand sets it to the same key it seeds with, sosickle.owner == owner.key()holds for every Sickle that exists. It is kept, following the cancel-versus-expire precedent: a free true check costs nothing and removing it would read as a deliberate loosening. Both structs now say so.The three
constraint = sickle.owner == authority.key()sites are nowsickle.is_owner(...), so the owner rule has one definition. They were equivalent; nothing made them stay equivalent.L02-R004 —
RaydiumRebalanceFor.fee_collector_account_0and_1areUncheckedAccount(instructions/raydium/rebalance_for.rs:197-201), as are those ofRaydiumRebalancePosition(instructions/raydium/rebalance_position.rs:206-210), and the adapter validates them (venue/adapters/raydium.rs:544-553,:758-767).OrcaRebalancePosition.fee_collector_account_aand_bareInterfaceAccountwithtoken::mintandtoken::authority(instructions/orca/rebalance_position.rs:208-221).RebalanceForkeeps the typed form too. The same account has two validation mechanisms across venues. Decided 2026-09-11: no change, with the measurement attached.Measured rather than reasoned about, one change at a time with
cargo build-sbf --tools-version v1.52against a zero baseline:change frame diagnostics RaydiumRebalanceForfee collectors -> typed15 RaydiumRebalancePositionfee collectors -> typed0 So the frame reason is real for the keeper struct and refuted for the owner one, whose comment asserted it anyway. All 15 diagnostics name
RaydiumRebalanceFor::try_accounts, andanchor buildexits 0 on them.Typing the owner one was still declined.
token::mintplustoken::authoritycheck exactly whatvalidate_collector_bytesalready checks, andassert_fee_collector_accountis shared with the keeper struct that cannot be typed, so the constraint would run a second equivalent check in front of the first rather than replacing it. No added safety, a duplicated enforcement, and an unmeasured compute cost on flows thatscripts/builder/budgets.tssays request the 1,400,000 maximum with a margin nobody has measured.What changed is the record: the keeper field carries the number and the toolchain, the owner field says the frame reason does not apply to it, and
fees.rs'sassert_fee_collector_accountno longer states that reason of all its callers when it is true of one.L02-R005 —
OrcaRebalancePosition.token_mint_aandtoken_mint_bareUncheckedAccount(instructions/orca/rebalance_position.rs:154-158) whileold_position_minton the same struct isInterfaceAccount<Mint>(:119).token_program_aandtoken_program_bareUncheckedAccount(:195-198) andassert_token_program_for_mintchecks them later (:590-591).anchor_spl::token_interface::TokenInterfaceexists and is used byinstructions/sickle/sweep_token.rs:2-3. Decided 2026-09-11: no change, with the measurement attached.Typing
token_mint_a/_b, andtoken_program_a/_balongside them, produces zero frame diagnostics. Unlike historical note 8's keeper struct there was never a stated reason here to refute -- the comments claim none -- so the finding is that the asymmetry withold_position_mintis undecided rather than deliberate.Declined for the same compute reason as historical note 8, and recorded at the field: this instruction composes with a Jupiter leg and a re-add in one transaction, typing adds
try_accountsdeserialization against an unmeasured margin, and Orca already validates both mints against the whirlpool. A measured CU figure for this flow would reopen it.L02-R006 —
instructions/orca/rebalance_position.rs:60-70and:234-242document a branch as "unreachable today" twice. TheSettingsCandidateRequiredbranch itself lives in the runner, not in this file. Resolved in this pass.
The note undercounted and inverted. Three copies, not two -- the Orca struct doc, the Orca field doc and the Raydium field doc -- and the branch itself carried no comment at all. The fact was duplicated in the layer that does not own it and absent from the layer that does, which is the opposite of where it belongs: the runner is where someone would be tempted to delete an arm nothing can reach.
venue/owner_rebalance.rs now carries the reasoning at the require!,
including what a future non-owner caller could do without it, and the three
handler comments point at it.
- L02-R007 —
instructions/orca/rebalance_position.rs:474-478cites "INFO #9 Step 4" andinstructions/raydium/rebalance_position.rs:167cites "INFO #9". Resolved in this pass.
The note cited two. There were 20 in instructions/, and all 20 are gone.
Every one sat on a sentence that already gave the reason -- "InterfaceAccount
so a Token-2022 pool mint's vault deserializes (INFO #9)" -- so the
reference added nothing the sentence did not already say. Deleting it loses
no information.
This note used to say the review is not in the repository and that each
reference was therefore unopenable. That was wrong, and contract 13 measured
it: TODO.md enumerates every referenced finding as a checklist entry and
README.md defines seven of the eight again, so the pointers resolved -- in
two documents, with nothing naming either. The case for deleting them was
always the sentence beside them, never that they dangled. Ten were continuation lines holding only (INFO #9).,
which folded onto the sentence above.
The rest were taken by contract 13: 19 in source and 6 in CLAUDE.md, read
one at a time. Two survive. intent.rs:1 already named TODO.md. The
MEDIUM #4 note in CLAUDE.md was left as it stood, because reshaping its
parenthesis into a sentence promoted a parity claim nobody had checked; that
paragraph was rewritten separately once the reference was read.
L02-R008 —
instructions/orca/harvest_for.rs:181-232andinstructions/orca/compound_for.rs:225-277hold the same pause, keeper, settings, and vault-derivation block.instructions/raydium/harvest_for.rs:183andinstructions/raydium/compound_for.rs:222repeat it for Raydium. The keeper rebalance and exit runners run the same checks inside the runner (venue/keeper_rebalance.rs:218-247,venue/keeper_exit.rs:244-275). Four handlers own a check that two runners own elsewhere. Resolved in this pass.The note undercounted. It is six handlers, not four: the two Meteora keeper claims landed after it was written and carry the same block. The four Meteora keeper handlers make it look like eight, but
meteora_rebalance_forandmeteora_exit_forneed the live-tick trigger and the debounce, which these two do not, so they stay with the other rebalance and exit flows.Asking what stopped the six copies diverging is what changed the fix. The answer is nothing, and the half that matters is not the pause line: it is that
PERMISSION_HARVESThas to sit besidevalidate_harvest_forandPERMISSION_COMPOUNDbesidevalidate_compound_for. A copy that keeps one and takes the other compiles, and grants a keeper holding only the harvest bit the compound flow. So the fix is not one shared block, it is one place that names the operation:KeeperClaim(venue/claim.rs:176-210) selects both halves,authorize_keeper_claim(venue/claim.rs:238-271) runs them, and the three venue manifests requireKeeperClaimPolicyof whatever type fills theirHarvestForandCompoundForslots (venue/clmm.rs:97-115,venue/dlmm.rs:33-53).The last sentence of this note originally read "so a keeper claim cannot skip the authorization and stay registered", which was false and is withdrawn. The bound makes a registered type able to authorize; it does not stop a call site choosing an entry point that does not. A review compiled the bypass. Contract 5 made the sentence true rather than softening it, by making the ungated all-yield body private and sealing the one entry point that skips authorization to
raydium_collect_fees.run_harvestkeeps no authorization.raydium_collect_feesenters it, because Raydium settles every reward slot through the same CPI, and its caller check is the account struct's owner constraint;venue/claim.rs:477-497says so and a unit test pins it.The vault-derivation block was a fourth copy of a different thing, and it moved to the venue layer rather than the runner: it is venue-specific, and Meteora already kept its copy in
prepare_all_yield(instructions/meteora/yield_adapter.rs:82-101). The Orca copy is now inprepare_orca_yield!(venue/adapters/orca.rs:306-333), which only the two keeper claims expand. Raydium's is a separate macro (venue/adapters/raydium.rs:222-263) rather than a line inprepare_raydium_yield!, becauseraydium_collect_feesexpands that macro too and pins its destinations with the loosertoken::authority = sickle.L02-R009 —
instructions/orca/open_position.rs:45-46says the allowlist is "validated ... in the handler". The handler body has no such check. The runner runs it (venue/open_increase.rs:48-49). The same text appears atinstructions/orca/open_position.rs:53-54and:62-64for the position PDA and the token account. The adapter checks both (venue/adapters/orca.rs:1190-1209).instructions/orca/rebalance_position.rs:104-105,instructions/orca/increase_liquidity.rs:70,instructions/orca/decrease_liquidity.rs:46, andinstructions/raydium/open_position.rs:59carry the same stale text. 22 handler files contain the phrase "in the handler". Resolved in this pass.All 85 occurrences are gone. 80 were in Orca and Raydium files and all of them were false; the 5 in
instructions/jupiter/swap.rswere true, and were rewritten anyway, because naming the location is the defect and a true location rots as soon as the code moves again -- which this contract did to six of these files.A
CHECKcomment now says what guarantees the account rather than which layer guarantees it: the program verifies it before any CPI, and where a checking function was already named the name stays, since a symbol moves with the code and a layer does not. Five sites were saying something different under the same words -- that the check sits in code rather than in an#[account(...)]constraint, which is whytry_accountsstays inside its SBF frame -- and those read "in code". Two mentions of a handler that are not check-location claims were left (instructions/raydium/rebalance_position.rs:397,intent.rs:1424).The sweep is comment-only: rebuilding after it produced a byte-identical
sickle.so, and a control line inserted in one of the same files does change that hash. The regenerated IDL differs only indocsstrings.L02-R010 —
instructions/orca/rebalance_position.rs:37says "the program has no swap support yet (Jupiter is Phase 3)".instructions/jupiter/swap.rsexists with 522 lines. Resolved in this pass.The comment now says what the builder actually appends: the re-add, and a balancing
jupiter_swapleg before it when the drained amounts do not match the new range.L02-R011 —
instructions/raydium/rebalance_for.rs:231-233says the memo program is "Pinned to its canonical ID" with noaddressconstraint. The adapter runs the check (venue/adapters/raydium.rs:674-678). The siblinginstructions/raydium/exit_for.rs:192-195has the same comment and the constraint. The two identical comments describe two different mechanisms. Resolved in this pass.Confirmed by enumeration: 24 structs in
instructions/name amemo_programand exactly two lack theaddressconstraint,RaydiumRebalanceForand Orca'sRebalanceFor. Orca's comment already explained itself. Raydium's now namesvalidate_keeper_static_accounts, and says why it differs from its eleven siblings, so the next reader does not restore the identical wording.L02-R012 — The pause check has two forms and three locations. 11 owner exit handlers call
assert_not_paused_unless_owner_exitin the handler with a flag that is always true (instructions/orca/close_position.rs:113-121).orca_open_positioncalls nothing and relies onvenue/open_increase.rs:47.instructions/orca/harvest_for.rs:183callsassert_not_pausedin the handler whileinstructions/orca/rebalance_for.rsrelies onvenue/keeper_rebalance.rs:218. Closed in this pass, without merging the two forms.Every count in the note holds. What it does not say is that the two forms are not two spellings of one check.
assert_not_paused_unless_owner_exitis the owner-exit carve-out CLAUDE.md records as an intentional divergence: the pause must never trap an owner's principal, fees, or rent inside a position. Merging the forms would delete that, which is the regressionvenue/open_increase.rs:37-41already warns the next reader about.The always-true flag is deliberate too, and documented: those handlers are owner-only by account constraint, so passing
is_ownerrather thantruekeeps the guard correct if one of them ever gains another caller class.What was actionable was the third location. The six keeper claims called
assert_not_pausedin the handler while every other keeper flow reached it through a runner; historical note 1's change moved them, so the handler-sideassert_not_pausedsites drop from 13 to 7 and the claim flows now match the rebalance and exit flows. The two forms remain, on purpose.L02-R013 — The allowlist check runs 3 times on an Orca owner rebalance:
venue/owner_rebalance.rs:439,instructions/orca/collect_rewards.rs:133insidetake_orca_reward_groups, andinstructions/orca/collect_rewards.rs:229insidecollect_orca_rewards_and_charge. Closed in this pass with no change.The count is right and the redundancy is not removable. Both helpers are shared with flows whose runner does not check the allowlist at all:
take_orca_reward_groupsis the only pre-CPI allowlist check on an Orca harvest and compound, and both helpers are the only ones on the standaloneorca_collect_rewards. Deduplicating to the runner copy would leave those three instructions handing an unallowlisted program to a PDA-signed CPI.That is why
authorize_keeper_claimruns the pause check but not the allowlist check, even thoughvalidate_active_flownext door runs both: every claim adapter already asserts the allowlist insideprepare_all_yield, before any CPI, and a second copy would buy nothing while moving the measured Meteora compute envelopes.venue/claim.rs:228-237says so at the point of temptation.
Related
- Dependency graph
- Entry points
- Shared runners
- Venue adapters
- Style guide
docs/dev/instructions/, one page per instruction.