Skip to content

Logging System

significant events only

Canister Code

log_and_store(msg)

log_and_store_with_level(msg, level)

log_event(event)

Tier 1: Native IC Logs (println)

4 KiB rolling buffer · ~100 lines

Transient · fetch_canister_logs API

Tier 2: Heap Logs (RefCell/thread_local)

Lost on upgrade · variable capacity

ICO_LOGS, RECENT_LOGS, TOKEN_CREATION_LOGS

Tier 3: Stable Memory Logs (StableBTreeMap)

Persistent across upgrades · 10K entries max

FIFO rotation · public query API

CanisterMemIdLog typeCapacity / rotationSize
Backend8,9CriticalLogEntry10K max, FIFO~2-5 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).

Canister method call

log_and_store(msg)

println!(msg)

Native IC 4KiB buffer

infer log level

extract error code

error/warn/success/info

TRANSFER_FAILED, SECURITY_*

store_critical_log()

(skip if stable mem >95%)

StableBTreeMap (if level

qualifies & mem available)

route_to_heap_logs()

(match on msg prefix)

ICO_LOGS or RECENT_LOGS

(based on [prefix] pattern)

/ons-voting (governance logs)

Summary

Stable

Native IC

/system-status

Backend Logs

LGE Stor.

Pool Mgr Events

Rate Limit

Propos. Mgr Events

Native IC Logs

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 canisterAudit 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: 10,000 entries max

  • 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 1000)
get_public_log_count() -> nat64PublicSame as get_critical_log_count
get_public_logs_paginated(start: nat64, limit: nat64) -> (vec CriticalLogEntry, nat64)PublicPaginated (max 1000/page), returns (entries, total)
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

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 types (23 variants):
    • 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 types (25 variants):
    • 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 types (12 variants): PoolCreated, FeesCollected, BuybackExecuted, FeeCollectionFailed, ProposalExecuted, LiquidityAdded, TransferCompleted, OhshiiSwapExecuted, AdminBuybackExecuted, CustomPoolCreated, CustomPoolLiquidityAdded, 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 OperationLogPublicStable (per-campaign), max 100/campaign
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 types (7 variants): CampaignStateChanged, CampaignParamsUpdated, ContributionModified, CreatorUpdated, SponsorModified, TokenModified, 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.