State and settings
This layer defines every program account, the authorization methods on the Sickle, the position settings rules, and the per-venue index limits those rules use.
Files
| Path | Lines | Purpose |
|---|---|---|
programs/sickle/src/state.rs | 3253 | The four program accounts, the venue enum, the fee enum, the settings structs, and their validation. |
programs/sickle/src/settings.rs | 510 | Reads and closes the position settings account that a close or rebalance instruction receives unchecked. |
programs/sickle/src/venue/family.rs | 314 | Classifies each venue as CLMM, DLMM, or non-position and resolves its index limits. |
programs/sickle/src/intent.rs | n/a | Not in this layer. Holds the authorization accounts. See section "Public items", last table. |
The production code is lines 1 to 1153 of state.rs. Its two unit-test modules
are lines 1155 to 3022 and 3023 to 3253.
Responsibilities
- Define the byte layout of the global config, the Sickle, the position settings, and the global keeper record.
- Decide who is the owner, who is the manager, and who is an authorized keeper for one Sickle.
- Decide in what capacity a caller may add liquidity on a keeper path.
- Store the protocol fee table and compute one fee amount.
- Store the allowlist and check a CPI target against it.
- Store the pause flag and check it.
- Validate position settings when the owner writes them and when a rebalance copies them.
- Decide whether a keeper harvest, compound, rebalance, or exit is allowed for one position.
- Compute the exact tick range a keeper rebalance must reopen.
- Refuse a keeper rebalance that makes no progress.
- Read, check, and close a position settings account that arrives as an unchecked account.
- Resolve the tick or bin limits for one venue.
Public items
state.rs
| Item | Kind | Meaning |
|---|---|---|
FeeAction | enum | The six fee types. The discriminant indexes GlobalConfig.fee_bps. |
Protocol | enum | The venue. The discriminant is serialized state and instruction data. |
RewardBehavior | enum | What automation does with rewards: None, Harvest, or Compound. |
RewardConfig | struct | The reward behavior and an optional harvest output mint. |
RewardConfig::SIZE | const | Serialized size of RewardConfig. |
RewardConfig::validate | fn | Rejects an output mint unless the behavior is Harvest. |
RebalanceConfig | struct | Width, buffers, caps, cutoffs, delay, and a nested reward config for auto-rebalance. |
RebalanceConfig::SIZE | const | Serialized size of RebalanceConfig. |
RebalanceConfig::validate_tick_width | fn | Checks that a tick range has the configured width in tick spaces. |
RebalanceConfig::expected_rebalance_range | fn | Computes the one tick range a keeper rebalance must reopen. |
RebalanceConfig::validate_bin_width | fn | Checks that an inclusive bin range has the configured width. |
RebalanceConfig::expected_rebalance_bin_range | fn | Computes the one bin range a keeper rebalance must reopen. |
ExitConfig | struct | The exit trigger band, per-side output mints, and caps for auto-exit. |
ExitConfig::SIZE | const | Serialized size of ExitConfig. |
PositionSettingsArgs | struct | The caller-supplied part of the position settings, passed to set_position_settings. |
From<&PositionSettings> for PositionSettingsArgs | impl | Exhaustively snapshots every caller-owned configuration field. |
PositionSettings | struct | The position settings account. |
PositionSettings::SIZE | const | Account size with the discriminator. |
PositionSettings::apply_args | fn | Exhaustively replaces every caller-owned configuration field. |
PositionSettings::transfer_from | fn | Carries identity, configuration, history, timestamp, and bump to a replacement. |
PositionSettings::validate | fn | Validates with the venue's static limits. Fails for Meteora and Jupiter. |
PositionSettings::validate_with_geometry | fn | Validates against a supplied geometry. |
PositionSettings::validate_automation_identity | fn | Checks that the stored Sickle, venue, pool, and position equal the live ones. |
PositionSettings::validate_harvest_for | fn | Requires reward automation in Harvest mode. |
PositionSettings::validate_compound_for | fn | Requires reward automation in Compound mode. |
PositionSettings::allows_automator_increase | fn | Whether a keeper with a permission bitmap may add liquidity. |
PositionSettings::validate_rebalance_for | fn | The keeper rebalance check for a CLMM tick range. |
PositionSettings::validate_dlmm_rebalance_for | fn | The keeper rebalance check for an inclusive bin range. |
PositionSettings::validate_rebalance_progress | fn | Rejects a CLMM rebalance that reopens the same range or an in-range two-cycle. |
PositionSettings::validate_dlmm_rebalance_progress | fn | The same rule for an inclusive bin range. |
PositionSettings::dlmm_rebalance_history | fn | pub(crate). Encodes the closed bin range without colliding with the sentinel. |
PositionSettings::validate_exit_for | fn | The keeper exit check. |
GlobalConfig | struct | The global config account. |
GlobalConfig::LEGACY_SIZE | const | Account size before the Meteora slot. |
GlobalConfig::METEORA_DLMM_PROGRAM_OFFSET | const | Byte offset of the Meteora slot. Equals LEGACY_SIZE. |
GlobalConfig::SIZE | const | Current account size. |
GlobalConfig::is_guardian_or_admin | fn | Whether a key may pause. |
GlobalConfig::assert_not_paused | fn | Returns Paused while paused. |
GlobalConfig::assert_not_paused_unless_owner_exit | fn | Returns Paused while paused unless the caller is the owner on an exit path. |
GlobalConfig::fee_amount | fn | The fee amount for one fee action and one measured amount. |
GlobalConfig::protocol_program | fn | The allowlisted program for one venue. |
GlobalConfig::set_protocol_program | fn | Writes one allowlist slot. |
GlobalConfig::assert_protocol_program | fn | Checks a CPI target against its slot. |
SickleAccount | struct | The Sickle account. |
SickleAccount::SIZE | const | Account size with the discriminator. |
SickleAccount::is_owner | fn | Whether a key is the owner. |
SickleAccount::is_authorized_automator | fn | The complete keeper authorization decision. |
SickleAccount::swap_authority | fn | The capacity in which a key may add liquidity on a keeper path. |
SwapAuthority | enum | Management for the owner, Automation(bitmap) for every other authorized key. |
GlobalAutomator | struct | One admin-approved global keeper. |
GlobalAutomator::SIZE | const | Account size with the discriminator. |
settings.rs
All four functions are pub(crate).
| Item | Kind | Meaning |
|---|---|---|
resolve_settings_candidate | fn | Reads the settings at the derived address and returns a SettingsUpdated { removed: true } event. |
load_settings_candidate | fn | Reads the settings at the derived address and returns the boxed struct. |
close_validated_settings_candidate | fn | Moves the lamports to the receiver, assigns the account to the System program, and resizes it to 0. |
transfer_position_settings | fn | Copies a position's settings onto the one a rebalance reopened, re-validates them, and stamps or clears the keeper history. |
venue/family.rs
| Item | Kind | Meaning |
|---|---|---|
VenueFamily | enum | Clmm, Dlmm, or NonPosition. |
PositionGeometry | enum | Clmm { min_tick_index, max_tick_index } or Dlmm { min_bin_id, max_bin_id }. |
PositionGeometry::family | fn | The family of a geometry. |
PositionGeometry::bounds | fn | The (min, max) pair of a geometry. |
Conformance | enum | Registered or NotApplicable. |
MeteoraDlmmVenue | enum | Zero-sized marker type for the Meteora venue. |
DlmmVenue | trait | Sealed marker for a DLMM venue type. Carries PROTOCOL. |
RegisteredClmmVenue | trait | Sealed marker for a CLMM venue type that implements the full CLMM manifest. |
RegisteredDlmmVenue | trait | Sealed marker for a DLMM venue type that implements the full DLMM manifest. |
protocol_family | fn | Generated. Maps every Protocol variant to a family. No wildcard arm. |
protocol_conformance | fn | Generated. Maps every Protocol variant to a conformance state. |
Protocol::family | fn | Calls protocol_family. |
Protocol::position_geometry | fn | Resolves the index limits. Takes Option<(i32, i32)> DLMM limits. |
Accounts that live in intent.rs
These accounts are part of the authorization flow and are documented in the Keeper authorization page. This table only records where they are.
| Account | Definition | Seeds | Size in bytes |
|---|---|---|---|
IntentDomain | intent.rs:696-704 | [b"intent_domain"] | 42 |
IntentNonce | intent.rs:718-727 | [b"intent_nonce", sickle] | 49 |
IntentSession | intent.rs:764-783 | [b"intent_session", sickle, signer] | 123 |
IntentAuthorization | intent.rs:822-849 | see layer 08 | see layer 08 |
DlmmIntentAuthorization | intent.rs:836-845 | see layer 08 | see layer 08 |
ActionIntent | intent.rs:898-930 | see layer 08 | see layer 08 |
The sizes come from the SIZE constants at intent.rs:704, intent.rs:726, and intent.rs:783.
Program accounts
All four accounts derive InitSpace; their retained SIZE constants add the
8-byte Anchor discriminator to the derived payload size. The fixed-size nested
settings structs derive their SIZE values the same way. The deployed numeric
sizes below and their serialized lengths are pinned together at
state.rs:1183-1216. GlobalConfig::LEGACY_SIZE remains explicit because the
migration addresses that historical prefix directly.
Global config
The global config (GlobalConfig) is defined at state.rs:837-860.
| Field | Type | Meaning |
|---|---|---|
admin | Pubkey | The admin. |
pending_admin | Pubkey | The key that may accept the admin role. |
guardian | Pubkey | The guardian. Pubkey::default() means none. |
fee_collector | Pubkey | The fee collector. |
fee_bps | [u16; 6] | The fee in bps per fee action. Indexed by the FeeAction discriminant. |
protocol_programs | [Pubkey; 3] | Allowlist slots for tags 0, 1, and 2. Pubkey::default() means disabled. |
paused | bool | The pause flag. |
bump | u8 | The PDA bump. |
meteora_dlmm_program | Pubkey | Allowlist slot for tag 3. Appended after the legacy layout. |
| Property | Value | Source |
|---|---|---|
| Seeds | [b"global_config"] | constants.rs:4, instructions/admin/initialize_global_config.rs:48 |
| Legacy size | 246 bytes | GlobalConfig::LEGACY_SIZE, state.rs:863-871, asserted state.rs:1454 |
| Size | 278 bytes | GlobalConfig::SIZE, state.rs:873, asserted state.rs:1189 |
| Meteora offset | 246 | state.rs:872, asserted state.rs:1455 |
| Created by | initialize_global_config. The admin pays and must be the upgrade authority. | instructions/admin/initialize_global_config.rs:39-51 |
| Grown by | migrate_global_config. The admin pays the rent shortfall. | instructions/admin/migrate_global_config.rs:54-71 |
| Closed by | Nothing. | no close = on this type |
The migration reads the legacy bytes unchecked and checks the discriminator, the length, the admin, and the bump. Then it resizes the account and writes the last 32 bytes (instructions/admin/migrate_global_config.rs:40-71). The test at state.rs:1423-1464 decodes the new layout with a legacy struct and proves that every old field keeps its offset.
Sickle
The Sickle (SickleAccount) is defined at state.rs:970-982.
| Field | Type | Meaning |
|---|---|---|
owner | Pubkey | The owner. |
manager | Pubkey | The manager. Pubkey::default() means none. |
automator | Pubkey | The custom keeper. Pubkey::default() means none. |
permissions | u8 | The permission bitmap of the custom keeper. |
referral_code | [u8; 32] | Written at creation. The program does not read it. |
version | u16 | Written as 1 at creation. The program does not read it. |
bump | u8 | The PDA bump. |
| Property | Value | Source |
|---|---|---|
| Seeds | [b"sickle", owner] | constants.rs:7, instructions/sickle/initialize_sickle.rs:17 |
| Size | 140 bytes | SickleAccount::SIZE, state.rs:985, asserted state.rs:1190 |
| Created by | initialize_sickle. The owner pays. | instructions/sickle/initialize_sickle.rs:13-20 |
| Closed by | Nothing. | no close = on this type |
Position settings
The position settings (PositionSettings) are defined at state.rs:239-282.
| Field | Type | Size | Meaning |
|---|---|---|---|
sickle | Pubkey | 32 | The Sickle that owns the settings. |
protocol | Protocol | 1 | The venue. |
pool | Pubkey | 32 | The pool. Required when auto_rebalance or auto_exit is on. |
position | Pubkey | 32 | The position. |
auto_rebalance | bool | 1 | Auto-rebalance consent. |
rebalance_config | RebalanceConfig | 64 | See the table below. |
automate_rewards | bool | 1 | Reward automation consent. |
reward_config | RewardConfig | 33 | Behavior and optional harvest output mint. |
auto_exit | bool | 1 | Auto-exit consent. |
exit_config | ExitConfig | 76 | See the table below. |
last_rebalance_ts | i64 | 8 | Unix time of the last rebalance. The program writes it. 0 means never. |
prev_tick_lower | i32 | 4 | Lower edge of the range the last keeper rebalance closed. The program writes it. |
prev_tick_upper | i32 | 4 | Upper edge of that range. (0, 0) means no history. |
bump | u8 | 1 | The PDA bump. |
The discriminator adds 8 bytes. The total is 298 bytes (PositionSettings::SIZE, state.rs:302, asserted at state.rs:1188 and state.rs:1395).
RebalanceConfig is defined at state.rs:98-110.
| Field | Type | Meaning |
|---|---|---|
tick_spaces_below | u32 | Width in tick spaces below the current tick when the keeper reopens. |
tick_spaces_above | u32 | Width in tick spaces above the current tick when the keeper reopens. |
buffer_ticks_below | i32 | Distance below the range at which a rebalance triggers. Negative is allowed. |
buffer_ticks_above | i32 | Distance above the range at which a rebalance triggers. Negative is allowed. |
dust_bps | u16 | The dust limit a keeper rebalance may leave. |
price_impact_bps | u16 | Price impact cap. |
slippage_bps | u16 | Slippage cap. |
cutoff_tick_low | i32 | Lower stop-loss limit. |
cutoff_tick_high | i32 | Upper stop-loss limit. |
delay_minutes | u8 | Minimum minutes between keeper rebalances. |
reward_config | RewardConfig | Nested reward config. |
ExitConfig is defined at state.rs:211-218.
| Field | Type | Meaning |
|---|---|---|
trigger_tick_low | i32 | Lower exit trigger. |
trigger_tick_high | i32 | Upper exit trigger. The band is half-open. |
exit_token_out_low | Pubkey | Output mint when the price exits below. Default = none. |
exit_token_out_high | Pubkey | Output mint when the price exits above. Default = none. |
price_impact_bps | u16 | Price impact cap. |
slippage_bps | u16 | Slippage cap. |
| Property | Value | Source |
|---|---|---|
| Seeds | [b"position_settings", sickle, position] | constants.rs:10, instructions/position/set_position_settings.rs:55 |
| Size | 298 bytes | state.rs:302, asserted state.rs:1188, state.rs:1395 |
| Created by | set_position_settings with init_if_needed. A separate payer pays. The owner signs. | instructions/position/set_position_settings.rs:36-58 |
| Created by | Every rebalance instruction creates the new position's settings with init. The funder pays. | instructions/orca/rebalance_for.rs:177-190, instructions/raydium/rebalance_for.rs:152-165, instructions/orca/rebalance_position.rs:161-174, instructions/raydium/rebalance_position.rs:154-167, instructions/meteora/rebalance_for.rs:99 |
| Closed by | unset_position_settings with close = receiver. The receiver must equal the owner. | instructions/position/unset_position_settings.rs:38-51 |
| Closed by | The keeper rebalances close the old settings with close = receiver. The runner checks that the receiver equals the owner. | instructions/orca/rebalance_for.rs:162-175, instructions/raydium/rebalance_for.rs:137-150, instructions/meteora/rebalance_for.rs:91, owner_rebalance.rs:243-247 via keeper_rebalance.rs:252 |
| Closed by | The close, owner rebalance, keeper exit, and Meteora close paths close it by hand with close_validated_settings_candidate. The receiver is the owner. | decrease_close.rs:344-346,389-391, owner_rebalance.rs:462,557-558, keeper_exit.rs:289,343-344, instructions/meteora/close_position.rs:41,131 |
Global keeper record
The global keeper record (GlobalAutomator) is defined at state.rs:1136-1144.
| Field | Type | Meaning |
|---|---|---|
authority | Pubkey | The keeper key. It equals the PDA seed. |
permissions | u8 | The permission bitmap. |
bump | u8 | The PDA bump. |
| Property | Value | Source |
|---|---|---|
| Seeds | [b"keeper", authority] | constants.rs:13, instructions/admin/set_global_automator.rs:30 |
| Size | 42 bytes | GlobalAutomator::SIZE, state.rs:1147, asserted state.rs:1191 |
| Created by | set_global_automator with init_if_needed. The admin pays. Zero bitmap rejected. | instructions/admin/set_global_automator.rs:26-49 |
| Closed by | revoke_global_automator with close = admin. | instructions/admin/revoke_global_automator.rs:24-30 |
Enums and tags
Protocol
The doc comment at state.rs:31-35 states the append-only rule: variants are serialized state and instruction data, and a new variant takes the next tag. The test at state.rs:1366-1383 asserts every tag and the PROTOCOL_COUNT constant.
| Variant | Tag | Family | Allowlist slot | Static limits |
|---|---|---|---|---|
OrcaWhirlpool | 0 | Clmm | protocol_programs[0] | -443636 to 443636 |
Jupiter | 1 | NonPosition | protocol_programs[1] | none. position_geometry fails |
RaydiumClmm | 2 | Clmm | protocol_programs[2] | -443636 to 443636 |
MeteoraDlmm | 3 | Dlmm | meteora_dlmm_program | read from the pair account |
Sources: state.rs:37-44, state.rs:936-943, family.rs:169-180, family.rs:193-223, /programs/sickle/src/orca.rs:23-24, /programs/sickle/src/raydium.rs:42.
FeeAction
| Variant | Index |
|---|---|
Harvest | 0 |
Compound | 1 |
Rebalance | 2 |
Exit | 3 |
Deposit | 4 |
Withdraw | 5 |
Source: state.rs:21-28. FEE_ACTION_COUNT is 6 (constants.rs:22).
Permission bits
| Constant | Value |
|---|---|
PERMISSION_HARVEST | 0x01 |
PERMISSION_COMPOUND | 0x02 |
PERMISSION_REBALANCE | 0x04 |
PERMISSION_EXIT | 0x08 |
PERMISSION_ALL | 0x0F |
Source: constants.rs:207-212.
Authorization methods
Each method runs its checks in the order listed. The first match wins.
SickleAccount::is_owner (state.rs:993-995)
- Return
owner == key.
Private role resolution (state.rs:997-1031)
is_designated_manageraccepts the exact configured manager, but never an unsetPubkey::default()slot.is_owner_or_managercombines that rule withis_owner.automator_permissionsreturns the complete custom-keeper bitmap when a custom keeper is configured. A different caller returnsNonewithout consulting the global record, even when the custom bitmap is zero.- Without a custom keeper, the global record returns its bitmap only when its
stored authority equals the caller (
state.rs:1149-1152).
All four helpers are private. The two public decisions below share them, so manager identity and custom-over-global precedence have one implementation.
SickleAccount::is_authorized_automator (state.rs:1047-1058)
- If
is_owner_or_manager(key), return true. - Resolve the custom-or-global bitmap through
automator_permissions. - Return true only when that bitmap carries every bit in
permission.
SickleAccount::swap_authority (state.rs:1090-1109)
- If
is_owner(key), returnSome(Management). - If
is_designated_manager(key), returnSome(Automation(PERMISSION_ALL)). - Resolve the custom-or-global bitmap through
automator_permissionsand returnSome(Automation(bitmap))only when it is nonzero. - Return
None.
The distinction in steps 3 is deliberate. A normal keeper instruction asks
for one exact action bit. The shared increase runner accepts any nonzero bitmap
only long enough to check it against the owner's flow-specific
PositionSettings::allows_automator_increase consent
(open_increase.rs:253-278).
PositionSettings::allows_automator_increase (state.rs:554-561)
- If
auto_rebalanceand the bitmap hasPERMISSION_REBALANCE, return true. - Return
automate_rewardsandreward_behavior == Compoundand the bitmap hasPERMISSION_COMPOUND.
GlobalConfig::is_guardian_or_admin (state.rs:877-879)
- Return
admin == key, orguardian != Pubkey::default()andguardian == key.
Global config helpers
Fees
fee_amount (state.rs:926-933) computes amount * bps / 10000 in u128 and floors. It returns MathOverflow only if the u128 math fails. The admin caps bps at MAX_FEE_BPS on write, so the fee never exceeds the amount.
| Constant | Value | Source |
|---|---|---|
BPS_DENOMINATOR | 10000 | constants.rs:15 |
MAX_FEE_BPS | 500 | constants.rs:18 |
Allowlist
protocol_program and set_protocol_program (state.rs:936-954) map each Protocol variant to its slot by an explicit match. assert_protocol_program (state.rs:959-967) returns ProtocolNotAllowed when the slot is Pubkey::default() or when the slot differs from the CPI target.
Pause
assert_not_paused (state.rs:884-887) returns Paused while paused is true. assert_not_paused_unless_owner_exit(owner_exit) (state.rs:913-916) returns Paused only when paused is true and owner_exit is false. The doc comment at state.rs:900-906 states that every current caller passes true.
Position settings rules
Write-time validation
validate_with_geometry (state.rs:385-502) runs these checks in order.
| Step | Condition | Error |
|---|---|---|
| 1 | geometry.family() == protocol.family() | InvalidPositionSettings |
| 2 | min_index < max_index | InvalidPositionSettings |
| 3 | If auto_rebalance or auto_exit: pool != Pubkey::default() | InvalidPositionSettings |
| 4a | If auto_rebalance: 2 * min_index and 2 * max_index do not overflow | InvalidBufferTicks |
| 4b | If auto_rebalance: delay_minutes > 0 | InvalidRebalanceDelay |
| 4c | If auto_rebalance: min_index <= cutoff_low < cutoff_high <= max_index | InvalidCutoffTicks |
| 4d | If auto_rebalance: each buffer in [2 * min_index, 2 * max_index] | InvalidBufferTicks |
| 4e | If auto_rebalance: slippage_bps <= 500 | InvalidSlippageBps |
| 4f | If auto_rebalance: 0 < price_impact_bps <= 5000 | InvalidPriceImpactBps |
| 4g | If auto_rebalance: 0 < dust_bps <= 5000 | InvalidDustBps |
| 4h | If auto_rebalance: below + above <= geometry.max_configured_space_sum() | InvalidTickWidth |
| 4i | If auto_rebalance: nested reward_config.validate() | InvalidHarvestTokenOut |
| 4j | If not auto_rebalance: cutoff_low == 0 and cutoff_high == 0 | AutoRebalanceNotSet |
| 4k | If not auto_rebalance: nested reward_config == RewardConfig::default() | AutoRebalanceNotSet |
| 5a | Top-level reward_config.validate() | InvalidHarvestTokenOut |
| 5b | If automate_rewards: reward_behavior != None | InvalidPositionSettings |
| 6a | If auto_exit: trigger_low != 0 or trigger_high != 0 | ExitTriggersNotSet |
| 6b | If auto_exit: min_index <= trigger_low < trigger_high <= max_index | InvalidExitTriggers |
| 6c | If auto_exit: slippage_bps <= 500 | InvalidSlippageBps |
| 6d | If auto_exit: 0 < price_impact_bps <= 5000 | InvalidPriceImpactBps |
| 6e | If not auto_exit: exit_config == ExitConfig::default() | AutoExitNotSet |
| Cap | Value | Source |
|---|---|---|
MAX_SLIPPAGE_BPS | 500 | constants.rs:28 |
MAX_PRICE_IMPACT_BPS | 5000 | constants.rs:29 |
MAX_DUST_BPS | 5000 | constants.rs:30 |
delay_minutes max | 255 | u8 type |
tick_spaces_* sum max | index span, less one for half-open CLMM ranges | PositionGeometry::max_configured_space_sum |
validate (state.rs:373-376) resolves the geometry with protocol.position_geometry(None) and calls validate_with_geometry. It fails for MeteoraDlmm and Jupiter. RewardConfig::validate (state.rs:74-81) accepts a non-default harvest_token_out only with RewardBehavior::Harvest. Enabled top-level reward automation must select either Harvest or Compound; disabled settings may retain either behavior for a later re-enable.
Identity check
validate_automation_identity (state.rs:507-522) returns InvalidSettingsIdentity unless the stored sickle, protocol, pool, and position all equal the arguments. The seeds cover only sickle and position. The runners call it before any venue account is parsed, for example keeper_rebalance.rs:268-273.
Consent checks
| Function | Condition | Error | Source |
|---|---|---|---|
validate_harvest_for | automate_rewards and reward_behavior == Harvest | AutoHarvestNotSet | state.rs:528-534 |
validate_compound_for | automate_rewards and reward_behavior == Compound | AutoCompoundNotSet | state.rs:539-545 |
Keeper rebalance check
validate_rebalance_for and validate_dlmm_rebalance_for (state.rs:586-618) both call validate_rebalance_for_geometry (state.rs:621-679). The DLMM version treats the upper edge as inclusive.
| Step | Condition | Error |
|---|---|---|
| 1 | auto_rebalance | AutoRebalanceNotSet |
| 2 | delay_minutes > 0 | InvalidRebalanceDelay |
| 3 | cutoff_tick_low < cutoff_tick_high | RebalanceConfigNotSet |
| 4 | If last_rebalance_ts != 0: now >= last_rebalance_ts + delay_minutes * 60 | RebalanceTooSoon |
| 5 | tick < lower - buffer_below, or tick >= upper + buffer_above. DLMM uses >. | TickWithinRange |
| 6 | cutoff_low < tick < cutoff_high | TickOutsideStopLossRange |
Expected range
expected_rebalance_range(current_tick, tick_spacing) (state.rs:163-176) computes the one range a keeper must reopen.
| Step | Computation | Error |
|---|---|---|
| 1 | tick_spacing > 0 | InvalidProtocolAccountData |
| 2 | aligned = current_tick.div_euclid(spacing) * spacing in i64 | none |
| 3 | lower = aligned - tick_spaces_below * spacing | none |
| 4 | upper = aligned + (tick_spaces_above + 1) * spacing | none |
| 5 | Convert both to i32 | InvalidTickRange |
validate_tick_width (state.rs:121-137) checks (upper - lower) / spacing == below + above + 1 with integer division. It returns InvalidTickWidth. The property test at state.rs:3189-3216 proves the expected range always passes the width check.
expected_rebalance_bin_range(active_id) (state.rs:194-202) returns [active - below, active + above] inclusive. validate_bin_width (state.rs:181-192) checks max - min + 1 == below + above + 1.
Progress check
validate_rebalance_progress and validate_dlmm_rebalance_progress (state.rs:723-757) both call validate_rebalance_progress_geometry (state.rs:770-803).
| Step | Condition | Error |
|---|---|---|
| 1 | (new_lower, new_upper) != (position_lower, position_upper) | RebalanceNoOp |
| 2 | Not all of: history present, target equals history, and tick still inside the current range | RebalanceNoOp |
History encoding
prev_tick_lower and prev_tick_upper store the range the last keeper rebalance closed. The CLMM wrapper selects the keeper's closed range or (0, 0) for an owner rebalance (settings.rs:185-199); the DLMM wrapper applies its inclusive-range encoding (instructions/meteora/rebalance_for.rs:196-208). Both pass that normalized value to PositionSettings::transfer_from, which writes the history once (state.rs:335-351). set_position_settings writes (0, 0) on every owner write (instructions/position/set_position_settings.rs:92-93).
| Stored pair | Meaning | Source |
|---|---|---|
(0, 0) | No history. | state.rs:274-277 |
(1, 0) | DLMM only. The real one-bin range [0, 0]. | state.rs:300 |
| other | The closed range. CLMM half-open, DLMM inclusive. | state.rs:265-280 |
dlmm_rebalance_history (state.rs:760-768) applies the (1, 0) rule before transfer. validate_rebalance_progress_geometry (state.rs:785-790) decodes (1, 0) back to [0, 0] only when upper_is_inclusive is true.
Last rebalance time
last_rebalance_ts is written only by PositionSettings::transfer_from, for both CLMM and DLMM rebalances (state.rs:335-351). set_position_settings does not touch it (instructions/position/set_position_settings.rs:79-82). Step 4 of the keeper rebalance check reads it.
Keeper exit check
validate_exit_for(current_tick) (state.rs:816-834) runs these checks.
| Step | Condition | Error |
|---|---|---|
| 1 | auto_exit | AutoExitNotSet |
| 2 | trigger_low != 0 or trigger_high != 0 | ExitTriggersNotSet |
| 3 | tick < trigger_low or tick >= trigger_high | TickWithinRange |
Both the CLMM runner (keeper_exit.rs:303) and the Meteora handler (instructions/meteora/exit_for.rs:210) call the same function. There is no inclusive-bin variant.
Settings candidate helpers
The close and rebalance instructions receive the position settings as an unchecked account. settings.rs resolves it.
load_settings_candidate (settings.rs:74-113) runs these checks.
| Step | Condition | Error | Result |
|---|---|---|---|
| 1 | candidate.key == find_program_address([seed, sickle, position]) | InvalidPositionSettings | |
| 2a | If owner == crate::ID: deserialize as PositionSettings | InvalidPositionSettings | |
| 2b | If owner == crate::ID: stored sickle equals the argument | InvalidPositionSettings | |
| 2c | If owner == crate::ID: stored position equals the argument | InvalidPositionSettings | Some(Box<settings>) |
| 3 | If owner == System and data is empty | None | |
| 4 | Otherwise | InvalidAccountOwner |
Step 3 ignores the lamport balance, so a donated lamport at the address does not block a close (settings.rs:37-39).
resolve_settings_candidate (settings.rs:49-64) calls load_settings_candidate and maps the result to a SettingsUpdated { removed: true } event.
close_validated_settings_candidate (settings.rs:122-142) requires owner == crate::ID. It adds the lamports to the receiver with checked math and sets the candidate to 0 lamports. Then it assigns the candidate to the System program and resizes it to 0. The callers run it after the last venue CPI (decrease_close.rs:389-391, owner_rebalance.rs:558, keeper_exit.rs:344, instructions/meteora/close_position.rs:131).
Position geometry
Protocol::position_geometry(dlmm_bounds) (family.rs:193-223) resolves the limits.
| Venue | dlmm_bounds must be | Result | Error |
|---|---|---|---|
OrcaWhirlpool | None | Clmm { -443636, 443636 } | InvalidPositionSettings if Some |
Jupiter | any | none | InvalidPositionSettings |
RaydiumClmm | None | Clmm { -443636, 443636 } | InvalidPositionSettings if Some |
MeteoraDlmm | Some((min, max)) | Dlmm { min, max } | InvalidPositionSettings if None. InvalidProtocolAccountData if min >= max |
set_position_settings supplies the Meteora limits from a key-checked, allowlist-checked LbPair account passed as a remaining account (instructions/position/set_position_settings.rs:116-154).
The venue_registry! macro (family.rs:124-180) generates protocol_family and protocol_conformance from one list. A Protocol variant that is missing from the list fails to compile.
How a call flows through it
This follows the keeper rebalance runner from its first settings check to the settings transfer. The runner is venue::keeper_rebalance.
- The runner reads
is_owneron the Sickle (keeper_rebalance.rs:262,state.rs:993). - The runner calls
validate_automation_identityon the position settings (keeper_rebalance.rs:268-273,state.rs:507-522). - After the venue state loads, the runner reads the clock and calls
validate_rebalance_for(keeper_rebalance.rs:295-302,state.rs:586-679). - The runner calls
expected_rebalance_rangeand requires the requested range to equal it. It returnsInvalidRangePlacementotherwise (keeper_rebalance.rs:303-310,state.rs:163-176). - The runner calls
validate_rebalance_progresswith the current range and the expected range (keeper_rebalance.rs:311-317,state.rs:723-802). - The runner calls
transfer_keeper_rebalance_settingswith the closed range as keeper history; that stage delegates totransfer_position_settings(keeper_rebalance.rs:190-221,319-329,settings.rs:159-200). - The transfer helper checks that the pool matches and checks the new width with
validate_tick_width(settings.rs:173-183,state.rs:121-137). - The wrapper resolves the CLMM geometry and calls
PositionSettings::transfer_from, which exhaustively copies the caller-owned fields, writes the replacement identity, timestamp and history, then validates against that geometry (settings.rs:185-199,state.rs:275-356). - Anchor closes the old settings account to
receiverwhen the handler returns (instructions/orca/rebalance_for.rs:162-175). The runner checkedreceiver == ownerbefore step 1 (keeper_rebalance.rs:252,owner_rebalance.rs:243-247).
Invariants the layer owns
- A stored settings account satisfies every write-time rule.
set_position_settingscallsvalidate_with_geometry(instructions/position/set_position_settings.rs:95). Every rebalance transfer reaches the same check throughPositionSettings::transfer_from(state.rs:335-351). Errors: the table in "Write-time validation". - A keeper acts on the exact position the owner consented to.
validate_automation_identity(state.rs:507-522). Error:InvalidSettingsIdentity. - A keeper rebalance waits
delay_minutesafter the last one, unless none has happened.validate_rebalance_for_geometrystep 4 (state.rs:656-661). Error:RebalanceTooSoon. - A keeper rebalance reopens the one range that
expected_rebalance_rangecomputes. The runner compares (keeper_rebalance.rs:307-310). Error:InvalidRangePlacement. - A keeper rebalance never reopens the range it closes, and never returns to the previous keeper range while the tick is still in range.
validate_rebalance_progress_geometry(state.rs:770-803). Error:RebalanceNoOp. - Keeper history is keeper-only. The CLMM wrapper selects
(0, 0)on an owner rebalance (settings.rs:185-199), andset_position_settingswrites(0, 0)(instructions/position/set_position_settings.rs:92-93). No error. - Only the owner is
is_owner. The manager reaches the automation surface through the privateis_designated_manager/is_owner_or_managerrules and never becomes the owner (state.rs:993-1017). Handlers returnUnauthorized. - A custom keeper on the Sickle replaces the global allowlist. Both public decisions resolve custom/global identity and precedence through
automator_permissions(state.rs:1019-1031,state.rs:1053-1057,state.rs:1103-1108). Result: false orNone. - An unset key slot never matches
Pubkey::default().is_designated_managerandis_guardian_or_adminenforce the two stored optional-authority rules (state.rs:997-1001,state.rs:878). - A fee never exceeds the amount.
fee_amountwithbps <= 500(state.rs:926-933). Property teststate.rs:3096-3115. - A CPI target equals its allowlist slot and the slot is enabled.
assert_protocol_program(state.rs:959-967). Error:ProtocolNotAllowed. - A close cannot hide a live settings account.
load_settings_candidaterequires the derived address (settings.rs:79-87). Error:InvalidPositionSettings. - Settings rent returns to the owner. Every close path checks
receiver == owner(instructions/position/unset_position_settings.rs:38,decrease_close.rs:335-339,owner_rebalance.rs:243-247,instructions/meteora/close_position.rs:41). Error:InvalidRentReceiver. - Meteora settings validate against the pair's own bin limits, never against CLMM limits.
validate_with_geometrystep 1 andposition_geometry(state.rs:386-389,family.rs:210-221). Error:InvalidPositionSettings. - Enabled reward automation always selects
HarvestorCompound; a disabled record may stage either behavior without granting keeper consent.validate_with_geometrystep 5b (state.rs:468-472). Error:InvalidPositionSettings.
Tests
| Location | Covers |
|---|---|
state.rs:1183-1216 | Derived nested-configuration and account sizes equal their deployed numeric values and serialized lengths. |
state.rs:1219-1248 | fee_amount floor, unset slot, Deposit/Withdraw indexes, no overflow at the cap. |
state.rs:1251-1296 | Pause and owner-exit bypass, guardian-or-admin, unset guardian. |
state.rs:1299-1363 | assert_protocol_program per slot, set_protocol_program touches one slot. |
state.rs:1366-1464 | Protocol tags 0 to 3, PositionSettings size 298 and tag offset 40, GlobalConfig legacy layout and offsets. |
state.rs:1467-1496 | Disabled slot rejected, geometry per venue. |
state.rs:1535-1566 | Empty and fully configured settings validate. |
state.rs:1568-1594 | The automation identity binds the stored Sickle, protocol, pool, and position independently. |
state.rs:1596-1689 | Active reward behavior, staged disabled behavior, DLMM pair domain, family mismatch. |
state.rs:1692-1766 | validate_harvest_for, validate_compound_for. |
state.rs:1769-2021 | validate_rebalance_for: consent, buffered range, stop-loss, negative buffers. |
state.rs:2023-2096 | DLMM inclusive range, validate_bin_width, expected_rebalance_bin_range, (1, 0) history encoding. |
state.rs:2099-2294 | Debounce boundary, fixed point, in-range two-cycle, out-of-range reversal, absent history, zero delay, first rebalance. |
state.rs:2297-2410 | Pool required, cutoff limits, buffer limits, bps caps. |
state.rs:2413-2508 | validate_tick_width, expected_rebalance_range alignment, negative floor, width by construction, degenerate inputs. |
state.rs:2511-2648 | Stale cutoffs, harvest token rule, exit triggers, exit caps, stale exit config. |
state.rs:2651-2710 | validate_exit_for: consent, inside band, outside band, single-sided band. |
state.rs:2722-2852 | is_authorized_automator: owner, manager, custom bits, custom overrides global, global bits, every role times every permission. |
state.rs:2855-2960 | swap_authority classes, zero bitmaps, owner narrower than owner-or-manager, custom overrides global. |
state.rs:2963-3021 | allows_automator_increase per flow. |
state.rs:3024-3253 | Seeded property tests: fee limit, exact floor, monotonic, no overflow, expected range width, floor alignment, ordered range. |
settings.rs:238-331, 334-509 | Complete CLMM settings transfer; close to receiver, absent account, address, discriminator, identity, and owner rejection. |
instructions/meteora/rebalance_for.rs:663-766 | Complete DLMM settings transfer, pair-local geometry, inclusive width, and zero-bin history encoding. |
family.rs:234-313 | Family and conformance per variant, trait registration, geometry resolution and rejections. |
instructions/position/set_position_settings.rs:223-298 | Meteora remaining-account checks, CLMM needs no remaining accounts. |
instructions/position/unset_position_settings.rs:72-173 | Owner signer, receiver, and settings PDA constraints at the generated entrypoint before close. |
instructions/admin/revoke_global_automator.rs:39-116 | Admin signer, stored admin link, and global-keeper PDA constraints at the generated entrypoint before close. |
tests/sickle/orca.ts:2829-2844 | The validator-backed settings handler rejects enabled reward automation with None. |
instructions/admin/migrate_global_config.rs:98- | Legacy byte migration. |
Gaps visible in the source:
| Gap | Where the code is |
|---|---|
The DLMM rebalance check tests only TickWithinRange. Debounce and stop-loss run through the shared function but are not tested with upper_is_inclusive. | state.rs:2024-2062 |
validate_bin_width with min > max is not tested. | state.rs:182-185 |
expected_rebalance_bin_range overflow to i32 is not tested. | state.rs:194-202 |
close_validated_settings_candidate with a non-program owner, and the receiver lamport overflow, are not tested. | settings.rs:126-136 |
assert_current on IntentDomain and the IntentSession methods are tested in intent.rs, not here. | intent.rs |
Simplification and audit notes
- L07-N006 —
state.rs:979-980:referral_codeandversionare written once byinstructions/sickle/initialize_sickle.rs:35-36and read by no on-chain code.SICKLE_VERSIONis 1 (constants.rs:133) and nothing branches on it. - L07-N009 —
state.rs:912-915assert_not_paused_unless_owner_exit(owner_exit: bool): the doc comment atstate.rs:899-905states that every caller passestruetoday. The parameter has one live value. - L07-N015 —
state.rs:936-954:protocol_programandset_protocol_programare two parallelmatchtables that map the same four variants to the same four storage slots. The arrayprotocol_programsand the separate fieldmeteora_dlmm_programforce the split. - L07-N017 —
settings.rs:79-87:load_settings_candidatederives the PDA withfind_program_addresson every call and discards the bump. The storedbumpfield atstate.rs:281is never compared to the derived one on this path. - L07-N018 —
settings.rs:55-62,instructions/position/unset_position_settings.rs:59-66,keeper_rebalance.rs:213-220,owner_rebalance.rs:530-537, andinstructions/meteora/rebalance_for.rs:489-496: five paths build aSettingsUpdatedevent by copying the same three flags out of aPositionSettings. - L07-N020 —
settings.rs:122-142re-implements Anchor'scloseconstraint by hand. The test atsettings.rs:340-368builds the runtime's in-memory account header withunsaferaw pointers so thatAccountInfo::resizeworks in a unit test. The test depends on an undocumented runtime layout. - L07-N021 —
family.rs:92-96Conformanceandfamily.rs:159-165protocol_conformancehave no caller outside thefamily.rstests. Neither is dead. The generated match carries no wildcard arm, so a newProtocolvariant that nobody registers fails to compile. The pending state this note also covered is removed. - L07-N022 —
family.rs:41-71PositionGeometry::familyandfamily.rs:184-186Protocol::familyhave one production caller,state.rs:387.VenueFamily::NonPositionis produced by the generated function and matched by no production code. - L07-N023 —
family.rs:213-216checksmin_bin_id < max_bin_idand returnsInvalidProtocolAccountData.state.rs:391checksmin_index < max_indexon the same values and returnsInvalidPositionSettings. One condition is checked twice with two errors. - L07-N024 —
state.rs:1156-3022andstate.rs:3024-3253: the file holds two test modules and 2097 lines of tests against 1153 lines of production code. The property tests carry their own xorshift RNG (state.rs:3034-3067).
Resolved
L07-N001 — All four program accounts derive
InitSpace, and each retainedSIZEconstant is now8 + Self::INIT_SPACE(state.rs:244-302,state.rs:836-872,state.rs:969-984,state.rs:1141-1152). The one explicit exception isGlobalConfig::LEGACY_SIZE: the migration addresses that historical prefix and its Meteora append offset directly, so it remains a named manual boundary.state_sizes_match_serialized_layoutspins all four derived sizes to both their deployed numeric values and real Anchor serialization (state.rs:1185-1218). Adding one to the derived Sickle size makes that test fail at 141 versus 140. No account layout or IDL change. Resolved in this pass.L07-N002 —
RewardConfig,RebalanceConfig, andExitConfignow deriveInitSpace; their compatibilitySIZEconstants delegate toSelf::INIT_SPACEinstead of repeating field widths (state.rs:58-113,state.rs:205-221). The same serialization test pins 33, 64, and 76 bytes respectively. Adding one to the derived reward-config size makes the test fail at 34 versus 33. No instruction or serialized-layout change. Resolved in this pass.L07-R001 —
state.rs:550-553requirescutoff_tick_low != 0at the point of use, withRebalanceConfigNotSet.state.rs:351-356acceptscutoff_tick_low == 0whencutoff_tick_high > 0. An owner can store a configuration that write-time validation accepts and that every keeper rebalance rejects. No test covers this case.L07-R002 —
state.rs:375validates the nestedrebalance_config.reward_configonly inside theauto_rebalancebranch. The top-levelreward_configis validated on every write atstate.rs:386. A stale nested harvest mint survives whenauto_rebalanceis off.L07-R003 —
state.rs:93-94:tick_spaces_belowandtick_spaces_aboveareu32with no write-time cap.expected_rebalance_rangerejects the overflow only at the keeper point of use (state.rs:165-166). An owner can store a width that no keeper rebalance can ever satisfy.All three were one defect wearing three hats: a value was being used as a proxy for whether it had been configured. The stop-loss band tested
cutoff_tick_low != 0, and tick 0 is the tick where the pool price is 1.0, so a band that legitimately started there was refused at every keeper rebalance as unconfigured. The width pair had no write-time bound at all, and the nested reward config was validated only on the branch that could not reach it.The band now tests
cutoff_tick_low < cutoff_tick_high, which is what the question actually asks, rejects the(0, 0)the old check existed to catch, and additionally rejects an inverted band that a non-zero low used to wave through.ExitConfigalready tested its triggers this way. The width pair is capped against the family's index span, and the nested reward config must be cleared while auto-rebalance is off, for the same reason stale cutoffs must be.No account layout change and no migration. The recorded decision was to add an explicit enabled flag; that was chosen against options which, as put at the time, all left tick 0 unusable. Asking the ordering question directly makes tick 0 usable and closes the write/use gap without touching a live per-position account, so the flag was not needed to get the decided outcome. Resolved in #185.
L07-N007 — Enabled top-level reward automation now requires a concrete
HarvestorCompoundbehavior at write time, afterRewardConfig::validatepreserves its more specific harvest-output error (state.rs:408-412). Disabled settings may retain either behavior for a later re-enable, because the flag still grants no keeper consent. A unit test pins both halves (state.rs:1466-1488), and the validator-backed settings test pins the handler path (tests/sickle/orca.ts:2805-2820). The client-side settings mirror applies the same asymmetric rule (management-ui/src/lib/sickle/settings-tx.ts:206-209), with a focused test for the rejection and staged-disabled case (management-ui/src/lib/sickle/settings-tx.test.ts:206-216). Removing the write-time guard makes the rejection case fail; treating the two fields as a symmetric enum makes the staged-disabled case fail; removing the client mirror guard makes its focused test fail. No account layout or IDL change. Resolved in this pass.L07-N008 — CLMM and DLMM retain their genuinely different pool and range checks, but no longer implement separate settings transfers. Both wrappers normalize the family-specific history and geometry, then call
PositionSettings::transfer_fromfor the complete identity, configuration, timestamp, history, bump, and validation write (settings.rs:159-200,instructions/meteora/rebalance_for.rs:192-218,state.rs:334-356). The DLMM zero-bin sentinel remains encoded before that call. Focused CLMM and DLMM tests pin every copied configuration field and each family's managed history (settings.rs:238-331,instructions/meteora/rebalance_for.rs:699-802). Redirecting the top-level reward config to the nested rebalance reward config makes both tests fail. No account layout, instruction, or IDL change. Resolved in this pass.L07-N010 —
PositionSettingsArgsremains the wire type, but its field projection is now compiler-enforced.From<&PositionSettings>constructs it exhaustively (state.rs:275-288), andapply_argsdestructures it exhaustively before writing the stored configuration (state.rs:309-332).set_position_settingsand the common transfer path both use that owner (instructions/position/set_position_settings.rs:74-78,state.rs:340-350), so adding an argument field cannot leave either path compiling with a silently omitted setting. The pre-refactor transfer tests passed before the extraction and the wrong-field mutation above proves the shared seam is load-bearing. No serialized layout or IDL change. Resolved in this pass.L07-N011 — The authorization comments now name the shared CLMM open/increase runner rather than its original Orca caller (
state.rs:547-554,state.rs:1060-1090,state.rs:1120-1124). The one runner still serves both Orca and Raydium (open_increase.rs:260). No behavior change. Resolved in this pass.L07-N012 — The venue-neutral transfer helper now calls its third argument
pool, matching both CLMM callers (settings.rs:159-175,keeper_rebalance.rs:304-318,owner_rebalance.rs:519-530). This rename landed with the common transfer extraction; this pass reconciles the audit inventory with that state. Resolved in this pass.L07-N013 —
automation_identity_binds_every_stored_keynow accepts the exact stored identity and rejects an independently substituted Sickle, protocol, pool, or position (state.rs:1573-1599). Removing any one of the four comparisons fromvalidate_automation_identitymakes the matching mutation pass unexpectedly and the focused test fail. No instruction, account, or IDL change. Resolved in this pass.L07-N016 — The settings module header now names every owner of its unchecked candidate and transfer helpers: Orca/Raydium closes, keeper exits, Meteora closes, owner rebalances, and both common CLMM transfer paths (
settings.rs:1-16). Resolved in this pass.L07-N019 — The hand-close precondition now names both valid authentication paths,
resolve_settings_candidateandload_settings_candidate(settings.rs:115-120). That matches the owner rebalance, which loads the record before closing it (owner_rebalance.rs:463,owner_rebalance.rs:559). Resolved in this pass.L07-N003 —
fee_bps_foris now private and remains owned by its sole caller,fee_amount(state.rs:918-932). The former public custom-keeper predicate is replaced by the private bitmap resolver shared by both public authorization decisions (state.rs:1019-1031). The global-record resolver is private for the same reason (state.rs:1146-1152). No IDL or instruction surface changes. Resolved in this pass.L07-N004 — The manager identity rule now has one spelling:
is_designated_managerrejects an unset slot and is used by both the owner-or-manager decision andswap_authority(state.rs:997-1017,state.rs:1095-1108). Removing the unset-slot guard makes the focused role test admitPubkey::default()and fail. Resolved in this pass.L07-N005 —
automator_permissionsnow owns custom/global identity, bitmap extraction, and custom-over-global precedence once (state.rs:1019-1031).is_authorized_automatorapplies the exact requested mask, whileswap_authorityrejects a zero bitmap before the shared increase runner checks the remaining bitmap against the owner's flow consent (state.rs:1047-1058,state.rs:1090-1108,open_increase.rs:253-278). The role tests now pin zero custom and global bitmaps (state.rs:2854-2910). Removing the zero filter, weakening the exact mask, or allowing the global record to bypass a configured custom keeper makes its focused test fail. Resolved in this pass.L07-N014 — The permission constants now sit with the test module's imports (
state.rs:1155-1162); no mid-module import remains. Resolved in this pass.
Related
- Events and errors
- Keeper authorization
- Fees
- Dependency graph, section 7, edge 3:
state.rsimportsPositionGeometryfromvenue::family.