Venue bindings
This layer builds the CPI instructions the program sends to Orca, Raydium, Meteora, and Jupiter, and reads the raw accounts those programs own.
The bindings are written by hand. No venue SDK crate is a dependency. Each binding serializes instruction data with Borsh, orders the account metas, and reads fields out of raw account bytes at fixed offsets. The venue program ID is never a constant in this layer. The caller passes the program from the allowlist (GlobalConfig protocol slots) on every call (programs/sickle/src/orca.rs:7-8, programs/sickle/src/raydium.rs:7-8). The helpers the bindings share live in orca.rs, and raydium.rs and meteora.rs import them from there (/programs/sickle/src/orca.rs:10-13).
Files
| Path | Lines | Purpose |
|---|---|---|
programs/sickle/src/orca.rs | 1803 | Orca Whirlpools instruction builders, Whirlpool and Position parsers, transfer-hook slice encoding, and the shared helpers. |
programs/sickle/src/raydium.rs | 1422 | Raydium CLMM instruction builders, PoolState and PersonalPositionState parsers, reward groups. |
programs/sickle/src/meteora.rs | 2436 | Meteora DLMM instruction builders, LbPair, PositionV2, BinArray, bitmap parsers, range checks. |
programs/sickle/src/instructions/jupiter/cpi.rs | 282 | Jupiter route forwarder, token-account owner probe, post-swap vault check. |
tests/fixtures/meteora-dlmm-layout.json | 559 | Cross-language offset contract for the Meteora accounts. meteora.rs asserts its constants against it. |
tests/fixtures/clmm-mainnet-accounts.json | 78 | Verbatim mainnet Whirlpool, Position, PoolState, and PersonalPositionState accounts the Orca and Raydium parser tests read. |
Responsibilities
- Hold the 8-byte instruction discriminators of every venue instruction the program calls.
- Serialize each venue instruction's arguments in the venue's Borsh layout.
- Order the account metas of each venue instruction with the correct signer and writable flags.
- Check the account discriminator and length before it reads a venue account.
- Read tick spacing, live tick, position range, and liquidity out of raw pool and position bytes.
- Read the initialized reward slots of a pool in raw slot order.
- Encode the transfer-hook remaining-account descriptor for the Orca
*_v2instructions. - Derive Meteora bin-array and bitmap-extension addresses and check the bin-array tail of a range.
- Forward an opaque Jupiter route and mark the Sickle PDA as a read-only signer.
- Hold the seeds of the venue PDAs the handlers derive.
- Hold the byte readers, the reward-slot scanner, the tick-range check, and the Memo program address that the venue bindings share.
The layer does not check the allowlist, the pause flag, or the caller role. It does not measure balances or charge fees. Those checks live in the handlers and the shared runners.
Legend for account tables
| Mark | Meaning |
|---|---|
| S | The account signs. |
| W | The account is writable. |
| none | Read-only and not a signer. |
Public items
programs/sickle/src/orca.rs
The Orca binding has no program-ID constant. It holds one fixed foreign address and one seed. The tick bounds and the Memo program address are defined here once; the Raydium and Meteora bindings re-export them.
| Item | Kind | Value | Meaning |
|---|---|---|---|
ORCA_POSITION_SEED | const | b"position" | Seed of the Orca position PDA [b"position", position_mint]. |
MIN_TICK_INDEX | const | -443636 | Lowest allowed tick. Shared with Raydium as MIN_TICK. |
MAX_TICK_INDEX | const | 443636 | Highest allowed tick. Shared with Raydium as MAX_TICK. |
WP_NFT_UPDATE_AUTH | const | 3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr | Orca's position-NFT metadata update authority. Passed as metadata_update_auth on open. |
MEMO_PROGRAM_ID | const | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr | The Memo program. A fixed account of every Orca *_v2 instruction. The one definition. |
WHIRLPOOL_REWARD_SLOT_COUNT | const | 3 | Number of reward slots in a Whirlpool. |
ACCOUNTS_TYPE_TRANSFER_HOOK_A | const | 0 | Borsh tag of the Orca AccountsType::TransferHookA slice. |
ACCOUNTS_TYPE_TRANSFER_HOOK_B | const | 1 | Borsh tag of TransferHookB. |
ACCOUNTS_TYPE_TRANSFER_HOOK_REWARD | const | 2 | Borsh tag of TransferHookReward, used by collect_reward_v2. |
Shared helpers
These items are pub(crate) unless marked. raydium.rs imports all of them and meteora.rs imports the readers and the Memo address.
| Item | Kind | Meaning |
|---|---|---|
read_u16, read_i32, read_u64, read_i64, read_u128, read_pubkey | fn | Read one little-endian field at an offset. Every parser checks the length first, so the Result is one error surface, not a reachable failure. |
RewardSlotLayout | struct | Where a pool keeps its reward-slot array: discriminator, array offset, slot count, slot stride, and the mint and vault offsets inside a slot. |
for_each_initialized_reward_slot | fn | Checks the discriminator and the full array extent, then calls a closure with (index, mint, vault) for every slot whose mint is not the default pubkey. |
validate_tick_range | fn, pub | Checks lower < upper and both ticks inside the global tick limits. Returns InvalidTickRange. Re-exported by raydium.rs. |
test_support | mod, test-only | anchor_discriminator, keys<N>, the mainnet fixture loader, and the float tick helper the other bindings' unit tests import. |
Orca discriminators
Every Orca discriminator is written as an 8-byte literal. The doc comment names the preimage sha256("global:<name>")[..8] or sha256("account:<Name>")[..8]. One table-driven unit test recomputes every instruction discriminator from its preimage (/programs/sickle/src/orca.rs:918-943) and one recomputes both account discriminators (/programs/sickle/src/orca.rs:954-967).
| Constant | Preimage | Bytes |
|---|---|---|
OPEN_POSITION_WITH_TOKEN_EXTENSIONS_DISCRIMINATOR | global:open_position_with_token_extensions | [212, 47, 95, 92, 114, 102, 131, 250] |
INCREASE_LIQUIDITY_V2_DISCRIMINATOR | global:increase_liquidity_v2 | [133, 29, 89, 223, 69, 238, 176, 10] |
DECREASE_LIQUIDITY_V2_DISCRIMINATOR | global:decrease_liquidity_v2 | [58, 127, 188, 62, 79, 82, 196, 96] |
COLLECT_FEES_V2_DISCRIMINATOR | global:collect_fees_v2 | [207, 117, 95, 191, 229, 180, 226, 15] |
COLLECT_REWARD_V2_DISCRIMINATOR | global:collect_reward_v2 | [177, 107, 37, 180, 160, 19, 49, 209] |
CLOSE_POSITION_WITH_TOKEN_EXTENSIONS_DISCRIMINATOR | global:close_position_with_token_extensions | [1, 182, 135, 59, 155, 25, 99, 223] |
WHIRLPOOL_ACCOUNT_DISCRIMINATOR | account:Whirlpool | [63, 149, 209, 12, 225, 128, 99, 9] |
POSITION_ACCOUNT_DISCRIMINATOR | account:Position | [170, 188, 143, 228, 122, 64, 247, 208] |
The Orca and Raydium increase_liquidity_v2 and decrease_liquidity_v2 discriminators are the same 8 bytes. Both programs use the same Anchor instruction name, so INCREASE_LIQUIDITY_V2_DISCRIMINATOR and DECREASE_LIQUIDITY_V2_DISCRIMINATOR are defined identically in both bindings. The program sends only the *_v2 forms; the v1 builders were removed in #171.
Orca argument structs
Every field is little-endian. data() returns the discriminator followed by the fields.
| Struct | Borsh layout after the discriminator | Length |
|---|---|---|
OpenPositionWithTokenExtensionsArgs | tick_lower_index: i32, tick_upper_index: i32, with_token_metadata: u8 | 17 bytes |
IncreaseLiquidityV2Args | liquidity_amount: u128, token_max_a: u64, token_max_b: u64, then remaining_accounts_info(hook_slices) | 41 bytes, 47 with two slices |
DecreaseLiquidityV2Args | liquidity_amount: u128, token_min_a: u64, token_min_b: u64, then remaining_accounts_info(hook_slices) | 41 bytes, 47 with two slices |
The function remaining_accounts_info encodes the Orca Option<RemainingAccountsInfo> argument.
| Input | Bytes |
|---|---|
| no slices | [0] |
slices [(type, len), ...] | [1], u32 slice count, then one (u8, u8) per slice |
Slice order equals the order of the appended hook account metas. Orca consumes that many trailing accounts per slice (/programs/sickle/src/orca.rs:126-129).
Orca instruction builders
Each builder returns an Instruction with the caller-supplied program ID. The Sickle PDA signs as position_authority through invoke_signed in the handler.
open_position_with_token_extensions_ix. Data: OpenPositionWithTokenExtensionsArgs.
| # | Account | Flags |
|---|---|---|
| 1 | funder | S W |
| 2 | sickle (position owner) | |
| 3 | position | W |
| 4 | position_mint | S W |
| 5 | position_token_account | W |
| 6 | whirlpool | |
| 7 | token_2022_program | |
| 8 | system_program | |
| 9 | associated_token_program | |
| 10 | metadata_update_auth |
modify_liquidity_v2_ix. Shared layout for increase_liquidity_v2 and decrease_liquidity_v2. Data: caller-supplied. Hook account metas trail account 15.
| # | Account | Flags |
|---|---|---|
| 1 | whirlpool | W |
| 2 | token_program_a | |
| 3 | token_program_b | |
| 4 | memo_program | |
| 5 | position_authority | S |
| 6 | position | W |
| 7 | position_token_account | |
| 8 | token_mint_a | |
| 9 | token_mint_b | |
| 10 | token_owner_account_a | W |
| 11 | token_owner_account_b | W |
| 12 | token_vault_a | W |
| 13 | token_vault_b | W |
| 14 | tick_array_lower | W |
| 15 | tick_array_upper | W |
| 16+ | hook accounts | as passed |
collect_fees_v2_ix. Data: discriminator, then remaining_accounts_info(hook_slices). Hook account metas trail account 13.
| # | Account | Flags |
|---|---|---|
| 1 | whirlpool | |
| 2 | position_authority | S |
| 3 | position | W |
| 4 | position_token_account | |
| 5 | token_mint_a | |
| 6 | token_mint_b | |
| 7 | token_owner_account_a | W |
| 8 | token_vault_a | W |
| 9 | token_owner_account_b | W |
| 10 | token_vault_b | W |
| 11 | token_program_a | |
| 12 | token_program_b | |
| 13 | memo_program | |
| 14+ | hook accounts | as passed |
collect_reward_v2_ix. Data: discriminator, reward_index: u8, then remaining_accounts_info. The builder derives the single TransferHookReward slice from the hook account count.
| # | Account | Flags |
|---|---|---|
| 1 | whirlpool | |
| 2 | position_authority | S |
| 3 | position | W |
| 4 | position_token_account | |
| 5 | reward_owner_account | W |
| 6 | reward_mint | |
| 7 | reward_vault | W |
| 8 | reward_token_program | |
| 9 | memo_program | |
| 10+ | hook accounts | as passed |
close_position_with_token_extensions_ix. Data: discriminator only. Orca sends all rent to receiver.
| # | Account | Flags |
|---|---|---|
| 1 | position_authority | S |
| 2 | receiver | W |
| 3 | position | W |
| 4 | position_mint | W |
| 5 | position_token_account | W |
| 6 | token_2022_program |
Orca account parsers
Every parser checks the data length and the account discriminator first. It returns InvalidProtocolAccountData on either failure. The offsets are private constants written as sums of the named field sizes, and a unit test checks that each sum equals its documented value (/programs/sickle/src/orca.rs:1024-1040). A second test lays a Whirlpool out field by field and checks that reward_infos lands on the constant (/programs/sickle/src/orca.rs:1042-1082).
| Parser | Account | Field | Offset | Type | How the offset is written |
|---|---|---|---|---|---|
parse_whirlpool_tick_spacing | Whirlpool | tick_spacing | 41 | u16 | sum 8 + 32 + 1 |
parse_whirlpool_current_tick_index | Whirlpool | tick_current_index | 81 | i32 | sum of 9 field sizes |
parse_whirlpool_reward_slots | Whirlpool | reward_infos[3] | 269 | array | tick-current offset + 10 named field sizes |
one WhirlpoolRewardInfo | 128 | stride | sum of 5 named field sizes | ||
mint in a slot | +0 | pubkey | literal | ||
vault in a slot | +32 | pubkey | previous offset + 32 | ||
parse_position_state | Position | whirlpool | 8 | pubkey | literal |
liquidity | 72 | u128 | sum 8 + 32 + 32 | ||
tick_lower_index | 88 | i32 | previous offset + 16 | ||
tick_upper_index | 92 | i32 | previous offset + 4 |
parse_position_state returns a PositionState with the four fields above. parse_whirlpool_reward_slots describes reward_infos as a RewardSlotLayout and collects a WhirlpoolRewardSlot list with index, mint, and vault from the shared scanner. The scanner skips a slot whose mint is the default pubkey and keeps the raw slot index, and its length check covers all 3 slots before any read.
validate_tick_range checks lower < upper and both ticks inside the global tick limits. It returns InvalidTickRange.
The Orca binding derives no tick-array address. The handler passes tick_array_lower and tick_array_upper as accounts. The hook accounts come from the handler through take_hook_accounts and hook_account_metas in fees.rs (programs/sickle/src/fees.rs:481-560). The binding only encodes the slice descriptor and appends the metas.
programs/sickle/src/raydium.rs
The Raydium binding has no program-ID constant. It holds two seeds and re-exports the tick bounds, the tick-range check, and the Memo program address from orca.rs under Raydium's names.
| Item | Kind | Value | Meaning |
|---|---|---|---|
RAYDIUM_POSITION_SEED | const | b"position" | Seed of PersonalPositionState PDA [b"position", nft_mint]. |
RAYDIUM_TICK_ARRAY_BITMAP_SEED | const | b"pool_tick_array_bitmap_extension" | Seed of the TickArrayBitmapExtension PDA [seed, pool_state]. |
MIN_TICK | re-export | -443636 | Lowest allowed tick. orca::MIN_TICK_INDEX. |
MAX_TICK | re-export | 443636 | Highest allowed tick. orca::MAX_TICK_INDEX. |
validate_tick_range | re-export | The shared range check. orca::validate_tick_range. | |
MEMO_PROGRAM_ID | re-export | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr | The Memo program, a fixed account of decrease_liquidity_v2. |
Raydium discriminators
Every Raydium discriminator is a literal. The unit tests recompute each one with sha256 (/programs/sickle/src/raydium.rs:649-680).
| Constant | Preimage | Bytes |
|---|---|---|
OPEN_POSITION_WITH_TOKEN22_NFT_DISCRIMINATOR | global:open_position_with_token22_nft | [77, 255, 174, 82, 125, 29, 201, 46] |
INCREASE_LIQUIDITY_V2_DISCRIMINATOR | global:increase_liquidity_v2 | [133, 29, 89, 223, 69, 238, 176, 10] |
DECREASE_LIQUIDITY_V2_DISCRIMINATOR | global:decrease_liquidity_v2 | [58, 127, 188, 62, 79, 82, 196, 96] |
CLOSE_POSITION_DISCRIMINATOR | global:close_position | [123, 134, 81, 0, 49, 68, 98, 98] |
POOL_STATE_ACCOUNT_DISCRIMINATOR | account:PoolState | [247, 237, 227, 245, 215, 195, 222, 70] |
PERSONAL_POSITION_ACCOUNT_DISCRIMINATOR | account:PersonalPositionState | [70, 111, 150, 126, 230, 15, 25, 117] |
Raydium argument structs
| Struct | Borsh layout after the discriminator | Length |
|---|---|---|
OpenPositionWithToken22NftArgs | tick_lower_index: i32, tick_upper_index: i32, tick_array_lower_start_index: i32, tick_array_upper_start_index: i32, liquidity: u128, amount_0_max: u64, amount_1_max: u64, with_metadata: u8, base_flag: Option<bool> | 58 or 59 bytes |
IncreaseLiquidityV2Args | liquidity: u128, amount_0_max: u64, amount_1_max: u64, base_flag: Option<bool> | 41 or 42 bytes |
DecreaseLiquidityV2Args | liquidity: u128, amount_0_min: u64, amount_1_min: u64 | 40 bytes |
extend_option_bool writes None as [0] and Some(b) as [1, b].
Note: The open struct declares its fields in the wire order, the four tick fields first, unlike the liquidity-first increase and decrease structs. The comment records the live-program failure of a liquidity-first encoding (/programs/sickle/src/raydium.rs:264-273).
The handler opens every position with liquidity = 0 and base_flag = None. A separate increase funds it (/programs/sickle/src/raydium.rs:275-279).
Raydium instruction builders
open_position_with_token22_nft_ix. Data: OpenPositionWithToken22NftArgs. The optional bitmap extension appends one writable meta.
| # | Account | Flags |
|---|---|---|
| 1 | payer | S W |
| 2 | sickle (NFT owner) | |
| 3 | position_nft_mint | S W |
| 4 | position_nft_account | W |
| 5 | pool_state | W |
| 6 | protocol_position | |
| 7 | tick_array_lower | W |
| 8 | tick_array_upper | W |
| 9 | personal_position | W |
| 10 | token_account_0 | W |
| 11 | token_account_1 | W |
| 12 | token_vault_0 | W |
| 13 | token_vault_1 | W |
| 14 | rent | |
| 15 | system_program | |
| 16 | token_program | |
| 17 | associated_token_program | |
| 18 | token_program_2022 | |
| 19 | vault_0_mint | |
| 20 | vault_1_mint | |
| 21 | tick_array_bitmap_extension | W (optional) |
increase_liquidity_v2_ix. Data: IncreaseLiquidityV2Args.
| # | Account | Flags |
|---|---|---|
| 1 | nft_owner (Sickle) | S |
| 2 | nft_account | |
| 3 | pool_state | W |
| 4 | protocol_position | |
| 5 | personal_position | W |
| 6 | tick_array_lower | W |
| 7 | tick_array_upper | W |
| 8 | token_account_0 | W |
| 9 | token_account_1 | W |
| 10 | token_vault_0 | W |
| 11 | token_vault_1 | W |
| 12 | token_program | |
| 13 | token_program_2022 | |
| 14 | vault_0_mint | |
| 15 | vault_1_mint | |
| 16 | tick_array_bitmap_extension | W (optional) |
decrease_liquidity_v2_ix. Data: DecreaseLiquidityV2Args. The optional bitmap comes first in the remaining tail, then one [vault W, recipient W, mint] group per initialized reward slot in slot order.
| # | Account | Flags |
|---|---|---|
| 1 | nft_owner (Sickle) | S |
| 2 | nft_account | |
| 3 | personal_position | W |
| 4 | pool_state | W |
| 5 | protocol_position | |
| 6 | token_vault_0 | W |
| 7 | token_vault_1 | W |
| 8 | tick_array_lower | W |
| 9 | tick_array_upper | W |
| 10 | recipient_token_account_0 | W |
| 11 | recipient_token_account_1 | W |
| 12 | token_program | |
| 13 | token_program_2022 | |
| 14 | memo_program | |
| 15 | vault_0_mint | |
| 16 | vault_1_mint | |
| 17 | tick_array_bitmap_extension | W (optional) |
| 18+ | reward groups | W, W, none per group |
Note: personal_position comes before pool_state here, and the vaults come before the tick arrays. The increase builder has the opposite order (/programs/sickle/src/raydium.rs:545-549).
close_position_ix. Data: discriminator only. Raydium sends all rent to nft_owner, so the Sickle PDA is writable.
| # | Account | Flags |
|---|---|---|
| 1 | nft_owner (Sickle) | S W |
| 2 | position_nft_mint | W |
| 3 | position_nft_account | W |
| 4 | personal_position | W |
| 5 | system_program | |
| 6 | token_program |
Raydium account parsers
PoolState is #[repr(C, packed)], so the offsets are packed byte sums (/programs/sickle/src/raydium.rs:101-105). A unit test checks that each sum equals its documented value (/programs/sickle/src/raydium.rs:1269-1283), and a second lays the account out field by field and checks that reward_infos lands on the constant (/programs/sickle/src/raydium.rs:1286-1334).
| Parser | Account | Field | Offset | Type | How the offset is written |
|---|---|---|---|---|---|
parse_pool_tick_spacing | PoolState | tick_spacing | 235 | u16 | sum 8 + 1 + 32 * 7 + 1 + 1 |
parse_pool_current_tick | PoolState | tick_current | 269 | i32 | previous + 2 + 16 + 16 |
parse_pool_reward_slots | PoolState | reward_infos[3] | 397 | array | tick-current offset + 11 named field sizes |
token_mint | +57 | pubkey | sum of 7 named field sizes | ||
token_vault | +89 | pubkey | previous + 32 | ||
one RewardInfo | 169 | stride | vault offset + 32 + 32 + 16 | ||
parse_personal_position_state | PersonalPositionState | nft_mint | 9 | pubkey | sum 8 + 1 |
pool_id | 41 | pubkey | previous + 32 | ||
tick_lower_index | 73 | i32 | previous + 32 | ||
tick_upper_index | 77 | i32 | previous + 4 | ||
liquidity | 81 | u128 | previous + 4 |
parse_personal_position_state returns a RaydiumPositionState with the five fields. parse_pool_reward_slots describes reward_infos as a RewardSlotLayout and collects a RaydiumRewardSlot list of mint and vault from the shared scanner in orca.rs, which skips a slot whose mint is the default pubkey. The Raydium slot does not carry the slot index. RaydiumRewardGroupKeys is the vault, recipient, mint triple that the decrease builder appends.
validate_tick_range is the shared check re-exported from orca.rs.
The Raydium binding derives no tick-array address. The caller supplies tick_array_lower_start_index and tick_array_upper_start_index as arguments and Raydium re-validates them (/programs/sickle/src/raydium.rs:264-273). The handler derives the bitmap PDA from RAYDIUM_TICK_ARRAY_BITMAP_SEED (programs/sickle/src/venue/adapters/raydium_flows.rs:1378-1394).
programs/sickle/src/meteora.rs
The Meteora binding has no program-ID constant. The layout fixture names the program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo (tests/fixtures/meteora-dlmm-layout.json:23). Every public layout constant is a literal that the fixture test asserts.
| Item | Kind | Value | Meaning |
|---|---|---|---|
BIN_ARRAY_SEED | const | b"bin_array" | Seed of [b"bin_array", lb_pair, index: i64 LE]. |
BIN_ARRAY_BITMAP_EXTENSION_SEED | const | b"bitmap" | Seed of [b"bitmap", lb_pair]. |
DLMM_EVENT_AUTHORITY_SEED | const | b"__event_authority" | Seed of Meteora's Anchor event authority. |
MEMO_PROGRAM_ID | re-export | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr | The Memo program. orca::MEMO_PROGRAM_ID. |
BIN_ARRAY_BITMAP_SIZE | const | 512 | Inline bitmap covers bin-array indexes -512 to 511. |
BASIS_POINT_MAX | const | 10000 | Maximum bps_to_remove. |
DEFAULT_BIN_PER_POSITION | const | 70 | Bins in the base PositionV2. |
POSITION_MAX_LENGTH | const | 1400 | Maximum position width in bins. |
MAX_RESIZE_LENGTH | const | 91 | Maximum length_to_add per resize. |
MAX_BIN_PER_ARRAY | const | 70 | Bins per BinArray. |
DLMM_STRATEGY_TYPE_COUNT | const | 9 | Number of Meteora strategy enum variants. |
KNOWN_LB_PAIR_VERSION | const | 1 | Highest accepted LbPair.version. |
KNOWN_POSITION_V2_VERSION | const | 1 | Highest accepted PositionV2.version. |
KNOWN_BIN_ARRAY_VERSION | const | 3 | Highest accepted BinArray.version. |
LB_PAIR_STATUS_ENABLED | const | 0 | LbPair.status value the program accepts. |
LB_PAIR_FUNCTION_UNDETERMINED | const | 0 | functionType value: undetermined. |
LB_PAIR_FUNCTION_LIQUIDITY_MINING | const | 1 | functionType value: liquidity mining. |
LB_PAIR_FUNCTION_LIMIT_ORDER | const | 2 | functionType value: limit order. Named by the tests and a doc comment only. |
| private per-bin constants | const | 16, 48, 32, 48, 32, 16, 16, 64 | The PositionV2 per-bin strides and pending-field offsets. All asserted against the fixture. |
EMPTY_REMAINING_ACCOUNTS_INFO | const | [0, 0, 0, 0] | Private. The empty RemainingAccountsInfo every range builder writes. |
Meteora discriminators
Every instruction discriminator is recomputed from its preimage in instruction_discriminators_data_and_account_metas_match_idl (meteora.rs:1754-1781). Every account discriminator is recomputed there too (meteora.rs:1782-1797) and compared to the fixture in constants_match_the_shared_layout_contract (meteora.rs:2123-2390).
| Constant | Preimage | Bytes |
|---|---|---|
INITIALIZE_POSITION2_DISCRIMINATOR | global:initialize_position2 | [143, 19, 242, 145, 213, 15, 104, 115] |
INCREASE_POSITION_LENGTH_DISCRIMINATOR | global:increase_position_length | [80, 83, 117, 211, 66, 13, 33, 149] |
CLOSE_POSITION_IF_EMPTY_DISCRIMINATOR | global:close_position_if_empty | [59, 124, 212, 118, 91, 152, 110, 157] |
CLAIM_FEE2_DISCRIMINATOR | global:claim_fee2 | [112, 191, 101, 171, 28, 144, 127, 187] |
CLAIM_REWARD2_DISCRIMINATOR | global:claim_reward2 | [190, 3, 127, 119, 178, 87, 157, 183] |
REMOVE_LIQUIDITY_BY_RANGE2_DISCRIMINATOR | global:remove_liquidity_by_range2 | [204, 2, 195, 145, 53, 145, 145, 205] |
ADD_LIQUIDITY_BY_STRATEGY2_DISCRIMINATOR | global:add_liquidity_by_strategy2 | [3, 221, 149, 218, 111, 141, 118, 213] |
LB_PAIR_ACCOUNT_DISCRIMINATOR | account:LbPair | [33, 11, 49, 98, 181, 101, 177, 13] |
POSITION_V2_ACCOUNT_DISCRIMINATOR | account:PositionV2 | [117, 176, 212, 199, 245, 180, 133, 182] |
BIN_ARRAY_ACCOUNT_DISCRIMINATOR | account:BinArray | [92, 142, 92, 220, 5, 148, 70, 181] |
BIN_ARRAY_BITMAP_EXTENSION_ACCOUNT_DISCRIMINATOR | account:BinArrayBitmapExtension | [80, 111, 124, 113, 55, 237, 18, 5] |
Meteora argument layouts
Meteora's RemainingAccountsInfo { slices: Vec<_> } is not optional. The four range builders always write the empty vector as the 4 zero bytes of EMPTY_REMAINING_ACCOUNTS_INFO (meteora.rs:129-134). This differs from the Orca Option encoding, where the empty case is 1 zero byte.
| Builder | Borsh layout after the discriminator | Length |
|---|---|---|
initialize_position2_ix | lower_bin_id: i32, width: i32 | 16 bytes |
increase_position_length_ix | length_to_add: u16, side: u8 | 11 bytes |
close_position_if_empty_ix | none | 8 bytes |
claim_fee2_ix | min_bin_id: i32, max_bin_id: i32, u32 0 | 20 bytes |
claim_reward2_ix | reward_index: u64, min_bin_id: i32, max_bin_id: i32, u32 0 | 28 bytes |
remove_liquidity_by_range2_ix | from_bin_id: i32, to_bin_id: i32, bps_to_remove: u16, u32 0 | 22 bytes |
add_liquidity_by_strategy2_ix | amount_x: u64, amount_y: u64, active_id: i32, max_active_bin_slippage: i32, min_bin_id: i32, max_bin_id: i32, strategy_type: u8, [u8; 64], u32 0 | 109 bytes |
DlmmLiquidityParameterByStrategy is the argument struct of the add. It derives AnchorSerialize and AnchorDeserialize because it is an instruction argument of the program itself (meteora.rs:186-202, programs/sickle/src/lib.rs:153-158). The builder writes it with that derived serializer; the struct's declaration order is the wire order (meteora.rs:1067).
Meteora instruction builders
Every Meteora builder appends event_authority and the DLMM program itself as the last two fixed accounts. dlmm_event_authority derives the authority from DLMM_EVENT_AUTHORITY_SEED (meteora.rs:784-786). The range builders append one writable meta per BinArray in the caller-supplied order.
initialize_position2_ix (meteora.rs:789-817).
| # | Account | Flags |
|---|---|---|
| 1 | payer | S W |
| 2 | position | S W |
| 3 | lb_pair | |
| 4 | owner (Sickle) | S |
| 5 | system_program | |
| 6 | event_authority | |
| 7 | dlmm_program |
increase_position_length_ix (meteora.rs:820-848).
| # | Account | Flags |
|---|---|---|
| 1 | funder | S W |
| 2 | lb_pair | |
| 3 | position | W |
| 4 | owner (Sickle) | S |
| 5 | system_program | |
| 6 | event_authority | |
| 7 | dlmm_program |
close_position_if_empty_ix (meteora.rs:850-868).
| # | Account | Flags |
|---|---|---|
| 1 | position | W |
| 2 | sender (Sickle) | S |
| 3 | rent_receiver | W |
| 4 | event_authority | |
| 5 | dlmm_program |
claim_fee2_ix (meteora.rs:871-925).
| # | Account | Flags |
|---|---|---|
| 1 | lb_pair | W |
| 2 | position | W |
| 3 | sender (Sickle) | S |
| 4 | reserve_x | W |
| 5 | reserve_y | W |
| 6 | user_token_x | W |
| 7 | user_token_y | W |
| 8 | token_x_mint | |
| 9 | token_y_mint | |
| 10 | token_program_x | |
| 11 | token_program_y | |
| 12 | memo_program | |
| 13 | event_authority | |
| 14 | dlmm_program | |
| 15+ | bin arrays | W |
claim_reward2_ix (meteora.rs:928-976).
| # | Account | Flags |
|---|---|---|
| 1 | lb_pair | W |
| 2 | position | W |
| 3 | sender (Sickle) | S |
| 4 | reward_vault | W |
| 5 | reward_mint | |
| 6 | user_token_account | W |
| 7 | token_program | |
| 8 | memo_program | |
| 9 | event_authority | |
| 10 | dlmm_program | |
| 11+ | bin arrays | W |
remove_liquidity_by_range2_ix (meteora.rs:979-1039). Account 3 is writable even when the key is the DLMM program sentinel (meteora.rs:1012-1014).
| # | Account | Flags |
|---|---|---|
| 1 | position | W |
| 2 | lb_pair | W |
| 3 | bin_array_bitmap_extension | W |
| 4 | user_token_x | W |
| 5 | user_token_y | W |
| 6 | reserve_x | W |
| 7 | reserve_y | W |
| 8 | token_x_mint | |
| 9 | token_y_mint | |
| 10 | sender (Sickle) | S |
| 11 | token_program_x | |
| 12 | token_program_y | |
| 13 | memo_program | |
| 14 | event_authority | |
| 15 | dlmm_program | |
| 16+ | bin arrays | W |
add_liquidity_by_strategy2_ix (meteora.rs:1042-1099). Same shape as the remove builder without the memo program.
| # | Account | Flags |
|---|---|---|
| 1 | position | W |
| 2 | lb_pair | W |
| 3 | bin_array_bitmap_extension | W |
| 4 | user_token_x | W |
| 5 | user_token_y | W |
| 6 | reserve_x | W |
| 7 | reserve_y | W |
| 8 | token_x_mint | |
| 9 | token_y_mint | |
| 10 | sender (Sickle) | S |
| 11 | token_program_x | |
| 12 | token_program_y | |
| 13 | event_authority | |
| 14 | dlmm_program | |
| 15+ | bin arrays | W |
Meteora account parsers
Every Meteora parser requires an exact data length. Every parser except the bitmap one also requires version <= KNOWN_*_VERSION. The offsets are literals. The unit test constants_match_the_shared_layout_contract compares every one of them to the fixture (meteora.rs:2123-2390). The fixture was checked against mainnet accounts at slot 442674353 (tests/fixtures/meteora-dlmm-layout.json:25).
| Parser | Account | Field | Offset | Type | In fixture assert |
|---|---|---|---|---|---|
parse_lb_pair | LbPair, 904 bytes | parameters.min_bin_id | 24 | i32 | yes |
parameters.max_bin_id | 28 | i32 | yes | ||
parameters.function_type | 35 | u8 | yes | ||
active_id | 76 | i32 | yes | ||
status | 82 | u8 | yes | ||
token_x_mint | 88 | pubkey | yes | ||
token_y_mint | 120 | pubkey | yes | ||
reserve_x | 152 | pubkey | yes | ||
reserve_y | 184 | pubkey | yes | ||
reward_infos[2], stride 144 | 264 | array | yes | ||
RewardInfo.mint, RewardInfo.vault | +0, +32 | pubkey | yes | ||
version | 882 | u8 | yes | ||
parse_position_v2 | PositionV2, 8120 + 112 per bin above 70 | lb_pair | 8 | pubkey | yes |
owner | 40 | pubkey | yes | ||
liquidity_shares[70], stride 16 | 72 | u128 | yes | ||
reward_infos[70], stride 48, pending at +32 | 1192 | struct | yes | ||
fee_infos[70], stride 48, pending at +32 | 4552 | struct | yes | ||
lower_bin_id | 7912 | i32 | yes | ||
upper_bin_id | 7916 | i32 | yes | ||
operator | 7960 | pubkey | yes | ||
lock_release_point | 7992 | u64 | yes | ||
fee_owner | 8001 | pubkey | yes | ||
version | 8033 | u8 | yes | ||
permissionless_operation_bits | 8034 | u8 | yes | ||
| extension bin: reward at +16, fee at +64 | 8120+ | struct | yes | ||
parse_bin_array | BinArray, 10136 bytes | index | 8 | i64 | yes |
version | 16 | u8 | yes | ||
lb_pair | 24 | pubkey | yes | ||
parse_bin_array_bitmap_extension | BinArrayBitmapExtension, 1576 bytes | lb_pair | 8 | pubkey | yes |
require_lb_pair_layout runs the LbPair guards once: exact size, discriminator, known version, and min_bin_id < max_bin_id (meteora.rs:219-233). parse_lb_pair builds the snapshot on top of it and sets has_initialized_reward when any reward slot has a non-zero mint (meteora.rs:235-258). parse_position_v2 derives the width from the two bin ids and requires the exact account size for that width. It then scans every bin for non-zero liquidity shares and pending fees or rewards (meteora.rs:455-502). The per-bin offsets differ between the base 70 bins and the extension bins (meteora.rs:437-453).
Reward parsers:
| Function | Returns | Rule |
|---|---|---|
parse_initialized_reward_info | DlmmRewardInfo for one index | Runs require_lb_pair_layout. Index below 2. Mint and vault both non-default, else InvalidDlmmReward. |
parse_initialized_reward_infos | list of (index, DlmmRewardInfo) | Runs require_lb_pair_layout. Skips a slot with default mint and vault. Rejects a half-initialized slot. |
Range and position checks:
| Function | Rule | Error |
|---|---|---|
parse_lb_pair_bin_bounds | Returns the pair's (min_bin_id, max_bin_id). | InvalidProtocolAccountData |
assert_position_pair_liquidity_mining | function_type 1, or 0 with an initialized reward. Every other value, limit order included, is rejected. | UnsupportedDlmmPair |
assert_position_pair_supported | The rule above and status == 0. | UnsupportedDlmmPair |
validate_position_not_owner | The position key differs from the owner wallet. | InvalidPosition |
position_v2_account_size | Width 1 to 1400. Size 8120 + max(0, width - 70) * 112. | InvalidDlmmPositionRange |
assert_sickle_position | assert_sickle_position_custody, and lb_pair equals the given pair. | InvalidPosition |
assert_sickle_position_custody | owner == sickle, operator default, fee_owner default or sickle, lock_release_point == 0, permission bits 0. | InvalidPosition |
validate_open_position_range | Width 1 to 70. Range inside the pair limits. | InvalidDlmmPositionRange |
resized_position_range | length_to_add 1 to 91. New width at most 1400. side 0 grows down, 1 grows up. Range inside the pair limits. | InvalidDlmmResize |
validate_claim_range | min <= max, inside the position and the pair. Width 1 to 1400. | InvalidDlmmPositionRange |
validate_add_liquidity_parameters | One amount non-zero. Slippage non-negative. active_id inside the pair. strategy_type < 9. Then validate_claim_range. | InvalidDlmmLiquidityParameters |
validate_remove_liquidity_bps | bps_to_remove 1 to 10000. | InvalidDlmmRemovalBps |
Bin-array and bitmap derivation:
| Function | Rule |
|---|---|
bin_array_index | floor(bin_id / 70) with Euclidean division. |
bin_array_pda | find_program_address([b"bin_array", lb_pair, index LE], dlmm_program). |
bin_array_bitmap_extension_pda | find_program_address([b"bitmap", lb_pair], dlmm_program). |
range_requires_bitmap_extension | True when the first index is below -512 or the last index is 512 or more. |
validate_remove_bitmap_extension | Inline range: the account key equals the DLMM program and is executable and writable. Overflow range: the key equals the extension PDA, the owner is the DLMM program, writable, not executable, and the parsed lb_pair matches. |
claim_bin_array_count | last_index - first_index + 1. |
validate_claim_bin_arrays | Exactly count accounts. Account i equals the PDA for first + i, is owned by the DLMM program, writable, not executable, and its parsed index and lb_pair match. |
programs/sickle/src/instructions/jupiter/cpi.rs
The Jupiter binding has no constants for program IDs. It has three private layout constants for SPL token accounts.
| Item | Kind | Value | Meaning |
|---|---|---|---|
TOKEN_ACCOUNT_OWNER_OFFSET | const | 32 | Offset of the token account owner field. |
TOKEN_ACCOUNT_LEN | const | 165 | Length of the base token account. |
TOKEN_2022_ACCOUNT_TYPE_ACCOUNT | const | 2 | Token-2022 account-type byte at offset 165 for an account. |
| Item | Kind | Meaning |
|---|---|---|
is_token_account_owned_by | fn | True when the data is at least 165 bytes, byte 165 is 2 or absent, and bytes 32 to 63 equal owner. It checks bytes only; the handler filters by program owner first, and the doc comment says so. |
vault_authority_intact | fn | Takes the reloaded spl_token_2022::state::Account. True when its owner == sickle, mint == expected_mint, no delegate, delegated_amount == 0, and no close_authority. |
route_instruction | fn | Builds the inner instruction from (pubkey, is_signer, is_writable) triples and the opaque route bytes. The Sickle PDA meta becomes a signer and keeps the write flag the outer instruction carried. Every other meta is copied as is. |
The route payload arrives as raw bytes from the handler and is forwarded unchanged (instructions/jupiter/cpi.rs:95, 106). The handler calls is_token_account_owned_by on every remaining account owned by a token program that is not one of the two declared vaults (programs/sickle/src/instructions/jupiter/swap.rs:33-46). It passes both reloaded vaults to vault_authority_intact after the CPI (instructions/jupiter/swap.rs:478-480).
Side-by-side: the three position venues
| Aspect | Orca | Raydium | Meteora |
|---|---|---|---|
| Open | open_position_with_token_extensions | open_position_with_token22_nft with liquidity = 0 | initialize_position2, then increase_position_length per 91 bins |
| Increase | increase_liquidity_v2 | increase_liquidity_v2 | add_liquidity_by_strategy2 |
| Decrease | decrease_liquidity_v2 | decrease_liquidity_v2 | remove_liquidity_by_range2 with bps_to_remove |
| Collect fees | collect_fees_v2 | decrease_liquidity_v2 with liquidity = 0 | claim_fee2 over a bin range |
| Collect rewards | collect_reward_v2 per slot, reward_index: u8 | Inside decrease_liquidity_v2, one [vault, recipient, mint] group per initialized slot | claim_reward2 per slot, reward_index: u64 |
| Close | close_position_with_token_extensions, rent to receiver | close_position, rent to the signer nft_owner | close_position_if_empty, rent to rent_receiver |
| Position account | Position PDA [b"position", position_mint] | PersonalPositionState PDA [b"position", nft_mint] | Direct opens use a PositionV2 keypair; intent rebalances use Sickle's per-nonce position PDA |
| NFT standard | Token-2022 mint, optional metadata extension | Token-2022 mint, no Metaplex metadata | None. The owner field of PositionV2 is the Sickle. |
| Reward slots | 3, 128 bytes each, initialized when mint non-default | 3, 169 bytes each, initialized when mint non-default | 2, 144 bytes each, initialized when mint and vault non-default |
| Range unit | tick, spacing from Whirlpool | tick, spacing from PoolState | bin, 70 bins per BinArray |
| Range accounts | tick_array_lower, tick_array_upper passed by the handler | tick_array_lower, tick_array_upper plus start-index arguments | one BinArray PDA per array in the range, derived and checked by the binding |
| Bitmap extension | none | optional trailing TickArrayBitmapExtension PDA | BinArrayBitmapExtension PDA or the program-ID sentinel, always account 3 |
| Hook forwarding | RemainingAccountsInfo slices plus trailing metas | none | always an empty slice vector |
| Memo program | every *_v2 instruction | decrease_liquidity_v2 | claim_fee2, claim_reward2, remove_liquidity_by_range2 |
| Extra fixed accounts | metadata_update_auth on open | protocol_position, rent on open | event_authority and the DLMM program on every instruction |
| Version check on parse | none | none | LbPair <= 1, PositionV2 <= 1, BinArray <= 3 |
| Length check on parse | minimum length | minimum length | exact length |
How a call flows through it
The representative call is the owner's orca_collect_fees. The handler helper collect_fees_and_charge drives the binding.
- The handler checks the Whirlpool program against the allowlist (
programs/sickle/src/venue/adapters/orca_flows.rs:974). - The handler checks that the pool and the position are owned by that program (
venue/adapters/orca_flows.rs:978-988). - The handler checks each pool mint's token program (
venue/adapters/orca_flows.rs:989-990). - For mint A then mint B,
resolve_pool_hookscallstake_hook_accounts. It receives the hook length orNone, pushes(ACCOUNTS_TYPE_TRANSFER_HOOK_A | B, length), and converts the consumed accounts into metas (venue/adapters/orca_flows.rs:359-402,fees.rs:481-490,519-560). - The handler records both owner-account balances (
venue/adapters/orca_flows.rs:999-1002). invoke_orca_fee_collectbuilds one ordered account list and callscollect_fees_v2_ixwith the 13 fixed keys, the slices, and the appended hook metas (venue/adapters/orca_flows.rs:881-915).- The binding writes the discriminator and
remaining_accounts_info(hook_slices)into the data (/programs/sickle/src/orca.rs:688-689). - The binding orders the 13 fixed metas, marks the Sickle as signer, and appends the hook metas (
/programs/sickle/src/orca.rs:705-729). - The helper calls
invoke_signedwith the Sickle seeds and the account infos in the same order (venue/adapters/orca_flows.rs:888-914). - The handler reloads both accounts and computes the deltas for the fee (
venue/adapters/orca_flows.rs:1005-1009).
Invariants the layer owns
- A venue account is read only after its discriminator matches. Every parser checks
data[..8]against the account discriminator, and both reward-slot parsers do so through the shared scanner. Error:InvalidProtocolAccountData(/programs/sickle/src/orca.rs:344-348,433-436,449-453,462-466;/programs/sickle/src/raydium.rs:172-176,235-238,246-249;meteora.rs:220-225,305-310,319-323,416-421). - A parser never reads past the data. Orca and Raydium require a minimum length up to the last read field. Meteora requires the exact account size. Error:
InvalidProtocolAccountData(same lines as above;meteora.rs:466-469). - A Meteora account with a newer layout version is rejected. Error:
InvalidProtocolAccountData(meteora.rs:223,308,419). - A tick range is increasing and inside the venue tick limits. One check serves both CLMM venues. Error:
InvalidTickRange(/programs/sickle/src/orca.rs:366-376, re-exported at/programs/sickle/src/raydium.rs:50-52). - The hook slice descriptor and the appended hook metas have the same order and count. The builder appends the metas in the order the caller passes, and
resolve_pool_hooksderives slices and metas from the same consumed prefix (/programs/sickle/src/orca.rs:126-129,622,venue/adapters/orca_flows.rs:359-402). - A reward slot counts as initialized only by its mint. The shared scanner skips a slot with the default mint for Orca and Raydium (
/programs/sickle/src/orca.rs:352-354). Meteora requires mint and vault to agree. Error:InvalidDlmmReward(meteora.rs:294-300). - Every
BinArrayin a claim or remove tail is the exact PDA for its index, in ascending order. Each one is owned by the DLMM program, is writable, and has a matching header. Error:InvalidDlmmBinArraysorInvalidAccountOwner(meteora.rs:743-782). - The bitmap account is the program-ID sentinel for an inline range and the exact extension PDA for an overflow range. Error:
InvalidDlmmBitmapExtension(meteora.rs:675-719). - A Meteora position the program manages has no operator, no foreign fee owner, no lock, and no permissionless bits. Error:
InvalidPosition(meteora.rs:514-530). - A Meteora position key never equals the owner wallet. Error:
InvalidPosition(meteora.rs:404-407). - The Sickle PDA enters a Jupiter route as a signer, with the write flag the outer instruction carried. Some venues (Byreal, HumidiFi, BisonFi, GoonFi V2) declare Jupiter's user authority
mut, and a downgrade to readonly failed those routes withPrivilegeEscalation; the flag grants the route nothing because only this program may change a Sickle-owned account's data or debit its lamports. No error. Only the signer bit is rewritten (instructions/jupiter/cpi.rs:100-104). - A Sickle-owned token account other than the two vaults never enters a Jupiter route. The probe is
is_token_account_owned_by. The handler returnsSwapVaultNotAllowed(instructions/jupiter/cpi.rs:39-48,instructions/jupiter/swap.rs:41-44). - After a route both vaults keep the Sickle as authority, the same mint, no delegate, and no
close_authority. The predicate isvault_authority_intact. The handler returnsSwapVaultTampered(instructions/jupiter/cpi.rs:58-68,instructions/jupiter/swap.rs:250-267).
Tests
| Location | Covers |
|---|---|
/programs/sickle/src/orca.rs:840-1803, 34 unit tests | Data layout of every args struct, with and without hook slices. Account order and flags of every builder, with and without hook tails. Position and Whirlpool parsers on synthetic bytes. Reward slot parser. Hook slice encoding. Byte readers. |
/programs/sickle/src/orca.rs:918-967 | Every instruction and account discriminator recomputed with sha256. |
/programs/sickle/src/orca.rs:1026-1042 | Offset constants checked against their documented values, and reward_infos derived from a field-by-field Whirlpool layout. |
/programs/sickle/src/orca.rs:1164-1221 | The parsers on the verbatim mainnet Whirlpool and Position in tests/fixtures/clmm-mainnet-accounts.json, with the live tick cross-checked against sqrt_price. |
/programs/sickle/src/raydium.rs:642-1422, 22 unit tests | All discriminators recomputed. Data layout of the 3 args structs with Option<bool>. Account order of the 4 builders. Bitmap and reward-group tails. Parsers on synthetic bytes. |
/programs/sickle/src/raydium.rs:889-951, 1176-1243 | The parsers on the verbatim mainnet PoolState and PersonalPositionState; offset constants checked against their documented values and reward_infos derived from a field-by-field PoolState layout. |
meteora.rs:1102-2390, 15 unit tests | Parsers with one mutation per guard. Two frozen mainnet LbPair accounts. Range and resize rules. Bin-array and bitmap checks on mock AccountInfo. Data and metas of all 7 builders. All 11 discriminators recomputed. |
meteora.rs:2123-2390 | Every layout constant, the per-bin strides and pending offsets included, compared to tests/fixtures/meteora-dlmm-layout.json. |
meteora.rs:1244-1251 | Reads management-ui/src/lib/dlmm/fixtures/mainnet-accounts.json for two real pairs. |
instructions/jupiter/cpi.rs:110-282, 7 unit tests | Token-account probe on classic, Token-2022, mint, and short data. Vault predicate on a built token account with one field mutated at a time. Signer upgrade of the Sickle meta, write flag forwarded, and a repeated Sickle occurrence. |
tests/fixtures/meteora-dlmm-layout.json | Asserted by meteora.rs:2123-2390, tests/builder-meteora.ts:5, tests/mainnet-meteora-acceptance-unit.ts:72, management-ui/src/lib/dlmm/meteora.test.ts:2. |
tests/fixtures/clmm-mainnet-accounts.json | Four verbatim mainnet accounts at one finalized slot. Read by /programs/sickle/src/orca.rs:1164-1221 and /programs/sickle/src/raydium.rs:889-951 through orca::test_support::mainnet_account. |
tests/surfpool-lifecycle.ts | Orca open, increase, decrease, collect fees, close, owner rebalance, harvest, compound, rebalance, exit on a mainnet fork with reward slots. |
tests/surfpool-t22-lifecycle.ts | Orca with Token-2022 pool mints. Hook forwarding through the decrease builder (2139-2400). |
tests/surfpool-raydium-lifecycle.ts | Raydium open, increase, decrease, collect fees, close, owner rebalance, harvest, compound, rebalance, exit with one ended reward slot. |
tests/surfpool-t22-raydium-lifecycle.ts | Raydium with Token-2022 pool mints. Open, increase, collect fees, owner rebalance, exit. |
tests/surfpool-meteora-lifecycle.ts | Meteora open, resize, add, remove, claim fees, claim reward, close, harvest, compound, rebalance, exit. |
tests/surfpool-jupiter.ts | jupiter_swap through a live Jupiter route. |
Gaps:
- No Surfpool suite exercises the standalone
orca_collect_rewardsinstruction. Onlytests/mainnet-acceptance.ts:1920and builder unit tests build it.collect_reward_v2_ixreaches a fork only inside the keeper flows. - No Surfpool Raydium suite passes a
TickArrayBitmapExtension. The wordbitmapdoes not appear intests/surfpool-raydium-lifecycle.tsortests/surfpool-t22-raydium-lifecycle.ts. - The mainnet fixture holds one position per pool, and each pool has one initialized reward slot. The synthetic tests cover the other reward-slot shapes.
Simplification and audit notes
Historical notes 1 to 15, 17, 18, 20, 21, 23, and 24 were resolved in #171 and are listed under "Resolved" at the end of this section. The numbering of the kept notes is unchanged, so both lists carry a prettier-ignore comment: without it, prettier renumbers a list from its first item.
- L05-N016 — A parser that scans the whole account.
parse_position_v2loops over every bin of the position to compute two booleans (meteora.rs:473-485). A 1400-bin position costs 1400 iterations with 3 slice scans each on every parse. Every caller pays this, including callers that use only the range or the owner. 13 handler files call it. Kept: both booleans are fields of the publicPositionV2Snapshotthose handlers consume, so a cheaper header-only parse needs a handler-side split, which is outside the bindings contract. - L05-N019 — Sentinel marked writable.
remove_liquidity_by_range2_ixandadd_liquidity_by_strategy2_ixmark account 3 writable even when it is the DLMM program ID (meteora.rs:1012-1014,1034-1036).validate_remove_bitmap_extensionrequires the sentinel to be executable and writable (meteora.rs:688-691). The test atmeteora.rs:2090-2119records this shape. Kept: the flag is part of the byte-identical CPI the deployed program sends, it matches Meteora's own SDK, and the on-chain check and the test agree with it. - L05-N022 — Derivation placement differs per venue. Meteora derives bin-array and bitmap PDAs in the binding (
meteora.rs:648-662). Raydium derives the bitmap PDA in a handler from a seed the binding exports (/programs/sickle/src/raydium.rs:42,venue/adapters/raydium_flows.rs:1378-1394). Orca derives nothing and receives every tick array as an account (/programs/sickle/src/orca.rs:542-543). Kept: moving the Raydium derivation into the binding means editing the handler that owns it, which is outside the bindings contract; the Orca and Raydium programs re-derive their own tick arrays inside the CPI. - L05-N025 — Fixture entries with no reader in this layer. The
seeds.positionentry (meteora-dlmm-layout.json:43-49) describes a PDA the program does not use. Meteora positions here are keypair accounts (meteora.rs:404-407,769). TheBinandOraclelayouts (meteora-dlmm-layout.json:432-494,513-547) have no parser inmeteora.rs. Kept: the fixture is the cross-language contract, the management UI decoder assertsBinandOracle, andmanagement-ui/src/lib/dlmm/pdas.ts:9-11points atseeds.positionas documentation; the fixture note now says so (meteora-dlmm-layout.json:17-19). - L05-N026 — Missing tests. See the gaps under Tests. They cover the Raydium bitmap on a fork and the standalone
orca_collect_rewardson a fork. Kept: both need a Surfpool suite change, which is outside the bindings contract; the hook-tail unit tests and the real-account parser tests that were the other half of this note landed in #171. - L05-N027 — Shared helpers hosted in a venue binding. The byte readers,
RewardSlotLayout,for_each_initialized_reward_slot,validate_tick_range, the tick bounds,MEMO_PROGRAM_ID, andtest_supportlive in/programs/sickle/src/orca.rs:265-376and706-748, and/programs/sickle/src/raydium.rs:32-34andmeteora.rs:22import them from there. The bindings layer has no module of its own for code that is not about one venue, so the first binding hosts it. A cross-cutting module would need amodline inlib.rs. - L05-N028 — Two paths to one value.
/programs/sickle/src/raydium.rs:50-52re-exportsorca::MIN_TICK_INDEXasMIN_TICKandorca::MAX_TICK_INDEXasMAX_TICK.venue/family.rs:198-207andstate.rs:1483-1490read the same two numbers through both paths as if they were per-venue. - L05-N029 — A constant only the tests name.
LB_PAIR_FUNCTION_LIMIT_ORDER(meteora.rs:46) is public. After the redundant match arm went, no non-test code reads it; a doc comment and the tests name it.
Resolved
- L05-R001 — A comment that cites the security review by item number.
instructions/jupiter/cpi.rs:50opens with "security review LOW–MED #5", a document that is not in this repository. Theme 6 of the summary lists the same pattern in layers 01, 02, and 08. Resolved in this pass.
The premise was false and contract 13 measured it before acting. 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. Not a dangling pointer; an
unnamed target.
The reference is gone anyway, on the layer 02 test rather than that one: the sentence beside it already said what the guard does, so the pointer added nothing. That test was applied to all 25 in scope one at a time, and two survive because their sentences do not carry the reason without them.
- L05-R002 — Dead code.
INCREASE_LIQUIDITY_DISCRIMINATOR,IncreaseLiquidityArgs,DECREASE_LIQUIDITY_DISCRIMINATOR,DecreaseLiquidityArgs,COLLECT_FEES_DISCRIMINATOR,modify_liquidity_ix, andcollect_fees_ix(orca.rs:70-122,226,499-531,593-620) have no caller outsideorca.rs. Only their own unit tests reference them (orca.rs:804-864,1104-1183). They are the v1 Orca instructions the*_v2builders replaced. Resolved in #171. - L05-R003 — Dead constant.
TICK_ARRAY_SIZE(raydium.rs:49) is public. Its only reference is a doc comment atprograms/sickle/src/instructions/raydium/open_position.rs:29. Resolved in #171. - L05-R004 — Stale comments about hook forwarding.
IncreaseLiquidityV2Argssays the program always serializesNoneand forwards no hook accounts (orca.rs:131-135,147-148). The struct carrieshook_slices(orca.rs:140-143) anddata()serializes them (orca.rs:155). TheCOLLECT_FEES_V2_DISCRIMINATORcomment says the same (orca.rs:228-235). Thecollect_fees_v2_ixcomment saysremaining_accounts_infoisNone(orca.rs:628-629) while the builder takeshook_slicesandhook_accounts(orca.rs:646-650). Resolved in #171. - L05-R005 — Tests that cannot fail. Five Orca discriminator tests compare a constant to the same literal (
orca.rs:786-794,821-829,847-855,856-864,865-874). The Raydium tests recompute every discriminator (raydium.rs:631-664). The Orca account and*_v2tests also recompute (orca.rs:882-895,1216-1231,1338-1348). Resolved in #171. - L05-R006 — Two Meteora account discriminators are not recomputed.
LB_PAIR_ACCOUNT_DISCRIMINATOR(meteora.rs:18) andBIN_ARRAY_ACCOUNT_DISCRIMINATOR(meteora.rs:63) are compared only to the fixture (meteora.rs:2092-2100,2220-2228). The fixture is hand-authored (tests/fixtures/meteora-dlmm-layout.json:3-5).PositionV2and the bitmap extension are recomputed (meteora.rs:1750-1757). Resolved in #171. - L05-R007 — Three copies of one address.
MEMO_PROGRAM_IDis defined inorca.rs:247,raydium.rs:54, andmeteora.rs:107. The Orca test checks equality with the Raydium copy (orca.rs:1277-1286). No test checks the Meteora copy. Resolved in #171. - L05-R008 — Duplicated tick-range check.
orca::validate_tick_range(orca.rs:434-444) andraydium::validate_tick_range(raydium.rs:281-291) have the same body.MIN_TICK_INDEX/MAX_TICK_INDEX(orca.rs:23-24) andMIN_TICK/MAX_TICK(raydium.rs:42-43) have the same values. Resolved in #171. - L05-R009 — Duplicated reward-slot parser.
parse_whirlpool_reward_slots(orca.rs:332-361) andparse_pool_reward_slots(raydium.rs:211-234) have the same shape: length and discriminator check, loop over 3 slots, skip default mint, read vault. The Orca one returns the slot index and the Raydium one does not. Resolved in #171. - L05-R010 — Three ways to read a field.
meteora.rs:182-212definesread_i32,read_u64,read_i64, andread_pubkey.orca.rs:371-393andraydium.rs:163-186inlinetry_into().map_err(...)at every read. Meteora usesPubkey::new_from_array, Orca and Raydium usePubkey::try_from. Resolved in #171. - L05-R011 — Duplicated test helpers.
anchor_discriminatorappears inorca.rs:876-880,raydium.rs:625-629, andmeteora.rs:1215-1219.keys<N>()appears inorca.rs:1057-1059andraydium.rs:886-888. Resolved in #171. - L05-R012 — Offsets with no fixture check.
LB_PAIR_ACTIVE_ID_OFFSET(meteora.rs:30) is not in the fixture assert (meteora.rs:2083-2271) although the fixture listsactiveIdat offset 76 (meteora-dlmm-layout.json:127). The private constants atmeteora.rs:83-88(USER_REWARD_INFO_SIZE,USER_REWARD_PENDING_OFFSET,FEE_INFO_SIZE,FEE_PENDING_OFFSET,POSITION_BIN_REWARD_INFO_OFFSET,POSITION_BIN_FEE_INFO_OFFSET) have fixture counterparts underUserRewardInfo,FeeInfo, andPositionBinData(meteora-dlmm-layout.json:350-400) but no assert. Theliquidity_sharesstride is the literal16atmeteora.rs:392. These constants drivehas_liquidityandhas_pending_yield. The close and settlement handlers read those two flags. Resolved in #171. - L05-R013 — Literal reward offsets in Orca and Raydium.
WHIRLPOOL_REWARD_INFOS_OFFSET = 269andWHIRLPOOL_REWARD_INFO_LEN = 128(orca.rs:287-288) andPOOL_REWARD_INFOS_OFFSET = 397,POOL_REWARD_INFO_LEN = 169,POOL_REWARD_MINT_OFFSET = 57,POOL_REWARD_VAULT_OFFSET = 89(raydium.rs:118-121) are literals with the derivation in a comment. The other offsets in the same files are sums of field sizes (orca.rs:274-280,raydium.rs:102-106). No test derives them from a full layout. Resolved in #171. - L05-R014 — Struct order differs from wire order.
OpenPositionWithToken22NftArgsdeclaresliquidityfirst (raydium.rs:319-329).data()writes the four tick fields first (raydium.rs:334-347). The comment atraydium.rs:304-313records the live failure a liquidity-first encoding caused. Resolved in #171. - L05-R015 — A check owned elsewhere. The Meteora claim, remove, and add builders write 4 zero bytes for the hook slices without a condition (
meteora.rs:850,908,965,1031). Their comments say the caller screens hook mints first (meteora.rs:841-845,898-902,957-959). The binding has no check of its own and cannot serve a hook mint. Resolved in #171. - L05-R016 — Double parse.
parse_initialized_reward_info(meteora.rs:253) andparse_initialized_reward_infos(meteora.rs:278) callparse_lb_pairagain on data the caller already parsed. The comment explains an SBF frame-size reason (meteora.rs:250-252). Resolved in #171. - L05-R017 — Redundant match arm.
assert_position_pair_liquidity_mininghasLB_PAIR_FUNCTION_LIMIT_ORDER => falsefollowed by_ => false(meteora.rs:337-338). Resolved in #171. - L05-R018 — Two encodings of one struct.
DlmmLiquidityParameterByStrategyderivesAnchorSerialize(meteora.rs:157-158), andadd_liquidity_by_strategy2_ixserializes its fields by hand (meteora.rs:1023-1030). The doc comment atmeteora.rs:166records that the upstream field name isparameteres. Resolved in #171. - L05-R019 — Split ownership of the Jupiter vault probe.
is_token_account_owned_by(instructions/jupiter/cpi.rs:33-42) checks bytes only. The program-owner filter lives in the handler (instructions/jupiter/swap.rs:34-36). The comment atinstructions/jupiter/cpi.rs:28-32describes the byte rule and not the split. Resolved in #171. - L05-R020 — A 7-parameter boolean.
vault_authority_intact(instructions/jupiter/cpi.rs:52-67) carries#[allow(clippy::too_many_arguments)]. The handler passes the reloaded vault fields one by one (instructions/jupiter/swap.rs:361-369). Resolved in #171. - L05-R021 — Module header describes staging, not the module.
meteora.rs:1-10talks about "Stage 2", "the first owner-lifecycle foundation", and "registry activation". None of those words name an item in the file. Resolved in #171. - L05-R022 — Fixture note is out of date.
tests/fixtures/meteora-dlmm-layout.json:13says a futureprograms/sickle/src/meteora.rsmust assert against it. The file exists and asserts atmeteora.rs:2083-2271. Resolved in #171.
The line citations in the resolved notes refer to the files as they were before #171.
Related
- Dependency graph
- Venue adapters, the layer above.
- Style guide