Skip to content

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.

User choiceGovernance canisterToken for votingScope 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 tokenThat token’s sons_governance (one per campaign)That LGE campaign tokenOnly 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).

User

Select DAO: OHSHII or SONS token

OHSHII selected

SONS token selected

ohshii_governance

sons_governance for that campaign

create_proposal / vote / execute

create_proposal / vote / execute

Targets: backend, storage, pool_manager, locker, OHSHII ledger/index/archive, OHSHII DEX pools

Targets: that LGE treasury, WASM, asset canister, pool


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 proposalCRITICAL proposal
Voting windowFixed 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 windowQuorum 450,000 VP + 30 voters, ≥ 75% approval and a positive guardian vote
Immediate rejectionDisabledSymmetric: 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 ExecuteAdminMethod whose 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_metadata when it targets the OHSHII token ledger, and admin_update_ico_info when it edits the OhShii root campaign dao-0000000001. On SONS (is_critical_admin_method): the equivalent AdminMethods — TreasuryWithdraw, RemoveLiquidity, TransferLiquidityPosition, CreatePoolAndAddLiquidity, AddLiquidityToPool, ModifyLock, DeleteLock, TransferLockBeneficiary (plus the pre-existing TransferLockCreator, 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 tierStandard 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_thresholds on ONS, is_critical_proposal on 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.

  • Scope: community signal / discussion.
  • Target: Motion(text).
  • Threshold: Standard.
  • What it does: records the vote; no on-chain execution.
  • Scope: upgrade the WASM of a Rust canister ONS controls (backend, dao_storage, pool_manager, ledger/index, …). Cannot target ohshii_governance itself — self-upgrades go through UpgradeGovernanceCanister.
  • 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 UpgradeGovernanceCanister only 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.
  • Scope: upgrade the ohshii_governance WASM itself; target_canister_id MUST be the governance canister.
  • Target: CanisterUpgrade(CanisterUpgradeData).
  • Threshold: Critical. Guardian-vetoable.
  • What it does: self-upgrade through the post_upgrade hook.
  • 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) then commit_proposed_batch.
  • 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_governance itself.
  • Target: CanisterUpgradeBatch(CanisterUpgradeBatchData) — an ordered list of 1–5 CanisterUpgradeSteps, 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’s batch_step_results. No automatic rollback — ledger downgrades are unsupported upstream; a partial failure ends in ExecutionFailed with 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.
  • Scope: upgrade an arbitrary ordered set of canisters1–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:

    • kindcode (a WASM install_code / install_chunked_code) or asset (a frontend asset-canister commit_proposed_batch);
    • categoryCore (a known OhShii canister: the launcher fleet — backend, dao_storage, pool_manager, the launcher frontend — the locker fleet, the quests canisters, and ohshii_governance itself) 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; UpgradeDappBatch is the general dapp-release lane.

  • Target: DappUpgradeBatch(DappUpgradeBatchData) — an ordered list of DappUpgradeSteps (target + category + typed payload (Wasm or AssetCommit) + 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_batch probe 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 then commit_proposed_batch; per-step progress persists in the proposal’s batch_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.

  • 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-encoded method_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_metadata when the target ledger is the OHSHII token, and admin_update_ico_info when it edits the root campaign dao-0000000001. Separately, a handful of self-targeted dangerous methods are redirected to CriticalGovernanceOperation; stopping the governance canister or the OHSHII ledger is hard-blocked; and remove_supported_token for the OHSHII ledger / admin_list_token / proposal_create_custom_pool (deprecated — replaced by the treasury-funded path) are rejected outright.
  • What it does: call_raw to 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).
  • Scope: elect, replace, or remove the platform guardian.
  • Target: SetGuardian(SetGuardianData { new_guardian }) — a non-anonymous Some(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.
  • 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) — or SetShieldPreauthMode(..).
  • 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 ExecuteAdminMethod the canister does not maintain a positive allowlist; create_proposal enforces 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_canister are hard-blocked on the governance canister and the OHSHII ledger; admin_add_controller_to_canister / admin_remove_controller_from_canister targeting 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 under ExecuteAdminMethod. 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_nameWhat it does
admin_set_feature_statusesSingle 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_statusUpdate 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_paramsUpdate global LGE parameters.
admin_update_ico_infoUpdate campaign info (description, website, telegram, etc.); guardians can also hide/unhide and freeze.
admin_delete_campaignDelete a campaign (cleanup; in production executed by ohshii_governance via an approved ONS proposal).
admin_create_pool_earlyCreate ICPSwap pool for a campaign before normal finalization.
admin_create_snapshot_with_workflowCreate a canister snapshot (orchestrated via backend). Target: any canister the backend can control.
admin_restore_snapshot_with_workflowRestore from snapshot. Not allowed under ExecuteAdminMethod; must use CriticalGovernanceOperation (any target).
admin_delete_canister_snapshotDelete a snapshot. Not allowed under ExecuteAdminMethod; must use CriticalGovernanceOperation.
admin_add_controller_to_canisterAdd a controller to a canister. If target is ohshii_governance, must use CriticalGovernanceOperation.
admin_remove_controller_from_canisterRemove a controller. If target is ohshii_governance, must use CriticalGovernanceOperation.
admin_set_burning_feeSet the OHSHII burning fee amount and token limit threshold for LGE participation.
proposal_treasury_create_custom_poolCritical. 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_liquidityCritical. 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_liquidityCritical. 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_nameWhat it does
admin_update_lock_statusUpdate lock status (e.g. Active, Frozen, Cancelled).
admin_delete_lockDelete a lock.
admin_transfer_lock_recipientChange the beneficiary of a lock.
admin_list_tokenAdd a token to the locker supported list (with metadata from ledger).
admin_update_tokenUpdate token config (e.g. min_lock_amount, logo).
remove_supported_tokenRemove 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_fixupReplay 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_fixupDismiss 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_nameWhat it does
proposal_fee_collect_to_treasuryCollect ICPSwap fees for a campaign and send to OhShii backend treasury.
proposal_fee_collect_and_buybackCollect fees and perform an OHSHII buyback with slippage protection.
proposal_pool_manager_withdrawWithdraw 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_idWithdraw 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_treasuryCritical. 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_forwardCritical. Idempotently re-drain the pending withdraw/forward legs of a removal operation by op id (same pinned-leg dedup model).
admin_reconcile_campaign_poolStandard. 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/transferFrom deposit 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 it ICP). 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 via proposal_pm_withdraw_account_id on 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_nameWhat it does
admin_set_guardian_principalSet 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_nameWhat it doesThreshold
set_world_id_configUpdate the World ID gate (rp_id, action, required_identifier, enabled, rp_key_name).Critical (→ CriticalGovernanceOperation)
set_world_id_request_presetToggle 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_capCap on register_world_id_verification calls per rolling 24h. 0 pauses.Standard
set_decideid_configUpdate 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_modeSwitch the DecideID active mode (Vc on-canister VC popup vs Oidc OAuth redirect).Critical (→ CriticalGovernanceOperation)
set_decideid_oidc_configPersist 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_capCap on register_decideid_verification calls per rolling 24h. 0 pauses.Standard
admin_clear_world_id_verifiedRemove a principal’s World ID flag (escape hatch).Critical (→ CriticalGovernanceOperation)
admin_clear_world_id_nullifierFree a spent World ID nullifier (incident response only — weakens sybil resistance if misused).Standard
admin_clear_decideid_verifiedRemove a principal’s DecideID flag (escape hatch).Critical (→ CriticalGovernanceOperation)
admin_clear_decideid_presentationFree a recorded JWT-VP digest (incident response only — weakens replay protection if misused).Standard
admin_reset_decideid_rate_limitDrop 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:

  1. 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.
  2. 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_verified remove 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 .enabledDecideID .enabledONS vote() for unverified caller
falsefalseAllowed — gate is off, anyone authenticated can vote
truefalseRejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS
falsetrueRejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS
truetrueRejected 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):

MethodCategoryThresholdWhat it changes
set_voting_parametersCriticalGovernanceOperation (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_feeExecuteAdminMethod (target = ohshii_governance)StandardFee in OHSHII tokens to create a proposal. Backend enforces ±50% change limit per execution.
set_rejection_costExecuteAdminMethod (target = ohshii_governance)StandardTokens 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 outcomeRefund
Approved (Motion) · Executed · ExecutionFailedFull (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 withheld rejection_cost to the OhShii backend treasury.
  • SONS (sons_governance) has a single governance canister and no separate treasury, so the withheld rejection_cost is retained in the governance canister — deducted from the refund and kept on-canister. It is not refunded and not forwarded anywhere. (The fee_recipient config 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.

  • Scope: community signal / discussion for one LGE campaign.
  • Target: None.
  • Threshold: Standard.
  • What it does: records the vote; no execution.
  • 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 UpdateCampaignInfo admin method (under ExecuteAdminMethod).
  • Scope: run one of the 30 AdminMethod actions (grouped by area below).
  • Target: AdminMethod(AdminMethodParams) — the variant selects the action; the flat AdminMethodParams carries 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).
  • 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 UpgradeGovernanceCanister only for the self-target validation, not for a threshold difference.)
  • What it does: validates the WASM hash (re-checked post-install) then install_code.
  • Scope: the non-critical config lane — fee fields only: proposal_creation_fee and/or rejection_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_recipient is not proposal-configurable (would break refund flows). Voting / runtime / delegation params are NOT allowed here — they go through CriticalGovernanceOperation.
  • Scope: self-upgrade this sons_governance WASM (target MUST be itself).
  • Target: CanisterUpgrade(CanisterUpgradeData).
  • Threshold: Critical. Guardian-vetoable.
  • What it does: self-upgrade through the post_upgrade hook.
  • 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(..) or ConfigUpdate(..) 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.
  • 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) then commit_proposed_batch.
  • 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–5 CanisterUpgradeSteps, 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-index UpgradeIndexCanister admin method remains available for index-only maintenance. See WORKFLOWS.md §5.3.
  • Scope: upgrade an arbitrary ordered set of canisters1–10 steps — in ONE proposal, executed verbatim in proposer order (shape validated, not release semantics). Each step carries one target + one kind (code WASM install / asset frontend-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’s sons_governance canister 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 of DappUpgradeSteps (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 in batch_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

AdminMethodWhat it doesTier
TransferLiquidityPositionTransfer 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
UpdatePositionIdUpdate the stored position_id (e.g. after migration).Standard
CollectFeesCollect ICPSwap trading fees into the treasury (100%). Supports the same optional registry-position targeting.Standard
CollectFeesAndBuybackCollect fees, then buy back a token with the collected ICP (slippage-protected).Standard
CreatePoolAndAddLiquidityCreate 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
AddLiquidityToPoolAdd 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
SweepPoolUnusedBalanceRecover 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
RemoveLiquidityRemove 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
TreasurySwapSwap treasury tokens via ICPSwap with slippage protection.Standard
TreasuryWithdrawWithdraw 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
CyclesTopupTop up a canister with cycles using treasury ICP (via the CMC).Standard

Vesting locks

AdminMethodWhat it doesTier
ModifyLockChange 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
DeleteLockRemove 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
TransferLockBeneficiaryChange who receives future unlocks (beneficiary). State-only — the lock’s custody subaccount is derived from the lock_id and never moves.Critical
TransferLockCreatorBatch-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

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_locks reserves fee × (number_of_unlocks + 1) per contributor (N unlock legs + 1 funding leg) and queues one idempotent funding op per lock (main → subaccount, gross − fee, pinned created_at_time, balance-gated, Duplicate→Ok). A timer drains the queue (kick at batch time + every 300 s); unlock payouts are gated on custody_funded == Some(true) and never fall back to the main account. Progress is observable via the public get_custody_ops_status query.
  • 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 definite InsufficientFunds on a funded isolated lock triggers a final settlement — everything still deliverable (balance − fee) is sent in one transfer and the lock completes (custody_final_settlement event); 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 TreasuryWithdraw at 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

AdminMethodWhat it doesTier
UpdateTokenMetadataUpdate token metadata (name, symbol, fee, logo) on the ledger.Standard
AddControllerAdd a controller to a controlled canister.Standard (Critical if target = self)
RemoveControllerRemove a controller (never the governance canister itself).Standard (Critical if target = self)
CreateSnapshotSnapshot a controlled canister.Standard
RestoreSnapshotRestore from snapshot (destructive — overwrites state).Critical (always)
DeleteSnapshotDelete a snapshot (irreversible).Critical (always)
StopCanisterStop a controlled canister. Cannot stop self.Standard
StartCanisterStart a previously stopped canister.Standard
UpgradeIndexCanisterUpgrade 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

AdminMethodWhat it doesTier
SetGuardianSet / replace / remove the guardian principal.Standard (add/replace reaches Critical via the guardian-target classification; removal keeps the 7-day hardening)
UpdateCampaignInfoWrite campaign description / website / socials (sanitized). The executable counterpart of the UpdateCampaign category.Standard
ToggleFeatureFlagToggle a governance feature flag (e.g. allow_new_locks, allow_fee_collection).Standard
RemovePermanentlyVerifiedUserForever 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
BatchSetFollowDAO-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

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 are None.

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”
MethodWhat it does
admin_remove_permanently_verified_userDirect-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_countAdmin-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)”
MethodWhat it does
create_lp_lockCreate 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_lockReclaim an expired LP lock (transfer position back to user). Rate-limited.
get_lp_lock_feesQuery 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.

  • 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-encoded VotingParamsUpdate (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_fee and set_rejection_cost, with Standard threshold. The canister enforces a ±50% change limit per execution to prevent governance attacks.
  • ConfigChange proposals carry ConfigUpdateData for non-critical fields only: new_proposal_creation_fee and new_rejection_cost (Standard threshold).
  • new_fee_recipient is 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), AddController and RemoveController are configured under ExecuteAdminMethod but are auto-submitted as CriticalGovernanceOperation when target_canister_id equals 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.

ParameterDefaultFloor / bound
standard_quorum_vp250,000 VP≥ 1
min_quorum_votes15 voters≥ 1
critical_quorum_vp350,000 VP≥ 1
critical_min_quorum_votes20 voters≥ 1
critical_approval_pct65 %1–99
critical_immediate_vp_threshold450,000 VP≥ 1
critical_immediate_min_votes30 voters≥ 1
critical_immediate_approval_pct75 %1–99
immediate_approval_pct / immediate_min_votes / immediate_approval_vp_threshold90 % / 20 / 500,000vestigial — NORMAL immediate close is disabled; kept for stable-storage compat
veto_override_approval_pct80 %1–99
veto_override_quorum_vp700,000 VP≥ 1 (never 0)
veto_override_min_votes50 voters≥ 1 (never 0)
veto_override_duration_ns1 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.)

ParameterDefaultFloor / bound
normal_voting_duration_ns † (SONS)3 daysguardian_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_ns3 d / 7 d / 7 dmin ≤ max
min_vp_to_propose10,000 VP≥ 1
min_lock_duration_days45 days≥ 1
max_voting_power_per_user (the VP cap)36,000> 0
days_in_month30> 0
min_lock_amount (SONS)configurable≥ 1
proposal_creation_fee30,000 OHSHII / 30k token±50 % per proposal; ≥ rejection_cost + 3×ledger_fee. ONS via set_proposal_creation_fee (ExecuteAdminMethod, Standard); SONS via ConfigChange
rejection_cost25,000 OHSHII / 25k token±50 % per proposal; ≤ creation_fee − 3×ledger_fee. Same routes as the fee
requires_verification (SONS only)trueboolean; CriticalGovernanceOperation only
LGE tier limits + VP thresholds (ONS)see Voter Benefits Protocolmonotonic; via set_voting_parameters

Hardcoded (only changeable by a canister upgrade).

ThingValue / ruleWhy not votable
Tier classification of each category / methodis_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 hardeninghard 7-day window, never immediate, guardian can’t vote on its own removalstops a guardian from fast-tracking or rushing its own removal; removal is vetoable-but-overridable like any Critical proposal
Veto-override floorswindow ≥ 1 day; quorum & min-voters never 0guarantees a veto can only delay, never permanently block
fee_recipientnot proposal-configurablechanging the fee destination can break proposal-refund flows
The ±50%-per-proposal fee clamp itselffixed guardprevents one proposal from making fees unreachable or removing spam protection
Quadratic VP formulasqrt(token_amount) × duration_months, capped at max_voting_power_per_userformula is fixed; only the cap value is votable
Delegation / batch capsMAX_FOLLOWERS_PER_FOLLOWEE 500, MAX_BATCH_SET_FOLLOW 300, MAX_BATCH_CREATOR_TRANSFERS 300, MAX_DELEGATIONS_PER_TICK 100instruction-budget safety
Guardian eligibility ban listinfra canisters / management canister / governance-self rejectedprivilege-escalation safety
ExecuteAdminMethod blocklist + per-method critical set + CriticalGovernanceOperation 11-method allowlistfixed method setswhich methods are dangerous is a policy invariant
Shield kill-switch targetsthe 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 close
  • min_quorum_votes — minimum voter count for the NORMAL standard close
  • immediate_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 close
  • critical_approval_pct — approval % for the CRITICAL standard close
  • critical_min_quorum_votes — minimum voters for the CRITICAL standard close (opt — two-axis model)
  • critical_immediate_min_votes — minimum voters for CRITICAL immediate approval
  • critical_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).

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.

TierConditionLimit (tokens)Fee
Guest!verified1,000,00030,000 OHSHII once per LGE, 100% to treasury
Humanverified && VP < 2,0004,000,000free
Fishverified && 2,000 ≤ VP < 10,00012,000,000free
Sharkverified && 10,000 ≤ VP < 30,00018,000,000free
Whaleverified && VP ≥ 30,00024,000,000free

Creator bypass unchanged: MAX_CREATOR_ALLOCATION = 80M tokens, regardless of tier (enforced in purchase_tokens_icrc2_impl).

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_voted
required = min(2, recent.len())
is_active_voter = votes >= required

Any 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.

effective_tier = if tier ∈ {Fish, Shark, Whale} && !is_active_voter
then Human
else tier

A 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.

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.

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).

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 via get_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_icrc2 calls get_lge_eligibility, uses effective_tier + max_purchase_limit_e8s + guest_fee_e8s.
  • transfer_ohshii_guest_fee — single atomic ICRC-2 transfer_from to OHSHII_TREASURY_ADDRESS (memo GUEST_FEE), triggered only when effective_tier == Guest and the user has not yet contributed in this LGE.

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.

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:

  1. 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.
  2. 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.

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)
  • 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.


  • Canister: Single ohshii_governance canister. In dfx.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() and get_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,000 votable/displayed VP.
  • Voting parameters: Stored in stable memory; updatable via CriticalGovernanceOperation (set_voting_parameters on 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_follower endpoints. 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.
  • Canister: One sons_governance per LGE campaign. Not in dfx.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() and get_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_cost via ConfigChange (Standard), voting/runtime parameters via CriticalGovernanceOperation (Critical). fee_recipient is 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.

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.

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. None and Some(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 the guardian == caller check.
  • 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 treat None as “no guardian”: is_guardian() returns false, guardian_veto rejects with NotAuthorized, 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.

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.

EndpointDAOKindPurposeLimits
guardian_veto(proposal_id)ONS + SONSGovernance 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 + SONSOperational (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 + SONSOperational (maintenance)Re-derive a Shield pre-auth cache context’s membership without a canister upgrade.
coordinate_shield_kill_switch(mode, justification)ONSOperational (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)ONSModeration (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)ONSDisaster recoveryClear 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()ONSDisaster recoveryCancel 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()ONSRole lifecycleHand 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 + SONSDisaster 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 + SONSDisaster 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 OR ohshii_governance self-call OR Guardian.
  • lock_storage.guardian_rebuild_indices(which) (OhShii Locker) — accepts admin OR ohshii_governance OR locker_backend synchronously, or any other principal via the Guardian ICC. Anti-race RebuildGuard; 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_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, UpgradeDappBatchplus 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() calls cascade_delegated_votes strictly inside the Open arm, never the Vetoed arm).
  • 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 minus rejection_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.

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_NULLIFIERS and USED_DECIDEID_PRESENTATIONS — anti-replay sets. Wiping enables vote duplication.
  • PROPOSALS and EXECUTED_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_USERS and DECIDEID_VERIFIED_USERS — primary verification records (the derivable union ANY_METHOD_VERIFIED_USERS CAN 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.
  • 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_failures counter 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.

  • 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-check type-checking, the canister-types.js + utils/canister.js bridges, Candid wire conventions, auth providers, and the TypeScript version policy.
  • Deploy/VOTER_VERIFICATION_GUIDE.md — Verifying WASM hashes for DAO proposals.