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-structures’ MemoryManager 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).
The hard rule (why this document exists)
Section titled “The hard rule (why this document exists)”ic-stable-structures’ MemoryManager 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:
- A removed MemoryId is RESERVED FOREVER. Never reassign it.
- New structures take the lowest NEVER-USED id (per the per-canister “next-free” line below), never a reserved/removed one.
- Never tighten a value/key type or shrink a
Boundedmax_sizeon an existing id (re-encodes old bytes — treat as a destructive migration). - New persisted record fields are
Option<T>, append-last (Candid tolerates missing trailingoptfields; bare-Tappends wipe config on upgrade). - One
MemoryManagerper canister. Never construct a secondMemoryManager::init(DefaultMemoryImpl::default())— two managers on the sameDefaultMemoryImplalias the same bucket table (a latent corruption hazard). - Any layout/MemoryId change requires an UPGRADE-PATH test (deploy the
current wasm → populate →
--mode upgradeto the new wasm → exercise every public query) as a deploy gate — unit tests and diff-reading cannot catch this class.
Launcher repo canisters
Section titled “Launcher repo canisters”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:
| id | static | type |
|---|---|---|
| 0 | LAUNCHED_TOKENS | StableBTreeMap<u64, LaunchedToken> |
| 1 | TOKEN_ID_COUNTER | StableCell<u64> |
| 2 | ICO_CREATION_STATUS | StableCell<FeatureStatus> |
| 3 | ICO_PARTICIPATION_STATUS | StableCell<FeatureStatus> |
| 4 | TOKEN_CREATION_STATUS | StableCell<FeatureStatus> |
| 5 | INDEX_CREATION_STATUS | StableCell<FeatureStatus> |
| 6 | SUBACCOUNT_COUNTER | StableCell<u32> |
| 8 | CRITICAL_LOGS | StableBTreeMap<u64, CriticalLogEntry> |
| 9 | CRITICAL_LOG_COUNTER | StableCell<u64> |
| 10 | LAST_PUBLIC_TIMER_RESTART | StableCell<u64> |
| 11 | TIMER_RESTART_IN_PROGRESS | StableCell<bool> |
| 12 | BURNING_FEE_CONFIG | StableCell<BurningFeeConfig> |
| 13 | ICO_FEE_CONFIG | StableCell<IcoFeeConfig> |
| 14 | LAST_HOUSEKEEPING_CHECK | StableCell<u64> |
| 15 | BITCOIN_GATEWAY_STATUS | StableCell<FeatureStatus> — Bitcoin cross-chain Gateway toggle (frontend-only enforcement) |
| 16 | SOLANA_GATEWAY_STATUS | StableCell<FeatureStatus> — Solana (Menese) Gateway toggle (also enforced in register_menese_user) |
| 17 | GUEST_FEE_CUSTODY_V2 | StableBTreeMap<String, ()> — OHSHII guest-fee v2 custody marker (campaign_id present ⇒ per-campaign sub, not MAIN); virgin bucket, SAFE |
| 18 | GUEST_FEE_FORWARD_DUE | StableBTreeMap<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 |
| 19 | TREASURY_LIQ_OPS | StableBTreeMap<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) |
| 20 | TREASURY_LIQ_OP_COUNTER | StableCell<u64> — monotonic id counter for TREASURY_LIQ_OPS; virgin bucket, SAFE (2026-07) |
| 21 | FINALIZE_DRAIN_PINS | StableBTreeMap<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) |
| 22 | PENDING_POOL_JOBS | StableBTreeMap<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) |
| 23 | MENESE_VOUCHED_USERS | StableBTreeMap<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) |
| 24 | MENESE_PARTNER_STATUS | StableCell<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) |
| 25 | SOLANA_GATEWAY_FEE_CONFIG | StableCell<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) |
| 26 | MENESE_TREASURY_ADDRESS | StableCell<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) |
| 27 | STANDALONE_TOKEN_UPDATE_TIMES | StableBTreeMap<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) |
| 28 | CLAIM_CREATED_AT | StableBTreeMap<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) |
| 29 | CAMPAIGN_FAILED_AT | StableBTreeMap<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) |
| 30 | USED_RELAUNCH_MEMOS | StableBTreeMap<u64, u64> |
| 31 | PENDING_RELAUNCH_COMMITS | StableBTreeMap<String, StorableRelaunchUpdate> |
| 36 | SHIELD_PREAUTH_MODE | StableCell<u8> (relocated from 34) |
| 37 | USED_POW_NONCES | StableBTreeMap<UsedPowNonceKey, ()> (relocated from 32) |
| 38 | BACKEND_WORKFLOW_RECORDS | StableBTreeMap<String, StorableBackendWorkflowRecord> (relocated from 33; folded into the single main MEMORY_MANAGER) |
| 39 | REFUND_CREATED_AT | StableBTreeMap<String, u64> (F2(b) per-(campaign,user) deterministic refund created_at_time) |
| 41 | WEDGED_FROZEN | StableBTreeMap<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–24and was OFFERING TWO BOUND IDS AS FREE. Ground truth, re-derived from the locker source:ARCHIVED_UNLOCK_AGGREGATES_ID = MemoryId::new(22)andARCHIVED_LP_OWNERS_ID = MemoryId::new(23)atdev-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 said24+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.
8. locker proxy + strongbox
Section titled “8. locker proxy + strongbox”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+.
Allocation policy
Section titled “Allocation policy”- 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 inmemory.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
MemoryManageron the sameDefaultMemoryImpl; tighten a type or shrink aBoundedmax_sizeon 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)”# 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/