Logging System
Architecture
Section titled “Architecture”| Canister | MemId | Log type | Capacity / rotation | Size |
|---|---|---|---|---|
| Backend | 8,9 | CriticalLogEntry | 10K max, FIFO | ~2-5 MB |
| Proposal Mgr | 7 | EventRecord | 10K max, prune | ~2-5 MB |
| LGE Gov | 4,5 | EventRecord | 10K max, prune | ~2-5 MB |
| Pool Mgr | 18,19 | PoolEventRecord | 10K max, FIFO | ~2-5 MB |
| DAO Storage | 18,19 | StorageEventRecord | 10K 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).
Three-tier logging across all canisters:
| Tier | Persistence | Capacity | Purpose |
|---|---|---|---|
| Native IC Logs | 4KiB rolling buffer per canister | ~100 lines | Debug, diagnostics (transient, viewable via management canister fetch_canister_logs) |
| Heap Logs | Lost on upgrade | Variable per canister | Real-time campaign/operation tracking |
| Stable Memory Logs | Persistent across upgrades | 10,000 entries FIFO per canister | 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.
Stable Memory Logs — Per Canister
Section titled “Stable Memory Logs — Per Canister”Backend (CriticalLogEntry)
Section titled “Backend (CriticalLogEntry)”-
Storage:
StableBTreeMap(MemoryId 8), counter inStableCell(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 vialog_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:Code Written by Meaning Action ICP_RECEIVEDpurchase_tokens_icrc2aftericrc2_transfer_fromOk and BEFOREdao_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. Thecontribution_idis deterministic on(user, campaign, amount, client_nonce)and matchesdao_storage::Contribution.contribution_idonce 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:
finalizereconciles unforwarded escrows into the campaign sub-account, and a confirmed orphan can be returned by a DAOproposal_treasury_withdraw. TheICP_RECEIVEDlog is purely an audit/observability aid — anyone can match ablock/contribution_idagainstdao_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 heaplog_and_store_with_level(msg, level)— same with explicit levelstore_critical_log(entry)— direct stable insert (used from modules outside lib.rs, e.g. security.rs)log_security_event(type, details)— security audit withSECURITY_*error codes, persists to stableroute_to_heap_logs(msg, campaign_id)— routes to ICO_LOGS or RECENT_LOGS based on message prefix
API endpoints:
| Method | Signature | Access | Notes |
|---|---|---|---|
get_critical_logs | (limit: opt nat64) -> vec CriticalLogEntry | Public | All stable logs, optional limit |
get_critical_log_count | () -> nat64 | Public | Total stable log count |
get_public_logs | (limit: opt nat64) -> vec CriticalLogEntry | Public | Most recent N (default 50, max 1000) |
get_public_log_count | () -> nat64 | Public | Same as get_critical_log_count |
get_public_logs_paginated | (start: nat64, limit: nat64) -> (vec CriticalLogEntry, nat64) | Public | Paginated (max 1000/page), returns (entries, total) |
get_rate_limit_events | (limit: nat64) -> vec RateLimitEvent | Public | Heap storage, max 500 |
get_campaign_logs | (campaign_id: text, user: opt principal) -> vec IcoLogEntry | Public | Heap storage, max 500 per query |
OHSHII Governance (EventRecord + GovernanceEvent)
Section titled “OHSHII Governance (EventRecord + GovernanceEvent)”- Storage:
StableBTreeMap(MemoryId 7), counter inStableCell - 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
- Proposal lifecycle:
- Key function:
log_event(event)inmemory.rs
API endpoints:
| Method | Signature | Access | Notes |
|---|---|---|---|
get_events | (start: nat64, limit: nat64) -> vec EventRecord | Public | Paginated, max 1000/page |
get_events_count | () -> nat64 | Public | Total event count |
get_proposal_events | (proposal_id: nat64) -> vec EventRecord | Public | Events 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 inStableCell(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
- Proposal lifecycle:
- Key function:
add_event(event)inmemory.rs
API endpoints:
| Method | Signature | Access | Notes |
|---|---|---|---|
get_recent_events | (limit: nat64) -> vec EventRecord | Public | Most recent N, max 1000 |
get_events_count | () -> nat64 | Public | Total event count |
get_proposal_events | (proposal_id: nat64) -> vec EventRecord | Public | Events for one proposal, max 500 |
Pool Manager (PoolEventRecord + PoolEvent)
Section titled “Pool Manager (PoolEventRecord + PoolEvent)”- Storage:
StableBTreeMap(MemoryId 18), counter inStableCell(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)inmemory.rs - Migration log: Separate
StableBTreeMap(MemoryId 14) for upgrade migration messages, up to 1,000 entries
API endpoints:
| Method | Signature | Access | Notes |
|---|---|---|---|
get_pool_events | (start: nat64, limit: nat64) -> vec PoolEventRecord | Public | Paginated, max 1000/page |
get_recent_pool_events | (limit: opt nat64) -> vec PoolEventRecord | Public | Most recent N, max 1000 |
get_pool_events_count | () -> nat64 | Public | Total event count |
get_logs | (campaign_id: opt text) -> vec PoolCreationLog | Public | Heap, pool creation logs |
get_operation_logs | (campaign_id: text) -> vec OperationLog | Public | Stable (per-campaign), max 100/campaign |
clear_logs | () -> Result | Authorized | Update call, clears heap logs |
DAO Storage (StorageEventRecord + StorageEvent)
Section titled “DAO Storage (StorageEventRecord + StorageEvent)”- Storage:
StableBTreeMap(MemoryId 18), counter inStableCell(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: Stringfield capturing the caller principal - Key function:
log_storage_event(event)inmemory.rs
API endpoints:
| Method | Signature | Access | Notes |
|---|---|---|---|
get_storage_events_query | (start: nat64, limit: nat64) -> vec StorageEventRecord | Public | Paginated, max 1000/page |
get_recent_storage_events_query | (limit: opt nat64) -> vec StorageEventRecord | Public | Most recent N (default 100, max 1000) |
get_storage_events_count_query | () -> nat64 | Public | Total event count |
Heap Logs (lost on upgrade)
Section titled “Heap Logs (lost on upgrade)”Backend
Section titled “Backend”| Store | Capacity | TTL | Purpose |
|---|---|---|---|
ICO_LOGS | 100 entries/campaign, 200 campaign buckets max | 1 hour | Campaign operations (purchase, finalize, refund) |
RECENT_LOGS | 50 entries | None | Treasury/conversion operations |
TOKEN_CREATION_LOGS | 100 entries/user, 500 users max (LRU eviction) | None | Token creation progress |
RATE_LIMIT_EVENTS | 1,000 entries | None | DoS/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].
Pool Manager
Section titled “Pool Manager”| Store | Capacity | TTL | Purpose |
|---|---|---|---|
PoolCreationLog (in State) | No cap | None | Pool creation success/failure records |
OperationLog (per campaign) | 100 per campaign | None | Pool operation logs (fee collection, buyback, etc.) |
Log Level Strategy
Section titled “Log Level Strategy”Only significant events go to stable memory. This prevents filling the 10K buffer with routine noise.
| Level | What gets persisted | Examples |
|---|---|---|
| error | Always | Transfer failures, governance creation failures, inter-canister call errors |
| warn | Always | Low cycles, expired campaigns updated, security events, lock frozen |
| success | Operation milestones | Finalize completed, upgrade completed, lock unlocked, transfer sent |
| info | Non-repetitive milestones only | Admin 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)
Scalability & Log Duration Estimates
Section titled “Scalability & Log Duration Estimates”Rotation limits
Section titled “Rotation limits”| Canister | Max entries | Rotation strategy |
|---|---|---|
| Backend | 10,000 | Removes oldest 100 when limit hit |
| OHSHII Governance | 10,000 | Prunes IDs 1..=(current_id - 10000) |
| LGE Governance | 10,000 | Prunes IDs 1..=(current_id - 10000) |
| Pool Manager | 10,000 | FIFO, removes oldest when limit hit |
| DAO Storage | 10,000 | FIFO, removes oldest when limit hit |
Pessimistic daily volume estimates
Section titled “Pessimistic daily volume estimates”Assumes a busy day with active LGE campaigns, frequent proposals, pool operations, and admin actions.
| Canister | Pessimistic daily entries | Days until 10K full | Reasoning |
|---|---|---|---|
| Backend | ~300-500 | 20-33 days | Multiple active campaigns with purchases + finalization + security events + privileged operations + hourly housekeeping cleanup events |
| OHSHII Governance | ~50-150 | 66-200 days | Active proposal creation/voting/execution + canister management + timer summaries. No per-tick logs. |
| LGE Governance (per instance) | ~100-300 | 33-100 days | Active 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-80 | 125-500 days | Event-driven only (no timer). Pool creation, fee collection, buyback — all infrequent. |
| DAO Storage | ~10-50 | 200-1000 days | Event-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.
Memory overhead per canister
Section titled “Memory overhead per canister”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):
Resource Limit Notes Stable memory per canister 500 GiB Per-canister cap; the actually-usable amount is also bounded by the subnet’s shared memory capacity (see “Subnet capacity” below) Wasm heap (Wasm32) 4 GiB Hard 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 GiB The opt-in wasm64-unknown-unknowntarget (not used by OhShii); an ICP platform limit, expected to grow toward stable-memory capacity over timePer-message stable memory access 2 GiB read / 2 GiB write Per replicated message; upgrades: 8 GiB read/write Subnet capacity 2 TiB Shared 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
Frontend Monitoring
Section titled “Frontend Monitoring”SystemStatusPage (/system-status)
Section titled “SystemStatusPage (/system-status)”Public dashboard with tabs for:
- Backend Stable Memory Logs: Paginated
CriticalLogEntrylist with level filtering (error/warn/success/info) - Pool Manager Events: Paginated
PoolEventRecordlist - OHSHII Governance Events: Paginated
EventRecordlist with event type formatting - DAO Storage: Summary + paginated
StorageEventRecordlist - 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).
OnsVotingPage Governance Logs
Section titled “OnsVotingPage Governance Logs”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
See also
Section titled “See also”- 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-checktype-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.