Skip to content

MemoryId Inventory

This document is a committed architectural contract. It is the authoritative allocation table for every ic-stable-structures MemoryId across all OhShii canisters (launcher repo + locker repo). Read it — and update it in the same commit — before any change that adds, removes, or relocates a stable structure (StableBTreeMap / StableCell / StableVec / StableLog).

It exists because a single, authoritative table is the only reliable defence against reusing a MemoryId: ic-stable-structuresMemoryManager never reclaims a bucket, so binding a new, layout-incompatible structure to a previously-used id makes the library decode the old bytes — which traps (fixed-size cells / primitives) or silently mis-decodes (BTree node layout).

ic-stable-structuresMemoryManager never reclaims a bucket. When a stable structure is removed, its MemoryId keeps the removed structure’s bytes on every canister that was deployed during that structure’s lifetime. Binding a new, layout-incompatible structure to that same id makes the library decode the stale bytes — which traps (fixed-size cells / primitives, below your Storable::from_bytes, so a decode fallback cannot help) or silently mis-decodes (BTree node layout). Therefore:

  1. A removed MemoryId is RESERVED FOREVER. Never reassign it.
  2. New structures take the lowest NEVER-USED id (per the per-canister “next-free” line below), never a reserved/removed one.
  3. Never tighten a value/key type or shrink a Bounded max_size on an existing id (re-encodes old bytes — treat as a destructive migration).
  4. New persisted record fields are Option<T>, append-last (Candid tolerates missing trailing opt fields; bare-T appends wipe config on upgrade).
  5. One MemoryManager per canister. Never construct a second MemoryManager::init(DefaultMemoryImpl::default()) — two managers on the same DefaultMemoryImpl alias the same bucket table (a latent corruption hazard).
  6. Any layout/MemoryId change requires an UPGRADE-PATH test (deploy the current wasm → populate → --mode upgrade to the new wasm → exercise every public query) as a deploy gate — unit tests and diff-reading cannot catch this class.

1. launcher backend (src/backend/src/memory.rs)

Section titled “1. launcher backend (src/backend/src/memory.rs)”

type Memory = VirtualMemory<DefaultMemoryImpl> over the single MEMORY_MANAGER.

Currently bound:

idstatictype
0LAUNCHED_TOKENSStableBTreeMap<u64, LaunchedToken>
1TOKEN_ID_COUNTERStableCell<u64>
2ICO_CREATION_STATUSStableCell<FeatureStatus>
3ICO_PARTICIPATION_STATUSStableCell<FeatureStatus>
4TOKEN_CREATION_STATUSStableCell<FeatureStatus>
5INDEX_CREATION_STATUSStableCell<FeatureStatus>
6SUBACCOUNT_COUNTERStableCell<u32>
8CRITICAL_LOGSStableBTreeMap<u64, CriticalLogEntry>
9CRITICAL_LOG_COUNTERStableCell<u64>
10LAST_PUBLIC_TIMER_RESTARTStableCell<u64>
11TIMER_RESTART_IN_PROGRESSStableCell<bool>
12BURNING_FEE_CONFIGStableCell<BurningFeeConfig>
13ICO_FEE_CONFIGStableCell<IcoFeeConfig>
14LAST_HOUSEKEEPING_CHECKStableCell<u64>
15BITCOIN_GATEWAY_STATUSStableCell<FeatureStatus> — Bitcoin cross-chain Gateway toggle (frontend-only enforcement)
16SOLANA_GATEWAY_STATUSStableCell<FeatureStatus> — Solana (Menese) Gateway toggle (also enforced in register_menese_user)
17GUEST_FEE_CUSTODY_V2StableBTreeMap<String, ()> — OHSHII guest-fee v2 custody marker (campaign_id present ⇒ per-campaign sub, not MAIN); virgin bucket, SAFE
18GUEST_FEE_FORWARD_DUEStableBTreeMap<String, u64> — Completed-campaign guest-pot forward schedule/latch (campaign_id → due ns = completion+7d); schedule ts, NOT a ledger created_at_time pin; virgin bucket, SAFE
19TREASURY_LIQ_OPSStableBTreeMap<u64, StorableTreasuryLiquidityOp> — treasury-funded ICPSwap liquidity op journal (ONS proposals; persisted before any fund movement, resumed by proposal_retry_treasury_liquidity); virgin bucket, SAFE (2026-07)
20TREASURY_LIQ_OP_COUNTERStableCell<u64> — monotonic id counter for TREASURY_LIQ_OPS; virgin bucket, SAFE (2026-07)
21FINALIZE_DRAIN_PINSStableBTreeMap<String, StorableFinalizeDrainPin> — LGE finalize LP-drain pin (C1/GAP-1/GAP-2 hardening). Keyed by campaign_id, written BEFORE the finalize campaign-sub → pool_manager MAIN LP transfer: pins the drawn AMOUNT and a FIXED created_at_time so a retry re-sends the byte-identical ledger tx (dedup via TxDuplicate→Ok) instead of recomputing a draw from an already-drained subaccount; drained=true = transfer provably landed (retry skips the transfer). Virgin bucket, SAFE (2026-07)
22PENDING_POOL_JOBSStableBTreeMap<String, StorablePendingPoolJob> — detached LGE pool-creation jobs (2026-07 creation-pipeline hardening). Keyed by campaign_id; persisted BEFORE the zero-delay timer that runs the pool phase after create_ico_with_icrc2_payment returns; re-armed from post_upgrade (timers do not survive upgrades); carries canister ids to reveal, liquidity params, at-most-once fund_transfer_started marker and bounded attempts; removed on completion or terminal Frozen. Virgin bucket, SAFE (2026-07)
23MENESE_VOUCHED_USERSStableBTreeMap<Principal, u64> — principals this backend vouched on Sovereign Send via registerPartnerUsers (value = first vouched_at ns). Working set for the admin_remove_menese_user bulk revocation lever. Growth bounded by the register_menese_user Shield global cap. Virgin bucket, SAFE (2026-07 Menese hardening)
24MENESE_PARTNER_STATUSStableCell<MenesePartnerStatus> — self-reported Menese partner status (last register ok/err ns, last remove ok ns, removed_total; all fields Option, Bound::Unbounded because it carries an upstream error String). Virgin bucket, SAFE (2026-07)
25SOLANA_GATEWAY_FEE_CONFIGStableCell<SolanaGatewayFeeConfig> — governance-configurable ICP→SOL gateway fee in bps (default 30, hard cap 1000). Replaces the hardcoded frontend BRIDGE_FEE_BPS. Virgin bucket, SAFE (2026-07)
26MENESE_TREASURY_ADDRESSStableCell<String> — cached Menese protocol fee-treasury address from Sovereign Send getFeeTreasuryAddress (a single global, Menese-controlled address, identical for every caller — NOT backend-derived; empty = never fetched). Virgin bucket, SAFE (2026-07)
27STANDALONE_TOKEN_UPDATE_TIMESStableBTreeMap<Principal, u64> — ledger canister id → last successful update_standalone_token ns timestamp; backs the 24h per-token creator-update cooldown. Virgin bucket, SAFE (2026-07 standalone token updatability)
28CLAIM_CREATED_ATStableBTreeMap<String, u64> — per-(campaign,user) deterministic CLAIM created_at_time (the claim-path sibling of id 39 REFUND_CREATED_AT): first attempt pins now, retries reuse it verbatim so an AMBIGUOUS claim transfer dedups at the token ledger (TxDuplicate→Ok in pool_manager) and the claimant self-heals by re-calling claim_tokens. Virgin bucket, SAFE (2026-07 reentrancy-audit remediation)
29CAMPAIGN_FAILED_ATStableBTreeMap<String, u64> — campaign_id → ns of the LAST backend-recorded transition into Failed (recorded by set_campaign_state_to_failed — incl. the frozen-LGE 30-day failsafe — the housekeeping classifier’s Failed branch, and admin_update_campaign_status). Anchors request_refund’s 7-day claim window and the residual sweep at max(recorded, expiry) + 7d, so campaigns failed AFTER expiry (resolved freeze, failsafe, late governance decision) still get a full claim window; absent → historical expiry + 7d unchanged. Overwrite-on-transition (latest failure wins). Virgin bucket, SAFE (2026-07 moderation corrections)
30USED_RELAUNCH_MEMOSStableBTreeMap<u64, u64>
31PENDING_RELAUNCH_COMMITSStableBTreeMap<String, StorableRelaunchUpdate>
36SHIELD_PREAUTH_MODEStableCell<u8> (relocated from 34)
37USED_POW_NONCESStableBTreeMap<UsedPowNonceKey, ()> (relocated from 32)
38BACKEND_WORKFLOW_RECORDSStableBTreeMap<String, StorableBackendWorkflowRecord> (relocated from 33; folded into the single main MEMORY_MANAGER)
39REFUND_CREATED_ATStableBTreeMap<String, u64> (F2(b) per-(campaign,user) deterministic refund created_at_time)
41WEDGED_FROZENStableBTreeMap<String, StorableWedgeRecord> — wedged-LGE marker (Invariant R / wedge-anchored sold-out failsafe, 2026-07): campaign_id → { first_wedged_at_ns (set-if-absent — re-wedges never push the failsafe deadline), attempts (failed automatic finalize retries), last_attempt_ns, reason (capped 200 chars) }. Written ONLY by finalize-failure freeze sites (rollback_finalizing_to_frozen_after_failure, the check_and_add_liquidity_if_needed freeze); cleared by a deliberate guardian/governance freeze and on terminal Completed/Failed commit. Decode-hardened sentinel first_wedged_at_ns = u64::MAX (a shape the failsafe predicate can NEVER fire from). Virgin bucket, SAFE

RESERVED — NEVER reuse: 7 (CAMPAIGN_ID_COUNTER), 32 (PENDING_TOKEN_PURCHASES), 33 (RECOVERY_QUEUE), 34 (PENDING_CLEANUP_CURSOR), 35 (RECENT_APPLIED_CONTRIBUTIONS), 40 (REFERRAL_REFUND_COVERED — removed in the FIX-2 treasury-cover removal, 2026-06). Each held a removed structure; the bucket still carries that structure’s stale bytes, so none of these ids may ever be rebound.

Next free id: 48+ (43–46 are SKIPPED, not free — they were bound by 3ef9dc2a on archive/pre-launch-admission-legal-hold, which is not an ancestor of master, and master has since rebound 41/42 to different structures; skipping costs nothing, being wrong is a mainnet decode trap no test catches. 47 AMBIGUOUS_TRANSFERS — 13a ambiguous/committed value-transfer journal (the ENFORCING half of the three-state outcome journal shipped in cb2866ca): StableBTreeMap<key_hex, StorableAmbiguousTransferRecord> holding the unresolved AND committed records that block an identical re-issue. Key = sha256 over (ledger, canonical source, recipient, recipient_subaccount, amount_e8s). Deliberately NOT the event log — a FIFO ring is fail-OPEN as an enforcement store (an aged-out record silently lifts the block). No eviction, no TTL; cleared only by DEFINITE_FAIL or one of the three governance levers. Virgin bucket, SAFE, 2026-08 (13a). For older ids: 42 CYCLE_HISTORY bound by the cycle-history observability feature, 2026-07 — sampled on the housekeeping tick, no new timer; 41 WEDGED_FROZEN bound by the wedged-LGE refund-guarantee work, 2026-07; 17 GUEST_FEE_CUSTODY_V2 + 18 GUEST_FEE_FORWARD_DUE bound by the OHSHII guest-fee v2 custody work, 2026-06; 19 TREASURY_LIQ_OPS + 20 TREASURY_LIQ_OP_COUNTER bound by the treasury-liquidity governance work, 2026-07; 21 FINALIZE_DRAIN_PINS bound by the LGE finalize LP-drain-pin hardening, 2026-07; 22 PENDING_POOL_JOBS bound by the LGE creation detached-pool work, 2026-07; 23–26 MENESE_VOUCHED_USERS / MENESE_PARTNER_STATUS / SOLANA_GATEWAY_FEE_CONFIG / MENESE_TREASURY_ADDRESS bound by the Menese Solana Gateway hardening, 2026-07; 27 STANDALONE_TOKEN_UPDATE_TIMES bound by the standalone token updatability work, 2026-07; 28 CLAIM_CREATED_AT bound by the reentrancy-audit claim-idempotency remediation, 2026-07; 29 CAMPAIGN_FAILED_AT bound by the Failed-transition refund-window anchor, 2026-07).

2. dao_storage (src/storage/src/memory.rs)

Section titled “2. dao_storage (src/storage/src/memory.rs)”

Currently bound: 0 CAMPAIGNS, 1 CONTRIBUTIONS, 2 TOKENS, 3 SPONSOR_INFO, 4 USER_SPONSORS, 5 WHALE_PASSES, 6 REFERRALS, 7 CAMPAIGN_BANNERS, 8 SPONSOR_CASE_INSENSITIVE_INDEX, 9 PRINCIPAL_SPONSOR_INDEX, 10 SPONSOR_USERS_INDEX, 11 CREATOR_TOKEN_INDEX, 12 CAMPAIGN_STATE_INDEX, 13 HIGHLIGHTED_CAMPAIGNS_INDEX, 14 TOKEN_ID_COUNTER, 15 CONTRIBUTION_ID_COUNTER, 16 SPONSOR_REGISTRATION_LIMIT, 17 RUGGYS_CHOICE_CACHE, 18 STORAGE_EVENT_LOG, 19 STORAGE_EVENT_COUNTER, 20 LEDGER_TO_CAMPAIGN_INDEX, 22 CONTRIBUTION_ID_INDEX, 24 CONTRIBUTORS_BY_CAMPAIGN, 25 SHIELD_PREAUTH_MODE (StableCell<u8> — bound on a virgin bucket, SAFE), 26 CONTRIBUTION_LIFECYCLE, 27 GUEST_FEE_PAID, 28 GUEST_FEE_AMOUNT (guest-fee gross snapshot for the failure refund, 2026-06), 29 GUEST_FEE_REFUNDED (per-(campaign,user) guest-fee refunded marker, 2026-06), 30 CAMPAIGN_SWEEP_DONE (per-campaign 7-day residual-ICP sweep marker, 2026-06), 31 CONTRIBUTIONS_OVERFLOW (StableBTreeMap<ContribRowKey, Contribution> — row-addressable overflow store for the contribution scalability migration, 2026-07; the authority for every contribution with seq >= frozen_len; key (campaign_id, seq) range-scannable per campaign via the derived Ord; value bound reuses Contribution’s Bounded{2000} with an append pre-flight; virgin bucket, SAFE), 32 CONTRIBUTION_OVERFLOW_COUNT (StableBTreeMap<String, u32> — per-campaign overflow row count → next_seq = frozen_len + count and the iteration bound; virgin bucket, SAFE), 33 USER_CAMPAIGN_TOKENS (StableBTreeMap<ContributorKey, u64> — per-(campaign,user) running sum of NON-REFUNDED overflow tokens for the O(log N) per-user VBP token cap; virgin bucket, SAFE).

RESERVED — NEVER reuse: 21 (DAO_MIGRATION_DONE, StableCell<u64>; migration code removed, const reserved), 23 (CONTRIB_INNER_MIGRATION_FLAG, one-shot flag; migration code removed, const reserved).

Next free id: 34+. (31 CONTRIBUTIONS_OVERFLOW + 32 CONTRIBUTION_OVERFLOW_COUNT + 33 USER_CAMPAIGN_TOKENS bound by the contribution scalability migration — the non-destructive inline-vec → row-store move, 2026-07.)

3. ohshii_governance / ONS (src/ohshii_governance/src/memory.rs)

Section titled “3. ohshii_governance / ONS (src/ohshii_governance/src/memory.rs)”

The most-migrated canister. A past proposals migration is the canonical correct pattern: it moved active maps to fresh high slots (PROPOSALS 0→43, EXECUTED_PROPOSALS 2→44, VOTER_HISTORY 3→46, WASM_CHUNKS 4→42) rather than reusing the vacated low ids — with an in-code comment “Using a fresh slot prevents reads against legacy/corrupted node layout.”

Currently bound (selected; full set in code): 1 PROPOSAL_COUNTER, 5 UPLOAD_SESSIONS, 6 SESSION_COUNTER, 7 EVENT_LOG, 8 TIMER_STATE, 9 EVENT_COUNTER, 10 PENDING_SELF_UPGRADE, 11 PROPOSAL_CREATION_FEE, 12 REJECTION_COST, 13 GUARDIAN_PRINCIPAL, 14 MODERATED_PROPOSAL_DESCRIPTIONS, 15 PENDING_DAPP_SELF_STEP (StableCell<StorablePendingDappSelfStep> — Candid Option<PendingDappSelfStep>, Unbounded, decode-hardened → None; virgin, SAFE; UpgradeDappBatch self-last step), 16 PENDING_ASSET_REVOKES (StableBTreeMap<StorableAssetRevokeKey, u64> — asset-Prepare revoke retry queue, key {of_principal, asset_target} natural dedup, value enqueued_at ns, Bound::Bounded{64}, decode-hardened → anonymous sentinel; virgin, SAFE), 40 DECODE_ERROR_COUNT, 41 LAST_DECODE_ERROR_TS, 42 WASM_CHUNKS, 43 PROPOSALS, 44 EXECUTED_PROPOSALS, 45 PROPOSAL_MAP_MIGRATION_STATE, 46 VOTER_HISTORY, 47 PROPOSAL_VOTES, 48 LAST_PUBLIC_TIMER_RESTART, 49 TIMER_RESTART_IN_PROGRESS, 50 PROPOSAL_RATE_LIMITS, 51 VOTING_PARAMS_CONFIG, 53 VERIFIED_USERS, 54 USED_NULLIFIERS, 55–58 World-ID, 59–69 DecideID, 70 ANY_METHOD_VERIFIED_USERS, 71–73/76–78 delegation, 74 REBUILD_STATE, 79 USED_POW_NONCES, 80 OVERRIDE_PROPOSAL_VOTES, 81 SHIELD_PREAUTH_MODE (StableCell<u8> — virgin bucket, SAFE), 82 OVERRIDE_FRESH_ATTESTED (StableBTreeMap<Vec<u8>, u64> — B1 override fresh-personhood marker, value = override_expiry_ns for GC; virgin, SAFE; holds TWO disjoint key shapes in the ONE bucket — the caller-driven SONS-routed path REUSED this id, adding NO new MemoryId: (a) the ONS-self marker "{proposal_id}:{voter}" (leading ASCII digit 0x30..=0x39) read by ONS vote(); (b) the DAO-DIMENSIONED marker [dao_len][dao][proposal_id:8be][voter] from make_override_marker_key (leading length-byte 0..=29) written by the caller-driven attest path and read by the did_attest_override query a SONS pulls — the leading-byte ranges can never overlap → zero collision even when dao == canister_self()), 83 OVERRIDE_PERSONHOOD_NULLIFIERS (StableBTreeMap<Vec<u8>, StorableOverrideNullifierClaim> — B1 one-human → one-ballot authority, key (dao, proposal_id, nullifier), value {voter, override_expiry_ns}; disjoint from 53/54; virgin, SAFE), 84 OVERRIDE_OUTCALL_GLOBAL_CAP (StableCell<u64> — A3 runtime-tunable canister-wide override-outcall cap, default 240/hr, floored on read AND write at OVERRIDE_OUTCALL_GLOBAL_CAP_FLOOR; virgin, SAFE), 87 NATIVE_BY_HOTKEY (StableBTreeMap<Vec<u8>, StorablePrincipal> — voting-hotkey reverse map hotkey → native, THE uniqueness + resolution authority; virgin, SAFE), 88 HOTKEYS_BY_NATIVE (StableBTreeMap<StorableHotkeyIndexKey, u8> — forward dual-index (native, hotkey), big-endian key codec cloned from StorableFollowerIndexKey; virgin, SAFE), 89 PENDING_HOTKEY (StableBTreeMap<Vec<u8>, StorablePendingHotkey> — two-step-consent pending requests hotkey → {native, requested_at_ns}, TTL-bounded/staleness-GC’d; virgin, SAFE), 17 DEFAULT_EXCLUDED_COUNTRIES (StableCell<StorableDefaultExcludedCountries> — platform-wide suggested-default excluded-countries list for regional participation eligibility, canonical ISO 3166-1 alpha-2 codes, seeded EMPTY, changed only via set_default_excluded_countries CriticalGovernanceOperation self-call; virgin bucket, SAFE), 18 PENDING_BATCH_DELETES (StableBTreeMap<StorableBatchDeleteKey, StorableBatchCleanupEntry> — event-driven asset-canister delete_batch retry queue; key {asset_target, batch_id_be} Bound::Bounded{64} decode-hardened → anonymous/empty sentinel; value {enqueued_at, attempts, next_attempt_at, opt proposal_id} fixed-layout Bounded{32} decode-hardened → retry-now default; enqueued on every terminal proposal transition that leaves a proposed batch behind, drained by process_pending_batch_deletes; virgin, SAFE), 19 PENDING_ORPHAN_PROBES (StableBTreeMap<StorableOrphanProbeKey, StorableBatchCleanupEntry> — orphan-batch discovery probes; key = asset target Bounded{32}; enqueued by the abandoned-session GC, drained by process_pending_orphan_probes; virgin, SAFE), 20 SURFACE_MODERATION (StableBTreeMap<Vec<u8>, StorableSurfaceModeration> — surface-moderation registry: which display fields OhShii’s own interfaces render for a subject. Key = one-byte kind tag + subject bytes (0x00 + principal for a token ledger, 0x01 + campaign id for an LGE / imported-DAO campaign) so the two keyspaces stay disjoint in the one bucket; value = the flag record (censor_logo/name/symbol/description/links, delisted, updated_at), Candid-encoded, Bound::Unbounded, decode-hardened fail-CLOSED to SurfaceModeration::all_censored(): a row exists only while at least one flag is set (an all-false write REMOVES it), so an undecodable row is positive evidence the subject is flagged and must not decode back to “clean”. Do not “fix” this to the usual empty-record default without first breaking that invariant. Written ONLY by guardian_set_surface_moderation / guardian_clear_surface_moderation (guardian-exclusive); an all-false write removes the row. Accepts any subject and mutates NO state of the subject; the flags never gate canister-side behaviour. Sibling of MemoryId 14 MODERATED_PROPOSAL_DESCRIPTIONS, deliberately a SEPARATE map. Virgin bucket, SAFE). 87/88/89 bound by the Voting Hotkey feature, 2026-07; aligned with SONS (same three ids). 17 bound by the regional participation eligibility feature, 2026-07. 18/19 bound by the event-driven asset-batch cleanup, 2026-07.

LEGACY-ACTIVE (bound; do NOT reuse, do NOT remove without care): 0 LEGACY_PROPOSALS, 2 LEGACY_EXECUTED_PROPOSALS — rebound from the 0→43/2→44 migration with the identical StableBTreeMap<u64, StorableProposal> type (Candid-compatible), read only by the one-time migration fn. Safe because the type is unchanged.

RESERVED — NEVER reuse: 3 (VOTER_HISTORY, abandoned by the proposals migration), 4 (WASM_CHUNKS, abandoned by the proposals migration — has the documented “fresh slot” comment), 52 (DAO_MIGRATION_FLAG, binding removed, const reserved).

93 DAO_CANISTER_REGISTRY (StableCell<StorableDaoCanisterRegistry> — the DAO-curated, DISPLAY-ONLY list of canisters ONS declares it operates, {written: bool, entries: Vec<ManagedCanister>}. Bound::Unbounded — a StableCell never consults Storable::BOUND, so the 30-entry cap is enforced by validate_dao_canister_registry at proposal creation AND execution, not here. Decode-hardened fail-SAFE to written: true (dao_canister_registry_decode_fallback), NOT to the virgin default: written: false on unreadable bytes would let the very next post_upgrade overwrite them with the platform seed — the VotingParamsConfig silent-config-loss class. Do NOT “simplify” it to Self::default(); two unit tests pin this. written is the never-reseed LATCH and is what makes a DAO-voted EMPTY registry permanent — the seed keys on it, never on entries.is_empty(). Seeded once from types::DAO_CANISTER_REGISTRY_SEED on init/post_upgrade. Virgin bucket, SAFE — and the virginity is load-bearing, not cosmetic: StableCell::init only calls from_bytes on a bucket that already carries the cell magic, so a recycled id would let another structure’s leftover bytes decide whether the registry gets seeded.)

Next free id: 21–39, 75, 85, 86, 95+. (94 AMBIGUOUS_TRANSFERS — 13a ambiguous/committed value-transfer journal (the ENFORCING half of the three-state outcome journal shipped in cb2866ca): StableBTreeMap<key_hex, StorableAmbiguousTransferRecord> holding the unresolved AND committed records that block an identical re-issue. Key = sha256 over (ledger, canonical source, recipient, recipient_subaccount, amount_e8s). Deliberately NOT the event log — a FIFO ring is fail-OPEN as an enforcement store (an aged-out record silently lifts the block). No eviction, no TTL; cleared only by DEFINITE_FAIL or one of the three governance levers. Virgin bucket, SAFE, 2026-08 (13a); allocated ABOVE the high-water mark for the same reason as 93 below, and kept aligned with SONS 94. 93 DAO_CANISTER_REGISTRY bound by the DAO canister registry feature, 2026-07 — aligned with SONS 93. Deliberate deviation from rule 2 (“new structures take the lowest never-used id”, which would say 21): this cell’s whole correctness argument rests on the bucket being virgin, so high-and-certain beats low-and-arguable, and the ONS/SONS layouts are kept aligned exactly as they were for 74/75, 90 and 91. Do not “correct” it back to 21.) (20 SURFACE_MODERATION bound by the surface-moderation registry, 2026-07; 82 OVERRIDE_FRESH_ATTESTED + 83 OVERRIDE_PERSONHOOD_NULLIFIERS + 84 OVERRIDE_OUTCALL_GLOBAL_CAP bound by the override fresh-personhood re-verification work, 2026-06; 87/88/89 bound by the Voting Hotkey feature, 2026-07; 17 DEFAULT_EXCLUDED_COUNTRIES bound by the regional participation eligibility feature, 2026-07; 18 PENDING_BATCH_DELETES + 19 PENDING_ORPHAN_PROBES bound by the event-driven asset-batch cleanup, 2026-07. 90 DEPLOYED_SOURCES is BOUND — a prior revision of this line wrongly listed 90 as free; it was already taken. 91 CYCLE_HISTORY_SELF + 92 CYCLE_HISTORY_OBSERVED bound by the cycle-history observability feature, 2026-07 — ONS keeps its OWN history AND persists the canister_status readings it already fetches for the timerless managed fleet.)

4. sons_governance / SONS (src/sons_governance/src/memory.rs) — DYNAMIC, per-campaign

Section titled “4. sons_governance / SONS (src/sons_governance/src/memory.rs) — DYNAMIC, per-campaign”

Currently bound (selected): 0 PROPOSALS … 20 UPLOAD_SESSIONS, 22 PENDING_SELF_UPGRADE, 23 DECODE_ERROR_COUNT, 24 LAST_DECODE_ERROR_TS, 25 WASM_CHUNKS (relocated from 21 because the bounds changed — move-to-fresh-id), 26 PROPOSAL_VOTES, 27–32 timer/fee/session, 34 PENDING_DAPP_SELF_STEP (StableCell<StorablePendingDappSelfStep> — Candid Option<PendingDappSelfStep>, Unbounded, decode-hardened → None; virgin, SAFE; UpgradeDappBatch self-last step), 71–78 delegation, 79 SONS_DELEGATION_REQUIRES_VERIFICATION, 80 PERMANENT_VERIFIED_USERS, 81 LGE_COMPLETED_LOCKED, 82 USED_POW_NONCES, 83 OVERRIDE_PROPOSAL_VOTES, 84 SHIELD_PREAUTH_MODE (StableCell<u8> — virgin, SAFE), 85 LAST_PULL_RECOVERY_TIMESTAMP, 86 OVERRIDE_FRESH_ATTESTED (StableBTreeMap<Vec<u8>, u64> — B1 SONS-local override fresh-personhood marker, key "<proposal_id>:<voter>", value = override_expiry_ns; the one-ballot nullifier authority lives on ONS, not here; virgin, SAFE), 35 PENDING_ASSET_REVOKES (StableBTreeMap<StorableRevokeKey, u64> — asset-Prepare revoke retry queue; key (of_principal, asset_target), value enqueued_at ns; virgin bucket, SAFE), 36 REFERRAL_CLAIMS (deferred referral — StableBTreeMap<Principal, StorableReferralClaim>, deferred-referral per-sponsor claims; Bounded 128; decode-hardened placeholder earned_e8s==0; virgin bucket, SAFE), 37 REFERRAL_COMPLETION_TS (deferred referral — StableCell<u64>, LGE completion ts = 7-day claim window start; virgin bucket, SAFE), 38 CUSTODY_OPS (per-lock custody isolation, 2026-07 — StableBTreeMap<String, StorableCustodyOp>, fund/sweep/refund queue keyed "F:{lock_id}"/"S:"/"R:", insert-if-absent preserves the pinned dedup key; Unbounded, decode-hardened placeholder lock_id __decode_error__ removed by the processor; virgin bucket, SAFE), 39 LOCK_SUBACCOUNTS (per-lock custody isolation, 2026-07 — StableBTreeMap<String, String>, APPEND-ONLY registry hex(subaccount 32B) → lock_id; treasury-integrity guard consumed by execute_treasury_withdraw; entries never removed; virgin bucket, SAFE), 87 NATIVE_BY_HOTKEY (voting-hotkey, 2026-07 — StableBTreeMap<Vec<u8>, StorablePrincipal>, reverse map hotkey→native = THE uniqueness + resolution authority; virgin bucket, SAFE), 88 HOTKEYS_BY_NATIVE (voting-hotkey, 2026-07 — StableBTreeMap<StorableHotkeyIndexKey, u8>, forward dual-index (native, hotkey) big-endian codec for O(log N + k) listing/cap; virgin bucket, SAFE), 89 PENDING_HOTKEY (voting-hotkey, 2026-07 — StableBTreeMap<Vec<u8>, StorablePendingHotkey>, two-step-consent pending requests hotkey→{native, requested_at_ns}, TTL-GC’d; Unbounded, decode-hardened; virgin bucket, SAFE), 42 PENDING_BATCH_DELETES (asset-batch cleanup port from ONS MemId 18, 2026-07 — StableBTreeMap<StorableBatchDeleteKey, StorableBatchCleanupEntry>, event-driven delete_batch retry queue keyed (asset_target, batch_id_be); Bounded 64/32; decode-hardened anonymous/empty sentinel; virgin bucket, SAFE), 43 PENDING_ORPHAN_PROBES (asset-batch cleanup port from ONS MemId 19, 2026-07 — StableBTreeMap<StorableOrphanProbeKey, StorableBatchCleanupEntry>, orphan-batch discovery-probe retry queue keyed asset_target; Bounded 32; decode-hardened; virgin bucket, SAFE).

RESERVED — NEVER reuse: 21 (former WASM_CHUNKS Unbounded; relocated to 25, abandoned bucket holds stale legacy chunk bytes on live per-campaign canisters), 33 (DAO_MIGRATION_FLAG, reserved, no live static).

93 DAO_CANISTER_REGISTRY (DAO canister registry, 2026-07 — StableCell<StorableDaoCanisterRegistry>, byte-compatible with the ONS cell at the SAME id. Same Bound::Unbounded + fail-SAFE written: true decode fallback. SONS gets NO platform seed (the platform knows nothing about a campaign DAO’s canisters beyond the intrinsic trio the panel reads certified), so written here only ever records “this DAO has decided at least once”; virgin bucket, SAFE).

Next free id: 44–70, 90, 92, 95+ (94 AMBIGUOUS_TRANSFERS — 13a ambiguous/committed value-transfer journal (the ENFORCING half of the three-state outcome journal shipped in cb2866ca): StableBTreeMap<key_hex, StorableAmbiguousTransferRecord> holding the unresolved AND committed records that block an identical re-issue. Key = sha256 over (ledger, canonical source, recipient, recipient_subaccount, amount_e8s). Deliberately NOT the event log — a FIFO ring is fail-OPEN as an enforcement store (an aged-out record silently lifts the block). No eviction, no TTL; cleared only by DEFINITE_FAIL or one of the three governance levers. Virgin bucket, SAFE, 2026-08 (13a); kept aligned with ONS 94. 93 DAO_CANISTER_REGISTRY bound by the DAO canister registry feature, 2026-07 — 92 intentionally SKIPPED so 93 matches ONS, ONS 92 being CYCLE_HISTORY_OBSERVED which SONS lacks: the same alignment rule that already made SONS skip 90 and 74/75. This also deviates from rule 2 on the ONS side; see the ONS section above for why, and do not “correct” either back to the lowest free id. 40 POOL_POSITIONS + 41 POOL_OP_INTENT bound by the treasury-liquidity governance work, 2026-07; 42 PENDING_BATCH_DELETES + 43 PENDING_ORPHAN_PROBES bound by the asset-batch cleanup port from ONS, 2026-07 — aligned with ONS MemIds 18/19; 87 NATIVE_BY_HOTKEY + 88 HOTKEYS_BY_NATIVE + 89 PENDING_HOTKEY bound by the Voting Hotkey feature, 2026-07 — aligned with ONS same three ids; 91 CYCLE_HISTORY_SELF bound by the cycle-history observability feature, 2026-07 — 90 intentionally SKIPPED so 91 matches ONS’s own self-history id, ONS 90 being DEPLOYED_SOURCES which SONS lacks). Ids 74 and 75 are intentionally SKIPPED in SONS to keep the ONS/SONS MemoryId layout aligned (ONS binds them for its VERIFIED_USERS-family slots, which SONS does not replicate) — do NOT allocate 74/75 in SONS even though they are per-canister-unbound. (Note: per-campaign SONS canisters are upgraded in place — apply the reserve rule to SONS wasm history exactly as for static canisters.)

Schema appends (no new MemoryIds): 2026-07 recipient-change feature — GovernanceLockRecord (LOCKS, id 10) gained 3 append-last Option<T> fields (recipient_change_count: Option<u32>, last_recipient_change_at: Option<u64>, recipient_permanent: Option<bool>, all #[serde(default)], mirrored as None in the fallback_lock_record decode placeholder). CreateLockArgs (Candid arg record, not persisted) gained a trailing recipient_permanent: opt bool.

5. pool_manager (src/pool_manager/src/memory.rs)

Section titled “5. pool_manager (src/pool_manager/src/memory.rs)”

Currently bound: 0–20 (fully append-only; POOLS, POOL_CANISTER_IDS, POSITION_IDS, POOL_TOKENS, CAMPAIGN_SUBACCOUNTS, CAMPAIGN_ACCOUNT_IDS, TOKEN_LEDGER_IDS, TOKEN_SYMBOLS, TOTAL_FEES_COLLECTED, FEE_DISTRIBUTIONS_COMPLETED, OPERATION_STATE, BACKEND_CANISTER, ADMIN_PRINCIPAL, COLLECTED_FEES_TOTAL, MIGRATION_LOG, MIGRATION_COMPLETED, CLOWN_APPROVALS_GIVEN, FEE_WITHDRAWALS, POOL_EVENT_LOG, POOL_EVENT_COUNTER, PASSCODE_PRICE).

RESERVED — NEVER reuse: 21 (DAO_MIGRATION_DONE, StableCell<u64>; migration code removed, const reserved).

Also bound (stale-line fix, 2026-07 audit): 22 MINT_ATTEMPTS, 23 REFERRAL_DRAIN_TS (F4 — per-campaign deterministic drain created_at_time), 24 REFERRAL_DRAIN_INITBAL (F4 — stable initial balance for the underfunded-clamp) — verified against src/pool_manager/src/memory.rs:63,70,78. Plus 25 TREASURY_POSITIONS (StableBTreeMap<String, StorableTreasuryPosition>), 26 TREASURY_OPS (StableBTreeMap<u64, StorableTreasuryOp>), 27 REMOVAL_OPS (StableBTreeMap<u64, StorableRemovalOp>), 28 REMOVAL_OP_COUNTER (StableCell<u64>) — treasury-liquidity governance work, 2026-07; and 29 POOL_CREATING_STARTED_AT (StableBTreeMap<String, u64>, campaign_id → ns when PoolState::Creating began; the 30-minute staleness escape that lets a wedged pool-creation retry, 2026-07 — src/pool_manager/src/memory.rs:105,506); and 30 REPATRIATIONS (StableBTreeMap<String, StorableRepatriationRecord> — wedged-LGE repatriation at-most-once records for repatriate_campaign_liquidity (PM MAIN → campaign custody subaccount, Invariant R “Failed implies refundable”); keyed campaign_id; the first call pins the fixed created_at_time_ns + amount_e8s + a sub-balance snapshot for the TooOld verify-before-repay delta probe; completed=true short-circuits retries; decode-hardened all-zero sentinel, fail-closed; virgin bucket, SAFE, 2026-07 wedged-LGE refund guarantee).

Next free id: 32 (31 AMBIGUOUS_TRANSFERS — 13a ambiguous/committed value-transfer journal (the ENFORCING half of the three-state outcome journal shipped in cb2866ca): StableBTreeMap<key_hex, StorableAmbiguousTransferRecord> holding the unresolved AND committed records that block an identical re-issue. Key = sha256 over (ledger, canonical source, recipient, recipient_subaccount, amount_e8s). Deliberately NOT the event log — a FIFO ring is fail-OPEN as an enforcement store (an aged-out record silently lifts the block). No eviction, no TTL; cleared only by DEFINITE_FAIL or one of the three governance levers. Virgin bucket, SAFE, 2026-08 (13a); pool_manager had NO outcome journal at all before 13a, so it builds the recording half AND the enforcing read, and its TWO withdrawals share ONE keyspace via the AccountIdentifier-hex canonical source. 25–28 bound by the treasury-liquidity governance work, 29 by the pool-creation staleness-escape hardening, 30 by the wedged-LGE refund-guarantee repatriation work, all 2026-07).


Locker repo canisters (canonical here; mirrored to dev-ohshii-locker/docs/MEMORY_ID_INVENTORY.md)

Section titled “Locker repo canisters (canonical here; mirrored to dev-ohshii-locker/docs/MEMORY_ID_INVENTORY.md)”

6. locker backend (dev-ohshii-locker/src/backend/src/memory.rs)

Section titled “6. locker backend (dev-ohshii-locker/src/backend/src/memory.rs)”

Currently bound: 0 SUPPORTED_TOKENS, 1 TRANSFER_MONITORS, 2 LOCK_CHECKS, 3 TEXT_LOGS, 4 CIRCUIT_BREAKERS, 5 CONFIG, 6 PENDING_RECLAIM_FIXUPS, 7 PENDING_UNLOCK_CONFIRMATIONS, 8 USED_POW_NONCES, 9 RECOVERED_ORPHAN_POSITIONS, 10 SHIELD_PREAUTH_MODE (StableCell<u8> — virgin, SAFE), 11 ORPHAN_PENDING_LP (StableBTreeMap<u128, OrphanPendingRecord> — pending-orphan LP journal, complement of id 9; appended on a virgin bucket, SAFE), 12 LOCK_CREATION_STATUS (StableCell<LockCreationStatus> — public lock-creation toggle, gates create_lock_v2; virgin, SAFE), 13 LP_SAGA_FUND_EVENTS (StableBTreeMap<u64, LpSagaFundEventRecord> — LP-lock saga fund-event journal; DEFINITE/AMBIGUOUS-classified money legs append here instead of a blind rollback, queryable via get_lp_saga_fund_events; virgin, SAFE), 14 LP_MINT_INTENTS (StableBTreeMap<StableString, LpMintIntentRecord> — write-ahead LP mint intent, written BEFORE the pool mint so a lost-response mint is recoverable; virgin, SAFE), 15 LP_SAGA_FUND_EVENT_SEQ (StableCell<u64> — monotonic id counter for LP_SAGA_FUND_EVENTS, ids never reused; virgin, SAFE), 16 ORPHAN_PENDING_LP_V2 (StableBTreeMap<StableString, OrphanPendingRecord> — composite (pool, position_id) orphan-pending keyspace; new writes land here, reads fall back to id 11; virgin, SAFE), 17 RECOVERED_ORPHAN_POSITIONS_V2 (StableBTreeMap<StableString, OrphanRecoveryRecord> — composite-key recovered-orphan dedup, complement of id 9; virgin, SAFE). Fully append-only; single MemoryManager. (13–17 all bound by the LP-lock saga hardening, 2026-07.) 18 RECOVERED_ORPHAN_LEGS (per-leg LP credit-idempotency marker). 19 CYCLE_HISTORY_SELF bound by the cycle-history observability feature, 2026-07 — sampled on the 60s process_unlocks tick, no new timer. RESERVED: none. Next free id: 20+.

7. lock_storage (dev-ohshii-locker/src/storage/src/stable_memory.rs)

Section titled “7. lock_storage (dev-ohshii-locker/src/storage/src/stable_memory.rs)”

Currently bound: 0 = MEMORY_MANAGER own bookkeeping id (not a data bucket), 1 LOCKS, 2 USER_LOCKS_INDEX, 3 TOKEN_LOCKS_INDEX, 5 UNLOCK_TIME_INDEX, 6 SUBACCOUNTS, 7 SUBACCOUNT_INDEX, 8 CONFIG, 10 STATUS_INDEX, 11 RETRY_QUEUE (bound but never populated), 12 CREATION_TIME_INDEX, 13 STORAGE_EVENT_LOG, 14 STORAGE_EVENT_COUNTER, 15 BACKED_UP_TOKENS, 16 TOKEN_BACKUP_COUNTER, 17 ARCHIVED_LOCKS, 18 ARCHIVED_USER_INDEX, 19 UNLOCK_SCHEDULES, 20 SHIELD_PREAUTH_MODE (StableCell<u8> — virgin, SAFE), 21 MIN_LOCK_DURATION_DAYS (StableCell<u64>, default 45 — ONS-synced VP-eligibility floor; virgin, SAFE). StorableLockRecord evolves only by append-last Option<T> fields.

RESERVED / virgin (never bound to a static): 4 (ACTIVE_LOCKS_INDEX_ID, const-only), 9 (TOKEN_CONFIG_ID, const-only). Next free id: 4, 9, 24+ (4 and 9 are virgin reserved consts — safest; id 21 is bound to MIN_LOCK_DURATION_DAYS; ids 22–23 are BOUND, 2026-07-15, to the cold-side derived state ARCHIVED_UNLOCK_AGGREGATES / ARCHIVED_LP_OWNERS).

⚠⚠ CORRECTED 2026-08-25 — this line read Next free id: 4, 9, 22–24 and was OFFERING TWO BOUND IDS AS FREE. Ground truth, re-derived from the locker source: ARCHIVED_UNLOCK_AGGREGATES_ID = MemoryId::new(22) and ARCHIVED_LP_OWNERS_ID = MemoryId::new(23) at dev-ohshii-locker/src/storage/src/stable_memory.rs:76-77 (negative control: MemoryId::new(99) → 0 hits, so the needle discriminates). The locker’s own copy of this inventory already said 24+ and named both bindings — so the two documents contradicted each other, and the wrong half was the one a launcher-side reader reaches first. Rebinding a live bucket is a mainnet decode trap that no test catches, which is exactly the failure this section exists to prevent.

ⓘ This section documents a canister that lives in the OTHER repo. When lock_storage’s memory map changes, BOTH inventories must move — nothing gates them against each other, and a divergence is invisible until someone binds an id.

proxy (dev-ohshii-locker/src/proxy/src/memory.rs): 0 TRANSFERS, 1 STABLE_LOGS, 2 CONFIG (gained a bare u64 pending_transfer_count with #[serde(default)] — VALUE-struct evolution on the same binding, not a collision; recomputed in post_upgrade). Next free: 3–6.

strongbox (dev-ohshii-locker/src/strongbox/src/memory.rs): 0 TRANSFER_LOGS, 1 AUTHORIZED_PROXIES (StableBTreeMap<Principal, ()> — bound on the formerly reserved-UNUSED LOG_COUNTER_ID id; SAFE because it was never bound to any structure (virgin bucket), unlike a removed-structure bucket which is reserved forever), 2 CONFIG. RESERVED: none (id 1’s reserved-unused status was resolved by the virgin-bucket bind). Next free: 3+.


  • Adding a structure: assign the lowest never-used id from the canister’s “Next free id” line. Update this document + the per-canister “Next free” line in the SAME commit.
  • Removing a structure: move its id to that canister’s RESERVED table with the reason. Leave a // RESERVED — never reuse: … comment at the const in memory.rs. Do NOT delete the const if other code still references it; if you delete it, the id is still reserved.
  • Relocating a structure (e.g. to escape a collision): bind it to a fresh never-used id, mark the old id RESERVED, and run the upgrade-path test.
  • Never: reuse a reserved id; construct a second MemoryManager on the same DefaultMemoryImpl; tighten a type or shrink a Bounded max_size on a live id; append a bare-T (non-Option) field to a persisted record.

Audit procedure (validate this inventory against the code)

Section titled “Audit procedure (validate this inventory against the code)”
Terminal window
# 1. Every MemoryId::new( site in both repos (must all map to a canister section above):
rg -n "MemoryId::new\(" --type rust src/ # run in each repo root
# 2. Per canister, full allocation history of its memory file (catches rebindings):
git -C <repo> log --all -p --follow -- <memory_file>
git -C <repo> log --all -p -S 'MemoryId::new(<N>)' -- <src_dir> # per suspicious id
# 3. Reserved-id check: confirm no RESERVED id is bound to a live static today.
# 4. Single-manager check: exactly ONE MemoryManager::init per canister.
rg -n "MemoryManager::init" --type rust src/