Sickle Sol

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.

CountValue
Handler files65
Root module1
Domain modules8
Production support8
Test-only support3
Lines, all Rust files15,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.

StyleWhat the handler body doesWhere the venue CPI is built
directWrites program state, or builds and invokes CPIs itself. No runner.The handler file, or a helper in the same file.
runnerCalls one venue::*::run_* function and emits the returned events.The adapter for the account struct, in venue/adapters/.
runner + implCalls 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.
intentCalls 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.

GroupFilesDispatch
Admininitialize_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_automatordirect
Sickleinitialize_sickle, set_manager, set_custom_automator, create_sickle_ata, sweep_tokendirect
Settingsset_position_settings, unset_position_settingsdirect
Orca ownerorca_open_position, orca_increase_liquidity, orca_decrease_liquidity, orca_collect_fees, orca_collect_rewards, orca_close_position, orca_rebalance_positionrunner
Orca keeperharvest_for, compound_for, rebalance_for, exit_forrunner
Raydium ownerraydium_open_position, raydium_increase_liquidity, raydium_decrease_liquidity, raydium_collect_fees, raydium_close_position, raydium_rebalance_positionrunner
Raydium keeperraydium_harvest_for, raydium_compound_for, raydium_rebalance_for, raydium_exit_forrunner
Meteora ownermeteora_open_position, meteora_increase_position_length, meteora_add_liquidity, meteora_remove_liquidity, meteora_claim_fees, meteora_claim_reward, meteora_close_positiondirect
Meteora keepermeteora_harvest_for, meteora_compound_for, meteora_rebalance_for, meteora_exit_for, meteora_intent_open_position, meteora_intent_increase_position_length, meteora_intent_add_liquidityrunner + impl, direct
Swapjupiter_swapdirect
Authorization (CLMM)initialize_intent_domain, set_intent_session, preauthorize_intent, cancel_intent_authorization, expire_intent_authorization, finalize_intentintent
Authorization (DLMM)preauthorize_dlmm_intent, cancel_dlmm_intent_authorization, expire_dlmm_intent_authorization, finalize_dlmm_intentintent

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.

PathLinesGroupDispatchBindingEmitsPurpose
instructions/admin/accept_admin.rs53AdmindirectnonenoneThe pending admin accepts the admin role.
instructions/intent/cancel_dlmm_intent_authorization.rs173Authorization (DLMM)intentintentcpi+logThe owner closes a Meteora authorization before use.
instructions/intent/cancel_intent_authorization.rs171Authorization (CLMM)intentintentcpi+logThe owner closes a CLMM authorization before use.
instructions/orca/compound_for.rs244Orca keeperrunnerorcacpi+logA keeper claims Orca yield as compound credit.
instructions/sickle/create_sickle_ata.rs138SickledirecttokennoneAnyone creates a vault for one mint.
instructions/orca/exit_for.rs281Orca keeperrunnerorcacpi+logA keeper closes an Orca position under an authorization.
instructions/intent/expire_dlmm_intent_authorization.rs106Authorization (DLMM)intentintentcpi+logAnyone closes an expired Meteora authorization.
instructions/intent/expire_intent_authorization.rs108Authorization (CLMM)intentintentcpi+logAnyone closes an expired CLMM authorization.
instructions/intent/finalize_dlmm_intent.rs135Authorization (DLMM)intentintentcpi+logProves and closes a Meteora action.
instructions/intent/finalize_intent.rs628Authorization (CLMM)intentintentcpi+logProves and closes a CLMM action. Holds the shared finalizer.
instructions/orca/harvest_for.rs200Orca keeperrunnerorcacpi+logA keeper collects Orca fees and rewards.
instructions/admin/initialize_global_config.rs78AdmindirectnonenoneThe upgrade authority creates the global config.
instructions/intent/initialize_intent_domain.rs64Authorization (CLMM)intentintentnoneThe upgrade authority creates the intent domain.
instructions/sickle/initialize_sickle.rs48Sickledirectnonecpi+logA wallet creates its Sickle.
instructions/jupiter/swap.rs615Swapdirectjupitercpi+logThe owner, or an action, swaps between two vaults.
instructions/meteora/add_liquidity.rs267Meteora ownerdirectmeteoracpi+logThe owner adds liquidity to a Meteora position.
instructions/meteora/claim_fees.rs318Meteora ownerdirectmeteoracpi+logThe owner claims Meteora fees for a bin range.
instructions/meteora/claim_reward.rs289Meteora ownerdirectmeteoracpi+logThe owner claims one Meteora reward slot.
instructions/meteora/close_position.rs149Meteora ownerdirectmeteoracpi+logThe owner closes an empty Meteora position.
instructions/meteora/compound_for.rs194Meteora keeperrunner + implmeteoracpi+logA keeper claims Meteora yield as compound credit.
instructions/meteora/exit_for.rs622Meteora keeperdirectmeteorareusableA keeper closes a Meteora position under an authorization.
instructions/meteora/harvest_for.rs194Meteora keeperrunner + implmeteoracpi+logA keeper collects Meteora yield.
instructions/meteora/increase_position_length.rs149Meteora ownerdirectmeteoracpi+logThe owner widens a Meteora position.
instructions/meteora/intent_add_liquidity.rs340Meteora keeperdirectmeteoracpi+logAn action adds Meteora liquidity within signed limits.
instructions/meteora/intent_increase_position_length.rs196Meteora keeperdirectmeteoracpi+logAn action widens a Meteora position within signed limits.
instructions/meteora/intent_open_position.rs279Meteora keeperdirectmeteoracpi+logAn action opens a Meteora position within signed limits.
instructions/meteora/keeper_flow.rs264Meteora sharednonemeteoranoneWhat the Meteora keeper flows share. It names no account struct, which is what keeps the module graph acyclic.
instructions/meteora/open_position.rs147Meteora ownerdirectmeteoracpi+logThe owner opens an empty Meteora position.
instructions/meteora/rebalance_for.rs847Meteora keeperdirectmeteorareusableA keeper moves a Meteora position to a new bin range.
instructions/meteora/remove_liquidity.rs274Meteora ownerdirectmeteoracpi+logThe owner removes Meteora liquidity for a bin range.
instructions/admin/migrate_global_config.rs147AdmindirectnonenoneThe admin grows a legacy global config.
instructions/meteora/yield_adapter.rs427Meteora sharednonemeteoranoneThe one ClaimAllYieldAdapter impl, applied to all four Meteora keeper structs.
instructions/orca/close_position.rs137Orca ownerrunnerorcacpi+logThe owner closes an empty Orca position.
instructions/orca/collect_fees.rs153Orca ownerrunnerorcacpi+logThe owner collects Orca fees.
instructions/orca/collect_rewards.rs85Orca ownerrunnerorcacpi+logThe owner collects Orca rewards.
instructions/orca/decrease_liquidity.rs149Orca ownerrunnerorcacpi+logThe owner removes Orca liquidity.
instructions/orca/increase_liquidity.rs184Orca ownerrunnerorcacpi+logThe owner, or an action, adds Orca liquidity.
instructions/orca/open_position.rs99Orca ownerrunnerorcacpi+logThe owner opens an empty Orca position.
instructions/orca/rebalance_position.rs242Orca ownerrunnerorcacpi+logThe owner moves an Orca position.
instructions/orca/rebalance_accounts.rs108Orca sharednoneorcanoneSix constraint-closed account composites shared by owner and keeper rebalance.
instructions/orca/yield_accounts.rs77Orca sharednoneorcanoneFive constraint-closed account composites shared by collect, harvest, and compound.
instructions/admin/pause.rs60AdmindirectnonenoneThe admin or the guardian pauses the program.
instructions/intent/preauthorize_dlmm_intent.rs136Authorization (DLMM)intentintentcpi+logStores a signed Meteora authorization.
instructions/intent/preauthorize_intent.rs150Authorization (CLMM)intentintentcpi+logStores a signed CLMM authorization.
instructions/raydium/close_position.rs131Raydium ownerrunnerraydiumcpi+logThe owner closes an empty Raydium position.
instructions/raydium/collect_fees.rs155Raydium ownerrunnerraydiumcpi+logThe owner collects Raydium fees and rewards.
instructions/raydium/compound_for.rs211Raydium keeperrunnerraydiumcpi+logA keeper claims Raydium yield as compound credit.
instructions/raydium/decrease_liquidity.rs154Raydium ownerrunnerraydiumcpi+logThe owner removes Raydium liquidity. Holds the reward-group parsers.
instructions/raydium/exit_for.rs270Raydium keeperrunnerraydiumreusableA keeper closes a Raydium position under an authorization.
instructions/raydium/harvest_for.rs172Raydium keeperrunnerraydiumcpi+logA keeper collects Raydium fees and rewards.
instructions/raydium/increase_liquidity.rs180Raydium ownerrunnerraydiumcpi+logThe owner, or an action, adds Raydium liquidity.
instructions/raydium/open_position.rs165Raydium ownerrunnerraydiumcpi+logThe owner opens an empty Raydium position. Holds the bitmap check.
instructions/raydium/rebalance_for.rs262Raydium keeperrunnerraydiumreusableA keeper moves a Raydium position under an authorization.
instructions/raydium/rebalance_position.rs235Raydium ownerrunnerraydiumcpi+logThe owner moves a Raydium position. Holds the shared Raydium helpers.
instructions/raydium/rebalance_accounts.rs111Raydium sharednoneraydiumnoneSeven constraint-closed account composites shared by owner and keeper rebalance.
instructions/raydium/yield_accounts.rs95Raydium sharednoneraydiumnoneFive constraint-closed account composites shared by collect, harvest, and compound.
instructions/orca/rebalance_for.rs307Orca keeperrunnerorcacpi+logA keeper moves an Orca position under an authorization.
instructions/admin/revoke_global_automator.rs116AdmindirectnonenoneThe admin removes a global keeper.
instructions/sickle/set_custom_automator.rs109SickledirectnonenoneThe owner names a custom keeper.
instructions/admin/set_fee.rs60AdmindirectnonenoneThe admin sets one fee.
instructions/admin/set_fee_collector.rs50AdmindirectnonenoneThe admin sets the fee collector.
instructions/admin/set_global_automator.rs160AdmindirectnonenoneThe admin adds a global keeper.
instructions/admin/set_guardian.rs54AdmindirectnonenoneThe admin sets the guardian.
instructions/intent/set_intent_session.rs141Authorization (CLMM)intentintentcpi+logThe owner sets or revokes a session key.
instructions/sickle/set_manager.rs59SickledirectnonenoneThe owner sets the manager.
instructions/position/set_position_settings.rs299Settingsdirectmeteoracpi+logThe owner writes position settings.
instructions/admin/set_protocol_program.rs87AdmindirectnonenoneThe admin sets one allowlist slot.
instructions/sickle/sweep_token.rs205Sickledirecttokencpi+logThe owner moves a vault balance to the owner.
instructions/admin/transfer_admin.rs51AdmindirectnonenoneThe admin names a pending admin.
instructions/admin/unpause.rs51AdmindirectnonenoneThe admin unpauses the program.
instructions/position/unset_position_settings.rs173Settingsdirectnonecpi+logThe owner closes position settings.
instructions/intent/preauthorize.rs63support-intentnoneShared transaction-A preauthorization checks.
instructions/jupiter/cpi.rs282support-jupiternoneJupiter instruction and custody helpers.
instructions/raydium/account_order_tests.rs621test support-raydiumnoneSource-level account-order tests.
instructions/orca/account_order_tests.rs569test support-orcanoneSource-level account-order tests.
instructions/test_utils.rs277test support-nonenoneHost entrypoint-dispatch fixture for direct handler tests.
instructions/mod.rs113----Module list and re-exports.

Responsibilities

  • Declare the account struct for one instruction, with Anchor constraints and a CHECK comment 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.

FileAccount structHandlerOther items
instructions/admin/accept_admin.rsAcceptAdminaccept_adminnone
instructions/intent/cancel_dlmm_intent_authorization.rsCancelDlmmIntentAuthorizationcancel_dlmm_intent_authorizationnone
instructions/intent/cancel_intent_authorization.rsCancelIntentAuthorizationcancel_intent_authorizationnone
instructions/orca/compound_for.rsCompoundForcompound_fornone
instructions/sickle/create_sickle_ata.rsCreateSickleAtacreate_sickle_atanone
instructions/orca/exit_for.rsExitForexit_fornone
instructions/intent/expire_dlmm_intent_authorization.rsExpireDlmmIntentAuthorizationexpire_dlmm_intent_authorizationnone
instructions/intent/expire_intent_authorization.rsExpireIntentAuthorizationexpire_intent_authorizationnone
instructions/intent/finalize_dlmm_intent.rsFinalizeDlmmIntentfinalize_dlmm_intentnone
instructions/intent/finalize_intent.rsFinalizeIntentfinalize_intentIntentFinalizationReceipt (struct), finalize_action_intent (fn, pub(crate)) shared with the DLMM finalizer
instructions/orca/harvest_for.rsHarvestForharvest_fornone
instructions/admin/initialize_global_config.rsInitializeGlobalConfiginitialize_global_confignone
instructions/intent/initialize_intent_domain.rsInitializeIntentDomaininitialize_intent_domainnone
instructions/sickle/initialize_sickle.rsInitializeSickleinitialize_sicklenone
instructions/jupiter/swap.rsJupiterSwapjupiter_swapnone
instructions/meteora/add_liquidity.rsMeteoraAddLiquiditymeteora_add_liquidityexecute_meteora_add_liquidity (fn, pub(crate)) shared with the intent add
instructions/meteora/claim_fees.rsMeteoraClaimFeesmeteora_claim_feesnone
instructions/meteora/claim_reward.rsMeteoraClaimRewardmeteora_claim_rewardnone
instructions/meteora/close_position.rsMeteoraClosePositionmeteora_close_positionnone
instructions/meteora/compound_for.rsMeteoraCompoundFormeteora_compound_fornone
instructions/meteora/exit_for.rsMeteoraExitFormeteora_exit_forprivate begin_exit_intent, remove_all_liquidity, charge_exit_principal_fees, close_position, emit_exit_events
instructions/meteora/harvest_for.rsMeteoraHarvestFormeteora_harvest_fornone
instructions/meteora/increase_position_length.rsMeteoraIncreasePositionLengthmeteora_increase_position_lengthnone
instructions/meteora/intent_add_liquidity.rsMeteoraIntentAddLiquiditymeteora_intent_add_liquiditysigned_distribution_matches (fn, pub(crate))
instructions/meteora/intent_increase_position_length.rsMeteoraIntentIncreasePositionLengthmeteora_intent_increase_position_lengthnone
instructions/meteora/intent_open_position.rsMeteoraIntentOpenPositionmeteora_intent_open_positionnone
instructions/meteora/open_position.rsMeteoraOpenPositionmeteora_open_positionnone
instructions/meteora/rebalance_for.rsMeteoraRebalanceFormeteora_rebalance_forprivate begin_rebalance_intent, remove_all_liquidity, close_old_position, charge_rebalance_principal_fees, emit_rebalance_events
instructions/meteora/remove_liquidity.rsMeteoraRemoveLiquiditymeteora_remove_liquiditynone
instructions/meteora/keeper_flow.rsnonenoneMeteoraHarvestReward, PreparedMeteoraHarvest (structs), reload_pool_vaults_after_principal_fee (fn)
instructions/meteora/yield_adapter.rsnonenoneimpl_meteora_yield_adapter! (macro) that implements the yield trait for all 4 Meteora keeper structs (:424-427)
instructions/admin/migrate_global_config.rsMigrateGlobalConfigmigrate_global_confignone
instructions/orca/close_position.rsOrcaClosePositionorca_close_positionnone
instructions/orca/collect_fees.rsOrcaCollectFeesorca_collect_feesnone
instructions/orca/collect_rewards.rsOrcaCollectRewardsorca_collect_rewardsnone
instructions/orca/decrease_liquidity.rsOrcaDecreaseLiquidityorca_decrease_liquiditynone
instructions/orca/increase_liquidity.rsOrcaIncreaseLiquidityorca_increase_liquiditynone
instructions/orca/open_position.rsOrcaOpenPositionorca_open_positionnone
instructions/orca/rebalance_position.rsOrcaRebalancePositionorca_rebalance_positionDrainOutcome moved with the legs that return it; see venue/adapters/orca_flows.rs
instructions/orca/rebalance_accounts.rsnonenoneOrcaRebalanceConfig, OrcaRebalanceOldPosition, OrcaRebalanceNewPosition, OrcaRebalanceMints, OrcaRebalanceVaults, OrcaRebalanceTickArrays (account composites)
instructions/orca/yield_accounts.rsnonenoneOrcaYieldConfig, OrcaYieldPositionAccounts, OrcaYieldMints, OrcaYieldPrograms, OrcaYieldRewardPrograms (account composites)
instructions/admin/pause.rsPausepausenone
instructions/intent/preauthorize_dlmm_intent.rsPreauthorizeDlmmIntentpreauthorize_dlmm_intentnone
instructions/intent/preauthorize_intent.rsPreauthorizeIntentpreauthorize_intentnone
instructions/raydium/close_position.rsRaydiumClosePositionraydium_close_positionnone
instructions/raydium/collect_fees.rsRaydiumCollectFeesraydium_collect_feesnone
instructions/raydium/compound_for.rsRaydiumCompoundForraydium_compound_fornone
instructions/raydium/decrease_liquidity.rsRaydiumDecreaseLiquidityraydium_decrease_liquiditynone
instructions/raydium/exit_for.rsRaydiumExitForraydium_exit_fornone
instructions/raydium/harvest_for.rsRaydiumHarvestForraydium_harvest_fornone
instructions/raydium/increase_liquidity.rsRaydiumIncreaseLiquidityraydium_increase_liquiditynone
instructions/raydium/open_position.rsRaydiumOpenPositionraydium_open_positionnone
instructions/raydium/rebalance_for.rsRaydiumRebalanceForraydium_rebalance_fornone
instructions/raydium/rebalance_position.rsRaydiumRebalancePositionraydium_rebalance_positionnone
instructions/raydium/rebalance_accounts.rsnonenoneRaydiumRebalanceConfig, RaydiumRebalancePool, RaydiumRebalanceNewPosition, RaydiumRebalanceTickArrays, RaydiumRebalanceVaults, RaydiumRebalanceFeeCollectors, RaydiumRebalancePrograms (account composites)
instructions/raydium/yield_accounts.rsnonenoneRaydiumYieldConfig, RaydiumYieldPosition, RaydiumYieldTickArrays, RaydiumYieldVaults, RaydiumYieldPrograms (account composites)
instructions/orca/rebalance_for.rsRebalanceForrebalance_forassert_token_2022_mint (fn)
instructions/admin/revoke_global_automator.rsRevokeGlobalAutomatorrevoke_global_automatornone
instructions/sickle/set_custom_automator.rsSetCustomAutomatorset_custom_automatornone
instructions/admin/set_fee.rsSetFeeset_feenone
instructions/admin/set_fee_collector.rsSetFeeCollectorset_fee_collectornone
instructions/admin/set_global_automator.rsSetGlobalAutomatorset_global_automatornone
instructions/admin/set_guardian.rsSetGuardianset_guardiannone
instructions/intent/set_intent_session.rsSetIntentSessionset_intent_sessionIntentSessionArgs (struct)
instructions/sickle/set_manager.rsSetManagerset_managernone
instructions/position/set_position_settings.rsSetPositionSettingsset_position_settingsnone
instructions/admin/set_protocol_program.rsSetProtocolProgramset_protocol_programnone
instructions/sickle/sweep_token.rsSweepTokensweep_tokennone
instructions/admin/transfer_admin.rsTransferAdmintransfer_adminnone
instructions/admin/unpause.rsUnpauseunpausenone
instructions/position/unset_position_settings.rsUnsetPositionSettingsunset_position_settingsnone

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.

ConstructOccurrencesFilesUse
#[event_cpi]4949Every handler that emits. It adds the event authority and the program account to the struct.
Signer<'info>8263The payer, the authority, and fresh position mints.
Account<'info, T>29464Program-owned accounts: the global config, the Sickle, position settings, keeper records, intent accounts.
InterfaceAccount<'info, T>12733Token accounts and mints that may live on Token or Token-2022.
UncheckedAccount<'info>42446Venue-owned accounts and program IDs. Each one carries a /// CHECK comment.
/// CHECK42446Anchor requires the comment. The text says where the check runs.
Program<'info, T>7739Token, Token-2022, Associated Token, and System.
Sysvar<'info, Rent>33Raydium open and rebalance need the rent sysvar.
Option<...>7724Optional accounts: the global keeper record, the intent accounts, the Raydium bitmap, the old settings candidate.
Box<...>25243Every deserialized account in a large struct, to keep try_accounts inside the SBF frame.
seeds = [...]16165Every program PDA is re-derived with bump = account.bump.
address = ...6236Fixed program IDs, the rent receiver as sickle.owner, the rent payer stored in an authorization.
constraint = ...8042The owner check, the position NFT balance, mint equality.
has_one1412Admin and owner links on small structs.
token::authority9631Every vault must be owned by the Sickle. Every fee-collector account must be owned by the fee collector.
token::mint4221Fee-collector accounts must match the vault mint.
close = ...1511Authorization and settings accounts close to a stored payer or to the owner.
init / init_if_needed10 / 1010 / 6New PDAs and vaults.
ctx.remaining_accounts5332Reward 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.rsreopen_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.rscharge_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.rssweep_exit_residuals (:224), finalize_action_intent (:342)
instructions/meteora/exit_for.rscharge_exit_principal_fees generated at :275, close_position (:278), emit_exit_events (:315)
instructions/meteora/keeper_flow.rsreload_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.rsclose_old_position (:352), charge_rebalance_principal_fees (:388), emit_rebalance_events (:395)
instructions/meteora/yield_adapter.rsprepare_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).

EmitterUsersWhy
emit_cpi! + emit!45 handler filesAnchor default.
ReusableEventEmitterinstructions/raydium/rebalance_for.rs, instructions/raydium/exit_for.rs, instructions/meteora/rebalance_for.rs, instructions/meteora/exit_for.rsThese flows emit many events after many CPIs. The reusable emitter allocates once (events.rs:53-63).
none16 files: the admin and Sickle-config handlers and instructions/intent/initialize_intent_domain.rsThe 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.

  1. lib.rs:266-278 receives the call and forwards the arguments to instructions::orca::open_position::orca_open_position.
  2. instructions/orca/open_position.rs:23-78 is the account struct. #[event_cpi] adds the event authority. global_config and sickle are PDAs with seeds and bump. whirlpool_program, whirlpool, position, and position_token_account are unchecked. metadata_update_auth has address = WP_NFT_UPDATE_AUTH (:76).
  3. instructions/orca/open_position.rs:86-95 builds an OpenRequest with the tick range and the metadata flag, then calls run_open.
  4. venue/open_increase.rs:163 calls validate_active_flow. That function runs the pause check and the allowlist check (:46-49).
  5. venue/open_increase.rs:164-167 runs the owner check and returns Unauthorized on failure.
  6. venue/open_increase.rs:174 calls the adapter's prepare_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).
  7. venue/open_increase.rs:175 calls the adapter's execute_open. The adapter builds the Orca instruction and invokes it (venue/adapters/orca.rs:1231-1260), then reads the new position state (:1262-1284).
  8. venue/open_increase.rs:176 calls validate_open_receipt. That function checks the receipt against the identity and requires an empty position (:102-151).
  9. venue/open_increase.rs:177-183 builds the PositionOpened event.
  10. instructions/orca/open_position.rs:96-97 emits 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.

  1. instructions/orca/rebalance_position.rs:67-186 is the account struct. Its six constraint-closed groups come from instructions/orca/rebalance_accounts.rs:12-108. receiver is checked by the runner (instructions/orca/rebalance_position.rs:137-141). old_position_settings is Option<UncheckedAccount> (instructions/orca/rebalance_position.rs:143-159). new_position_settings is Option<Box<Account<PositionSettings>>> with init (instructions/orca/rebalance_position.rs:161-174).
  2. instructions/orca/rebalance_position.rs:202 reads the bump of the new settings from ctx.bumps.
  3. instructions/orca/rebalance_position.rs:203-219 builds an OwnerRebalanceRequest with the range, the minimum tokens, and the Orca-specific request, then calls run_owner_rebalance.
  4. venue/owner_rebalance.rs:438-442 runs the pause check, the allowlist check, and the owner check.
  5. venue/owner_rebalance.rs:455 checks the rent accounts. :459-489 resolves the old settings candidate and :490-493 requires the new settings account exactly when old settings exist.
  6. venue/owner_rebalance.rs:495 calls the adapter's prepare_owner_rebalance (venue/adapters/orca.rs:479-558). The adapter reads the old position through read_old_position (venue/adapters/orca_flows.rs:52-60). It re-derives the new position through assert_new_position_derivation (venue/adapters/orca_flows.rs:65-89). It builds the two account bundles through drain_accounts (venue/adapters/orca_flows.rs:851-856) and reopen_accounts (venue/adapters/orca_flows.rs:860-875).
  7. venue/owner_rebalance.rs:505 calls the adapter's execute_drain_and_close. The adapter calls withdraw_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 through resolve_pool_hooks (venue/adapters/orca_flows.rs:359-402, called at :759). It removes all liquidity through drain_position_liquidity (venue/adapters/orca_flows.rs:448-540, called at :768) and collects the fees through collect_position_fees (venue/adapters/orca_flows.rs:548-610, called at :778). It charges the fees through charge_drain_fees (venue/adapters/orca_flows.rs:643-720, called at :798), collects the rewards by calling collect_orca_rewards_and_charge directly (:815), and closes the position through close_drained_position (venue/adapters/orca_flows.rs:411-428, called at :825).
  8. venue/owner_rebalance.rs:506-507 validates the receipt and requires the closed accounts to be empty.
  9. venue/owner_rebalance.rs:518 calls transfer_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 to PositionSettings::transfer_from (state.rs:335-351).
  10. venue/owner_rebalance.rs:542 calls the adapter's execute_reopen. The adapter calls reopen_orca_position (venue/adapters/orca_flows.rs:214-251).
  11. venue/owner_rebalance.rs:543 validates the reopened position. :558 closes the old settings candidate to the receiver.
  12. instructions/orca/rebalance_position.rs:221-241 emits 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.

  1. 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 is Unauthorized.
  2. A keeper instruction requires an authorized keeper. Every runner calls is_authorized_automator with the permission bit its operation names: harvest and compound through authorize_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 is Unauthorized.
  3. 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 as GlobalConfig::assert_not_paused, or as assert_not_paused_unless_owner_exit for 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 through authorize_keeper_claim (venue/claim.rs:246), finalize_dlmm_intent through finalize_action_intent (instructions/intent/finalize_intent.rs:350), and meteora_intent_add_liquidity through the execute_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's sweep_token (state.rs:892), the 2 settings instructions, and the 8 authorization instructions other than the two finalizers. The error is Paused.
  4. 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 and jupiter_swap check it in the handler (instructions/meteora/open_position.rs:66, instructions/jupiter/swap.rs:440).
  5. A fixed program ID must equal its constant. The memo program is checked by address = MEMO_PROGRAM_ID on 22 structs, by the adapter on RaydiumRebalanceFor (venue/adapters/raydium.rs:660-664), and by the helper on RebalanceFor (venue/adapters/orca_flows.rs:748-752). The error is InvalidAccountOwner.
  6. Rent returns to the owner. Close handlers pin receiver with address = 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).
  7. 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-299 for the two Orca claims, venue/adapters/raydium.rs:182-223 for the two Raydium ones, instructions/meteora/yield_adapter.rs:83-102 for the two Meteora ones). The owner collects are not covered: their destinations carry only token::authority = sickle.

Check placement by handler class

Handler classPause checkOwner or keeper checkAllowlist check
Orca and Raydium open, increaserunner venue/open_increase.rs:47runner :164-167, or the intent pathrunner :48-49
Orca and Raydium decrease, collect, close, orca_collect_rewardshandler, owner-exit form (instructions/orca/close_position.rs:119, instructions/raydium/decrease_liquidity.rs:139, instructions/orca/collect_rewards.rs:58)struct constraintadapter or helper (venue/adapters/orca.rs:1456, venue/adapters/raydium_flows.rs:826)
Orca and Raydium owner rebalancerunner venue/owner_rebalance.rs:438runner :439-442runner :438
Orca, Raydium, and Meteora harvest, compoundrunner venue/claim.rs:246runner venue/claim.rs:252-270Orca: reward parser (venue/adapters/orca_flows.rs:1073). Raydium: validate_decrease_accounts (venue/adapters/raydium_flows.rs:826)
Orca and Raydium keeper rebalancerunner venue/keeper_rebalance.rs:235runner :255runner :235
Orca and Raydium keeper exitrunner, owner-exit form venue/keeper_exit.rs:246runner :266runner :249
Meteora ownerhandler, 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, exithandler (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_swaphandler instructions/jupiter/swap.rs:440request validator instructions/jupiter/swap.rs:72-100handler instructions/jupiter/swap.rs:441
Admin, Sickle, Settingsnonestruct constraint or has_onenot applicable
Authorization except the two finalizers (8)noneupgrade authority, owner, session key, or anyone, per instruction (see layer 08)not applicable
finalize_intent, finalize_dlmm_intentshared helper instructions/intent/finalize_intent.rs:342executor (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_lengthhandler 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 pathhandler or shared macro

Tests

Test locationWhat it covers
tests/sickle/core.tsAdmin, Sickle, settings, and sweep instructions on the legacy validator.
tests/sickle/orca.ts, orca-lifecycle.tsOrca owner and keeper instructions against a cloned Orca program.
tests/sickle/raydium.tsRaydium owner and keeper instructions against a cloned Raydium program.
tests/sickle/intents.tsThe CLMM authorization instructions.
tests/sickle/jupiter.tsjupiter_swap.
tests/surfpool-*.tsFull lifecycles on a mainnet fork for Orca, Raydium, Meteora, Jupiter, and Token-2022 pools.
programs/sickle/tests/clmm_conformance.rsCompile-time proof that each Orca and Raydium account struct implements the runner traits it needs.
instructions/*/account_order_tests.rsFrozen flattened account order and meta flags for ten nested Orca and Raydium yield/rebalance instructions.
Rust unit tests inside handler files30 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 filesExisting runtime coverageAdditional value of the host entrypoint test
accept_admin, transfer_adminRole 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_programAdmin 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_automatorOwner 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_automatorCreate, 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_tokenATA 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_authorizationCLMM 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_settingsSuccessful 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

  1. 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. In venue/adapters/, orca.rs has four unit tests, raydium.rs has none, and raydium_flows.rs has 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.

  1. 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) and CompoundFor (instructions/orca/compound_for.rs:55-173) adopt five composites from instructions/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. authority and global_automator stay outside because compound alone makes authority writable and the keeper PDA seeds depend on it. The frozen pre-refactor tables in instructions/orca/account_order_tests.rs:1-335 prove 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 generated try_accounts runtime gate. Resolved on 2026-09-17.

  2. 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.

  3. 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_program also stays at the parent: Anchor's parent-level init cannot 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.

  4. 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.52 remains the frame gate. Resolved on 2026-09-17.

  5. L02-R001 — instructions/meteora/rebalance_for.rs:242-348 and instructions/meteora/exit_for.rs:177-261 each hold a private begin_*_intent with the same shape. Their remove_all_liquidity pair 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_liquidity was real duplication: 77 lines each, byte-identical after substituting MeteoraRebalanceFor for MeteoraExitFor. Nothing held them together, so it is now one meteora_remove_all_liquidity! in keeper_flow.rs and expand-check.sh confirms both expansions still match the hand-written code they replaced.

    begin_*_intent is 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 convention venue::open_increase::validate_active_flow already 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.

  6. L02-R002 — instructions/meteora/yield_adapter.rs:418-421 implements the yield trait for MeteoraCompoundFor, MeteoraRebalanceFor, and MeteoraExitFor through 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-29 imports 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.rs imports nothing from any sibling, so it was never in a cycle at all. The two real back-edges came from exit_for.rs and rebalance_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.rs held 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 in venue/adapters/mod.rs, exported with pub(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.rs names no account struct, so every handler may import it. yield_adapter.rs names all four and is imported by none. The graph is a DAG; nothing in the directory now imports a sibling handler except intent_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.

  7. L02-R003 — The owner check has three spellings. constraint = sickle.is_owner(&authority.key()) at instructions/orca/close_position.rs:50. constraint = sickle.owner == authority.key() at instructions/position/set_position_settings.rs:47, instructions/position/unset_position_settings.rs:32, instructions/intent/set_intent_session.rs:43. has_one = owner at instructions/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_automator and set_manager derive the Sickle from owner.key(), the signer, so a caller can only ever reach their own and no constraint is doing the work. Within that pair the first adds has_one = owner and the second adds nothing, which is what made them look inconsistent.

    That has_one is unreachable. initialize_sickle is the only writer of owner and sets it to the same key it seeds with, so sickle.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 now sickle.is_owner(...), so the owner rule has one definition. They were equivalent; nothing made them stay equivalent.

  8. L02-R004 — RaydiumRebalanceFor.fee_collector_account_0 and _1 are UncheckedAccount (instructions/raydium/rebalance_for.rs:197-201), as are those of RaydiumRebalancePosition (instructions/raydium/rebalance_position.rs:206-210), and the adapter validates them (venue/adapters/raydium.rs:544-553, :758-767). OrcaRebalancePosition.fee_collector_account_a and _b are InterfaceAccount with token::mint and token::authority (instructions/orca/rebalance_position.rs:208-221). RebalanceFor keeps 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.52 against a zero baseline:

    changeframe diagnostics
    RaydiumRebalanceFor fee collectors -> typed15
    RaydiumRebalancePosition fee 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, and anchor build exits 0 on them.

    Typing the owner one was still declined. token::mint plus token::authority check exactly what validate_collector_bytes already checks, and assert_fee_collector_account is 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 that scripts/builder/budgets.ts says 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's assert_fee_collector_account no longer states that reason of all its callers when it is true of one.

  9. L02-R005 — OrcaRebalancePosition.token_mint_a and token_mint_b are UncheckedAccount (instructions/orca/rebalance_position.rs:154-158) while old_position_mint on the same struct is InterfaceAccount<Mint> (:119). token_program_a and token_program_b are UncheckedAccount (:195-198) and assert_token_program_for_mint checks them later (:590-591). anchor_spl::token_interface::TokenInterface exists and is used by instructions/sickle/sweep_token.rs:2-3. Decided 2026-09-11: no change, with the measurement attached.

    Typing token_mint_a/_b, and token_program_a/_b alongside 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 with old_position_mint is 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_accounts deserialization against an unmeasured margin, and Orca already validates both mints against the whirlpool. A measured CU figure for this flow would reopen it.

  10. L02-R006 — instructions/orca/rebalance_position.rs:60-70 and :234-242 document a branch as "unreachable today" twice. The SettingsCandidateRequired branch 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.

  1. L02-R007 — instructions/orca/rebalance_position.rs:474-478 cites "INFO #9 Step 4" and instructions/raydium/rebalance_position.rs:167 cites "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.

  1. L02-R008 — instructions/orca/harvest_for.rs:181-232 and instructions/orca/compound_for.rs:225-277 hold the same pause, keeper, settings, and vault-derivation block. instructions/raydium/harvest_for.rs:183 and instructions/raydium/compound_for.rs:222 repeat 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_for and meteora_exit_for need 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_HARVEST has to sit beside validate_harvest_for and PERMISSION_COMPOUND beside validate_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 require KeeperClaimPolicy of whatever type fills their HarvestFor and CompoundFor slots (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_harvest keeps no authorization. raydium_collect_fees enters 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-497 says 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 in prepare_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 in prepare_raydium_yield!, because raydium_collect_fees expands that macro too and pins its destinations with the looser token::authority = sickle.

  2. L02-R009 — instructions/orca/open_position.rs:45-46 says 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 at instructions/orca/open_position.rs:53-54 and :62-64 for 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, and instructions/raydium/open_position.rs:59 carry 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.rs were 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 CHECK comment 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 why try_accounts stays 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 in docs strings.

  3. L02-R010 — instructions/orca/rebalance_position.rs:37 says "the program has no swap support yet (Jupiter is Phase 3)". instructions/jupiter/swap.rs exists with 522 lines. Resolved in this pass.

    The comment now says what the builder actually appends: the re-add, and a balancing jupiter_swap leg before it when the drained amounts do not match the new range.

  4. L02-R011 — instructions/raydium/rebalance_for.rs:231-233 says the memo program is "Pinned to its canonical ID" with no address constraint. The adapter runs the check (venue/adapters/raydium.rs:674-678). The sibling instructions/raydium/exit_for.rs:192-195 has 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 a memo_program and exactly two lack the address constraint, RaydiumRebalanceFor and Orca's RebalanceFor. Orca's comment already explained itself. Raydium's now names validate_keeper_static_accounts, and says why it differs from its eleven siblings, so the next reader does not restore the identical wording.

  5. L02-R012 — The pause check has two forms and three locations. 11 owner exit handlers call assert_not_paused_unless_owner_exit in the handler with a flag that is always true (instructions/orca/close_position.rs:113-121). orca_open_position calls nothing and relies on venue/open_increase.rs:47. instructions/orca/harvest_for.rs:183 calls assert_not_paused in the handler while instructions/orca/rebalance_for.rs relies on venue/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_exit is 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 regression venue/open_increase.rs:37-41 already 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_owner rather than true keeps 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_paused in the handler while every other keeper flow reached it through a runner; historical note 1's change moved them, so the handler-side assert_not_paused sites drop from 13 to 7 and the claim flows now match the rebalance and exit flows. The two forms remain, on purpose.

  6. L02-R013 — The allowlist check runs 3 times on an Orca owner rebalance: venue/owner_rebalance.rs:439, instructions/orca/collect_rewards.rs:133 inside take_orca_reward_groups, and instructions/orca/collect_rewards.rs:229 inside collect_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_groups is the only pre-CPI allowlist check on an Orca harvest and compound, and both helpers are the only ones on the standalone orca_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_claim runs the pause check but not the allowlist check, even though validate_active_flow next door runs both: every claim adapter already asserts the allowlist inside prepare_all_yield, before any CPI, and a second copy would buy nothing while moving the measured Meteora compute envelopes. venue/claim.rs:228-237 says so at the point of temptation.

Source: docs/dev/layers/02-instruction-handlers.md in vfat-io/sickle-programs, vendored here by its sync workflow. Edit the markdown there; this page is rebuilt from it on deploy.