Keeper authorization
This layer stores owner-signed limits for one keeper action and proves them before the action's transaction ends.
Note: Every path:line below is relative to programs/sickle/src/.
Files
| Path | Lines | Purpose |
|---|---|---|
intent.rs | 3486 | Signed payload types, digest, ed25519 parser, nonce, session key, authorization, action ledger, and unit tests |
instructions/intent/initialize_intent_domain.rs | 64 | One-time creation of the deployment domain |
instructions/intent/set_intent_session.rs | 141 | Owner creates or changes a session key; active sessions must expire in the future |
instructions/intent/preauthorize.rs | 63 | Shared live-context and signer checks for transaction A |
instructions/intent/preauthorize_intent.rs | 150 | Transaction A for a CLMM action |
instructions/intent/preauthorize_dlmm_intent.rs | 136 | Transaction A for a Meteora DLMM action |
instructions/intent/cancel_intent_authorization.rs | 171 | Owner closes a pending CLMM authorization |
instructions/intent/cancel_dlmm_intent_authorization.rs | 173 | Owner closes a pending DLMM authorization |
instructions/intent/expire_intent_authorization.rs | 108 | Anyone closes an expired CLMM authorization |
instructions/intent/expire_dlmm_intent_authorization.rs | 106 | Anyone closes an expired DLMM authorization |
instructions/intent/finalize_intent.rs | 628 | Last instruction of a CLMM action transaction. Holds the shared proof. |
instructions/intent/finalize_dlmm_intent.rs | 135 | Last instruction of a DLMM action transaction |
Responsibilities
- Define the fixed-size signed payload for a CLMM action (
AuthorizationV2) and for a DLMM action (DlmmAuthorizationV1). - Compute the digest that the owner or the session key signs.
- Read the ed25519 instruction from the Instructions sysvar and check that it signed that digest.
- Reject a payload that is not in canonical form.
- Run the Sickle, nonce, expiry, session, digest, and signer checks once for both authorization families.
- Keep one nonce stream per Sickle that never decreases.
- Store and check the session key and its generation.
- Store the authorization in transaction A.
- Create the action ledger in transaction B and require a finalizer later in the same transaction.
- Track measured credits and debits per asset during transaction B.
- Prove the baseline, dust, and swap-output rules in the finalizer.
- Sweep an exit's credit to the owner and close both temporary accounts.
- Return rent to the payer of each temporary account.
Public items
intent.rs
| Item | Kind | Meaning |
|---|---|---|
ED25519_PROGRAM_ID | const | The native ed25519 program. intent.rs:39 |
FINALIZE_ACTION_INTENT_INDEX | const | Index of the action account in both finalizers' account lists. intent.rs:51 |
EXIT_SIDE_NONE, _LOW, _HIGH | const | Exit side markers written into the action at initiation. intent.rs:54-56 |
dlmm_intent_position_pda | fn | Derives the per-Sickle, per-nonce Meteora replacement position. intent.rs:58-72 |
IntentAction | enum | The action type: Rebalance, Compound, or Exit. intent.rs:78 |
IntentAction::permission_bit | fn | Maps the action type to the PERMISSION_* bit. intent.rs:87 |
IntentSignerKind | enum | Owner or Session. intent.rs:99 |
IntentAssetBound | struct | One signed asset: mint, vault, max_in, min_out. intent.rs:108 |
SwapEdge | struct | One signed swap direction. intent.rs:123 |
RebalanceIntentParams | struct | The signed new tick range. intent.rs:138 |
ExitIntentParams | struct | The signed exit trigger band and both target mints. intent.rs:151 |
AuthorizationV2 | struct | The CLMM signed payload. intent.rs:168 |
AuthorizationV2::active_assets | fn | Checked slice of the first asset_count assets. intent.rs:233-237 |
AuthorizationV2::active_edges | fn | Checked slice of the first edge_count edges. intent.rs:239-243 |
AuthorizationV2::verify_withdrawal_floor_arguments | fn | Requires the decrease minima to equal the signed pair. intent.rs:247 |
AuthorizationV2::validate_canonical | fn | Ordered canonical-form orchestrator. intent.rs:263-273 |
| canonical validation stages | fn | Header/counts, assets, edges, dust, withdrawal, slot, signer, and action envelopes. intent.rs:275-481 |
DlmmActionParameters | struct | The signed bin range and distribution. intent.rs:526 |
DlmmAuthorizationV1 | struct | The DLMM payload. Embeds an AuthorizationV2. intent.rs:599 |
DlmmAuthorizationV1::validate_canonical | fn | Outer checks, then the embedded check, then the parameter check. intent.rs:609 |
serialize_authorization | fn | Borsh bytes of the CLMM payload. intent.rs:635 |
intent_digest | fn | The CLMM signed digest. intent.rs:641 |
serialize_dlmm_authorization | fn | Borsh bytes of the DLMM payload. intent.rs:660 |
dlmm_intent_digest | fn | The DLMM signed digest. intent.rs:667 |
derive_domain_value | fn | The stored domain value from the program ID and a salt. intent.rs:688 |
IntentDomain | account | The immutable deployment domain. intent.rs:697 |
IntentDomain::assert_current | fn | Requires the stored version to equal the constant. intent.rs:706 |
IntentNonce | account | The per-Sickle nonce stream. intent.rs:719 |
consume_intent_nonce | fn | Initializes on first use, checks, then increments. intent.rs:732 |
IntentSession | account | A session key with scope, expiry, and generation. intent.rs:765 |
IntentSession::check_covers | fn | Requires the session to cover one authorization at one slot. intent.rs:788 |
IntentAuthorization | account | The stored CLMM authorization. intent.rs:823 |
DlmmIntentAuthorization | account | The stored DLMM authorization. intent.rs:837 |
IntentLedgerEntry | struct | One ledger row: baseline, credit, debited, swap totals, limits. intent.rs:855 |
allowed_dust | fn | Floor of credit * dust_bps / 10_000 in u128. intent.rs:886 |
ActionIntent | account | The action ledger for one transaction B. intent.rs:899 |
ActionIntent::entry_index | fn | Ledger row by mint. intent.rs:955 |
ActionIntent::entry_index_by_vault | fn | Ledger row by vault. intent.rs:959 |
ActionIntent::set_baseline | fn | Writes one vault's pre-flow balance. intent.rs:970 |
ActionIntent::credit_drain | fn | Adds a measured post-fee credit to one mint. intent.rs:996 |
ActionIntent::apply_swap | fn | Debits the input and credits the output along a signed edge. intent.rs:1013 |
ActionIntent::apply_debit | fn | Debits a non-swap outflow against credit. intent.rs:1065 |
ActionIntent::residual | fn | credit - debited for one entry. intent.rs:1083 |
ActionIntent::verify_withdrawal_floor_credit | fn | Requires measured pool credits to meet the signed minima. intent.rs:1092 |
ActionIntent::verify_completion | fn | The finalizer's three rules per entry. intent.rs:1104 |
validate_complete_set | fn | Requires every mandatory vault in the signed set and rejects unrelated extras. intent.rs:1134 |
Ed25519SignatureOffsets | struct | Named seven-field view of the native ed25519 offsets header. intent.rs:1226-1283 |
parse_ed25519_preauthorization | fn | Pure parser of the ed25519 instruction data. intent.rs:1296 |
verify_preauthorization_signature | fn | Loads the preceding instruction and calls the parser. intent.rs:1330 |
IntentInitAccounts | struct | The raw accounts a transaction B initiator passes in. intent.rs:1438 |
load_intent_authorization | fn | Owner and discriminator check, then boxed deserialize. intent.rs:1453 |
load_dlmm_intent_authorization | fn | Same for the DLMM account. intent.rs:1465 |
begin_intent_action | fn | Opens the ledger for a CLMM action. intent.rs:1751 |
begin_dlmm_intent_action | fn | Opens the ledger for a DLMM action. intent.rs:1778 |
snapshot_extra_baselines | fn | Reads baselines of signed vaults the initiator does not declare. intent.rs:1809 |
write_action_intent | fn | Writes discriminator plus Borsh into the ledger account. intent.rs:1845 |
| transaction-B validation helpers | fn | validate_intent_action_identity 1486, validate_intent_action_window 1521, validate_intent_action_session 1536 |
| transaction-B account and ledger builders | fn | create_action_intent_account 1555, build_action_intent 1636, begin_intent_action_with_finalizer 1696 |
| other private helpers | fn | edges_are_acyclic 479, validate_for_action 547, is_intent_finalizer_leg 1373, require_specific_intent_finalizer 1395, load_intent_session 1467, IntentActionExpectation 1477 |
| test-only helpers | fn | ActionIntent::snapshot_baselines 972, is_finalize_intent_leg 1339, is_finalize_dlmm_intent_leg 1358 |
Instruction handlers
| Item | Kind | Meaning |
|---|---|---|
InitializeIntentDomain | accounts | Upgrade authority signs. instructions/intent/initialize_intent_domain.rs:22 |
initialize_intent_domain | fn | Writes version, value, bump. instructions/intent/initialize_intent_domain.rs:54 |
IntentSessionArgs | struct | active, permissions, pool, expiry_slot. instructions/intent/set_intent_session.rs:14 |
SetIntentSession | accounts | Owner signs. init_if_needed session PDA. instructions/intent/set_intent_session.rs:31 |
set_intent_session | fn | Validates signer, scope, and active expiry; writes, increments generation, emits. instructions/intent/set_intent_session.rs:67-122 |
PreauthorizeIntent | accounts | Transaction A accounts. instructions/intent/preauthorize_intent.rs:35 |
preauthorize_intent | fn | Transaction A handler. instructions/intent/preauthorize_intent.rs:101 |
PreauthorizeDlmmIntent | accounts | DLMM transaction A accounts. instructions/intent/preauthorize_dlmm_intent.rs:27 |
preauthorize_dlmm_intent | fn | DLMM transaction A handler. instructions/intent/preauthorize_dlmm_intent.rs:83 |
CancelIntentAuthorization | accounts | Owner closes. instructions/intent/cancel_intent_authorization.rs:17 |
cancel_intent_authorization | fn | Emits only. Anchor close does the work. instructions/intent/cancel_intent_authorization.rs:51 |
CancelDlmmIntentAuthorization | accounts | Same for DLMM. instructions/intent/cancel_dlmm_intent_authorization.rs:14 |
cancel_dlmm_intent_authorization | fn | Emits only. instructions/intent/cancel_dlmm_intent_authorization.rs:48 |
ExpireIntentAuthorization | accounts | No signer. instructions/intent/expire_intent_authorization.rs:18 |
expire_intent_authorization | fn | Checks the expiry slot, emits. instructions/intent/expire_intent_authorization.rs:71 |
ExpireDlmmIntentAuthorization | accounts | Same for DLMM. instructions/intent/expire_dlmm_intent_authorization.rs:15 |
expire_dlmm_intent_authorization | fn | Checks the expiry slot, emits. instructions/intent/expire_dlmm_intent_authorization.rs:68 |
FinalizeIntent | accounts | 11 named accounts plus remaining accounts. instructions/intent/finalize_intent.rs:37 |
IntentFinalizationReceipt | struct | The two events the shared proof returns. instructions/intent/finalize_intent.rs:115 |
finalize_action_intent | fn | The shared proof and exit sweep. pub(crate). instructions/intent/finalize_intent.rs:342 |
finalize_intent | fn | Calls the shared proof, emits. instructions/intent/finalize_intent.rs:394 |
FinalizeDlmmIntent | accounts | Same order as FinalizeIntent. instructions/intent/finalize_dlmm_intent.rs:20 |
finalize_dlmm_intent | fn | Calls the shared proof, emits. instructions/intent/finalize_dlmm_intent.rs:87 |
AuthorizationV2 byte layout
The payload is fixed-size Borsh. Every unused field is zero. The size constant
is at intent.rs:211-231, and a test asserts the total at intent.rs:2191.
| Offset | Size | Field | Type |
|---|---|---|---|
| 0 | 1 | version | u8. Must equal 2. |
| 1 | 1 | action | u8 enum IntentAction |
| 2 | 32 | sickle | Pubkey |
| 34 | 32 | executor | Pubkey |
| 66 | 32 | pool | Pubkey |
| 98 | 32 | position | Pubkey |
| 130 | 4 | rebalance_params.new_tick_lower | i32 |
| 134 | 4 | rebalance_params.new_tick_upper | i32 |
| 138 | 4 | exit_params.trigger_tick_low | i32 |
| 142 | 4 | exit_params.trigger_tick_high | i32 |
| 146 | 32 | exit_params.token_out_low | Pubkey |
| 178 | 32 | exit_params.token_out_high | Pubkey |
| 210 | 8 | withdraw_min_a | u64 |
| 218 | 8 | withdraw_min_b | u64 |
| 226 | 2 | dust_bps | u16 |
| 228 | 8 | valid_from_slot | u64 |
| 236 | 8 | expiry_slot | u64 |
| 244 | 8 | nonce | u64 |
| 252 | 1 | signer_kind | u8 enum |
| 253 | 32 | session_signer | Pubkey |
| 285 | 8 | session_generation | u64 |
| 293 | 1 | asset_count | u8 |
| 294 | 1 | edge_count | u8 |
| 295 | 240 | assets | 3 × IntentAssetBound |
| 535 | 128 | edges | 2 × SwapEdge |
| 663 | end |
| Nested struct | Size | Fields in order |
|---|---|---|
IntentAssetBound | 80 | mint 32, vault 32, max_in u64, min_out u64 |
SwapEdge | 64 | input_mint 32, output_mint 32 |
DlmmAuthorizationV1 byte layout
The DLMM payload wraps a complete CLMM payload. The size constant is at
intent.rs:607, and a test asserts the total and the embedded offset at
intent.rs:2199-2216.
| Offset | Size | Field | Type |
|---|---|---|---|
| 0 | 1 | version | u8. Must equal 1. |
| 1 | 1 | protocol | u8 enum Protocol. Must be MeteoraDlmm |
| 2 | 663 | authorization | AuthorizationV2 |
| 665 | 4 | parameters.min_bin_id | i32 |
| 669 | 4 | parameters.max_bin_id | i32 |
| 673 | 4 | parameters.active_id | i32 |
| 677 | 4 | parameters.max_active_bin_slippage | i32 |
| 681 | 1 | parameters.strategy_type | u8 |
| 682 | 64 | parameters.strategy_parameters | [u8; 64] |
| 746 | end |
Account sizes
| Account | Size in bytes | Source |
|---|---|---|
IntentDomain | 42 | intent.rs:704 |
IntentNonce | 49 | intent.rs:726 |
IntentSession | 123 | intent.rs:783 |
IntentAuthorization | 736 | intent.rs:849, test at 1994 |
DlmmIntentAuthorization | 819 | intent.rs:845, test at 2134 |
IntentLedgerEntry | 120 | intent.rs:880 |
ActionIntent | 755 | intent.rs:930-949 |
Limits
| Limit | Value | Source |
|---|---|---|
| Assets per authorization | 1 to 3 | constants.rs:191, intent.rs:284 |
| Swap edges per authorization | 0 to 2 | constants.rs:195, intent.rs:288 |
dust_bps maximum | 5000 bps | constants.rs:30, intent.rs:393 |
dust_bps maximum for Compound | 100 bps | constants.rs:36, intent.rs:401 |
dust_bps for Exit | 0 bps | intent.rs:406 |
dust_bps for Rebalance | 1 bps or more | intent.rs:395-397 |
| DLMM inclusive width, signed | 1 to 55 bins | constants.rs:113, intent.rs:557 |
| DLMM inclusive width, in transaction B | f(reward slots): 55, 55, 48 | constants.rs:103, meteora.rs:377-394 |
| DLMM assets and edges | exactly 2 assets, 0 edges | intent.rs:627 |
| DLMM pool | any non-default pool | intent.rs:618 |
| ed25519 header | 16 bytes | intent.rs:1224 |
| ed25519 message size | 32 bytes | intent.rs:1277-1280 |
| Finalizer action account index | 7 | intent.rs:51 |
How a call flows through it
Digest and domain separation
The digest is a sha256 over six slices. intent.rs:641-657
| Slice | Content |
|---|---|
| 1 | INTENT_DIGEST_PREFIX = SICKLE_ACTION_INTENT |
| 2 | authorization.version as 1 byte |
| 3 | The program ID |
| 4 | The IntentDomain PDA address |
| 5 | The stored 32-byte domain value |
| 6 | The 663-byte Borsh payload |
The DLMM digest uses the same shape with SICKLE_DLMM_ACTION_INTENT and the
746-byte payload. intent.rs:667-683. The domain value is
sha256(SICKLE_INTENT_DOMAIN || program_id || salt). intent.rs:688-690.
The admin picks the salt once. instructions/intent/initialize_intent_domain.rs:56-60.
Transaction A: preauthorize_intent
- Anchor derives the Sickle, domain, nonce, optional session, and new
authorization PDAs. The authorization seed is
[b"intent_authorization", sickle, nonce_le].instructions/intent/preauthorize_intent.rs:43-91 - Anchor checks that
instructions_sysvaris the sysvar address.instructions/intent/preauthorize_intent.rs:95 - The handler requires the domain version to equal 1.
instructions/intent/preauthorize_intent.rs:105,intent.rs:706-712 - The handler runs
validate_canonical.instructions/intent/preauthorize_intent.rs:106 - The shared path requires
authorization.sickleto equal the Sickle key.instructions/intent/preauthorize.rs:29-33 consume_intent_noncewrites the Sickle key into a fresh nonce account, requiresnonce == next_nonce, then increments with a checked add.instructions/intent/preauthorize.rs:35,intent.rs:732-756- The shared path requires
current_slot < expiry_slot.instructions/intent/preauthorize.rs:37-42 - For a session-signed payload the shared path requires the session account
and runs
check_covers.instructions/intent/preauthorize.rs:44-52 - The shared path invokes the family digest closure after those checks.
instructions/intent/preauthorize.rs:55,instructions/intent/preauthorize_intent.rs:120-127 verify_preauthorization_signatureloads the current instruction index and requires it to be at least 1. It loads the previous instruction and requires its program to be the ed25519 program.intent.rs:1334-1343Ed25519SignatureOffsets::parserequires 1 signature and zero padding, then decodes the seven named u16 fields.intent.rs:1241-1263- The typed header requires each of the three instruction-index fields to
equal the ed25519 instruction's own index or
u16::MAX.intent.rs:1262-1277 - The parser requires
message_size == 32, a signature that fits, and reads the public key and message by offset with bounds checks.intent.rs:1277-1280,1304-1316 - The parser requires the message to equal the digest and returns the public
key.
intent.rs:1318-1323 - The shared path requires the returned key to equal the owner or the
session_signer.instructions/intent/preauthorize.rs:56-61 - The handler stores the digest, the payload, the payer, and the bump, then
emits
IntentAuthorized.instructions/intent/preauthorize_intent.rs:130-150
The DLMM handler validates DlmmAuthorizationV1, then calls the same shared
path with dlmm_intent_digest; only its account seed, stored type, and
DlmmIntentAuthorized event remain family-specific.
instructions/intent/preauthorize_dlmm_intent.rs:83-136
A DLMM rebalance also derives its replacement PositionV2 as
[b"dlmm_intent_position", sickle, nonce_le]. The initiator rejects any
other new_position; the open leg rechecks the same address and signs the
Meteora initialization CPI with that PDA. The transaction therefore needs no
off-chain destination keypair. instructions/meteora/rebalance_for.rs:239-253,
instructions/meteora/intent_open_position.rs:93-119,187-203
Transaction B: a keeper rebalance
The representative initiator is the shared keeper rebalance runner.
- The runner requires the authorization and action accounts to be present.
The error is
IntentRequired.venue/keeper_rebalance.rs:126-134 load_intent_authorizationrequires the account owner to be this program and deserializes with the discriminator check into aBox.intent.rs:1449-1456begin_intent_actionrequiressickle,action,pool,position, andexecutorto match.intent.rs:1501-1526- It requires
valid_from_slot <= current_slot < expiry_slot.intent.rs:1526-1536 - For a session-signed authorization it reloads the session account and runs
check_coversagain.intent.rs:1545-1558 require_specific_intent_finalizerscans every later instruction for this program, the finalizer discriminator, and the action key at index 7.intent.rs:1729-1735,intent.rs:1404-1425- It derives the action PDA
[b"action_intent", sickle, nonce_le]and creates it. An empty PDA takescreate_account. A pre-funded PDA takestransferof the deficit,allocate, andassign.intent.rs:1564-1642 - It builds the ledger rows from the signed assets and returns a boxed
ActionIntentwithrent_payerset to the transaction B payer.intent.rs:1641-1685 - The runner requires the signed tick range to equal the instruction range.
It requires
dust_bpsto be at most the position settings value. It requires the decrease minima to equal the signed pair.venue/keeper_rebalance.rs:152-161 validate_complete_setrequires both pool vaults in the signed set.venue/keeper_rebalance.rs:171-178- The runner writes the two pool vault baselines and
snapshot_extra_baselinesreads any other signed vault from the remaining accounts.venue/keeper_rebalance.rs:179-185,intent.rs:1815-1838 - After the venue CPIs the runner requires the measured pool credits to meet
the signed minima, credits each side, and writes the ledger with
write_action_intent.venue/keeper.rs:70-82,venue/keeper_rebalance.rs:357-364 - A swap instruction in intent mode requires the executor signer, a signed
edge, and the signed vaults, then calls
apply_swap.instructions/jupiter/swap.rs:72-98,496-497 - An increase instruction in intent mode calls
apply_debitper pool token.venue/open_increase.rs:372-383 finalize_intentruns the shared proof and closes both accounts.instructions/intent/finalize_intent.rs:394-410
The finalizer proof
- Anchor requires the executor signer to equal
action_intent.executor, the authorization nonce to equal the action nonce, and both rent receivers to equal the stored payers.instructions/intent/finalize_intent.rs:40-99 - The proof requires the program to be unpaused and the action to belong to
the Sickle.
instructions/intent/finalize_intent.rs:350-352 - It requires the position settings account to name the Sickle, the expected
position, and the pool. The expected position is the new position for a
rebalance.
instructions/intent/finalize_intent.rs:129-156 - For an exit it requires
exit_side != EXIT_SIDE_NONE.instructions/intent/finalize_intent.rs:157-161 - It requires exactly
asset_countremaining accounts, or three times that for an exit.instructions/intent/finalize_intent.rs:167-190 - It decodes each vault as an
InterfaceAccountand requires the key, mint, and owner to match the ledger row.instructions/intent/finalize_intent.rs:200-215 - For an exit it derives each owner ATA with the vault's token program and
requires the remaining account to equal it. It transfers the residual of
every swept entry with
transfer_checked. It then debits the ledger.instructions/intent/finalize_intent.rs:224-284 - It reloads every vault and calls
verify_completion.instructions/intent/finalize_intent.rs:287-296,374-376 verify_completionrequiresbalance >= baseline,residual <= allowed_dust, andswapped_out >= min_outwhenmin_out > 0.intent.rs:1104-1124- For an exit it closes the position settings to the owner and emits
SettingsUpdated.instructions/intent/finalize_intent.rs:298-316,381-386 - Anchor closes the authorization to
authorization_rent_receiverand the action toaction_rent_receiver.instructions/intent/finalize_intent.rs:70-99
Cancel and expire
| Instruction | Signer | Extra check | Rent receiver |
|---|---|---|---|
cancel_intent_authorization | owner | none | stored rent_payer. instructions/intent/cancel_intent_authorization.rs:28-32 |
cancel_dlmm_intent_authorization | owner | none | stored rent_payer. instructions/intent/cancel_dlmm_intent_authorization.rs:25-29 |
expire_intent_authorization | none | slot >= expiry_slot. instructions/intent/expire_intent_authorization.rs:54-69 | stored rent_payer. instructions/intent/expire_intent_authorization.rs:26-30 |
expire_dlmm_intent_authorization | none | slot >= expiry_slot. instructions/intent/expire_dlmm_intent_authorization.rs:51-66 | stored rent_payer. instructions/intent/expire_dlmm_intent_authorization.rs:23-27 |
None of the four touches the nonce account. The consumed nonce stays consumed.
Sequence diagram
sequenceDiagram
participant O as Owner or session key
participant K as Keeper
participant E as ed25519 program
participant P as Sickle program
participant V as Venue program
Note over O,P: Transaction A
O->>K: signature over intent_digest(payload)
K->>E: ed25519 verify instruction
K->>P: preauthorize_intent(payload)
P->>P: validate_canonical, consume nonce, check_covers
P->>P: read previous instruction from sysvar, compare digest
P->>P: store IntentAuthorization
Note over K,V: Transaction B
K->>P: rebalance_for(...)
P->>P: load authorization, begin_intent_action
P->>P: scan later instructions for finalize_intent
P->>P: create ActionIntent, snapshot baselines
P->>V: decrease, collect, close, open
P->>P: credit_drain, write_action_intent
K->>P: jupiter_swap (intent mode)
P->>P: apply_swap
K->>P: orca_increase_liquidity (intent mode)
P->>V: increase liquidity
P->>P: apply_debit
K->>P: finalize_intent
P->>P: verify_completion, exit sweep if Exit
P->>P: close IntentAuthorization and ActionIntent
Invariants the layer owns
- Only a payload in canonical form is stored.
validate_canonicalreturnsNoncanonicalIntentfor every shape fault. The faults are: wrong version, out-of-range count, unsorted or duplicate asset or edge, default key, non-zero padding, self-edge, edge endpoint outside the asset set, cycle, inverted slot window, signer-kind field mismatch, non-zero foreign action block.intent.rs:263-481 - Every swap output has a positive floor. The check returns
SwapOutputFloorRequired.intent.rs:367-370 - The dust limit follows the action type. Out of range returns
InvalidDustBps. A Compound above 100 bps or an Exit above 0 bps returnsIntentDustPolicyExceeded.intent.rs:387-409 - A Rebalance or Exit has at least one positive withdrawal minimum, and a
Compound has none. The errors are
WithdrawalFloorRequiredandNoncanonicalIntent.intent.rs:408-423 - The session signer is never the executor. The check returns
NoncanonicalIntent.intent.rs:434-458 - The digest covers the program ID, the domain PDA, and the domain value. A
mismatch returns
IntentDigestMismatchfrom the parser.intent.rs:641-657,1318-1321 - The ed25519 instruction is self-contained. Any offset that names another
instruction returns
InvalidEd25519Instruction.intent.rs:1266-1276 - The ed25519 instruction immediately precedes the preauthorization. A
missing or foreign predecessor returns
MissingIntentSignature.intent.rs:1330-1339 - The nonce never decreases. A wrong nonce returns
IntentNonceMismatchand the increment is checked.intent.rs:742-755 - A session change increments the generation, so a stored authorization with
the old generation fails
check_coverswithSessionGenerationMismatch.instructions/intent/set_intent_session.rs:105-108,intent.rs:799-802 - The session covers the action, the pool, and the slot. The errors are
InvalidIntentSession,SessionInactive,SessionExpired, andSessionScopeExceeded.intent.rs:788-813 - Transaction B requires a later finalizer on the same action account. The
error is
MissingIntentFinalizer.intent.rs:1404-1425 - A swap debit never exceeds the signed
max_inor the measured credit. The error isIntentBudgetExceeded.intent.rs:1031-1047 - A swap direction must be a signed edge. The error is
IntentEdgeNotAllowed.intent.rs:1020-1023 - Any debit never exceeds credit. The error is
IntentBudgetExceeded.intent.rs:1020-1037 - Every vault ends at or above its baseline. The error is
VaultBelowBaseline.intent.rs:1110 - Residual credit is at most the floor-rounded dust allowance. The error is
IntentDustExceeded.intent.rs:1111-1115 - Every positive signed
min_outis met. The error isIntentMinOutNotMet.intent.rs:1112-1117 - Every mandatory vault is in the signed set with the signed vault key. An
extra asset must be reachable from a mandatory credit and must reach a
sink. The error is
IntentLedgerMismatch.intent.rs:1134-1216 - Rent returns only to the stored payer of each account. The error is
InvalidRentReceiver.instructions/intent/finalize_intent.rs:55-63,instructions/intent/cancel_intent_authorization.rs:28-32,instructions/intent/expire_intent_authorization.rs:26-30 - An exit sweeps only to the owner's ATA for each mint, derived with the
vault's token program. The error is
IntentLedgerMismatch.instructions/intent/finalize_intent.rs:234-282
Tests
| Location | Covers |
|---|---|
intent.rs:2129-2187 | snapshot_extra_baselines accepts a Token-2022 vault and rejects a foreign-owned vault |
intent.rs:2191-2216 | AuthorizationV2 is 663 bytes, DlmmAuthorizationV1 is 746 bytes and embeds the base at offset 2 |
intent.rs:2219-2231 | DLMM canonical form admits any non-default pool: no pair pin |
intent.rs:2234-2251 | DLMM structural width cap equals the widest f entry, 55 bins |
intent.rs:2254-2322 | DLMM canonical form rejects 11 mutations and accepts a zeroed exit distribution |
intent.rs:2325-2362 | DLMM digest changes with every parameter field and differs from the CLMM digest |
intent.rs:2365-2425 | Every account SIZE equals its serialized length plus 8 |
intent.rs:2428-2446 | One nonce stream, replay rejected, increment by 1 |
intent.rs:2450-2774 | 24 canonical-form tests plus corrupt-count checks on both active slices and their complete-set caller |
intent.rs:2779-2821 | Digest changes with program, domain key, domain value, and payload |
intent.rs:2824-2879 | Finalizer discriminators are distinct and require the action key at index 7 |
intent.rs:2910-2975 | ed25519 parser: both self-reference forms, malformed header, cross-instruction offsets, all three source bounds |
intent.rs:2980-3046 | Ledger arithmetic: credit, swap, debit, both budget caps, overflow, edge reuse |
intent.rs:3051-3144 | allowed_dust floors, verify_completion all three rules, withdrawal floors, pre-existing balance |
intent.rs:3149-3215 | validate_complete_set: mandatory pairs, extras, target, intermediate |
intent.rs:3240-3272 | check_covers matrix |
intent.rs:3379-3421 | Shared vectors: Borsh bytes, digest, and the TS-built ed25519 instruction |
intent.rs:3424-3459 | Shared DLMM vector: Borsh bytes, embedded base, digest |
instructions/intent/set_intent_session.rs:59-65,124-140 | Active expiry is strict; an inactive update remains available for revocation |
tests/sickle/intents.ts:542-559 | The live handler rejects an expired activation and accepts an expired inactive update |
instructions/intent/cancel_intent_authorization.rs:61-171 | CLMM cancellation pins owner signer, stored rent receiver, authorization PDA, and family discriminator |
instructions/intent/cancel_dlmm_intent_authorization.rs:63-173 | DLMM cancellation pins owner signer, stored rent receiver, authorization PDA, and family discriminator |
instructions/intent/expire_intent_authorization.rs:54-108 | CLMM expiry is inclusive and the event preserves the Sickle, nonce, and expiry slot |
instructions/intent/expire_dlmm_intent_authorization.rs:51-106 | DLMM expiry is inclusive and the family event preserves the Sickle, nonce, and expiry slot |
tests/sickle/intents.ts:963-1107 | The live CLMM handler pins payer custody, family identity, expiry, dual events, close, refund, and nonce replay |
tests/sickle/intents.ts:1109-1192 | The live DLMM handler pins payer custody, expiry, dual events, close, and refund |
instructions/intent/finalize_intent.rs:499-600 | Finalizer identity, account shape, exit sweep selection, and exact credit/residual event mapping |
instructions/intent/finalize_intent.rs:603-627, instructions/intent/finalize_dlmm_intent.rs:112-134 | The action account sits at index 7 in both finalizers |
tests/builder-intent.ts:104-330 | The TS codec reproduces the same vectors and ed25519 bytes |
What the shared vectors assert
tests/fixtures/intent-vectors.json holds 2 vectors and 1 ed25519
instruction. Both cargo test and tests/builder-intent.ts assert them.
| Vector | Action | Signer | Assets | Edges | Asserts |
|---|---|---|---|---|---|
rebalance-owner-minimal | rebalance | owner | 2 | 1 | Borsh bytes, digest for a fixed program and domain |
exit-session-maximal | exit | session | 3 | 2 | Borsh bytes, digest for a fixed program and domain |
ed25519Instruction | web3.js wire form with 0xffff offsets over vector 0 |
The vectors assert the layout, the digest, and the parser's acceptance of the
web3.js form. They do not assert a Compound payload, an owner-kind exit, the
explicit own-index offset form, or any rejection path. Rust unit tests cover
those. The DLMM vector in tests/fixtures/meteora-intent-vectors.json asserts
one rebalance payload only.
Gaps
| Gap | Evidence |
|---|---|
| The pre-funded action PDA path has no test | intent.rs:1596-1640. No test in intent.rs or tests/*.ts names a pre-funded PDA. |
require_specific_intent_finalizer has no Rust test | intent.rs:1404-1425. Only the pure is_intent_finalizer_leg is tested. |
| The side-effecting transaction-B orchestrator has no direct Rust test | intent.rs:1705-1746 still needs Clock, Rent, session-account deserialization, sysvar scanning, and CPI. Its pure identity, window, and ledger-builder slices are covered at intent.rs:2001-2108. |
| The side-effecting finalizer orchestrator has no direct Rust test | instructions/intent/finalize_intent.rs:342-392 still needs typed vault reloads, token CPI, settings closure, and Anchor account closing. Its pure identity, account-shape, sweep-selection, and event-builder slices are covered at instructions/intent/finalize_intent.rs:499-600. |
Simplification and audit notes
- L08-N002 —
instructions/intent/cancel_intent_authorization.rs:17-49andinstructions/intent/cancel_dlmm_intent_authorization.rs:14-46are the same struct with a different seed and type. The same holds forinstructions/intent/expire_intent_authorization.rs:18-52andinstructions/intent/expire_dlmm_intent_authorization.rs:15-49, and forinstructions/intent/finalize_intent.rs:37-113andinstructions/intent/finalize_dlmm_intent.rs:20-85. - L08-N008 —
write_action_intentatintent.rs:1845-1855writes the discriminator and Borsh bytes by hand into a raw account.load_intent_authorizationatintent.rs:1453-1460repeats the owner and discriminator checks thatAccount::try_fromperforms. The comments atintent.rs:1427-1437give the 4 KiB SBF frame as the reason for both. - L08-N012 — The digest hashes
authorization.versiontwice: as its own slice atintent.rs:650and as byte 0 of the payload slice atintent.rs:654. - L08-N013 — The
min_outrule lives in two places.validate_canonicalrequires it positive for every swap output atintent.rs:367-370, andverify_completionenforces it only when positive atintent.rs:1116-1121. The comment atintent.rs:357-366explains the split, but the check has two owners. - L08-N014 — The comment at
intent.rs:47-50cites "theZAP_IN_SICKLE_INDEXpattern". No file in the repository defines that identifier. - L08-N017 — The module doc at
intent.rs:1-2citesTODO.md. Comments atintent.rs:259,:640,:883,:1100,:1127,:1398,:1483, and:1554cite "spec item" numbers from that file. The code comments depend on an external document. - L08-N018 —
DlmmActionParameters::validate_for_actionatintent.rs:573-574requiresrebalance_params.new_tick_lowerto equalmin_bin_id. The field names say tick, and the DLMM values are bin ids. - L08-N019 —
consume_intent_nonceatintent.rs:738-741andset_intent_sessionatinstructions/intent/set_intent_session.rs:93-100both detect a freshinit_if_neededaccount by a defaultPubkeyfield. The same sentinel pattern appears twice. - L08-N021 —
begin_intent_action_with_finalizerdoes not reloadIntentDomain. The comment atintent.rs:1511-1515states that the stored authorization proves the domain existed. Transaction B therefore trusts the account's existence rather than a check. - L08-N022 —
FinalizeIntent.position_settingsatinstructions/intent/finalize_intent.rs:105-106has onlymut. Its identity is checked in the body atinstructions/intent/finalize_intent.rs:129-156by three field comparisons, not by seeds. - L08-N023 —
FINALIZE_ACTION_INTENT_INDEX = 7atintent.rs:51is a hand-counted index into two account structs. Two drift-guard tests atinstructions/intent/finalize_intent.rs:603andinstructions/intent/finalize_dlmm_intent.rs:112protect it.
Resolved
L08-N003 — The two exported finalizer wrappers had no runtime caller; the live CLMM and DLMM initializers already pass their family discriminator directly to
require_specific_intent_finalizerthroughbegin_intent_action_with_finalizer(intent.rs:1700-1741). The unused wrappers are removed, and the six handler comments now point to the livebegin_intent_actionentry point. Resolved in this pass.L08-N004 —
ActionIntent::snapshot_baselines,is_finalize_intent_leg, andis_finalize_dlmm_intent_leghad no production caller. They are now private#[cfg(test)]helpers, so a non-test build cannot depend on them and they do not enlarge the program API or artifact.intent.rs:973-988,intent.rs:1340-1375. Resolved in this pass.L08-N001 —
preauthorize_intentandpreauthorize_dlmm_intentcarried separate copies of the Sickle bind, nonce consumption, expiry and session checks, family digest invocation, ed25519 verification, and expected-signer comparison.instructions/intent/preauthorize.rs:16-63now owns that exact error-ordered sequence. Each handler first validates its own canonical payload, passes its family digest as a closure, then stores its distinct authorization account and emits its distinct event (instructions/intent/preauthorize_intent.rs:101-150,instructions/intent/preauthorize_dlmm_intent.rs:83-136). The account structs stay separate because their payload types, seeds, and stored account types are different. Resolved in this pass.L08-R001 — The expire structs carried
constraint = authorization.sickle == sickle.key()and the cancel structs did not, and the note asked who owns that check. Neither does: it cannot fire. The seeds derive the account fromsickle, andpreauthorize_intentrequires the same equality before it stores the authorization, so the stored field and the deriving key always agree.Mirroring it onto the cancel structs was the obvious move and is the wrong one, because a check that can never fail reads as protection while giving none. Both struct pairs now carry a comment saying why the constraint is unreachable and why the asymmetry is deliberate, which is the form that survives someone deleting it as dead weight. Resolved in #184.
L08-N006 — The 184-line transaction-B initializer mixed signed identity and slot validation, session reload, Instructions-sysvar scanning, two-path PDA creation, and ledger construction. It now preserves that order while delegating to
validate_intent_action_identity,validate_intent_action_window,validate_intent_action_session,create_action_intent_account, andbuild_action_intent(intent.rs:1488-1684); the orchestrator isintent.rs:1700-1741. Focused tests pin every identity field, the inclusive/exclusive slot bounds, and the signed ledger mapping (intent.rs:1994-2101), and deliberate mutations of each seam make its named test fail. The Clock, Rent, sysvar, session-account, and CPI orchestration remains covered by the funded runtime suites rather than claimed as unit coverage. Resolved in this pass.L08-N015 —
IntentActionExpectationnow has its own two-line type comment (intent.rs:1478-1480), while the transaction-B contract and SBF frame rationale are attached tobegin_intent_action_with_finalizer(intent.rs:1686-1700). Resolved in this pass.L08-N016 — The two authorization comments now name the exact 736-byte size (
intent.rs:1425,intent.rs:1444), and the initializer comment names the exact 755-byte ledger size (intent.rs:1695). Resolved in this pass.L08-N024 — Every
IntentInitAccountsfield is now borrowed, includingpayerandsystem_program(intent.rs:1433-1441). The seven CLMM/DLMM call sites pass references instead of making the struct own a second pair ofAccountInfoclones (venue/keeper_rebalance.rs:138-144,venue/keeper_exit.rs:143-149,venue/compound.rs:126-131,234-239,331-336,instructions/meteora/rebalance_for.rs:301-309,instructions/meteora/exit_for.rs:208-216). Resolved in this pass.L08-N007 — The 217-line finalizer mixed identity and account-shape checks, typed vault loading, exit sweep CPI, post-sweep reload and proof, settings closure, and event construction. It now preserves that order while delegating to focused helpers (
instructions/intent/finalize_intent.rs:121-334); the shared orchestrator isinstructions/intent/finalize_intent.rs:342-392. Focused tests pin the action-specific settings identity, remaining-account count, in-kind versus target exit sweep selection, and exact credit/residual event mapping (instructions/intent/finalize_intent.rs:499-600), and four deliberate mutations make their named tests fail. Typed vault reloads, token CPI, settings closure, and Anchor account closing remain covered by the funded runtime suites rather than claimed as unit coverage. Resolved in this pass.L08-N005 — The 178-line canonical validator mixed fourteen signed-payload rules in one body.
validate_canonicalis now an ordered ten-line orchestrator (intent.rs:263-273) over header/counts, assets, edges and output floors, dust, withdrawal floors, slot window, signer separation, and action-envelope stages (intent.rs:275-481). Its order and error variants are unchanged. The 23 pre-existing canonicalization tests plus a focused cross-stage error-order test cover the complete sequence (intent.rs:2411-2461,2490-2735); eight deliberate stage-bypass mutations each make the corresponding pre-existing test fail. Resolved in this pass.L08-N009 — The native ed25519 offsets header is now decoded once into the named seven-field
Ed25519SignatureOffsetsand validates its three instruction references before the parser reads any payload bytes (intent.rs:1221-1278).parse_ed25519_preauthorizationretains the same signature, public-key, message, and digest bounds in the same order (intent.rs:1291-1319). Its five pre-existing parser tests remain the behavior anchor, with the bounds case expanded to cover all three byte sources (intent.rs:2871-2936). Deliberately swapping the public-key field or bypassing self-reference validation makes the pre-existing tests fail; removing the signature bound makes the expanded bounds test fail. Resolved in this pass.L08-N011 —
AuthorizationV2::active_assetsandactive_edgesnow use checked slice access and returnNoncanonicalIntentwhen a stored count exceeds its fixed array (intent.rs:233-243).validate_complete_setpropagates those errors before it indexes its local reachability array (intent.rs:1129-1137). A focused test covers both accessors and the production caller for corrupt asset and edge counts (intent.rs:2463-2488). Deliberately accepting an oversized asset slice or bypassing the checked accessor invalidate_complete_setmakes that test fail. Resolved in this pass.L08-N010 — The apparent seven copies are already calls into one owner:
snapshot_extra_baselinesvalidates every undeclared signed vault and rejects missing or unrelated tail accounts (intent.rs:1804-1833). The call sites supply venue-specific declared vaults and remaining-account grammars (venue/keeper_rebalance.rs:182-187,venue/keeper_exit.rs:190-195,venue/compound.rs:166-171,261-266,371-376,instructions/meteora/rebalance_for.rs:344-349,instructions/meteora/exit_for.rs:254-259). The compound and Meteora callers also decode venue-specific reward accounts before the shared call; moving that work intointent.rswould invert the dependency or require a callback and would change error ordering. The call repetition is the API boundary, not duplicated logic, so no code change is warranted. Resolved in this pass.L08-N020 — Active session creation and update now require
current_slot < expiry_slot, returningSessionExpiredbefore storage; inactive updates deliberately skip the Clock read so revocation remains available after expiry (instructions/intent/set_intent_session.rs:59-65,87-90). A focused test pins the strict boundary, a past expiry, and the inactive carve-out (instructions/intent/set_intent_session.rs:124-140); the validator suite pins the handler wiring and the stored inactive state (tests/sickle/intents.ts:542-559). Widening the bound to<=, removing the inactive carve-out, or bypassing the validator in the handler makes a named test fail. Resolved in this pass.
Related
Design note
The two-transaction flow has no EVM counterpart. The reference lets the keeper
choose minAmountOut at call time. The CLAUDE.md entry "Phase-2
ActionIntent" records why Solana requires an owner-signed limit set first,
and why the session key may not equal the executor.