Sickle Sol

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

PathLinesPurpose
intent.rs3486Signed payload types, digest, ed25519 parser, nonce, session key, authorization, action ledger, and unit tests
instructions/intent/initialize_intent_domain.rs64One-time creation of the deployment domain
instructions/intent/set_intent_session.rs141Owner creates or changes a session key; active sessions must expire in the future
instructions/intent/preauthorize.rs63Shared live-context and signer checks for transaction A
instructions/intent/preauthorize_intent.rs150Transaction A for a CLMM action
instructions/intent/preauthorize_dlmm_intent.rs136Transaction A for a Meteora DLMM action
instructions/intent/cancel_intent_authorization.rs171Owner closes a pending CLMM authorization
instructions/intent/cancel_dlmm_intent_authorization.rs173Owner closes a pending DLMM authorization
instructions/intent/expire_intent_authorization.rs108Anyone closes an expired CLMM authorization
instructions/intent/expire_dlmm_intent_authorization.rs106Anyone closes an expired DLMM authorization
instructions/intent/finalize_intent.rs628Last instruction of a CLMM action transaction. Holds the shared proof.
instructions/intent/finalize_dlmm_intent.rs135Last 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

ItemKindMeaning
ED25519_PROGRAM_IDconstThe native ed25519 program. intent.rs:39
FINALIZE_ACTION_INTENT_INDEXconstIndex of the action account in both finalizers' account lists. intent.rs:51
EXIT_SIDE_NONE, _LOW, _HIGHconstExit side markers written into the action at initiation. intent.rs:54-56
dlmm_intent_position_pdafnDerives the per-Sickle, per-nonce Meteora replacement position. intent.rs:58-72
IntentActionenumThe action type: Rebalance, Compound, or Exit. intent.rs:78
IntentAction::permission_bitfnMaps the action type to the PERMISSION_* bit. intent.rs:87
IntentSignerKindenumOwner or Session. intent.rs:99
IntentAssetBoundstructOne signed asset: mint, vault, max_in, min_out. intent.rs:108
SwapEdgestructOne signed swap direction. intent.rs:123
RebalanceIntentParamsstructThe signed new tick range. intent.rs:138
ExitIntentParamsstructThe signed exit trigger band and both target mints. intent.rs:151
AuthorizationV2structThe CLMM signed payload. intent.rs:168
AuthorizationV2::active_assetsfnChecked slice of the first asset_count assets. intent.rs:233-237
AuthorizationV2::active_edgesfnChecked slice of the first edge_count edges. intent.rs:239-243
AuthorizationV2::verify_withdrawal_floor_argumentsfnRequires the decrease minima to equal the signed pair. intent.rs:247
AuthorizationV2::validate_canonicalfnOrdered canonical-form orchestrator. intent.rs:263-273
canonical validation stagesfnHeader/counts, assets, edges, dust, withdrawal, slot, signer, and action envelopes. intent.rs:275-481
DlmmActionParametersstructThe signed bin range and distribution. intent.rs:526
DlmmAuthorizationV1structThe DLMM payload. Embeds an AuthorizationV2. intent.rs:599
DlmmAuthorizationV1::validate_canonicalfnOuter checks, then the embedded check, then the parameter check. intent.rs:609
serialize_authorizationfnBorsh bytes of the CLMM payload. intent.rs:635
intent_digestfnThe CLMM signed digest. intent.rs:641
serialize_dlmm_authorizationfnBorsh bytes of the DLMM payload. intent.rs:660
dlmm_intent_digestfnThe DLMM signed digest. intent.rs:667
derive_domain_valuefnThe stored domain value from the program ID and a salt. intent.rs:688
IntentDomainaccountThe immutable deployment domain. intent.rs:697
IntentDomain::assert_currentfnRequires the stored version to equal the constant. intent.rs:706
IntentNonceaccountThe per-Sickle nonce stream. intent.rs:719
consume_intent_noncefnInitializes on first use, checks, then increments. intent.rs:732
IntentSessionaccountA session key with scope, expiry, and generation. intent.rs:765
IntentSession::check_coversfnRequires the session to cover one authorization at one slot. intent.rs:788
IntentAuthorizationaccountThe stored CLMM authorization. intent.rs:823
DlmmIntentAuthorizationaccountThe stored DLMM authorization. intent.rs:837
IntentLedgerEntrystructOne ledger row: baseline, credit, debited, swap totals, limits. intent.rs:855
allowed_dustfnFloor of credit * dust_bps / 10_000 in u128. intent.rs:886
ActionIntentaccountThe action ledger for one transaction B. intent.rs:899
ActionIntent::entry_indexfnLedger row by mint. intent.rs:955
ActionIntent::entry_index_by_vaultfnLedger row by vault. intent.rs:959
ActionIntent::set_baselinefnWrites one vault's pre-flow balance. intent.rs:970
ActionIntent::credit_drainfnAdds a measured post-fee credit to one mint. intent.rs:996
ActionIntent::apply_swapfnDebits the input and credits the output along a signed edge. intent.rs:1013
ActionIntent::apply_debitfnDebits a non-swap outflow against credit. intent.rs:1065
ActionIntent::residualfncredit - debited for one entry. intent.rs:1083
ActionIntent::verify_withdrawal_floor_creditfnRequires measured pool credits to meet the signed minima. intent.rs:1092
ActionIntent::verify_completionfnThe finalizer's three rules per entry. intent.rs:1104
validate_complete_setfnRequires every mandatory vault in the signed set and rejects unrelated extras. intent.rs:1134
Ed25519SignatureOffsetsstructNamed seven-field view of the native ed25519 offsets header. intent.rs:1226-1283
parse_ed25519_preauthorizationfnPure parser of the ed25519 instruction data. intent.rs:1296
verify_preauthorization_signaturefnLoads the preceding instruction and calls the parser. intent.rs:1330
IntentInitAccountsstructThe raw accounts a transaction B initiator passes in. intent.rs:1438
load_intent_authorizationfnOwner and discriminator check, then boxed deserialize. intent.rs:1453
load_dlmm_intent_authorizationfnSame for the DLMM account. intent.rs:1465
begin_intent_actionfnOpens the ledger for a CLMM action. intent.rs:1751
begin_dlmm_intent_actionfnOpens the ledger for a DLMM action. intent.rs:1778
snapshot_extra_baselinesfnReads baselines of signed vaults the initiator does not declare. intent.rs:1809
write_action_intentfnWrites discriminator plus Borsh into the ledger account. intent.rs:1845
transaction-B validation helpersfnvalidate_intent_action_identity 1486, validate_intent_action_window 1521, validate_intent_action_session 1536
transaction-B account and ledger buildersfncreate_action_intent_account 1555, build_action_intent 1636, begin_intent_action_with_finalizer 1696
other private helpersfnedges_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 helpersfnActionIntent::snapshot_baselines 972, is_finalize_intent_leg 1339, is_finalize_dlmm_intent_leg 1358

Instruction handlers

ItemKindMeaning
InitializeIntentDomainaccountsUpgrade authority signs. instructions/intent/initialize_intent_domain.rs:22
initialize_intent_domainfnWrites version, value, bump. instructions/intent/initialize_intent_domain.rs:54
IntentSessionArgsstructactive, permissions, pool, expiry_slot. instructions/intent/set_intent_session.rs:14
SetIntentSessionaccountsOwner signs. init_if_needed session PDA. instructions/intent/set_intent_session.rs:31
set_intent_sessionfnValidates signer, scope, and active expiry; writes, increments generation, emits. instructions/intent/set_intent_session.rs:67-122
PreauthorizeIntentaccountsTransaction A accounts. instructions/intent/preauthorize_intent.rs:35
preauthorize_intentfnTransaction A handler. instructions/intent/preauthorize_intent.rs:101
PreauthorizeDlmmIntentaccountsDLMM transaction A accounts. instructions/intent/preauthorize_dlmm_intent.rs:27
preauthorize_dlmm_intentfnDLMM transaction A handler. instructions/intent/preauthorize_dlmm_intent.rs:83
CancelIntentAuthorizationaccountsOwner closes. instructions/intent/cancel_intent_authorization.rs:17
cancel_intent_authorizationfnEmits only. Anchor close does the work. instructions/intent/cancel_intent_authorization.rs:51
CancelDlmmIntentAuthorizationaccountsSame for DLMM. instructions/intent/cancel_dlmm_intent_authorization.rs:14
cancel_dlmm_intent_authorizationfnEmits only. instructions/intent/cancel_dlmm_intent_authorization.rs:48
ExpireIntentAuthorizationaccountsNo signer. instructions/intent/expire_intent_authorization.rs:18
expire_intent_authorizationfnChecks the expiry slot, emits. instructions/intent/expire_intent_authorization.rs:71
ExpireDlmmIntentAuthorizationaccountsSame for DLMM. instructions/intent/expire_dlmm_intent_authorization.rs:15
expire_dlmm_intent_authorizationfnChecks the expiry slot, emits. instructions/intent/expire_dlmm_intent_authorization.rs:68
FinalizeIntentaccounts11 named accounts plus remaining accounts. instructions/intent/finalize_intent.rs:37
IntentFinalizationReceiptstructThe two events the shared proof returns. instructions/intent/finalize_intent.rs:115
finalize_action_intentfnThe shared proof and exit sweep. pub(crate). instructions/intent/finalize_intent.rs:342
finalize_intentfnCalls the shared proof, emits. instructions/intent/finalize_intent.rs:394
FinalizeDlmmIntentaccountsSame order as FinalizeIntent. instructions/intent/finalize_dlmm_intent.rs:20
finalize_dlmm_intentfnCalls 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.

OffsetSizeFieldType
01versionu8. Must equal 2.
11actionu8 enum IntentAction
232sicklePubkey
3432executorPubkey
6632poolPubkey
9832positionPubkey
1304rebalance_params.new_tick_loweri32
1344rebalance_params.new_tick_upperi32
1384exit_params.trigger_tick_lowi32
1424exit_params.trigger_tick_highi32
14632exit_params.token_out_lowPubkey
17832exit_params.token_out_highPubkey
2108withdraw_min_au64
2188withdraw_min_bu64
2262dust_bpsu16
2288valid_from_slotu64
2368expiry_slotu64
2448nonceu64
2521signer_kindu8 enum
25332session_signerPubkey
2858session_generationu64
2931asset_countu8
2941edge_countu8
295240assets3 × IntentAssetBound
535128edges2 × SwapEdge
663end
Nested structSizeFields in order
IntentAssetBound80mint 32, vault 32, max_in u64, min_out u64
SwapEdge64input_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.

OffsetSizeFieldType
01versionu8. Must equal 1.
11protocolu8 enum Protocol. Must be MeteoraDlmm
2663authorizationAuthorizationV2
6654parameters.min_bin_idi32
6694parameters.max_bin_idi32
6734parameters.active_idi32
6774parameters.max_active_bin_slippagei32
6811parameters.strategy_typeu8
68264parameters.strategy_parameters[u8; 64]
746end

Account sizes

AccountSize in bytesSource
IntentDomain42intent.rs:704
IntentNonce49intent.rs:726
IntentSession123intent.rs:783
IntentAuthorization736intent.rs:849, test at 1994
DlmmIntentAuthorization819intent.rs:845, test at 2134
IntentLedgerEntry120intent.rs:880
ActionIntent755intent.rs:930-949

Limits

LimitValueSource
Assets per authorization1 to 3constants.rs:191, intent.rs:284
Swap edges per authorization0 to 2constants.rs:195, intent.rs:288
dust_bps maximum5000 bpsconstants.rs:30, intent.rs:393
dust_bps maximum for Compound100 bpsconstants.rs:36, intent.rs:401
dust_bps for Exit0 bpsintent.rs:406
dust_bps for Rebalance1 bps or moreintent.rs:395-397
DLMM inclusive width, signed1 to 55 binsconstants.rs:113, intent.rs:557
DLMM inclusive width, in transaction Bf(reward slots): 55, 55, 48constants.rs:103, meteora.rs:377-394
DLMM assets and edgesexactly 2 assets, 0 edgesintent.rs:627
DLMM poolany non-default poolintent.rs:618
ed25519 header16 bytesintent.rs:1224
ed25519 message size32 bytesintent.rs:1277-1280
Finalizer action account index7intent.rs:51

How a call flows through it

Digest and domain separation

The digest is a sha256 over six slices. intent.rs:641-657

SliceContent
1INTENT_DIGEST_PREFIX = SICKLE_ACTION_INTENT
2authorization.version as 1 byte
3The program ID
4The IntentDomain PDA address
5The stored 32-byte domain value
6The 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

  1. 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
  2. Anchor checks that instructions_sysvar is the sysvar address. instructions/intent/preauthorize_intent.rs:95
  3. The handler requires the domain version to equal 1. instructions/intent/preauthorize_intent.rs:105, intent.rs:706-712
  4. The handler runs validate_canonical. instructions/intent/preauthorize_intent.rs:106
  5. The shared path requires authorization.sickle to equal the Sickle key. instructions/intent/preauthorize.rs:29-33
  6. consume_intent_nonce writes the Sickle key into a fresh nonce account, requires nonce == next_nonce, then increments with a checked add. instructions/intent/preauthorize.rs:35, intent.rs:732-756
  7. The shared path requires current_slot < expiry_slot. instructions/intent/preauthorize.rs:37-42
  8. For a session-signed payload the shared path requires the session account and runs check_covers. instructions/intent/preauthorize.rs:44-52
  9. The shared path invokes the family digest closure after those checks. instructions/intent/preauthorize.rs:55, instructions/intent/preauthorize_intent.rs:120-127
  10. verify_preauthorization_signature loads 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-1343
  11. Ed25519SignatureOffsets::parse requires 1 signature and zero padding, then decodes the seven named u16 fields. intent.rs:1241-1263
  12. 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
  13. 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
  14. The parser requires the message to equal the digest and returns the public key. intent.rs:1318-1323
  15. The shared path requires the returned key to equal the owner or the session_signer. instructions/intent/preauthorize.rs:56-61
  16. 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.

  1. The runner requires the authorization and action accounts to be present. The error is IntentRequired. venue/keeper_rebalance.rs:126-134
  2. load_intent_authorization requires the account owner to be this program and deserializes with the discriminator check into a Box. intent.rs:1449-1456
  3. begin_intent_action requires sickle, action, pool, position, and executor to match. intent.rs:1501-1526
  4. It requires valid_from_slot <= current_slot < expiry_slot. intent.rs:1526-1536
  5. For a session-signed authorization it reloads the session account and runs check_covers again. intent.rs:1545-1558
  6. require_specific_intent_finalizer scans every later instruction for this program, the finalizer discriminator, and the action key at index 7. intent.rs:1729-1735, intent.rs:1404-1425
  7. It derives the action PDA [b"action_intent", sickle, nonce_le] and creates it. An empty PDA takes create_account. A pre-funded PDA takes transfer of the deficit, allocate, and assign. intent.rs:1564-1642
  8. It builds the ledger rows from the signed assets and returns a boxed ActionIntent with rent_payer set to the transaction B payer. intent.rs:1641-1685
  9. The runner requires the signed tick range to equal the instruction range. It requires dust_bps to be at most the position settings value. It requires the decrease minima to equal the signed pair. venue/keeper_rebalance.rs:152-161
  10. validate_complete_set requires both pool vaults in the signed set. venue/keeper_rebalance.rs:171-178
  11. The runner writes the two pool vault baselines and snapshot_extra_baselines reads any other signed vault from the remaining accounts. venue/keeper_rebalance.rs:179-185, intent.rs:1815-1838
  12. 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
  13. 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
  14. An increase instruction in intent mode calls apply_debit per pool token. venue/open_increase.rs:372-383
  15. finalize_intent runs the shared proof and closes both accounts. instructions/intent/finalize_intent.rs:394-410

The finalizer proof

  1. 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
  2. The proof requires the program to be unpaused and the action to belong to the Sickle. instructions/intent/finalize_intent.rs:350-352
  3. 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
  4. For an exit it requires exit_side != EXIT_SIDE_NONE. instructions/intent/finalize_intent.rs:157-161
  5. It requires exactly asset_count remaining accounts, or three times that for an exit. instructions/intent/finalize_intent.rs:167-190
  6. It decodes each vault as an InterfaceAccount and requires the key, mint, and owner to match the ledger row. instructions/intent/finalize_intent.rs:200-215
  7. 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
  8. It reloads every vault and calls verify_completion. instructions/intent/finalize_intent.rs:287-296,374-376
  9. verify_completion requires balance >= baseline, residual <= allowed_dust, and swapped_out >= min_out when min_out > 0. intent.rs:1104-1124
  10. For an exit it closes the position settings to the owner and emits SettingsUpdated. instructions/intent/finalize_intent.rs:298-316,381-386
  11. Anchor closes the authorization to authorization_rent_receiver and the action to action_rent_receiver. instructions/intent/finalize_intent.rs:70-99

Cancel and expire

InstructionSignerExtra checkRent receiver
cancel_intent_authorizationownernonestored rent_payer. instructions/intent/cancel_intent_authorization.rs:28-32
cancel_dlmm_intent_authorizationownernonestored rent_payer. instructions/intent/cancel_dlmm_intent_authorization.rs:25-29
expire_intent_authorizationnoneslot >= expiry_slot. instructions/intent/expire_intent_authorization.rs:54-69stored rent_payer. instructions/intent/expire_intent_authorization.rs:26-30
expire_dlmm_intent_authorizationnoneslot >= expiry_slot. instructions/intent/expire_dlmm_intent_authorization.rs:51-66stored 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

  1. Only a payload in canonical form is stored. validate_canonical returns NoncanonicalIntent for 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
  2. Every swap output has a positive floor. The check returns SwapOutputFloorRequired. intent.rs:367-370
  3. The dust limit follows the action type. Out of range returns InvalidDustBps. A Compound above 100 bps or an Exit above 0 bps returns IntentDustPolicyExceeded. intent.rs:387-409
  4. A Rebalance or Exit has at least one positive withdrawal minimum, and a Compound has none. The errors are WithdrawalFloorRequired and NoncanonicalIntent. intent.rs:408-423
  5. The session signer is never the executor. The check returns NoncanonicalIntent. intent.rs:434-458
  6. The digest covers the program ID, the domain PDA, and the domain value. A mismatch returns IntentDigestMismatch from the parser. intent.rs:641-657,1318-1321
  7. The ed25519 instruction is self-contained. Any offset that names another instruction returns InvalidEd25519Instruction. intent.rs:1266-1276
  8. The ed25519 instruction immediately precedes the preauthorization. A missing or foreign predecessor returns MissingIntentSignature. intent.rs:1330-1339
  9. The nonce never decreases. A wrong nonce returns IntentNonceMismatch and the increment is checked. intent.rs:742-755
  10. A session change increments the generation, so a stored authorization with the old generation fails check_covers with SessionGenerationMismatch. instructions/intent/set_intent_session.rs:105-108, intent.rs:799-802
  11. The session covers the action, the pool, and the slot. The errors are InvalidIntentSession, SessionInactive, SessionExpired, and SessionScopeExceeded. intent.rs:788-813
  12. Transaction B requires a later finalizer on the same action account. The error is MissingIntentFinalizer. intent.rs:1404-1425
  13. A swap debit never exceeds the signed max_in or the measured credit. The error is IntentBudgetExceeded. intent.rs:1031-1047
  14. A swap direction must be a signed edge. The error is IntentEdgeNotAllowed. intent.rs:1020-1023
  15. Any debit never exceeds credit. The error is IntentBudgetExceeded. intent.rs:1020-1037
  16. Every vault ends at or above its baseline. The error is VaultBelowBaseline. intent.rs:1110
  17. Residual credit is at most the floor-rounded dust allowance. The error is IntentDustExceeded. intent.rs:1111-1115
  18. Every positive signed min_out is met. The error is IntentMinOutNotMet. intent.rs:1112-1117
  19. 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
  20. 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
  21. 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

LocationCovers
intent.rs:2129-2187snapshot_extra_baselines accepts a Token-2022 vault and rejects a foreign-owned vault
intent.rs:2191-2216AuthorizationV2 is 663 bytes, DlmmAuthorizationV1 is 746 bytes and embeds the base at offset 2
intent.rs:2219-2231DLMM canonical form admits any non-default pool: no pair pin
intent.rs:2234-2251DLMM structural width cap equals the widest f entry, 55 bins
intent.rs:2254-2322DLMM canonical form rejects 11 mutations and accepts a zeroed exit distribution
intent.rs:2325-2362DLMM digest changes with every parameter field and differs from the CLMM digest
intent.rs:2365-2425Every account SIZE equals its serialized length plus 8
intent.rs:2428-2446One nonce stream, replay rejected, increment by 1
intent.rs:2450-277424 canonical-form tests plus corrupt-count checks on both active slices and their complete-set caller
intent.rs:2779-2821Digest changes with program, domain key, domain value, and payload
intent.rs:2824-2879Finalizer discriminators are distinct and require the action key at index 7
intent.rs:2910-2975ed25519 parser: both self-reference forms, malformed header, cross-instruction offsets, all three source bounds
intent.rs:2980-3046Ledger arithmetic: credit, swap, debit, both budget caps, overflow, edge reuse
intent.rs:3051-3144allowed_dust floors, verify_completion all three rules, withdrawal floors, pre-existing balance
intent.rs:3149-3215validate_complete_set: mandatory pairs, extras, target, intermediate
intent.rs:3240-3272check_covers matrix
intent.rs:3379-3421Shared vectors: Borsh bytes, digest, and the TS-built ed25519 instruction
intent.rs:3424-3459Shared DLMM vector: Borsh bytes, embedded base, digest
instructions/intent/set_intent_session.rs:59-65,124-140Active expiry is strict; an inactive update remains available for revocation
tests/sickle/intents.ts:542-559The live handler rejects an expired activation and accepts an expired inactive update
instructions/intent/cancel_intent_authorization.rs:61-171CLMM cancellation pins owner signer, stored rent receiver, authorization PDA, and family discriminator
instructions/intent/cancel_dlmm_intent_authorization.rs:63-173DLMM cancellation pins owner signer, stored rent receiver, authorization PDA, and family discriminator
instructions/intent/expire_intent_authorization.rs:54-108CLMM expiry is inclusive and the event preserves the Sickle, nonce, and expiry slot
instructions/intent/expire_dlmm_intent_authorization.rs:51-106DLMM expiry is inclusive and the family event preserves the Sickle, nonce, and expiry slot
tests/sickle/intents.ts:963-1107The live CLMM handler pins payer custody, family identity, expiry, dual events, close, refund, and nonce replay
tests/sickle/intents.ts:1109-1192The live DLMM handler pins payer custody, expiry, dual events, close, and refund
instructions/intent/finalize_intent.rs:499-600Finalizer 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-134The action account sits at index 7 in both finalizers
tests/builder-intent.ts:104-330The 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.

VectorActionSignerAssetsEdgesAsserts
rebalance-owner-minimalrebalanceowner21Borsh bytes, digest for a fixed program and domain
exit-session-maximalexitsession32Borsh bytes, digest for a fixed program and domain
ed25519Instructionweb3.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

GapEvidence
The pre-funded action PDA path has no testintent.rs:1596-1640. No test in intent.rs or tests/*.ts names a pre-funded PDA.
require_specific_intent_finalizer has no Rust testintent.rs:1404-1425. Only the pure is_intent_finalizer_leg is tested.
The side-effecting transaction-B orchestrator has no direct Rust testintent.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 testinstructions/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

  1. L08-N002 — instructions/intent/cancel_intent_authorization.rs:17-49 and instructions/intent/cancel_dlmm_intent_authorization.rs:14-46 are the same struct with a different seed and type. The same holds for instructions/intent/expire_intent_authorization.rs:18-52 and instructions/intent/expire_dlmm_intent_authorization.rs:15-49, and for instructions/intent/finalize_intent.rs:37-113 and instructions/intent/finalize_dlmm_intent.rs:20-85.
  2. L08-N008 — write_action_intent at intent.rs:1845-1855 writes the discriminator and Borsh bytes by hand into a raw account. load_intent_authorization at intent.rs:1453-1460 repeats the owner and discriminator checks that Account::try_from performs. The comments at intent.rs:1427-1437 give the 4 KiB SBF frame as the reason for both.
  3. L08-N012 — The digest hashes authorization.version twice: as its own slice at intent.rs:650 and as byte 0 of the payload slice at intent.rs:654.
  4. L08-N013 — The min_out rule lives in two places. validate_canonical requires it positive for every swap output at intent.rs:367-370, and verify_completion enforces it only when positive at intent.rs:1116-1121. The comment at intent.rs:357-366 explains the split, but the check has two owners.
  5. L08-N014 — The comment at intent.rs:47-50 cites "the ZAP_IN_SICKLE_INDEX pattern". No file in the repository defines that identifier.
  6. L08-N017 — The module doc at intent.rs:1-2 cites TODO.md. Comments at intent.rs:259, :640, :883, :1100, :1127, :1398, :1483, and :1554 cite "spec item" numbers from that file. The code comments depend on an external document.
  7. L08-N018 — DlmmActionParameters::validate_for_action at intent.rs:573-574 requires rebalance_params.new_tick_lower to equal min_bin_id. The field names say tick, and the DLMM values are bin ids.
  8. L08-N019 — consume_intent_nonce at intent.rs:738-741 and set_intent_session at instructions/intent/set_intent_session.rs:93-100 both detect a fresh init_if_needed account by a default Pubkey field. The same sentinel pattern appears twice.
  9. L08-N021 — begin_intent_action_with_finalizer does not reload IntentDomain. The comment at intent.rs:1511-1515 states that the stored authorization proves the domain existed. Transaction B therefore trusts the account's existence rather than a check.
  10. L08-N022 — FinalizeIntent.position_settings at instructions/intent/finalize_intent.rs:105-106 has only mut. Its identity is checked in the body at instructions/intent/finalize_intent.rs:129-156 by three field comparisons, not by seeds.
  11. L08-N023 — FINALIZE_ACTION_INTENT_INDEX = 7 at intent.rs:51 is a hand-counted index into two account structs. Two drift-guard tests at instructions/intent/finalize_intent.rs:603 and instructions/intent/finalize_dlmm_intent.rs:112 protect it.

Resolved

  1. 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_finalizer through begin_intent_action_with_finalizer (intent.rs:1700-1741). The unused wrappers are removed, and the six handler comments now point to the live begin_intent_action entry point. Resolved in this pass.

  2. L08-N004 — ActionIntent::snapshot_baselines, is_finalize_intent_leg, and is_finalize_dlmm_intent_leg had 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.

  3. L08-N001 — preauthorize_intent and preauthorize_dlmm_intent carried 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-63 now 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.

  4. 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 from sickle, and preauthorize_intent requires 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.

  5. 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, and build_action_intent (intent.rs:1488-1684); the orchestrator is intent.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.

  6. L08-N015 — IntentActionExpectation now has its own two-line type comment (intent.rs:1478-1480), while the transaction-B contract and SBF frame rationale are attached to begin_intent_action_with_finalizer (intent.rs:1686-1700). Resolved in this pass.

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

  8. L08-N024 — Every IntentInitAccounts field is now borrowed, including payer and system_program (intent.rs:1433-1441). The seven CLMM/DLMM call sites pass references instead of making the struct own a second pair of AccountInfo clones (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.

  9. 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 is instructions/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.

  10. L08-N005 — The 178-line canonical validator mixed fourteen signed-payload rules in one body. validate_canonical is 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.

  11. L08-N009 — The native ed25519 offsets header is now decoded once into the named seven-field Ed25519SignatureOffsets and validates its three instruction references before the parser reads any payload bytes (intent.rs:1221-1278). parse_ed25519_preauthorization retains 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.

  12. L08-N011 — AuthorizationV2::active_assets and active_edges now use checked slice access and return NoncanonicalIntent when a stored count exceeds its fixed array (intent.rs:233-243). validate_complete_set propagates 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 in validate_complete_set makes that test fail. Resolved in this pass.

  13. L08-N010 — The apparent seven copies are already calls into one owner: snapshot_extra_baselines validates 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 into intent.rs would 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.

  14. L08-N020 — Active session creation and update now require current_slot < expiry_slot, returning SessionExpired before 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.

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.

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