Governance: ONS vs SONS
This document explains the two governance paths: ONS (OHSHII Governance, OhShii ecosystem) and SONS (LGE Governance, one DAO per LGE campaign), and lists all proposal types and what they do. For how these mechanisms add up to capture resistance — the threat model, the honest limits, and the comparison with the SNS — see Governance & Capture Resistance.
When to use which
Section titled “When to use which”| User choice | Governance canister | Token for voting | Scope of proposals |
|---|---|---|---|
| OHSHII (or “ONS”) | ohshii_governance (single fixed canister) | OHSHII token (quadratic voting on locked tokens) | Launcher canisters, OhShii Locker, the OHSHII token suite (ledger/index/archive), and the OHSHII DEX liquidity positions |
| Select a SONS token | That token’s sons_governance (one per campaign) | That LGE campaign token | Only that LGE: treasury, WASM, asset canister, pool |
The frontend uses a single Governance page (OnsVotingPage.jsx). The user selects the DAO via a dropdown: OHSHII (ohshii_governance) or one of the SONS tokens. The selected value sets governanceCanisterId: for OHSHII it is OHSHII_GOVERNANCE_CANISTER_ID; for SONS it is the campaign’s governance_canister_id from storage. All subsequent proposal list, create, vote, and execute calls go to that canister.
Failed campaigns and refunds are not governance actions: when a campaign fails (expires without selling out), contributors request refunds via the backend method request_refund within 7 days (contributors on campaigns with deferred referral custody (referral_deferred=Some(true)) reclaim ~95% — the net ~90% plus the held referral 5%, tier-independent; legacy pre-escrow campaigns refund ~90%; the creator stays ~70% in both cases). A Guest-tier contributor is also refunded the 30,000 OHSHII Guest fee on a failed campaign (idempotent, snapshotted at contribution time). See WORKFLOWS.md (Campaign failure and refunds).
Proposal flow diagram
Section titled “Proposal flow diagram”Two-axis voting model (ONS and SONS)
Section titled “Two-axis voting model (ONS and SONS)”Every proposal has a type (NORMAL or CRITICAL) and two possible closing
modes. The model is identical for ONS (ohshii_governance) and SONS
(sons_governance). The values below are the defaults; they remain
DAO-tunable via proposal (see Voting parameters and config via proposals).
| NORMAL proposal | CRITICAL proposal | |
|---|---|---|
| Voting window | Fixed 3 days (a guardian may shorten a NORMAL proposal it creates down to 1 day) | Fixed 7 days |
| Standard close (at deadline) | Quorum 250,000 VP + 15 voters, then simple majority > 50% (adopt > reject) | Quorum 350,000 VP + 20 voters, then ≥ 65% approval |
| Immediate approval (before deadline) | Disabled — a NORMAL proposal always runs its full window | Quorum 450,000 VP + 30 voters, ≥ 75% approval and a positive guardian vote |
| Immediate rejection | Disabled | Symmetric: 450,000 VP + 30 voters, approval ≤ 25% |
Which proposals are CRITICAL is decided by the code — see
is_critical_category / proposal_uses_critical_thresholds /
admin_method_call_is_critical (ONS) and is_critical_proposal /
is_critical_admin_method (SONS). A proposal runs the CRITICAL tier when any
of these holds:
- its category is critical (the upgrade categories,
CriticalGovernanceOperation,SetGuardian) — see the category list below; - its target is an add/replace-guardian proposal;
- it is an
ExecuteAdminMethodwhose raw call is in the per-method critical set — on ONS (admin_method_call_is_critical): the treasury / balance withdrawals, liquidity removal, ICPSwap position transfers, the treasury-funded liquidity operations (proposal_treasury_create_custom_pool,proposal_treasury_add_liquidity,proposal_retry_treasury_liquidity,proposal_remove_liquidity_to_treasury,proposal_retry_treasury_forward), OhShii Locker lock mutations (admin_update_lock_status/admin_delete_lock/admin_transfer_lock_recipient), and campaign deletion; plus two conditional cases —proposal_update_token_metadatawhen it targets the OHSHII token ledger, andadmin_update_ico_infowhen it edits the OhShii root campaigndao-0000000001. On SONS (is_critical_admin_method): the equivalentAdminMethods —TreasuryWithdraw,RemoveLiquidity,TransferLiquidityPosition,CreatePoolAndAddLiquidity,AddLiquidityToPool,ModifyLock,DeleteLock,TransferLockBeneficiary(plus the pre-existingTransferLockCreator,BatchSetFollow,UpgradeIndexCanister). Pool creation / liquidity addition run the critical tier because they move treasury funds in a single proposal — the old Standard tier was defensible only while funding them required a prior critical withdrawal.
These per-method criticals stay under the ExecuteAdminMethod category (they
target other canisters, so they cannot use the self-only
CriticalGovernanceOperation) but run the full CRITICAL tier — 7-day window,
higher quorum/approval, and guardian-vetoable.
Guardian-removal proposals keep their extra hardening: a hard 7-day window, never an immediate decision, and the guardian cannot vote on its own removal. Like every Critical proposal, removal is vetoable-but-overridable — the guardian may veto its own removal, but cannot block the community override that completes it.
Reading the per-category “Threshold” field (hardcoded vs votable)
Section titled “Reading the per-category “Threshold” field (hardcoded vs votable)”In the per-category lists below, the Threshold is just the tier — Standard or Critical. Two different things hide behind that word:
- Which tier a category uses is HARDCODED — it changes only with a canister
upgrade (
is_critical_category/proposal_uses_critical_thresholdson ONS,is_critical_proposalon SONS). So “Standard” / “Critical” below is the fixed classification, not a number. - The numeric values of each tier are DAO-VOTABLE DEFAULTS — quorum VP, voter counts, approval %, voting-window lengths and the veto-override bar live in stable memory and are changed by proposal (see Voting parameters and config via proposals), within fixed safety floors.
- Hardcoded safety floors no vote can lower: guardian-removal runs a hard 7-day window and can never pass immediately or be vetoed; the veto-override window can never drop below 1 day and its quorum / minimum-voters can never be zero.
The complete votable-vs-hardcoded inventory (defaults, floors, which proposal changes each) is in Voting parameters and config via proposals.
ONS (OHSHII Governance): all proposal categories and methods
Section titled “ONS (OHSHII Governance): all proposal categories and methods”OHSHII Governance uses proposal categories for the type of action, and for ExecuteAdminMethod it uses AdminMethodCallData (target canister + method name + Candid-encoded args). The following lists every category and the main executable methods.
The ProposalCategory enum defines 15 variants, but create_proposal (via
validate_category_target_pairing in ohshii_governance/src/lib.rs) rejects 5
legacy categories as retired (see “Retired categories” at the end of this
section). The 10 categories that can be created today (the 9 below plus
TreasuryCyclesWithdraw, the cycles-ledger treasury recharge — Standard tier,
not guardian-vetoable) each get a subsection below — scope, target, threshold,
and what it does. For how to read “Threshold”,
see Reading the per-category Threshold field.
Motion (ONS)
Section titled “Motion (ONS)”- Scope: community signal / discussion.
- Target:
Motion(text). - Threshold: Standard.
- What it does: records the vote; no on-chain execution.
UpgradeCanister (ONS)
Section titled “UpgradeCanister (ONS)”- Scope: upgrade the WASM of a Rust canister ONS controls (backend, dao_storage, pool_manager, ledger/index, …). Cannot target
ohshii_governanceitself — self-upgrades go throughUpgradeGovernanceCanister. - Target:
CanisterUpgrade(CanisterUpgradeData). - Threshold: Critical. Guardian-vetoable. (Every canister upgrade is a code change to a trusted component, so it clears the same bar as a governance self-upgrade. The category is kept distinct from
UpgradeGovernanceCanisteronly for the self-target validation, not for a threshold difference.) - What it does: validates the staged WASM hash + size (re-verified after install — TOCTOU), then
install_code/install_chunked_code.
UpgradeGovernanceCanister (ONS)
Section titled “UpgradeGovernanceCanister (ONS)”- Scope: upgrade the
ohshii_governanceWASM itself;target_canister_idMUST be the governance canister. - Target:
CanisterUpgrade(CanisterUpgradeData). - Threshold: Critical. Guardian-vetoable.
- What it does: self-upgrade through the
post_upgradehook.
UpgradeAssetCanister (ONS)
Section titled “UpgradeAssetCanister (ONS)”- Scope: commit a frontend (asset canister) batch.
- Target:
AssetUpgrade(AssetUpgradeData). - Threshold: Critical. Guardian-vetoable. (A frontend upgrade is a code/content change to a trusted surface, so it now clears the same critical bar as every other upgrade.)
- What it does:
validate_commit_proposed_batch(at creation and again at execution) thencommit_proposed_batch.
UpgradeCanisterBatch (ONS)
Section titled “UpgradeCanisterBatch (ONS)”- Scope: upgrade a token’s ledger suite — index, ledger, and archive canisters — in ONE proposal, executed sequentially in the canonical safe order Index → Ledger → Archive(s). The Ledger step is mandatory (it anchors the suite); the Index step is optional (an auxiliary canister a DAO may not maintain — including it is recommended when one exists); archives are proposer-supplied and explicit — there is no hidden expansion: an archive spawned after the vote ships in the next batch. Cannot target
ohshii_governanceitself. - Target:
CanisterUpgradeBatch(CanisterUpgradeBatchData)— an ordered list of 1–5CanisterUpgradeSteps, each carrying a target canister, a suite role (Index | Ledger | Archive), a WASM hash + size, an optional Candid-encoded upgrade arg, and its own upload session. - Threshold: Critical. Guardian-vetoable.
- What it does: validates the role order and suite shape at creation — with read-only suite probes (
archives()on the ledger,ledger_id()on the index) applied verify-when-verifiable: a probe that answers and contradicts the proposal rejects it; a probe the ledger flavor does not expose logs a warning and proceeds on the proposer’s explicit list. Stages one upload session per step, then executes sequentially with stop-on-first-error: an all-or-nothing controller pre-flight (one uncontrolled target means zero installs), per-step idempotent skip (live module hash already equals the step hash), hash revalidation (TOCTOU), install, post-install verify, and per-step progress persisted in the proposal’sbatch_step_results. No automatic rollback — ledger downgrades are unsupported upstream; a partial failure ends inExecutionFailedwith the per-step record, and recovery is fix-forward: a fresh batch proposal whose already-committed steps auto-skip. See WORKFLOWS.md §5.3 for the full flow.
UpgradeDappBatch (ONS)
Section titled “UpgradeDappBatch (ONS)”-
Scope: upgrade an arbitrary ordered set of canisters — 1–10 steps — in ONE proposal, executed verbatim in the order the proposer specifies (no canonical order; the chain validates step shape, not release semantics). Each step carries one target + one kind + one category + its own git provenance:
- kind —
code(a WASMinstall_code/install_chunked_code) orasset(a frontend asset-canistercommit_proposed_batch); - category — Core (a known OhShii canister: the launcher fleet — backend, dao_storage, pool_manager, the launcher frontend — the locker fleet, the quests canisters, and
ohshii_governanceitself) or Custom (any other principal, rendered with a warning in the proposal view); - provenance — per step (
git_repo_url+git_commit_hash+build_instructions, all required non-empty), because a dapp batch mixes artifacts from several repos.
The governance canister itself may be upgraded only as the FINAL step (and nothing may follow it). The ledger-suite batch (
UpgradeCanisterBatch) remains the specialized suite path;UpgradeDappBatchis the general dapp-release lane. - kind —
-
Target:
DappUpgradeBatch(DappUpgradeBatchData)— an ordered list ofDappUpgradeSteps (target + category + typed payload (WasmorAssetCommit) + per-step provenance). -
Threshold: Critical. Guardian-vetoable.
-
What it does: validates the shape at creation — step count (1–10), pairwise-distinct targets, self-step-last, per-payload shape, Core membership (target must be a known OhShii canister), per-step provenance, and for each asset step an
validate_commit_proposed_batchprobe on the target. Stages one upload session per code step (30-minute window; asset steps consume no session — their bytes already live on the target asset canister). Execution runs an all-or-nothing controller pre-flight over EVERY target — asset targets included (one uncontrolled target means zero installs and zero commits), then sequential, stop-on-first-error: code steps re-validate the staged hash (TOCTOU), install, and verify the module hash; asset steps re-validate thencommit_proposed_batch; per-step progress persists in the proposal’sbatch_step_results. Fix-forward recovery (no rollback): a fresh batch — committed code steps auto-skip by live module hash; asset steps never auto-skip (a committed batch is consumed). See WORKFLOWS.md §5.4 for the full flow.
ExecuteAdminMethod (ONS)
Section titled “ExecuteAdminMethod (ONS)”- Scope: call a named admin method on a canister ONS controls. The general-purpose lane, and the home of the actions the retired categories used to cover.
- Target:
AdminMethodCall(AdminMethodCallData)=target_canister_id+method_name+ Candid-encodedmethod_args+description. - Threshold: Standard by default, with a per-method CRITICAL set (
admin_method_call_is_critical): treasury / balance withdrawals (proposal_treasury_withdraw,proposal_pool_manager_withdraw,proposal_pm_withdraw,proposal_pm_withdraw_account_id), liquidity removal (proposal_remove_liquidity,proposal_remove_custom_pool_liquidity), ICPSwap position transfers (proposal_transfer_pool_position,proposal_transfer_custom_pool_position), the treasury-funded liquidity operations (proposal_treasury_create_custom_pool,proposal_treasury_add_liquidity,proposal_retry_treasury_liquidity,proposal_remove_liquidity_to_treasury,proposal_retry_treasury_forward), OhShii Locker lock mutations (admin_update_lock_status,admin_delete_lock,admin_transfer_lock_recipient), and campaign deletion (admin_delete_campaign) run the CRITICAL tier (7-day window, higher quorum/approval, guardian-vetoable) without leaving this category. Two methods are conditionally critical:proposal_update_token_metadatawhen the target ledger is the OHSHII token, andadmin_update_ico_infowhen it edits the root campaigndao-0000000001. Separately, a handful of self-targeted dangerous methods are redirected to CriticalGovernanceOperation; stopping the governance canister or the OHSHII ledger is hard-blocked; andremove_supported_tokenfor the OHSHII ledger /admin_list_token/proposal_create_custom_pool(deprecated — replaced by the treasury-funded path) are rejected outright. - What it does:
call_rawto the target method. This is not a positive allowlist — the canister enforces a blocklist + redirect + per-method critical tier (see ExecuteAdminMethod: allowed method names and targets below).
SetGuardian (ONS)
Section titled “SetGuardian (ONS)”- Scope: elect, replace, or remove the platform guardian.
- Target:
SetGuardian(SetGuardianData { new_guardian })— a non-anonymousSome(p)= add/replace;None/ anonymous = remove. - Threshold: Critical for add/replace (guardian-vetoable). Removal is hardened: a hardcoded 7-day window, never immediate, and the guardian cannot vote on its own removal; like every Critical proposal it is vetoable-but-overridable (see Guardian Overridable Veto).
- What it does: validates the principal (rejects anonymous, the governance canister, the management canister, and OhShii infra canisters), sets the guardian, then best-effort syncs the guardian caches on the SONS + non-ONS canisters.
CriticalGovernanceOperation (ONS)
Section titled “CriticalGovernanceOperation (ONS)”- Scope: the high-risk lane — controller add/remove and snapshot restore/delete targeting the governance canister, voting-parameter changes, and the centralized Shield kill switch. Cannot stop the governance canister.
- Target:
AdminMethodCall(..)restricted to an 11-method allowlist — the four self-management ops (admin_add_controller_to_canister,admin_remove_controller_from_canister,admin_restore_snapshot_with_workflow,admin_delete_canister_snapshot),set_voting_parameters, and the six voter-verification setters/revocations (set_world_id_config,admin_clear_world_id_verified,set_decideid_config,admin_clear_decideid_verified,set_decideid_mode,set_decideid_oidc_config) — orSetShieldPreauthMode(..). - Threshold: Critical. Guardian-vetoable.
- What it does: executes the allowlisted method, or fans the requested Shield pre-auth mode out to the 5 core canisters (backend, dao_storage, ohshii_governance, locker backend, lock_storage).
Retired ONS categories (rejected at creation)
Section titled “Retired ONS categories (rejected at creation)”UpdateLedger, ListTokenOnLocker, AddTokenController, ModifyLock, and
DeleteLock still exist in the enum but create_proposal rejects them with
“Proposal category … is retired and can no longer be used.” Their old actions
are now performed via ExecuteAdminMethod: token-ledger metadata via the
ledger’s admin methods, locker token listing via admin_list_token, and locker
lock edits via admin_update_lock_status / admin_delete_lock on the locker
backend (see the ExecuteAdminMethod tables below). Their legacy Campaign /
Token / Lock targets are no-ops in the executor.
ONS ExecuteAdminMethod: allowed method names and targets
Section titled “ONS ExecuteAdminMethod: allowed method names and targets”When the category is ExecuteAdminMethod, the proposal carries AdminMethodCallData: target_canister_id, method_name, method_args, description. In production, these actions are performed only when an ONS proposal is approved and executed (or not at all if no proposal path exists). In production these run only through an approved ONS proposal executed by ohshii_governance; any direct invocation is a bootstrap/test path and must not be relied on. The following methods are exposed in the Create Proposal form and validated at creation time.
Note — this is the form surface, not a canister allowlist. Under
ExecuteAdminMethodthe canister does not maintain a positive allowlist;create_proposalenforces a blocklist + redirect + per-method critical tier instead —admin_restore_snapshot_with_workflow,admin_delete_canister_snapshot,set_voting_parameters, and the six voter-verification setters/revocations (set_world_id_config,admin_clear_world_id_verified,set_decideid_config,admin_clear_decideid_verified,set_decideid_mode,set_decideid_oidc_config) are redirected to CriticalGovernanceOperation;admin_stop_canister/proposal_stop_canisterare hard-blocked on the governance canister and the OHSHII ledger;admin_add_controller_to_canister/admin_remove_controller_from_canistertargeting the governance canister itself must use CriticalGovernanceOperation; and the per-method critical set (admin_method_call_is_critical— withdrawals, liquidity removal, position transfers, lock mutations, campaign deletion) runs the critical tier IN-PLACE underExecuteAdminMethod. Only CriticalGovernanceOperation keeps a real 11-method allowlist (the two controller ops + the two snapshot ops +set_voting_parameters+ the six voter-verification methods). The tables below are therefore the methods surfaced in the UI, not the full set the canister would accept.
Backend (ohshii_launcher_backend):
| method_name | What it does |
|---|---|
admin_set_feature_statuses | Single multi-toggle method. Enable/disable any subset of the 7 platform feature toggles in one call — LGE creation, LGE participation, token creation, index canister creation, the Bitcoin Gateway and Solana Gateway (cross-chain), and OhShii Locker public lock creation — each with an optional popup message. The request carries one optional record per feature; only the provided ones change. The locker_lock_creation toggle is forwarded to the locker backend; it gates only the public create_lock_v2 path (not on-behalf finalization vesting, and never SONS-governance locks). This is the method the “Feature Toggles” proposal uses; the proposal UI shows a current→proposed diff. |
admin_set_ico_creation_status | (Legacy single-feature setter; retained for compatibility.) Enable/disable LGE creation platform-wide (with optional message). |
admin_set_ico_participation_status | (Legacy.) Enable/disable LGE participation. |
admin_set_token_creation_status | (Legacy.) Enable/disable token creation. |
admin_set_index_creation_status | (Legacy.) Enable/disable index canister creation. |
admin_update_campaign_status | Update campaign state (Active, Finalizing, Completed, Failed, Frozen) and/or tokens_sold. Completed is terminal and must be reached by the finalization flow; Finalizing is set (via an approved ONS proposal executed by ohshii_governance) to start/retry finalization. |
admin_update_ico_params | Update global LGE parameters. |
admin_update_ico_info | Update campaign info (description, website, telegram, etc.); guardians can also hide/unhide and freeze. |
admin_delete_campaign | Delete a campaign (cleanup; in production executed by ohshii_governance via an approved ONS proposal). |
admin_create_pool_early | Create ICPSwap pool for a campaign before normal finalization. |
admin_create_snapshot_with_workflow | Create a canister snapshot (orchestrated via backend). Target: any canister the backend can control. |
admin_restore_snapshot_with_workflow | Restore from snapshot. Not allowed under ExecuteAdminMethod; must use CriticalGovernanceOperation (any target). |
admin_delete_canister_snapshot | Delete a snapshot. Not allowed under ExecuteAdminMethod; must use CriticalGovernanceOperation. |
admin_add_controller_to_canister | Add a controller to a canister. If target is ohshii_governance, must use CriticalGovernanceOperation. |
admin_remove_controller_from_canister | Remove a controller. If target is ohshii_governance, must use CriticalGovernanceOperation. |
admin_set_burning_fee | Set the OHSHII burning fee amount and token limit threshold for LGE participation. |
proposal_treasury_create_custom_pool | Critical. Create a custom ICPSwap pool funded from the backend treasury: the backend moves the passcode reserve (and, when add_liquidity = true, the token amounts + fee buffers) to the pool_manager with ledger-deduplicated (pinned created_at_time) funding legs, then the pool_manager creates the pool and mints a full-range position owned by the pool_manager. Amounts always define the initial price; a public front-run of the pool creation degrades deterministically to add-liquidity on the existing pool. Every step is journaled on-chain; a failed op is resumed by proposal_retry_treasury_liquidity. |
proposal_treasury_add_liquidity | Critical. Add treasury liquidity to an EXISTING ICPSwap pool: funds move backend → pool_manager, the pool is verified against the ICPSwap factory (anti-lookalike check), and a new full-range position is minted per proposal and tracked in the pool_manager’s treasury-position registry. |
proposal_retry_treasury_liquidity | Critical. Idempotently resume a failed treasury create/add operation by op id: re-sends only the funding legs not yet confirmed (ledger dedup via the pinned created_at_time), then re-drives the pool_manager saga from its persisted phase journal. A retry against an op that actually completed returns success without moving funds. |
OhShii Locker backend (ohshii_locker_backend):
| method_name | What it does |
|---|---|
admin_update_lock_status | Update lock status (e.g. Active, Frozen, Cancelled). |
admin_delete_lock | Delete a lock. |
admin_transfer_lock_recipient | Change the beneficiary of a lock. |
admin_list_token | Add a token to the locker supported list (with metadata from ledger). |
admin_update_token | Update token config (e.g. min_lock_amount, logo). |
remove_supported_token | Remove a token from the supported list (locks become Cancelled; users can reclaim). OHSHII (7rrmx-tyaaa-aaaal-qsraq-cai) can never be removed — it is a permanent supported token; removing it would cancel every active OHSHII lock. The proposal is rejected for the OHSHII ledger at the frontend, at ONS proposal creation, and (authoritatively) in the locker backend. |
admin_retry_reclaim_fixup | Replay a journaled reclaim fixup entry. Used when reclaim_lp_lock successfully transferred a position but the subsequent storage update failed; this method CAS-updates the on-chain lock record to reconcile the audit trail. Takes a single lock_id: text argument. |
admin_clear_reclaim_fixup | Dismiss a journaled reclaim fixup entry that has already been reconciled offline. Takes a single lock_id: text argument. Does NOT perform any storage update. |
The reclaim fixup queue can be inspected via the admin_list_reclaim_fixups : () -> (vec ReclaimFixupEntry) query method on the locker backend. It is a query method, so it is not called via a governance proposal — it can be read directly by any caller in the locker’s is_admin() set, which includes the ONS OHSHII Governance canister.
Pool manager:
| method_name | What it does |
|---|---|
proposal_fee_collect_to_treasury | Collect ICPSwap fees for a campaign and send to OhShii backend treasury. |
proposal_fee_collect_and_buyback | Collect fees and perform an OHSHII buyback with slippage protection. |
proposal_pool_manager_withdraw | Withdraw ICP or an ICRC-1 token from the pool manager to a recipient. Supports an optional source subaccount (raw 32-byte) and an optional recipient subaccount. |
proposal_pm_withdraw_account_id | Withdraw ICP held under a legacy ICP AccountIdentifier (e.g. LGE deposit funds) by resolving the AccountIdentifier back to its raw subaccount, then transferring to a recipient. ICP-only; used when the source is a 64-hex AccountIdentifier rather than a raw 32-byte subaccount. |
proposal_remove_liquidity_to_treasury | Critical. Remove liquidity from a pool_manager-owned position (treasury-registry positions, or campaign positions only when the campaign is terminal) and forward both withdrawn assets to the backend treasury in the same operation. Withdraws move the exact recorded amounts (never a balance-driven sweep) and forward legs are fee-netted with pinned created_at_time. |
proposal_retry_treasury_forward | Critical. Idempotently re-drain the pending withdraw/forward legs of a removal operation by op id (same pinned-leg dedup model). |
admin_reconcile_campaign_pool | Standard. Backfill pool_manager’s campaign-keyed pool records (POOLS / POSITION_IDS / POOL_CANISTER_IDS / POOL_TOKENS / TOKEN_LEDGER_IDS / TOKEN_SYMBOLS) for a campaign whose ICPSwap pool + position exist on-chain but were never recorded (e.g. a legacy/externally-seeded pool like the OHSHII root pool). It discovers the pool from the ICPSwap factory (getPool on the campaign-token/ICP pair, 0.3% tier) and proves ownership via getUserPositionIdsByPrincipal before writing — it moves no funds and can only adopt a position pool_manager actually owns. After it runs, every campaign-keyed query (get_pool_state / get_position_id / get_pool_details / has_pool_position / campaign_has_liquidity_position) and every UI surface reading them become truthful. Also directly admin-callable for operators. |
Token standard requirement (pool create / add liquidity / swap). Every pool-creation, add-liquidity, and treasury-swap proposal — on both ONS and SONS — accepts only tokens that genuinely implement the ICRC standard they are used as. Each non-ICP token ledger is checked against its advertised
icrc1_supported_standards: ICRC-1 is required for every leg, and ICRC-2 is additionally required whenever that leg uses the approve/transferFromdeposit path (i.e. its standard is ICRC-2), so an ICRC-1-only token is still usable via the transfer+deposit path. The check runs fail-closed at execution before any funds move (backend / pool_manager / SONS) and is mirrored as a hard block at proposal creation in the Create Proposal form (the ledger inputs show a live metadata panel — symbol, decimals, fee, and ICRC-1/ICRC-2 support — and submission is blocked for a non-conforming token). The ICP ledger is exempt (ICPSwap always labels itICP). A swap whose pool-metadata input standard normalizes to neither ICRC-1 nor ICRC-2 is rejected rather than silently defaulting to the ICRC-2 path.Pool initialization uses the ledger’s own standard (ICRC-2 preferred). When a proposal creates a pool (as opposed to adding to an existing one), the standard the pool is initialized with for each non-ICP token is taken from the ledger’s advertised
icrc1_supported_standards, not from the proposal: a ledger that advertises both ICRC-1 and ICRC-2 initializes the pool as ICRC-2, one that advertises only ICRC-1 initializes as ICRC-1, and one that advertises neither is rejected. The same resolved standard drives both the pool’s registered standard and the deposit path, so they cannot disagree. Adding liquidity to an already-existing pool does not change its standard.
Withdrawal proposals — memo and source format. The withdrawal proposals (Treasury Token Withdrawal, Pool Manager Withdrawal, OHSHII Governance Withdrawal, and the SONS TreasuryWithdraw admin method) accept an optional memo. The memo is validated against the target ledger’s advertised maximum memo length (
icrc1:max_memo_length) — in the Create Proposal form and again before the on-chain transfer; when the ledger does not advertise a maximum, the memo field is disabled. The withdrawal source may be given as a raw 32-byte subaccount (reachable directly by all withdraw methods). A source given as a legacy ICP AccountIdentifier is reachable only viaproposal_pm_withdraw_account_idon the pool manager, which resolves it to the underlying subaccount; the Pool Manager Withdrawal proposal routes to it automatically when the source is an AccountIdentifier.
OHSHII governance (guardian):
| method_name | What it does |
|---|---|
admin_set_guardian_principal | Set or remove the guardian principal (also available as category SetGuardian). |
OHSHII governance (voter verification):
ohshii_governance ships with TWO independently-togglable proof-of-personhood methods. Either method’s enabled = true activates the vote gate; either method’s verification satisfies it. See docs/WORLD_ID_VERIFICATION.md and docs/DECIDEID_VERIFICATION.md for the full specs.
| method_name | What it does | Threshold |
|---|---|---|
set_world_id_config | Update the World ID gate (rp_id, action, required_identifier, enabled, rp_key_name). | Critical (→ CriticalGovernanceOperation) |
set_world_id_request_preset | Toggle what the World ID widget requests at proof time (V4Capable vs V3Legacy, stored in WorldIdConfig.request_preset, default V4Capable). Controls only the widget request shape — never what the canister accepts. | Standard |
set_world_id_daily_cap | Cap on register_world_id_verification calls per rolling 24h. 0 pauses. | Standard |
set_decideid_config | Update the DecideID gate (issuer canister, origin, credential_type, derivation_origin, enabled, presentation_max_age_seconds). Cannot enable while issuer is anonymous (Not Configured). | Critical (→ CriticalGovernanceOperation) |
set_decideid_mode | Switch the DecideID active mode (Vc on-canister VC popup vs Oidc OAuth redirect). | Critical (→ CriticalGovernanceOperation) |
set_decideid_oidc_config | Persist the public OIDC config (client_id, endpoint URLs, redirect URI, aud/iss). The client_secret has its own admin-only setter (never in a proposal body). | Critical (→ CriticalGovernanceOperation) |
set_decideid_daily_cap | Cap on register_decideid_verification calls per rolling 24h. 0 pauses. | Standard |
admin_clear_world_id_verified | Remove a principal’s World ID flag (escape hatch). | Critical (→ CriticalGovernanceOperation) |
admin_clear_world_id_nullifier | Free a spent World ID nullifier (incident response only — weakens sybil resistance if misused). | Standard |
admin_clear_decideid_verified | Remove a principal’s DecideID flag (escape hatch). | Critical (→ CriticalGovernanceOperation) |
admin_clear_decideid_presentation | Free a recorded JWT-VP digest (incident response only — weakens replay protection if misused). | Standard |
admin_reset_decideid_rate_limit | Drop the per-caller 5/hour register-attempt counter. opt principal clears one caller; null clears all. | Standard |
Governed v3/v4 request preset. set_world_id_request_preset flips a stored
WorldIdConfig.request_preset (default V4Capable) that controls only what the
World ID widget asks the user to produce — v4-capable proofs (World ID 4.0) or
the legacy v3 request shape. It never changes what the canister accepts:
the v4 /verify endpoint accepts legacy v3 proofs natively (the canister sends the
matching v3 wire shape), so v3 World ID holders keep verifying regardless of the
preset. The widget-side allow_legacy_proofs IDKit flag — a client widget flag,
not a verify-request-body field — stays on, which is what keeps v3 holders working.
The preset is a governance-controllable config: the setter is admin/self-callable
(is_admin_or_self_guard), so it can be flipped by a NORMAL-lane
ExecuteAdminMethod proposal once the DAO is autonomous, and by an authorized
admin during the pre-DAO transition phase. V4Capable is the steady state;
V3Legacy is a deliberate emergency narrowing-rollback only — it would otherwise
lock out new World App users who can produce only v4 proofs. The preset never
gates the canister’s accept path, so toggling it can never turn away an already-valid
verification.
The two methods are independent: enabling one does not affect the other; disabling one does not clear existing verifications via that method (verifications persist across method-disable, by design — see the load-bearing comment on is_any_method_verified in memory.rs).
Universal Participation Gate — combined behavior
The same gate applies to BOTH vote()
AND create_proposal() on ONS. The two functions duplicate the
following block (with a different action verb in the error message);
the canonical reference is vote():
let any_gate_required = world_id_cfg.enabled || decideid_cfg.enabled;if any_gate_required && !memory::is_any_method_verified(caller) { return Err("Voter verification required (World ID or DecideID) before voting on ONS proposals.");}create_proposal() returns "Proposer verification required (World ID or DecideID) before creating ONS proposals." instead — same gate,
different action verb so the frontend can disambiguate. Both messages
match the regex verification required that the frontend uses to
mount the in-form <VerificationCTA> banner.
For SONS DAOs the equivalent gate covers vote(), create_proposal(),
follow(), and set_accepts_followers(true), governed by the per-DAO
requires_verification flag (a CriticalGovernanceOperation
proposal can flip it). Verification storage is shared with ONS —
one verification unlocks every OhShii DAO. The SONS-side check uses a
bounded-wait (10s) ICC to ONS behind the V-4 breaker, with a HARD
INVARIANT on the vote() .await placement; the per-DAO flag is
exposed via requires_verification : () -> (bool) query.
The is_any_method_verified helper reads the verified-user sets (VERIFIED_USERS for World ID, DECIDEID_VERIFIED_USERS for DecideID) directly, NOT the per-method enabled flags. This makes the gate satisfy two invariants at the same time:
- OR satisfaction — a verification via either method satisfies the gate, regardless of which method’s flag is currently
true. A user who verified via World ID two months ago still passes a gate that today only has DecideID enabled. - Persistence on disable — turning a method off (Required → Optional → Not Configured) never clears existing verifications. Only the explicit admin/DAO escape hatches
admin_clear_world_id_verified/admin_clear_decideid_verifiedremove individual flags.
What happens when BOTH methods are disabled: the OR over the two enabled flags is false, so any_gate_required = false and the gate does not fire. ONS voting becomes open to any authenticated wallet — no verification required, no per-method check. Anonymous principals are still rejected by the reject_anonymous guard on vote(). All other voting rules (proposal status, vote rate limit, voting power) still apply.
WorldID .enabled | DecideID .enabled | ONS vote() for unverified caller |
|---|---|---|
| false | false | Allowed — gate is off, anyone authenticated can vote |
| true | false | Rejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS |
| false | true | Rejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS |
| true | true | Rejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS |
Operator note: a DAO that wants to halt voting WITHOUT lifting the sybil filter must leave at least one method enabled = true and use set_world_id_daily_cap(0) / set_decideid_daily_cap(0) to block new verifications. Setting both methods to enabled = false removes the sybil gate entirely; it does not pause voting.
Restore/delete snapshot, add/remove controller targeting the governance canister, and set_voting_parameters are only allowed under CriticalGovernanceOperation. Stopping the governance canister or the OHSHII ledger is never allowed.
Voting parameters and fee methods on ohshii_governance (ONS):
| Method | Category | Threshold | What it changes |
|---|---|---|---|
set_voting_parameters | CriticalGovernanceOperation (target = ohshii_governance) | Critical (see Two-axis voting model) | Quorum (standard/critical), approval %, min votes, voting durations, max VP per user, burn exemption and purchase limit params. All optional fields in VotingParamsUpdate; only provided fields are updated. |
set_proposal_creation_fee | ExecuteAdminMethod (target = ohshii_governance) | Standard | Fee in OHSHII tokens to create a proposal. Backend enforces ±50% change limit per execution. |
set_rejection_cost | ExecuteAdminMethod (target = ohshii_governance) | Standard | Tokens deducted from refund when a proposal is rejected/expired. Must be ≤ proposal_creation_fee; ±50% change limit. |
Current values are exposed via get_voting_parameters() (includes fees) and get_proposal_creation_fee() / get_rejection_cost().
Proposal fee refund policy (per outcome). The creation fee is snapshotted as fee_paid and returned by the resolution timer according to the proposal’s terminal outcome (amounts net of the ledger transfer fee, so they track whatever fee / rejection cost the DAO had voted at creation):
| Terminal outcome | Refund |
|---|---|
| Approved (Motion) · Executed · ExecutionFailed | Full (fee_paid − ledger_fee) — a proposal the DAO approved is refunded in full, including when its execution trapped |
Rejected · Expired (an Open proposal that missed quorum) | Partial (fee_paid − rejection_cost − ledger_fee) — see the rejection-cost destination below |
Abandoned paid upload (a PendingUpload proposal whose upload never completes and expires) | Forfeit — no refund; a FeeForfeited audit event is emitted |
Vetoed / VetoOverrideExpired are non-terminal: the timer finalizes a vetoed proposal to Approved (full) or Rejected (veto upheld → partial), so its refund follows that final outcome. A proposal stuck in Executing past a generous TTL (and not awaiting a live self-upgrade) is reaped to ExecutionFailed and refunded in full. Refunds are idempotent (a retry is an exact ledger Duplicate, never a second payment); the guardian can drain owed refunds out-of-band via force_process_refunds (see What the Guardian CAN do). Full flow + diagram: WORKFLOWS.md §5.5.
Where the withheld rejection_cost goes. On a rejected or expired proposal the proposer is refunded fee_paid − rejection_cost − ledger_fee; the rejection_cost portion is withheld. The destination differs by DAO:
- ONS (
ohshii_governance) forwards the withheldrejection_costto the OhShii backend treasury. - SONS (
sons_governance) has a single governance canister and no separate treasury, so the withheldrejection_costis retained in the governance canister — deducted from the refund and kept on-canister. It is not refunded and not forwarded anywhere. (Thefee_recipientconfig field exists but is not consumed by any SONS transfer path.)
SONS (LGE Governance): all proposal categories and admin methods
Section titled “SONS (LGE Governance): all proposal categories and admin methods”Each LGE has one sons_governance canister. Proposals are either a category (e.g. UpgradeCanister, ExecuteAdminMethod) or a ConfigChange. For ExecuteAdminMethod, the target is an AdminMethod variant and parameters are in AdminMethodParams.
The 10 SONS categories each get a subsection below. For how to read “Threshold”, see Reading the per-category Threshold field. Unlike ONS, no SONS category is retired — all 10 are live.
Motion (SONS)
Section titled “Motion (SONS)”- Scope: community signal / discussion for one LGE campaign.
- Target:
None. - Threshold: Standard.
- What it does: records the vote; no execution.
UpdateCampaign (SONS)
Section titled “UpdateCampaign (SONS)”- Scope: a motion about changing campaign info (description, website, socials).
- Target:
None. - Threshold: Standard.
- What it does: signal-only — no execution. The actual on-chain write is the
UpdateCampaignInfoadmin method (under ExecuteAdminMethod).
ExecuteAdminMethod (SONS)
Section titled “ExecuteAdminMethod (SONS)”- Scope: run one of the 30
AdminMethodactions (grouped by area below). - Target:
AdminMethod(AdminMethodParams)— the variant selects the action; the flatAdminMethodParamscarries its fields. - Threshold: Standard, except the methods marked ★ (and self-targeting controller ops / snapshot restore-delete) which escalate to Critical.
- What it does: dispatches to the executor for the chosen
AdminMethod(30-arm match; all implemented).
UpgradeCanister (SONS)
Section titled “UpgradeCanister (SONS)”- Scope: upgrade a child canister this DAO controls (ledger, index, …). Must not target the governance canister — self-upgrades go through
UpgradeGovernanceCanister. - Target:
CanisterUpgrade(CanisterUpgradeData). - Threshold: Critical. Guardian-vetoable. (Every canister upgrade is a code change to a trusted component; the category is kept distinct from
UpgradeGovernanceCanisteronly for the self-target validation, not for a threshold difference.) - What it does: validates the WASM hash (re-checked post-install) then
install_code.
ConfigChange (SONS)
Section titled “ConfigChange (SONS)”- Scope: the non-critical config lane — fee fields only:
proposal_creation_feeand/orrejection_cost(±50% per proposal; at least one required). - Target:
ConfigUpdate(ConfigUpdateData)carrying only fee fields. - Threshold: Standard.
- What it does: applies the fee deltas.
fee_recipientis not proposal-configurable (would break refund flows). Voting / runtime / delegation params are NOT allowed here — they go through CriticalGovernanceOperation.
UpgradeGovernanceCanister (SONS)
Section titled “UpgradeGovernanceCanister (SONS)”- Scope: self-upgrade this
sons_governanceWASM (target MUST be itself). - Target:
CanisterUpgrade(CanisterUpgradeData). - Threshold: Critical. Guardian-vetoable.
- What it does: self-upgrade through the
post_upgradehook.
CriticalGovernanceOperation (SONS)
Section titled “CriticalGovernanceOperation (SONS)”- Scope: the high-risk lane — voting / runtime / delegation config changes, plus the admin methods that must be critical (AddController / RemoveController targeting the governance canister, RestoreSnapshot, DeleteSnapshot). Carries the Democratic Emergency Exemption (below).
- Target:
AdminMethod(..)orConfigUpdate(..)carrying the voting / runtime / delegation fields (incl.requires_verification). - Threshold: Critical. Guardian-vetoable.
- What it does: runs the critical admin method, or applies the critical config update.
UpgradeAssetCanister (SONS)
Section titled “UpgradeAssetCanister (SONS)”- Scope: commit a frontend (asset canister) batch for the campaign frontend.
- Target:
AssetUpgrade(AssetUpgradeData). - Threshold: Critical. Guardian-vetoable. (A frontend upgrade is a code/content change to a trusted surface, so it now clears the same critical bar as every other upgrade.)
- What it does:
validate_commit_proposed_batch(create + execute) thencommit_proposed_batch.
UpgradeCanisterBatch (SONS)
Section titled “UpgradeCanisterBatch (SONS)”- Scope: upgrade this campaign token’s ledger suite (index + ledger + archives) in ONE proposal, in the canonical safe order Index → Ledger → Archive(s). Every step is validated against the DAO’s managed canisters: the Ledger step (mandatory) must be the campaign ledger, the Index step (optional) must be the configured index canister, and archive steps must appear in the ledger’s own
archives()list — campaign ledgers are launcher-created stock ICRC ledgers, so a failing archive probe is treated as a real anomaly and rejects the proposal (stricter than the ONS verify-when-verifiable stance). Must not target the governance canister. - Target:
CanisterUpgradeBatch(CanisterUpgradeBatchData)— an ordered list of 1–5CanisterUpgradeSteps, same shape as ONS (target + role + WASM hash/size + optional upgrade arg + upload session). - Threshold: Critical. Guardian-vetoable.
- What it does: same execution model as the ONS block — sequential, stop-on-first-error, all-or-nothing controller pre-flight, per-step idempotent skip, TOCTOU hash revalidation, post-install verify, progress persisted in
batch_step_results, and fix-forward recovery via a fresh batch (no automatic rollback). The single-indexUpgradeIndexCanisteradmin method remains available for index-only maintenance. See WORKFLOWS.md §5.3.
UpgradeDappBatch (SONS)
Section titled “UpgradeDappBatch (SONS)”- Scope: upgrade an arbitrary ordered set of canisters — 1–10 steps — in ONE proposal, executed verbatim in proposer order (shape validated, not release semantics). Each step carries one target + one kind (
codeWASM install /assetfrontend-asset commit) + one category + its own git provenance (git_repo_url+git_commit_hash+build_instructions, all required non-empty). The SONS delta is the Core definition: Core is the campaign’ssons_governancecanister itself ONLY — every other target is Custom (rendered with a warning). The governance canister itself may be upgraded only as the FINAL step. The ledger-suite batch (UpgradeCanisterBatch) remains the specialized suite path. - Target:
DappUpgradeBatch(DappUpgradeBatchData)— an ordered list ofDappUpgradeSteps (target + category + typed payload + per-step provenance). The SONS code payload omits the ONS-only chunk-store fields (the chunk store is always the target itself). - Threshold: Critical. Guardian-vetoable.
- What it does: identical to the ONS block — validates the shape at creation (step count 1–10, distinct targets, self-step-last, per-payload shape, Core = self-only, per-step provenance, per-asset-step
validate_commit_proposed_batch); stages one upload session per code step (30-minute window; asset steps consume none); executes with an all-or-nothing controller pre-flight over EVERY target — asset targets included, then sequential stop-on-first-error: code steps re-validate + install + verify the module hash, asset steps re-validate +commit_proposed_batch, per-step progress inbatch_step_results. Fix-forward recovery (no rollback): committed code steps auto-skip by live module hash; asset steps never auto-skip. See WORKFLOWS.md §5.4.
SONS AdminMethod (under ExecuteAdminMethod) — by area
Section titled “SONS AdminMethod (under ExecuteAdminMethod) — by area”All 30 AdminMethod actions run through the ExecuteAdminMethod category
(target AdminMethod(AdminMethodParams)), except where the Tier column says
Critical — those are forced through CriticalGovernanceOperation. ★ = critical
via is_critical_admin_method.
LP, pool & treasury
| AdminMethod | What it does | Tier |
|---|---|---|
| TransferLiquidityPosition ★ | Transfer an ICPSwap liquidity position to a new owner (transferPosition). Targets the legacy campaign position by default, or a specific registry position via the optional pool_canister_id + pool_position_id pair (ownership re-verified at execution against the registry ∪ the pool’s live position list). On the legacy path a successful transfer clears the stored position_id. | Critical |
| UpdatePositionId | Update the stored position_id (e.g. after migration). | Standard |
| CollectFees | Collect ICPSwap trading fees into the treasury (100%). Supports the same optional registry-position targeting. | Standard |
| CollectFeesAndBuyback | Collect fees, then buy back a token with the collected ICP (slippage-protected). | Standard |
| CreatePoolAndAddLiquidity ★ | Create a custom ICPSwap pool and add full-range liquidity, funded from the SONS treasury (main account). Fee tier is restricted to 3000 (0.3%); deposits use exact approvals and the mint consumes the actually-credited amounts; the new position is recorded in the DAO’s pool-position registry (get_pool_positions), and the primary PoolInfo slot is only set when it was empty (the LGE pool reference is never repointed). | Critical |
| AddLiquidityToPool ★ | Add treasury liquidity to an EXISTING ICPSwap pool: the pool is verified against the ICPSwap factory (anti-lookalike check), amounts are given per token ledger and mapped to the pool’s token order, and a new full-range position is minted per proposal and recorded in the pool-position registry. The spendable-balance check nets out pending per-lock custody funding on the campaign token so lock-earmarked funds are never consumed. | Critical |
| SweepPoolUnusedBalance | Recover stranded deposits (a failed pool operation’s unused balance) from an ICPSwap pool back to the SONS treasury (main account). Idempotent; funds can only land on the DAO’s own main account. | Standard |
| RemoveLiquidity ★ | Remove liquidity from the position (tokens return to treasury; None = remove all). Targets the legacy campaign position by default, or a specific registry position via pool_canister_id + pool_position_id. | Critical |
| TreasurySwap | Swap treasury tokens via ICPSwap with slippage protection. | Standard |
| TreasuryWithdraw ★ | Withdraw ICP / ICRC-1 from the SONS treasury to a recipient — from the main account or an optional raw 32-byte source subaccount (and an optional recipient subaccount). Custody isolation: a source_subaccount that names any per-lock custody subaccount (append-only registry) is rejected at proposal creation AND at execution — lock-backing funds can never be withdrawn as treasury. An optional memo is validated against the target ledger’s advertised maximum memo length (icrc1:max_memo_length) before the transfer; disabled when the ledger does not advertise one. | Critical |
| CyclesTopup | Top up a canister with cycles using treasury ICP (via the CMC). | Standard |
Vesting locks
| AdminMethod | What it does | Tier |
|---|---|---|
| ModifyLock ★ | Change a vesting lock’s status (valid transitions only). For isolated locks: a forced Completed queues a balance-driven sweep of the lock’s custody subaccount back to the treasury; Cancelled leaves the residual IN the subaccount, claimable by the lock’s creator via claim_cancelled_lock (locker-product cancel-then-reclaim parity). | Critical |
| DeleteLock ★ | Remove a vesting lock from governance storage. For isolated locks, the un-vested remainder in the lock’s custody subaccount is swept back to the treasury (main account) by a queued custody op; for legacy locks the tokens simply remain commingled in the main account (unchanged). To let the user reclaim instead, Cancel (ModifyLock) — a Delete after a Cancel forfeits the pending claim to the treasury. | Critical |
| TransferLockBeneficiary ★ | Change who receives future unlocks (beneficiary). State-only — the lock’s custody subaccount is derived from the lock_id and never moves. | Critical |
| TransferLockCreator ★ | Batch-reassign the lock CREATOR (voting-power holder); reads creator_transfers. Already-cast votes are NOT retroactively updated; new creators do NOT inherit follow/delegation state; verification is action-time. Batch capped at MAX_BATCH_CREATOR_TRANSFERS; 2-pass validate-then-apply. State-only — the lock’s custody subaccount is derived from the lock_id and never moves. | Critical |
Per-lock custody isolation (2026-07)
Section titled “Per-lock custody isolation (2026-07)”Every non-LP lock created after this change custodies its tokens in a
dedicated 32-byte subaccount of the SONS canister, derived as
SHA-256("ohshii-sons-lock:v1:" ‖ len(campaign_id) ‖ campaign_id ‖ len(lock_id) ‖ lock_id)
and stored on the record (lock_subaccount). The main account is treasury-only:
- Vesting locks (LGE finalize):
batch_create_vesting_locksreservesfee × (number_of_unlocks + 1)per contributor (N unlock legs + 1 funding leg) and queues one idempotent funding op per lock (main → subaccount,gross − fee, pinnedcreated_at_time, balance-gated, Duplicate→Ok). A timer drains the queue (kick at batch time + every 300 s); unlock payouts are gated oncustody_funded == Some(true)and never fall back to the main account. Progress is observable via the publicget_custody_ops_statusquery. - Manual locks (
create_additional_lock): the ICRC-2 pull deposits DIRECTLY into the lock’s subaccount (funded from birth; fee math unchanged). If the record write fails after the deposit, a balance-driven refund op returns the funds to the caller automatically. - Unlocks: each tranche is paid
from_subaccount = lock_subaccount. The fee reserve is sized at creation-time fee, so if the ledger fee later RISES the reserve can be overdrawn on ANY tranche: a definiteInsufficientFundson a funded isolated lock triggers a final settlement — everything still deliverable (balance − fee) is sent in one transfer and the lock completes (custody_final_settlementevent); the beneficiary absorbs the accumulated fee delta instead of the lock freezing with the residual stranded. - Treasury integrity: an append-only registry (subaccount → lock_id) is
consulted by
TreasuryWithdrawat creation and execution; no treasury operation can ever draw from lock custody, and no lock operation can draw from the main account or another lock’s subaccount. - Legacy locks (created before this change,
lock_subaccount = None) keep the historical main-account behavior byte-identically. - LP locks custody an ICPSwap position (NFT-like), not a fungible balance — out of scope for subaccount isolation.
Token & canister management
| AdminMethod | What it does | Tier |
|---|---|---|
| UpdateTokenMetadata | Update token metadata (name, symbol, fee, logo) on the ledger. | Standard |
| AddController | Add a controller to a controlled canister. | Standard (Critical if target = self) |
| RemoveController | Remove a controller (never the governance canister itself). | Standard (Critical if target = self) |
| CreateSnapshot | Snapshot a controlled canister. | Standard |
| RestoreSnapshot | Restore from snapshot (destructive — overwrites state). | Critical (always) |
| DeleteSnapshot | Delete a snapshot (irreversible). | Critical (always) |
| StopCanister | Stop a controlled canister. Cannot stop self. | Standard |
| StartCanister | Start a previously stopped canister. | Standard |
| UpgradeIndexCanister ★ | Upgrade the index canister (WASM from an upload session). Cannot target the governance canister. Single-index maintenance only — whole-suite upgrades use the UpgradeCanisterBatch category. Critical because it installs code on a managed canister. | Critical |
Governance, campaign, verification & delegation
| AdminMethod | What it does | Tier |
|---|---|---|
| SetGuardian | Set / replace / remove the guardian principal. | Standard (add/replace reaches Critical via the guardian-target classification; removal keeps the 7-day hardening) |
| UpdateCampaignInfo | Write campaign description / website / socials (sanitized). The executable counterpart of the UpdateCampaign category. | Standard |
| ToggleFeatureFlag | Toggle a governance feature flag (e.g. allow_new_locks, allow_fee_collection). | Standard |
| RemovePermanentlyVerifiedUser | Forever Verified escape hatch. Evict one principal from the per-DAO permanent-verified cache. Param target_user_principal (non-anonymous). Idempotent. Companion to the admin direct call admin_remove_permanently_verified_user. | Standard |
| BatchSetFollow ★ | DAO-driven batch follow assignment: one followee for N followers, bypassing per-caller follow-consent (the proposal IS the authorization). Skips the L1–L4 rate limits, FollowOpGuard, and L6/L10 verification ICCs. Still enforces: followee accepts_followers=true (re-checked at execution), bipartite/no-cycle invariant, no self-follow, MAX_FOLLOWERS_PER_FOLLOWEE, override semantics, MAX_BATCH_SET_FOLLOW. | Critical |
SONS Democratic Emergency Exemption
Section titled “SONS Democratic Emergency Exemption”create_proposal() skips the async verify_caller_for_governance verification
gate when ALL the following hold:
- proposal category is
CriticalGovernanceOperation, - proposal target is
ConfigUpdate(data), data.is_emergency_verification_off() == true, i.e.new_requires_verification == Some(false)AND all other ConfigUpdate fields areNone.
The exemption exists because without it the DAO can soft-lock: with the
verification gate ON, a proposer needs BOTH stake AND verification to submit any
proposal — including the one that would flip the flag OFF. The Democratic
Emergency Exemption lets ANY staked principal submit the narrow rescue payload;
cached verified users (PERMANENT_VERIFIED_USERS) then vote on it
without ICCs to ONS. No admin backdoor; the DAO rescues itself.
Each invocation emits SystemLifecycle { event_type = "verification_gate_emergency_exemption_invoked", campaign_id = "caller=<p>" }.
The rejected alternatives were an admin backdoor and a timer-based auto-disable on ONS unreachability — both rejected because they break the DAO model or create DDoS incentives respectively.
SONS Forever Verified — admin direct-call methods
Section titled “SONS Forever Verified — admin direct-call methods”| Method | What it does |
|---|---|
admin_remove_permanently_verified_user | Direct-call eviction for the incident-response fast path (gated by is_admin_guard). Removes a principal from PERMANENT_VERIFIED_USERS; returns was_present. Anti-anonymous. Emits SystemLifecycle{event_type="permanent_verified_user_evicted_via_admin"}. Gated by is_admin_guard. |
admin_get_permanent_verified_user_count | Admin-gated cardinality metric — returns the cache size only (privacy invariant: NO public query exposes the principals themselves). |
SONS LP Lock methods (non-proposal, direct calls)
Section titled “SONS LP Lock methods (non-proposal, direct calls)”| Method | What it does |
|---|---|
create_lp_lock | Create an LP lock by pairing governance token with ICP on ICPSwap. Mints a full-range position and locks it. VP calculated from governance token amount. |
reclaim_lp_lock | Reclaim an expired LP lock (transfer position back to user). Rate-limited. |
get_lp_lock_fees | Query pending trading fees for a locked LP position. |
See LP_LOCK_LIQUIDITY.md for the full deposit-to-lock flow and token ratio calculation.
Voting parameters and config via proposals
Section titled “Voting parameters and config via proposals”Both ONS (ohshii_governance) and SONS (sons_governance) allow the DAO to change voting and fee configuration via proposals, so the community can tune quorum, approval thresholds, durations, and fees without code upgrades.
ONS (OHSHII Governance)
Section titled “ONS (OHSHII Governance)”- Voting parameters (quorum, approval %, durations, max VP per user, burn exemption and purchase limit params) are stored in stable memory and exposed via
get_voting_parameters(). They are not hardcoded. - To change them, create a proposal with category CriticalGovernanceOperation, target canister = ohshii_governance, method
set_voting_parameters, and Candid-encodedVotingParamsUpdate(all fields optional; only provided fields are updated). Threshold: Critical (see Two-axis voting model). - Proposal creation fee and rejection cost can be changed via ExecuteAdminMethod (target = ohshii_governance), methods
set_proposal_creation_feeandset_rejection_cost, with Standard threshold. The canister enforces a ±50% change limit per execution to prevent governance attacks.
SONS (LGE Governance)
Section titled “SONS (LGE Governance)”- ConfigChange proposals carry ConfigUpdateData for non-critical fields only:
new_proposal_creation_feeandnew_rejection_cost(Standard threshold). new_fee_recipientis deliberately blocked in proposal flows because changing fee destination can break proposal refund behavior.- Voting/runtime parameters in
ConfigUpdateData(new_voting_duration_ns, quorum/approval thresholds, min votes, min VP to propose, min lock days, min/max/default voting durations, max VP per user, min lock amount, etc.) must use CriticalGovernanceOperation and therefore the critical tier (see Two-axis voting model). - In
CreateProposalForm(SONS path),AddControllerandRemoveControllerare configured under ExecuteAdminMethod but are auto-submitted as CriticalGovernanceOperation whentarget_canister_idequals the selected governance canister (self-target). - Fee and rejection cost changes remain limited to ±50% of the current value per proposal to prevent a single proposal from making fees unreachable or removing spam protection. Larger changes require multiple sequential proposals.
- The Create Proposal form (SONS path) offers standard Config Change methods for fee settings and a critical method for voting parameter changes.
Parameter reference — votable defaults vs hardcoded
Section titled “Parameter reference — votable defaults vs hardcoded”This is the authoritative list of what the DAO can change by vote and what is hardcoded (changeable only by a canister upgrade). Defaults are the values a freshly-deployed canister starts with.
Votable (DAO-tunable) — voting thresholds. ONS changes all of these in one
set_voting_parameters call (CriticalGovernanceOperation); on ONS they are
snapshotted per-proposal (grandfathering). SONS splits them: the standard-tier
ones via ConfigChange, the critical-tier ones (marked †) via
CriticalGovernanceOperation.
| Parameter | Default | Floor / bound |
|---|---|---|
standard_quorum_vp | 250,000 VP | ≥ 1 |
min_quorum_votes | 15 voters | ≥ 1 |
critical_quorum_vp | 350,000 VP | ≥ 1 |
critical_min_quorum_votes † | 20 voters | ≥ 1 |
critical_approval_pct | 65 % | 1–99 |
critical_immediate_vp_threshold † | 450,000 VP | ≥ 1 |
critical_immediate_min_votes | 30 voters | ≥ 1 |
critical_immediate_approval_pct † | 75 % | 1–99 |
immediate_approval_pct / immediate_min_votes / immediate_approval_vp_threshold | 90 % / 20 / 500,000 | vestigial — NORMAL immediate close is disabled; kept for stable-storage compat |
veto_override_approval_pct | 80 % | 1–99 |
veto_override_quorum_vp | 700,000 VP | ≥ 1 (never 0) |
veto_override_min_votes | 50 voters | ≥ 1 (never 0) |
veto_override_duration_ns | 1 day | ≥ 1 day (hard floor) |
Votable — durations, proposing & economics. (ONS: set_voting_parameters,
Critical. SONS: the critical-tier durations marked † via
CriticalGovernanceOperation, the rest via ConfigChange.)
| Parameter | Default | Floor / bound |
|---|---|---|
normal_voting_duration_ns † (SONS) | 3 days | guardian_min ≤ normal ≤ critical |
critical_voting_duration_ns † (SONS) | 7 days | ≥ normal |
guardian_min_voting_duration_ns † (SONS) | 1 day | ≤ normal |
min / max / default_voting_duration_ns | 3 d / 7 d / 7 d | min ≤ max |
min_vp_to_propose | 10,000 VP | ≥ 1 |
min_lock_duration_days | 45 days | ≥ 1 |
max_voting_power_per_user (the VP cap) | 36,000 | > 0 |
days_in_month | 30 | > 0 |
min_lock_amount (SONS) | configurable | ≥ 1 |
proposal_creation_fee | 30,000 OHSHII / 30k token | ±50 % per proposal; ≥ rejection_cost + 3×ledger_fee. ONS via set_proposal_creation_fee (ExecuteAdminMethod, Standard); SONS via ConfigChange |
rejection_cost | 25,000 OHSHII / 25k token | ±50 % per proposal; ≤ creation_fee − 3×ledger_fee. Same routes as the fee |
requires_verification (SONS only) | true | boolean; CriticalGovernanceOperation only |
| LGE tier limits + VP thresholds (ONS) | see Voter Benefits Protocol | monotonic; via set_voting_parameters |
Hardcoded (only changeable by a canister upgrade).
| Thing | Value / rule | Why not votable |
|---|---|---|
| Tier classification of each category / method | is_critical_category / proposal_uses_critical_thresholds / admin_method_call_is_critical (ONS); is_critical_proposal + is_critical_admin_method (SONS) | which proposals are Critical is a policy invariant, not a number |
| Guardian-removal hardening | hard 7-day window, never immediate, guardian can’t vote on its own removal | stops a guardian from fast-tracking or rushing its own removal; removal is vetoable-but-overridable like any Critical proposal |
| Veto-override floors | window ≥ 1 day; quorum & min-voters never 0 | guarantees a veto can only delay, never permanently block |
fee_recipient | not proposal-configurable | changing the fee destination can break proposal-refund flows |
| The ±50%-per-proposal fee clamp itself | fixed guard | prevents one proposal from making fees unreachable or removing spam protection |
| Quadratic VP formula | sqrt(token_amount) × duration_months, capped at max_voting_power_per_user | formula is fixed; only the cap value is votable |
| Delegation / batch caps | MAX_FOLLOWERS_PER_FOLLOWEE 500, MAX_BATCH_SET_FOLLOW 300, MAX_BATCH_CREATOR_TRANSFERS 300, MAX_DELEGATIONS_PER_TICK 100 | instruction-budget safety |
| Guardian eligibility ban list | infra canisters / management canister / governance-self rejected | privilege-escalation safety |
| ExecuteAdminMethod blocklist + per-method critical set + CriticalGovernanceOperation 11-method allowlist | fixed method sets | which methods are dangerous is a policy invariant |
| Shield kill-switch targets | the 5 core canisters (SONS excluded) | a fixed, non-caller-supplied set |
Voting Parameters Grandfathering (Snapshot at Creation)
Section titled “Voting Parameters Grandfathering (Snapshot at Creation)”When a proposal is created, the decision-relevant voting parameters are snapshotted into a voting_params_snapshot field on the proposal record. All subsequent quorum and threshold decisions — immediate approval, immediate rejection, and expiry evaluation — use the snapshotted values rather than the current configuration.
This ensures proposals are always judged by the rules active when they were created (grandfathering rule). If a governance proposal later changes voting thresholds, already-open proposals continue to be evaluated against the parameters they were created under.
Snapshotted parameters:
standard_quorum_vp— VP quorum for the NORMAL standard closemin_quorum_votes— minimum voter count for the NORMAL standard closeimmediate_approval_vp_threshold/immediate_min_votes/immediate_approval_pct— legacy NORMAL-path immediate fields (immediate approval is disabled for NORMAL proposals; kept for backward compatibility)critical_quorum_vp— VP quorum for the CRITICAL standard closecritical_approval_pct— approval % for the CRITICAL standard closecritical_min_quorum_votes— minimum voters for the CRITICAL standard close (opt — two-axis model)critical_immediate_min_votes— minimum voters for CRITICAL immediate approvalcritical_immediate_approval_pct— approval % for CRITICAL immediate approval (opt — two-axis model)critical_immediate_vp_threshold— VP threshold for CRITICAL immediate approval (opt — two-axis model)veto_override_*— veto-override round params (opt)
Backward compatibility: Proposals created before a given field existed have None for that field (or voting_params_snapshot = None entirely) and fall back to the current configuration / default constants. Per-type voting durations are NOT snapshotted — the expiry_timestamp is computed once at creation and stored on the proposal.
This applies to both ONS (ohshii_governance) and SONS (sons_governance) canisters.
Voter Benefits Protocol (LGE Participation Tiers)
Section titled “Voter Benefits Protocol (LGE Participation Tiers)”LGE participation is gated by the Voter Benefits Protocol, a 5-tier system
built on two independent, orthogonal axes composed only at eligibility-check
time. Authoritative call: ohshii_governance.get_lge_eligibility(principal).
Axis 1 — Tier (capital)
Section titled “Axis 1 — Tier (capital)”Pure function of the caller’s verified flag (WorldID) + current VP. Never
modified by governance behavior. The backend reads effective_tier from
get_lge_eligibility and enforces max_purchase_limit_e8s.
| Tier | Condition | Limit (tokens) | Fee |
|---|---|---|---|
Guest | !verified | 1,000,000 | 30,000 OHSHII once per LGE, 100% to treasury |
Human | verified && VP < 2,000 | 4,000,000 | free |
Fish | verified && 2,000 ≤ VP < 10,000 | 12,000,000 | free |
Shark | verified && 10,000 ≤ VP < 30,000 | 18,000,000 | free |
Whale | verified && VP ≥ 30,000 | 24,000,000 | free |
Creator bypass unchanged: MAX_CREATOR_ALLOCATION = 80M tokens, regardless of
tier (enforced in purchase_tokens_icrc2_impl).
Axis 2 — Active voter (behavior)
Section titled “Axis 2 — Active voter (behavior)”Pure function of the caller’s vote history on the last 3 ONS proposals that
are countable for that caller. Never persisted; recomputed on every call.
The canister scans newest-to-oldest with headroom (3 + 12 proposals, so up to
15 recent IDs) and keeps walking backward until it has collected 3 countable
proposals or the scan window is exhausted.
This is intentionally per-wallet. A young proposal can count for Alice but not Bob: if Alice already voted on it, it enters Alice’s denominator and numerator; if Bob did not vote and the proposal has not provided a fair 24-hour window, it is forgiven and skipped for Bob.
scanned = memory::get_latest_n_proposals(3 + 12)
for p in scanned newest-to-oldest: user_voted = round-1 vote exists OR override-round vote exists
if p.status == PendingUpload: skip else if user_voted: include p in denominator and numerator else if p.status == Open and age < 24h: skip else if p.status == Open and age >= 24h: include p as missed else if p.status in {Approved, Executing, Rejected, Executed, ExecutionFailed}: include p as missed else if p.status in {Vetoed, VetoOverrideExpired} and vetoed_at - created_at < 24h: skip else: include p as missed
stop once 3 proposals have been included
votes = included proposals where user_votedrequired = min(2, recent.len())is_active_voter = votes >= requiredAny direct vote counts — no VP threshold on the vote itself. A veto-override vote also counts, so users who participate in the second round are not scored as missing the proposal.
Fast democratic resolution is not punished: Approved, Rejected,
Executed, ExecutionFailed (and the transient Executing) count even if the
DAO reached a legitimate decision in less than 24 hours. Fast Guardian vetoes
are different: a Vetoed proposal that was killed before 24 hours is skipped
for non-voters, but still counts for voters who actually participated.
If fewer than 3 countable proposals are found in the scan window, the denominator
is smaller. Example: if only 2 proposals are countable, required = min(2, 2) = 2; if 1 is countable, required = 1; if 0 are countable, required = 0 so no
one is demoted just because governance has no fair sample yet.
Composition (demotion)
Section titled “Composition (demotion)”effective_tier = if tier ∈ {Fish, Shark, Whale} && !is_active_voter then Human else tierA verified Fish/Shark/Whale that misses the 2-of-3 rule participates as Human (4M limit) until they vote on the next ONS proposal — at which point the full tier is restored on the very next purchase. The underlying verified flag, lock ownership, and VP are all untouched; the demotion is a pure derived value returned alongside the capital tier.
Fail-safe posture
Section titled “Fail-safe posture”If the backend’s ICC to ohshii_governance.get_lge_eligibility fails for any
reason (network, decode, governance canister stopped), the backend falls
back to Human limit (4M) + Guest fee required. This is the strictest
realistic tier and prevents an outage from being exploited to exceed
limits.
Governance-tunable parameters
Section titled “Governance-tunable parameters”All tier limits and VP thresholds live in VotingParamsConfig and are
editable via set_voting_parameters (ONS CriticalGovernanceOperation
proposal). Monotonicity is enforced:
purchase_limit_guest ≤ purchase_limit_human ≤ purchase_limit_fish ≤ purchase_limit_shark ≤ purchase_limit_whale
and min_vp_fish ≤ min_vp_shark ≤ min_vp_whale. The Guest fee amount
is BurningFeeConfig.burn_amount_e8s (repurposed field name, preserved for
stable-storage compatibility).
API surface
Section titled “API surface”ohshii_governance:
get_lge_eligibility(principal) -> variant { Ok: LgeEligibility; Err: text }— update (does ICC to lock_storage for VP). Backend-authoritative path.get_lge_eligibility_for_vp(principal, nat64) -> LgeEligibility— query. Frontend fast path after fetching VP viaget_voting_power.get_active_voter_status(principal) -> ActiveVoterStatus— query (axis 2 only).tier_for_vp(principal, nat64) -> ParticipationTier— query (axis 1 only).
backend:
purchase_tokens_icrc2callsget_lge_eligibility, useseffective_tier+max_purchase_limit_e8s+guest_fee_e8s.transfer_ohshii_guest_fee— single atomic ICRC-2transfer_fromtoOHSHII_TREASURY_ADDRESS(memoGUEST_FEE), triggered only wheneffective_tier == Guestand the user has not yet contributed in this LGE.
Sponsor Fee Tier Governance (Future)
Section titled “Sponsor Fee Tier Governance (Future)”The sponsor fee tiers used in LGE creation and token purchases are currently hardcoded but can be made governance-configurable via ONS proposals in the future. This section documents the current mechanics and the structure for a potential UpdateSponsorFeeTiers proposal type.
Current hardcoded thresholds
Section titled “Current hardcoded thresholds”Every fee-bearing transaction has a fixed 10% total fee (100/1000 of the token cost): 5% OhShii platform fee (always to treasury) + 5% referral/sponsor pool (split between sponsor and treasury based on VP). The referral pool is divided based on the sponsor’s OHSHII quadratic voting power (VP):
- < 2,000 VP (or no sponsor): 0% to sponsor, 10% to backend treasury (5% platform + 5% unearned referral)
- 2,000 VP: 2% to sponsor, 8% to backend treasury (5% platform + 3% unearned referral)
- 3,000 VP: 3% to sponsor, 7% to backend treasury (5% platform + 2% unearned referral)
- 5,000 VP: 5% to sponsor, 5% to backend treasury (platform fee only)
The total fee is always 10%; the 5% platform fee always goes to treasury, and the 5% referral pool split changes based on VP. The campaign always receives 90% of the token cost regardless of sponsor tier.
How VP is checked (per-transaction, not per-campaign)
Section titled “How VP is checked (per-transaction, not per-campaign)”VP is not checked once at campaign creation and cached. It is resolved independently for each fee-bearing transaction at the time the transaction occurs:
- At LGE creation (
allocate_ico_funds): The sponsor is the LGE creator’s referrer (the person whose sponsor code the creator used). VP of that referrer is checked once when the creation fee is paid. - At each token purchase (
process_fees_for_reserved_purchase): The sponsor is the buyer’s individual referrer (the person whose sponsor code the buyer used). VP of that referrer is checked at the time of each purchase, independently.
Because VP is resolved per-transaction, different buyers in the same campaign can have different sponsors with different VP tiers. A sponsor whose VP changes (e.g. by locking or unlocking OHSHII tokens) will see the updated tier applied to all future transactions where they are the referrer.
Proposed governance extension
Section titled “Proposed governance extension”A future UpdateSponsorFeeTiers proposal type (or an ExecuteAdminMethod variant) could allow the DAO to adjust these thresholds via ONS proposals. The proposal would carry:
threshold_tier_2_percent: VP threshold for 2% sponsor share (currently 2,000)threshold_tier_3_percent: VP threshold for 3% sponsor share (currently 3,000)threshold_tier_5_percent: VP threshold for 5% sponsor share (currently 5,000)
Governance behavior notes
Section titled “Governance behavior notes”- Scope: Changes affect all future transactions — both LGE creations and token purchases. Existing campaigns are affected for future purchases (since VP is checked per-transaction, not cached at creation).
- Earnings not retroactive: Sponsor earnings from past transactions (already transferred) are not recalculated or adjusted.
- Fixed total fee by design: The overall 10% total fee (5% platform always to treasury + 5% referral pool split between sponsor 0-5% and treasury 5-0%) is fixed by design and should not be governance-configurable, as it is a core economic guarantee. The campaign always receives 90%.
- Transfer thresholds: Minimum transfer amounts (30,000 e8s campaign, 20,000 e8s sponsor) are safety mechanisms and should remain hardcoded.
This extension point exists if the DAO wishes to fine-tune sponsor incentives in the future without code upgrades.
OHSHII Governance (ONS) – summary
Section titled “OHSHII Governance (ONS) – summary”- Canister: Single
ohshii_governancecanister. Indfx.json; deployed with the launcher. - Voting power: OHSHII token quadratic voting power (based on locked tokens). Supports three lock types: OneTime (single unlock date), Linear (vesting schedule), and LPLock (ICPSwap liquidity position lock — VP based on governance token amount and lock duration, same formula as OneTime). Lock points are not used for voting. Lock points are a separate linear scoring system for tokens listed on OhShii Locker, used by third-party dapps for utility locks and gating (no quadratic cap). Frontend uses
ohshii_governance.get_voting_parameters()andget_voting_power()(or lock_storage for quadratic VP). - Cap application rule: the VP cap is applied on the sum of all eligible locks for the same principal (aggregated per user, not per single lock only). Example with cap
36,000:36,000 + 4,000 => 36,000votable/displayed VP. - Voting parameters: Stored in stable memory; updatable via CriticalGovernanceOperation (
set_voting_parameterson self). Fee and rejection cost updatable via ExecuteAdminMethod (Standard threshold). See Voting parameters and config via proposals. - Guardian: Optional guardian principal can create proposals with 1-day voting duration. Frontend shows “Guardian” badge and “Resign as Guardian” when applicable.
- Vote delegation (Liquid Democracy): ONS supports per-principal vote delegation via the
follow/unfollow/set_accepts_followers/dismiss_followerendpoints. A verified voter with non-zero VP can delegate to a single explicitly opted-in delegate; when the delegate votes on an ONS proposal, every follower’s vote is automatically applied within one timer tick using each follower’s own snapshot voting power. Bipartite graph by construction (a principal cannot be both a delegate and a follower simultaneously). Full lifecycle, security model, and API reference: LIQUID_DEMOCRACY.md. - Timer: ohshii_governance runs a timer (e.g. 5 min) for proposal expiry and auto-execution. The same timer also drives the delegation cascade (
process_pending_delegations) which applies up to 100 queued delegated votes per tick synchronously. Status and public restart are shown on System Status and OnsVotingPage. - Where it appears: System Status page (OHSHII Governance card), OnsVotingPage when “OHSHII” is selected (Following tab + Voting Power section show delegation state), CreateProposalForm ONS path.
LGE Governance (SONS) – summary
Section titled “LGE Governance (SONS) – summary”- Canister: One
sons_governanceper LGE campaign. Not indfx.json; created at LGE creation by the backend (create_empty_governance_canister+ install WASM in src/backend/src/lib.rs). - Voting power: The LGE campaign token (and locks/vesting on that governance). Supports three lock types: OneTime, Linear, and LPLock (ICPSwap LP position lock — VP based on governance token amount and lock duration). Frontend uses the selected token’s governance canister for
get_voting_parameters()andget_voting_power(). - Cap application rule: same aggregation logic applies in SONS governance: cap is enforced on the principal aggregated VP across eligible locks.
- Config and voting parameters: Split by risk level:
proposal_creation_fee/rejection_costvia ConfigChange (Standard), voting/runtime parameters via CriticalGovernanceOperation (Critical).fee_recipientis not proposal-configurable. See Voting parameters and config via proposals. - Timer: Each sons_governance has its own timer (e.g. 1 min for unlocks, 5 min for proposals). OnsVotingPage shows the selected governance canister’s timer status and module hash.
- Where it appears: OnsVotingPage when a SONS token is selected; CreateProposalForm SONS path.
Snapshot and restore
Section titled “Snapshot and restore”Both ohshii_governance and sons_governance support create/restore/delete snapshot proposals. When the target is the governance canister itself, snapshot/restore is delegated to the ohshii_launcher_backend as orchestrator, because a canister cannot stop itself (the call would never return).
- OHSHII Governance (ONS) self-snapshot/restore: The backend is already a controller of ohshii_governance. The ohshii_governance notifies the backend to run stop → snapshot/restore → start; no controller change.
- LGE Governance (SONS) self-snapshot/restore: The governance canister is not created with the backend as controller. So the governance canister adds the OhShii backend as a temporary controller, notifies the backend to run the workflow with cleanup_backend_controller_after_start = true, and the backend removes itself from the governance controllers after start. This keeps the backend from retaining control and is required so the backend can call stop/start on the target. The Create Proposal form shows the target canister when creating a snapshot proposal; when the target is the current (SONS) governance canister, this temporary-controller flow is used.
Backend methods: backend_orchestrate_snapshot_workflow, backend_orchestrate_restore_workflow. Full workflow diagrams: WORKFLOWS.md.
Guardian role — capabilities and limits
Section titled “Guardian role — capabilities and limits”The Guardian is a single emergency-operator principal stored in GUARDIAN_PRINCIPAL (memory id 13). It is set via the SetGuardian proposal category (Critical threshold). The Guardian exists for scenarios where the proposal pipeline itself is degraded and a Critical proposal cannot be executed — typically because the index that backs get_proposals_paginated is corrupted or because a freshly-deployed canister needs an immediate emergency action before the DAO can be assembled.
Guardian principal — validity constraints
Section titled “Guardian principal — validity constraints”The guardian is an emergency human operator. Every guardian-set entry point — the SetGuardian proposal (validated at creation AND re-checked at execution), the admin-only admin_set_guardian_principal (ONS, pre-DAO), and the SONS update_config full-config override — runs validate_guardian_choice, which enforces:
- Never the anonymous principal.
NoneandSome(anonymous)both mean “remove the guardian” and normalise to “no guardian” — the guardian is never stored as anonymous. Were it stored as anonymous, any unauthenticated caller would satisfy theguardian == callercheck. - Never an OhShii infrastructure canister (launcher backend/frontend, dao_storage, pool_manager, ohshii_governance, proxy, strongbox, locker backend/frontend, lock_storage, OHSHII ledger), never the management canister, and never the governance canister itself.
None(no guardian) is valid and means nobody holds the role — it never means “everyone”. All caller checks treatNoneas “no guardian”:is_guardian()returnsfalse,guardian_vetorejects withNotAuthorized, and the immediate-approval guardian-yes gate is simply skipped (no guardian → no veto window to protect).
Bad choices are rejected at proposal creation (fail fast, before a vote is spent) and re-checked at execution (defence in depth). The constraint is identical on ONS and SONS.
What the Guardian CAN do
Section titled “What the Guardian CAN do”The Guardian cannot impact decentralization. None of the actions below can change a vote outcome or rewrite governance state. The guardian’s only governance-affecting action is the overridable veto — and a veto is overridable by the community (see Guardian Overridable Veto), so the community always keeps the final word. Every other capability is maintenance, disaster recovery, or emergency DoS defence: it rebuilds a derivable secondary index from its source-of-truth, sets or refreshes the Shield firewall, hides a description on the frontend, hands the role back, or drives an idempotent recovery of owed funds to a fixed treasury destination (the refund drain and the stuck-pool-funds claim) — never naming a recipient, spending the treasury at the guardian’s discretion, or touching proposals, votes, anti-replay sets, or DAO-voted parameters (see What the Guardian CANNOT do).
ONS vs SONS guardian. The ONS guardian (ohshii_governance) holds the full toolkit
below. A SONS (per-campaign sons_governance) guardian holds a deliberately narrower
set — only the rows marked ONS + SONS: the overridable veto, its own canister-local
Shield pre-auth mode and cache refresh, the refund drain (force_process_refunds), and the
stuck-pool-funds recovery (guardian_claim_pool_funds, which on SONS sweeps the campaign
governance canister’s own pool position back to its treasury). The cross-canister kill
switch, description moderation, index rebuilds and resign are ONS-only; a SONS guardian
cannot rebuild indices, moderate, run the kill switch, or resign (its role is
set/replaced/removed through the campaign DAO’s own SetGuardian proposal). Every one of
these — including the funds recovery — either rebuilds a derivable index, drives an
idempotent drain/recovery to a fixed treasury destination, or opens a community-override
veto; none can name a recipient, change a tally, or spend the treasury at the guardian’s
discretion.
| Endpoint | DAO | Kind | Purpose | Limits |
|---|---|---|---|---|
guardian_veto(proposal_id) | ONS + SONS | Governance brake (overridable) | Cast a blocking-but-overridable veto on a vetoable Open proposal: moves it to Vetoed and opens a veto-override round. Never a permanent kill — see Guardian Overridable Veto. | none — least-gated path by design |
shield_set_preauth_mode(mode) | ONS + SONS | Operational (firewall) | Set this canister’s Shield pre-auth mode (Disabled / Enforce / TrustedOnly). DoS-defence control; does not alter any tally. | — (emergency control, intentionally un-gated) |
guardian_refresh_preauth_cache(context) | ONS + SONS | Operational (maintenance) | Re-derive a Shield pre-auth cache context’s membership without a canister upgrade. | — |
coordinate_shield_kill_switch(mode, justification) | ONS | Operational (firewall, emergency) | Fan one pre-auth mode across the 5 core canisters at once (launcher backend, dao_storage, ohshii_governance, locker backend, lock_storage — SONS excluded). | sync gate only; 10-s bounded-wait ICC per target |
update_proposal_description_moderation(gov_id, proposal_id, moderated) | ONS | Moderation (cosmetic, FE-only) | Mask / unmask a proposal description on the frontend only — a view control. Never changes proposal state, eligibility, or the vote. Applies to any target governance canister (incl. SONS proposals). | — (guardian-exclusive) |
guardian_rebuild_indexes(which) | ONS | Disaster recovery | Clear and timer-rebuild a derivable secondary index from its source-of-truth. Targets are the explicit GovRebuildableIndex enum: All, FollowersByFollowee, FollowersCount, VotersByProposal, RpKeysCache, JwksCache, AnyMethodVerifiedUsers. | 10-min cooldown + 50B cycles floor; 5-fail circuit-breaker; 4-h watchdog |
guardian_abort_rebuild() | ONS | Disaster recovery | Cancel an in-flight rebuild cascade. Resets stable state and heap re-entrancy flag; partial state on the rebuilt index is preserved as-is. | — |
guardian_resign() | ONS | Role lifecycle | Hand back the role by setting GUARDIAN_PRINCIPAL = canister_self() — the role becomes unexercisable by any human; only a Critical SetGuardian vote can re-elect one. | — |
force_process_refunds() | ONS + SONS | Disaster recovery (maintenance) | Force a full proposal-resolution drain (expire → execute approved → reap stuck-Executing → process refunds) out-of-band, without waiting for the resolution timer. It only drains owed refunds/execution and is idempotent — it cannot pay a refund twice and cannot change a vote, a proposal outcome, or a DAO-voted parameter (so it does not affect decentralization). | sync guardian gate only; idempotent (per-proposal refund_processed flag + the refund processing guard + deterministic refund timestamp) |
guardian_claim_pool_funds(...) | ONS + SONS | Disaster recovery (funds) | Trigger the on-chain recovery of tokens stranded in an ICPSwap pool after a failed treasury swap or add-liquidity proposal — sweeps the pool’s unused balance and any transferred-but-not-deposited deposit-subaccount (the two official ICPSwap reclaim scenarios), both tokens, back to the executing canister’s own treasury. The destination is fixed to that treasury account — the guardian cannot name a recipient — so it moves owed funds home without touching a vote, an outcome, or a DAO-voted parameter (it does not affect decentralization). Idempotent (re-running on an already-swept pool is a no-op) and per-side non-fatal. On ONS the guardian picks which canister holds the funds (Backend for a failed swap, PoolManager for failed liquidity), which then sweeps its own pool position; on SONS the funds are always on the campaign governance canister itself. | Shield-wrapped, but guardian/admin/governance-self T-bypass the pipeline; per-pool single-flight guard; fails closed while a treasury op is mid-flight on the pool (so it can never steal an in-flight deposit) |
The two Shield pre-auth endpoints (shield_set_preauth_mode, guardian_refresh_preauth_cache),
the kill switch, and guardian_claim_pool_funds are also callable by admin and
governance-self (is_admin_guardian_or_governance_for_preauth) — they are not
guardian-exclusive. update_proposal_description_moderation is the only guardian-exclusive
operational endpoint.
Equivalent storage-side rebuilds (disaster recovery, reached from the same Guardian role
via a 10-s bounded-wait is_guardian ICC to ohshii_governance):
dao_storage.admin_rebuild_indexes(which)(launcher) — accepts admin ORohshii_governanceself-call OR Guardian.lock_storage.guardian_rebuild_indices(which)(OhShii Locker) — accepts admin ORohshii_governanceORlocker_backendsynchronously, or any other principal via the Guardian ICC. Anti-raceRebuildGuard; 30/hour authorized-rebuild quota + 90-s per-run cooldown; a probe-rate cap bounds ICC burn from non-guardian spam.
Stuck-pool-funds recovery — the execution arm. guardian_claim_pool_funds on ONS is a
thin proxy: it resolves the fixed target canister (backend for a failed treasury swap,
pool_manager for failed treasury liquidity — never an arbitrary principal) and calls that
canister’s sweep_pool_unused_balance(pool). On SONS the guardian method does the recovery
in-canister (the campaign governance canister owns the position). Each sweep_pool_unused_balance
runs the two official ICPSwap reclaim steps for both tokens — deposit any
transferred-but-not-deposited subaccount balance, then withdraw the pool’s unused balance —
with live per-token fees (never hardcoded), all-Nat math, per-side non-fatal, funds
landing only on that canister’s own treasury (MAIN) account. It acquires the canister’s
per-pool single-flight guard and refuses (fails closed) while a treasury op is mid-flight
on the pool, so a recovery can never race and consume an in-flight deposit. The receivers
are admin/ohshii_governance-gated (not directly guardian-callable) — the guardian reaches
them only through the ONS proxy, and admins/ONS proposals can also call them directly.
Guardian Overridable Veto
Section titled “Guardian Overridable Veto”guardian_veto is a blocking but overridable veto — never a permanent kill. It exists so that a captured or mistaken guardian can only delay a critical proposal by one override window, never block it (security property D1). Both ohshii_governance (ONS) and every per-campaign sons_governance (SONS) instance expose guardian_veto with identical semantics.
Vetoable proposals. The guardian may veto a proposal that is still Open and whose target is a critical operation. The vetoable set is exactly the critical set (is_vetoable_proposal on ONS, is_vetoable_proposal on SONS): the critical categories — SetGuardian (add/replace and removal), UpgradeCanister, UpgradeGovernanceCanister, UpgradeAssetCanister, CriticalGovernanceOperation, UpgradeCanisterBatch, UpgradeDappBatch — plus the per-method critical ExecuteAdminMethod calls (ONS admin_method_call_is_critical: treasury/pool withdrawals, liquidity removal, position transfers, lock mutations, campaign deletion, and the OHSHII-ledger / root-campaign conditionals) and the critical SONS admin methods (is_critical_admin_method: TreasuryWithdraw, RemoveLiquidity, TransferLiquidityPosition, ModifyLock, DeleteLock, TransferLockBeneficiary, TransferLockCreator, BatchSetFollow, UpgradeIndexCanister). Vetoability is decided from the proposal target payload (method_name / method_args), never the category label. A guardian-removal proposal is also vetoable, with a deliberate safeguard: the guardian may not vote in either round on its own removal, the veto only opens the community override round (it never kills the removal), and the guardian cannot block that override — so the community always completes the removal at the override supermajority. Making removal vetoable raises the bar to remove the guardian from a simple critical majority to the fresh-personhood override supermajority, so a stockpile of bought verified identities cannot remove the watchdog without live human proofs. The one residual is bounded and availability-only: a contested removal (one the guardian vetoes) runs through the fresh-personhood override, so a sustained attestation outage can delay it — a liveness bound, never a capture, since the guardian’s only power is the overridable veto.
Veto-override round. A veto moves the proposal Open → Vetoed (a non-terminal status) and opens a second voting round:
- Fresh personhood re-attestation required (caller-driven). Casting an
override-round vote requires a fresh World ID personhood re-attestation bound
to that specific override — the standing verification that admitted the voter in
round 1 is not enough. The human attests directly on the ecosystem canister
(ONS), with the calling principal as the subject (
attest_fresh_override_personhood). For a SONS override the human attests their own personhood on ONS as well (attest_fresh_override_personhood_for_sons): ONS trusts no SONS assertion — the campaign-DAO id is inert key material, the verified caller is the subject — and the campaign DAO only reads whether the human attested (a query), checking per-DAO eligibility locally in SONS. One verified human spends one per-override nullifier (keyed to that exact(DAO, proposal)round), so personhood cannot be reused to multiply override weight. The gate reads an immutable per-proposal snapshot (override_requires_fresh_attest), so it cannot be flipped mid-round; an override that did not opt in at veto time is exempt. On a SONS override, a sustained outage of the attestation dependency auto-extends the window (+24h) so an outage cannot silently uphold a veto. - Direct-only — single votes, no liquid-democracy cascade. An override-round vote is recorded for the caller alone; it does not enqueue delegated follower votes even when the caller is a delegate with followers. Only ROUND 1 votes trigger the cascade. The override quorum (≥50 unique voters by default) is therefore a count of real, direct voters — a popular delegate cannot clear the override bar alone. Identical on ONS and SONS (
vote()callscascade_delegated_votesstrictly inside theOpenarm, never theVetoedarm). - Separate vote tally and separate vote map, so a round-1 voter may vote again.
- No immediate decision — the timer finalizes it at
override_expiry_timestamp(default window: 24 hours from the veto). - The override succeeds (→
Approved, executes) only if it clears the override bar; otherwise the veto stands (→Rejected, refund minusrejection_cost, paid exactly once).
Why World ID, not DecideID, for the override. The fresh re-attestation is World-ID-only by design. The override exists to annul a take-over mounted with acquired or farmed verified identities — a stockpile of already-verified principals aimed at a Critical proposal — and only a proof with a per-human anchor defeats that. World ID’s nullifier is stable across principals (deterministic in (app_id, action, World account)), so a fresh per-(DAO, proposal) action lets the round de-duplicate one human to one ballot however many verified principals that human controls; DecideID is principal-bound — re-verifying with it would just re-confirm each bought principal, never collapsing the bloc to its real humans — so it powers normal eligibility (World ID OR DecideID) but cannot power the override. This raises the cost of a bought-identity override capture and keeps it community-annullable; it does not eliminate a coalition of enough distinct real humans. See Governance & Capture Resistance.
Override bar — DAO-tunable, snapshotted per-proposal at creation: ≥ 80% adopt, ≥ 700,000 VP quorum, ≥ 50 unique voters, within a 24-hour window. All four are DAO-tunable via proposal and are surfaced live on the Governance Parameters panel (ONS and SONS). The canister floors these values — a parameter change can never set the window below 1 day or the quorum / minimum-voters to zero, so the override always stays reachable (D1 cannot be voted away). If the override does not pass within the window, the veto stands and the proposal is Rejected.
Phase 2 — VetoOverrideExpired. Once the override window closes but before the timer finalizes, query getters surface a derived VetoOverrideExpired status. It is never persisted — the stored status stays Vetoed.
Guardian-removal hardening. A proposal to remove the guardian gets a hard 7-day minimum voting window, can never pass immediately, and the guardian may not vote on its own removal. Removal is vetoable-but-overridable like any Critical proposal: the guardian may veto it, but cannot vote in the override round and cannot block the community completing the removal at the override supermajority.
Immediate-approval gate. A vetoable proposal cannot pass immediately (before its deadline) unless the guardian has also cast a positive vote — preserving a real window for the guardian to veto instead of being raced by a fast quorum.
guardian_veto carries no rate limit, no cycle-reserve floor and no Shield: it is an emergency defensive action and must remain the least-gated path in the canister — gating it would let an attacker lock the guardian out of the brake. The only gate is the guardian authorization check, enforced under full consensus.
What the Guardian CANNOT do (HARD WALL)
Section titled “What the Guardian CANNOT do (HARD WALL)”The GovRebuildableIndex enum is the structural allowlist. Any index NOT on that enum is permanently outside Guardian reach. Explicitly hard-walled (cross-referenced in code as HARD_WALLED_LABELS):
USED_NULLIFIERSandUSED_DECIDEID_PRESENTATIONS— anti-replay sets. Wiping enables vote duplication.PROPOSALSandEXECUTED_PROPOSALS— primary governance state. Not derivable.VOTING_PARAMS_CONFIG— DAO-voted parameters. Reset would lose every value the community voted in.GUARDIAN_PRINCIPAL— the auth root of the Guardian role itself.VERIFIED_USERSandDECIDEID_VERIFIED_USERS— primary verification records (the derivable unionANY_METHOD_VERIFIED_USERSCAN be rebuilt from them, but they themselves cannot).FOLLOWS— primary delegation state.PENDING_DELEGATIONS/PENDING_DELEGATIONS_INDEX— in-flight delegation queue applied by the cascade timer.
Operational safety primitives
Section titled “Operational safety primitives”- Write-freeze: while a cascade is active, every mutator of the targeted index returns
Err("maintenance_in_progress: index <X> rebuild active, ETA ~Ns"). The frontend surfaces this as a maintenance banner (no Unhandled Promise Rejection). - Failure circuit-breaker: a stable
consecutive_failurescounter on the rebuild state, bumped before each chunk and reset on success, auto-aborts the cascade after 5 trap-faulted ticks. - Wall-clock watchdog: cascade auto-aborts after 4 hours regardless of progress.
Full engineering reference: DISASTER_RECOVERY.md.
See also
Section titled “See also”- DISASTER_RECOVERY.md — Guardian rebuild endpoints, derivable indices, write-freeze contract, operator runbook.
- ARCHITECTURE.md — Canister map and governance scope table.
- WORKFLOWS.md — Create LGE, token, vote, create/execute proposal, backend vs frontend upgrade, self-snapshot flows.
- OHSSHII_LOCKER_INTEGRATION.md — OhShii Locker token list and lock management via ONS proposals, SONS vesting vs Locker.
- LP_LOCK_LIQUIDITY.md — LP Lock mechanics: ICPSwap V3 liquidity provision, sqrtPriceX96, token ratio calculation, deposit-to-lock flow.
- LOGGING_SYSTEM.md — Stable memory event logging for both ONS and SONS governance canisters, event types, and monitoring.
- 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.
- LIQUID_DEMOCRACY.md — Vote delegation on ONS: follow / unfollow / accept-followers toggle / dismiss-follower flows, the snapshot-based cascade model, four-layer rate limits, the cross-canister batch VP fetch contract, event log reference, and FAQ.
- 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.