Skip to content

Logging System

flowchart TD
Code["Canister Code<br/>log_and_store(msg)<br/>log_and_store_with_level(msg, level)<br/>log_event(event)"]
T1["Tier 1: Native IC Logs (println)<br/>4 KiB rolling buffer · ~100 lines<br/>Transient · fetch_canister_logs API"]
T2["Tier 2: Heap Logs (RefCell/thread_local)<br/>Lost on upgrade · variable capacity<br/>ICO_LOGS, RECENT_LOGS, TOKEN_CREATION_LOGS"]
T3["Tier 3: Stable Memory Logs (StableBTreeMap)<br/>Persistent across upgrades · 10K entries max (50K backend)<br/>FIFO rotation · public query API"]
Code --> T1
T1 --> T2
T2 -->|"significant events only"| T3
CanisterMemIdLog typeCapacity / rotationSize
Backend8,9CriticalLogEntry50K max (MAX_CRITICAL_LOGS), FIFO~10-25 MB
Proposal Mgr7EventRecord10K max, prune~2-5 MB
LGE Gov4,5EventRecord10K max, prune~2-5 MB
Pool Mgr18,19PoolEventRecord10K max, FIFO~2-5 MB
DAO Storage18,19StorageEventRecord10K max, FIFO~2-5 MB

Total stable log overhead: ~10-25 MB across all 5 canister types (< 0.001% of 500 GiB stable memory limit per canister).

flowchart TD
Call["Canister method call"]
LAS["log_and_store(msg)"]
P["println!(msg)"]
Pout["Native IC 4KiB buffer"]
Infer["infer log level<br/>extract error code"]
Inferout["error/warn/success/info<br/>TRANSFER_FAILED, SECURITY_*"]
Store["store_critical_log()<br/>(skip if stable mem >95%)"]
Storeout["StableBTreeMap (if level<br/>qualifies & mem available)"]
Route["route_to_heap_logs()<br/>(match on msg prefix)"]
Routeout["ICO_LOGS or RECENT_LOGS<br/>(based on [prefix] pattern)"]
Call --> LAS
LAS --> P --> Pout
LAS --> Infer --> Inferout
LAS --> Store --> Storeout
LAS --> Route --> Routeout
flowchart TD
subgraph SystemStatus["/system-status"]
BackendLogs["Backend Logs"]
PoolMgrEvents["Pool Mgr Events"]
ProposMgrEvents["Propos. Mgr Events"]
LGEStor["LGE Stor."]
RateLimit["Rate Limit"]
NativeICLogs["Native IC Logs"]
BackendLogs --> LGEStor
PoolMgrEvents --> RateLimit
ProposMgrEvents --> NativeICLogs
end
subgraph OnsVoting["/ons-voting (governance logs)"]
Summary["Summary"]
Stable["Stable"]
NativeIC["Native IC"]
end

Three-tier logging across all canisters:

TierPersistenceCapacityPurpose
Native IC Logs4KiB rolling buffer per canister~100 linesDebug, diagnostics (transient, viewable via management canister fetch_canister_logs)
Heap LogsLost on upgradeVariable per canisterReal-time campaign/operation tracking
Stable Memory LogsPersistent across upgrades10,000 entries FIFO per canister (50,000 on the backend)Audit trail, monitoring, DAO transparency

All stable log query endpoints are public (no admin restriction) for DAO transparency. Native IC logs are fetched via the IC management canister and are also publicly accessible.


  • Storage: StableBTreeMap (MemoryId 8), counter in StableCell (MemoryId 9)

  • Capacity: 50,000 entries max (MAX_CRITICAL_LOGS)

  • Rotation: Removes oldest 100 when limit hit

  • Safety: Skips storage if stable memory >95% full (degrades gracefully to println-only)

  • Timer: 300s (5 min) housekeeping cycle

  • Fields: timestamp, payment_memo, campaign_id, user_principal, message, error_code, log_level

  • Levels: error, warn, success, info (auto-inferred from message keywords, or explicit via log_and_store_with_level)

  • Error codes: Auto-extracted from message patterns (GOVERNANCE_CREATION_FAILED, TRANSFER_FAILED, SECURITY_*, etc.). The purchase flow emits one explicit forensic code:

    CodeWritten byMeaningAction
    ICP_RECEIVEDpurchase_tokens_icrc2 after icrc2_transfer_from Ok and BEFORE dao_storage.add_contribution_atomic (src/backend/src/purchase.rs)ICP has landed in the backend canister. Message body carries block=N amount_e8s=X contribution_id=Y. The contribution_id is deterministic on (user, campaign, amount, client_nonce) and matches dao_storage::Contribution.contribution_id once applied.None on the happy path — the entry is observability only. Recovery is fully on-chain (see below); there is no off-chain reconciler.

    Orphan recovery is on-chain. Purchases use per-contribution escrow custody, so a deposit that landed but never became a contribution is recoverable on-chain without any external job: finalize reconciles unforwarded escrows into the campaign sub-account, and a confirmed orphan can be returned by a DAO proposal_treasury_withdraw. The ICP_RECEIVED log is purely an audit/observability aid — anyone can match a block / contribution_id against dao_storage.lookup_contribution(contribution_id) (a free query) — it is not consumed by any off-chain server. On-chain escrow state is the source of truth; there is no off-chain reconciler (see WORKFLOWS.md and ARCHITECTURE.md).

  • Key functions (in lib.rs / memory.rs):

    • log_and_store(msg) — println + infer level + store stable + route to heap
    • log_and_store_with_level(msg, level) — same with explicit level
    • store_critical_log(entry) — direct stable insert (used from modules outside lib.rs, e.g. security.rs)
    • log_security_event(type, details) — security audit with SECURITY_* error codes, persists to stable
    • route_to_heap_logs(msg, campaign_id) — routes to ICO_LOGS or RECENT_LOGS based on message prefix

API endpoints:

MethodSignatureAccessNotes
get_critical_logs(limit: opt nat64) -> vec CriticalLogEntryPublicAll stable logs, optional limit
get_critical_log_count() -> nat64PublicTotal stable log count
get_public_logs(limit: opt nat64) -> vec CriticalLogEntryPublicMost recent N (default 50, max 3000)
get_public_log_count() -> nat64PublicSame as get_critical_log_count
get_public_logs_paginated(start: nat64, limit: nat64) -> (vec CriticalLogEntry, nat64)PublicPaginated (max 3000/page), returns (entries, total)
get_campaign_finalize_logs(campaign_id: text, limit: opt nat64) -> vec CriticalLogEntryPublicNewest-first finalization timeline for one campaign (filters the stable store to the [finalize_step] / [finalize_ico] / [post_finalization] tags; default 50, max 200)
get_finalize_drain_pin(campaign_id: text) -> opt FinalizeDrainPinPublicDurable finalize LP-drain pin: drained=true means the campaign-sub → pool_manager ICP leg provably landed
get_rate_limit_events(limit: nat64) -> vec RateLimitEventPublicHeap storage, max 500
get_campaign_logs(campaign_id: text, user: opt principal) -> vec IcoLogEntryPublicHeap storage, max 500 per query

Finalization step telemetry ([finalize_step]): every LGE finalization pass writes one stable entry per pipeline milestone — state flip to Finalizing, escrow reconcile, existing-position pre-gate, LP drain, pool creation / add-liquidity, pool_id persistence, position verification, post-finalization (vesting locks, position transfer, governance handoff), terminal Completed commit, rollback to Frozen, and the Failed flip — successes AND failures. Message format: [finalize_step] campaign_id: <id> step=<step> target=<canister> outcome=<ok|err> <detail>; campaign_id is set explicitly on the entry, and failures carry an error_code of FINALIZE_STEP_<STEP>. The frontend “LGE Finalization Status” box on the campaign page consumes this feed via get_campaign_finalize_logs.


OHSHII Governance (EventRecord + GovernanceEvent)

Section titled “OHSHII Governance (EventRecord + GovernanceEvent)”
  • Storage: StableBTreeMap (MemoryId 7), counter in StableCell
  • Capacity: 10,000 events max
  • Rotation: Prunes events with ID <= (current_id - 10000)
  • Timer: 300s (5 min) for proposal expiry, execution, refunds
  • Event types53 variants. The groups below are illustrative, not exhaustive; the authoritative enumeration is the GovernanceEvent type in the canonical .did, which is gated against the Rust enum so the two cannot drift.
    • Proposal lifecycle: ProposalCreated, VoteCast, ProposalApproved, ProposalExpired, ProposalRejected, ProposalExecutionStarted, ProposalExecutionFailed
    • Execution results: ChunksTransferredToStore, UpgradeCompleted, AssetUpgradeCompleted, AdminMethodExecuted, FeeCollected, CustomPoolCreated
    • Moderation: GuardianVeto, ProposalDescriptionModerationUpdated
    • Upload: UploadStarted, UploadCompleted, UploadSessionExpired
    • Operational: CanisterManagementAction, ControllerChanged, TimerCycleSummary, TransferResult, SystemLifecycle
  • Key function: log_event(event) in memory.rs

API endpoints:

MethodSignatureAccessNotes
get_events(start: nat64, limit: nat64) -> vec EventRecordPublicPaginated, max 1000/page
get_events_count() -> nat64PublicTotal event count
get_proposal_events(proposal_id: nat64) -> vec EventRecordPublicEvents for one proposal, max 500

LGE Governance (EventRecord + GovernanceEvent)

Section titled “LGE Governance (EventRecord + GovernanceEvent)”

One instance per LGE campaign (dynamic canister, not in dfx.json).

  • Storage: StableBTreeMap (MemoryId 4), counter in StableCell (MemoryId 5)
  • Capacity: 10,000 events max
  • Rotation: Prunes events with ID <= (current_id - 10000)
  • Timer: 60s (1 min) for lock unlocks + proposal checks (proposals checked every 5th cycle = 5 min)
  • Event types49 variants. The groups below are illustrative, not exhaustive; the authoritative enumeration is the GovernanceEvent type in that campaign’s canonical .did.
    • Proposal lifecycle: ProposalCreated, VoteCast, ProposalApproved, ProposalExpired, ProposalRejected, ProposalExecutionStarted, ProposalExecuted, ProposalExecutionFailed
    • Config: ConfigUpdatedByAdmin, ConfigUpdated, AdminAdded, AdminRemoved, GovernanceFinalized
    • Upload: UploadStarted, UploadCompleted, UploadSessionExpired
    • Execution results: UpgradeCompleted, ChunksTransferredToStore, AssetUpgradeCompleted
    • Operational: LockStateChanged, TreasuryOperation, TimerCycleSummary, ErrorOccurred, SystemLifecycle, CanisterManagementAction
  • Key function: add_event(event) in memory.rs

API endpoints:

MethodSignatureAccessNotes
get_recent_events(limit: nat64) -> vec EventRecordPublicMost recent N, max 1000
get_events_count() -> nat64PublicTotal event count
get_proposal_events(proposal_id: nat64) -> vec EventRecordPublicEvents for one proposal, max 500

Pool Manager (PoolEventRecord + PoolEvent)

Section titled “Pool Manager (PoolEventRecord + PoolEvent)”
  • Storage: StableBTreeMap (MemoryId 18), counter in StableCell (MemoryId 19)
  • Capacity: 10,000 events max (MAX_POOL_EVENTS)
  • Rotation: FIFO, removes oldest when limit hit
  • Timer: None (event-driven only — logs on pool creation, fee collection, buyback, etc.)
  • Event types21 variants: PoolCreated, FeesCollected, BuybackExecuted, FeeCollectionFailed, ProposalExecuted, LiquidityAdded, TransferCompleted, OhshiiSwapExecuted, AdminBuybackExecuted, CustomPoolCreated, CustomPoolLiquidityAdded, PoolStateResetForRelaunch, LedgerRelinked, PoolRedeployed, OperationStep, RateLimitSaturated, TreasuryLiquidityAdded, TreasuryLiquidityRemoved, CampaignPoolReconciled, PoolUnusedBalanceSwept, Error
  • Key function: log_pool_event(event) in memory.rs
  • Migration log: Separate StableBTreeMap (MemoryId 14) for upgrade migration messages, up to 1,000 entries

API endpoints:

MethodSignatureAccessNotes
get_pool_events(start: nat64, limit: nat64) -> vec PoolEventRecordPublicPaginated, max 1000/page
get_recent_pool_events(limit: opt nat64) -> vec PoolEventRecordPublicMost recent N, max 1000
get_pool_events_count() -> nat64PublicTotal event count
get_logs(campaign_id: opt text) -> vec PoolCreationLogPublicHeap, pool creation logs
get_operation_logs(campaign_id: text) -> vec OperationLogPublicHeap (lost on upgrade), max 100/campaign — do not rely on it for durable history; use get_pool_events instead
clear_logs() -> ResultAuthorizedUpdate call, clears heap logs

DAO Storage (StorageEventRecord + StorageEvent)

Section titled “DAO Storage (StorageEventRecord + StorageEvent)”
  • Storage: StableBTreeMap (MemoryId 18), counter in StableCell (MemoryId 19)
  • Capacity: 10,000 events max (MAX_STORAGE_EVENTS)
  • Rotation: FIFO, removes oldest when limit hit
  • Timer: None (event-driven only — logs on admin state changes, contribution modifications, etc.)
  • Event types8 variants: CampaignStateChanged, CampaignParamsUpdated, ContributionModified, CreatorUpdated, SponsorModified, TokenModified, RateLimitSaturated, Error
  • Admin tracking: All event types include an admin: String field capturing the caller principal
  • Key function: log_storage_event(event) in memory.rs

API endpoints:

MethodSignatureAccessNotes
get_storage_events_query(start: nat64, limit: nat64) -> vec StorageEventRecordPublicPaginated, max 1000/page
get_recent_storage_events_query(limit: opt nat64) -> vec StorageEventRecordPublicMost recent N (default 100, max 1000)
get_storage_events_count_query() -> nat64PublicTotal event count

StoreCapacityTTLPurpose
ICO_LOGS100 entries/campaign, 200 campaign buckets max1 hourCampaign operations (purchase, finalize, refund)
RECENT_LOGS50 entriesNoneTreasury/conversion operations
TOKEN_CREATION_LOGS100 entries/user, 500 users max (LRU eviction)NoneToken creation progress
RATE_LIMIT_EVENTS1,000 entriesNoneDoS/cycle-drain monitoring

Routing: route_to_heap_logs(msg, campaign_id) checks message prefix to decide which heap store receives the entry. Prefixes: [create_ico_campaign], [finalize_ico], [check_expired_icos], [refund_campaign], [purchase_tokens], [admin_upgrade_index, [admin_create_governance, [SECURITY], [post_upgrade].

StoreCapacityTTLPurpose
PoolCreationLog (in State)No capNonePool creation success/failure records
OperationLog (per campaign)100 per campaignNonePool operation logs (fee collection, buyback, etc.)

Only significant events go to stable memory. This prevents filling the 10K buffer with routine noise.

LevelWhat gets persistedExamples
errorAlwaysTransfer failures, governance creation failures, inter-canister call errors
warnAlwaysLow cycles, expired campaigns updated, security events, lock frozen
successOperation milestonesFinalize completed, upgrade completed, lock unlocked, transfer sent
infoNon-repetitive milestones onlyAdmin actions, lifecycle events (init, post_upgrade), config changes

What does NOT go to stable memory (native IC logs only):

  • Timer tick start/end messages (runs every 60s–300s = 288–1440/day)
  • “Processing N campaigns” / “Found N locks ready” (routine checks with no state change)
  • “Housekeeping cycle completed” with 0 actions
  • Debug diagnostics, query-only operations
  • Idle lock processing cycles (0 unlocks)

CanisterMax entriesRotation strategy
Backend10,000Removes oldest 100 when limit hit
OHSHII Governance10,000Prunes IDs 1..=(current_id - 10000)
LGE Governance10,000Prunes IDs 1..=(current_id - 10000)
Pool Manager10,000FIFO, removes oldest when limit hit
DAO Storage10,000FIFO, removes oldest when limit hit

Assumes a busy day with active LGE campaigns, frequent proposals, pool operations, and admin actions.

CanisterPessimistic daily entriesDays until 10K fullReasoning
Backend~300-50020-33 daysMultiple active campaigns with purchases + finalization + security events + privileged operations + hourly housekeeping cleanup events
OHSHII Governance~50-15066-200 daysActive proposal creation/voting/execution + canister management + timer summaries. No per-tick logs.
LGE Governance (per instance)~100-30033-100 daysActive lock unlock processing (30 locks/cycle × every 60s but only logged on actual unlock) + proposal lifecycle + treasury ops. Worst case: 300 locks unlocking over a few days.
Pool Manager~20-80125-500 daysEvent-driven only (no timer). Pool creation, fee collection, buyback — all infrequent.
DAO Storage~10-50200-1000 daysEvent-driven only. Campaign state changes, contribution modifications — mostly during active LGE periods.

Worst case (all canisters under sustained heavy load simultaneously): Backend fills in ~20 days, at which point FIFO rotation preserves the most recent 10K entries. This is acceptable — the 10K buffer provides a rolling window of the last 2-4 weeks of significant operations.

Each stable log entry is ~200-500 bytes serialized. At 10,000 entries:

  • ~2-5 MB per canister for the log StableBTreeMap
  • Well within the 500 GiB stable memory limit (typically <0.001% of capacity)

IC memory limits (per the canonical IC resource limits):

ResourceLimitNotes
Stable memory per canister500 GiBPer-canister cap; the actually-usable amount is also bounded by the subnet’s shared memory capacity (see “Subnet capacity” below)
Wasm heap (Wasm32)4 GiBHard limit of the 32-bit address space. OhShii canisters build to wasm32-unknown-unknown, so this 4 GiB heap ceiling is the one that applies to us.
Wasm heap (Wasm64)6 GiBThe opt-in wasm64-unknown-unknown target (not used by OhShii); an ICP platform limit, expected to grow toward stable-memory capacity over time
Per-message stable memory access2 GiB read / 2 GiB writePer replicated message; upgrades: 8 GiB read/write
Subnet capacity2 TiBShared across all canisters on the subnet

Our 10K-entry log buffers use ~2-5 MB each — negligible relative to any of these limits.

Sources: Canister Resource Limits, Canister Storage


Public dashboard with tabs for:

  • Backend Stable Memory Logs: Paginated CriticalLogEntry list with level filtering (error/warn/success/info)
  • Pool Manager Events: Paginated PoolEventRecord list
  • OHSHII Governance Events: Paginated EventRecord list with event type formatting
  • DAO Storage: Summary + paginated StorageEventRecord list
  • Rate Limit Events: Heap-based DoS monitoring events

Each tab supports Native IC Logs via the IC management canister fetch_canister_logs (4KiB rolling buffer per canister).

Expandable “Governance Canister Status & Logs” section with three sub-tabs:

  • Summary: Canister ID, total events count, loaded events count
  • Stable Events: Paginated event list with level filtering (error/warn/success/info), formatted per GovernanceEvent variant
  • Native IC Logs: Raw 4KiB rolling buffer from the management canister, sorted by timestamp descending

  • ARCHITECTURE.md — Canister map and governance scope table.
  • WORKFLOWS.md — Create LGE, token, vote, create/execute proposal, backend vs frontend upgrade, self-snapshot flows.
  • GOVERNANCE.md — ONS vs SONS, proposal categories, snapshot/restore.
  • OHSSHII_LOCKER_INTEGRATION.md — OhShii Locker token list and lock management via ONS proposals, SONS vesting vs Locker.
  • WORLD_ID_VERIFICATION.md — World ID voter verification: flow, privacy model, three-state config (not_configured / optional / required), threshold-ECDSA RP signatures, rate limits, governance controls and operator checklist.
  • FRONTEND.md — Frontend architecture: React + JSX + JSDoc @ts-check type-checking, the canister-types.js + utils/canister.js bridges, Candid wire conventions, auth providers, and the TypeScript version policy.
  • Deploy/VOTER_VERIFICATION_GUIDE.md — Verifying WASM hashes for DAO proposals.