Governance & Price Oracle
This document covers the governance canister's proposal system, the SetDomPrice proposal type introduced in BL-076, the price oracle feed mechanism, and how USD-denominated campaign goals work in Otter Camp.
Canister IDs:
- Staging:
wkep6-mqaaa-aaaao-a6dha-cai(governance-suite asset canister) - Production:
powzt-dyaaa-aaaaj-qrqra-cai
Proposal System Overview
The governance canister manages DAO proposals from creation through voting to execution. Each proposal has a category that determines the voting threshold required for passage and the action executed on approval.
Proposal Lifecycle
Created ──► Open (voting window) ──► Tallied ──► Approved ──► Executed
│
Rejected| State | Description |
|---|---|
Created | Proposal submitted, not yet open for voting |
Open | Voting window active — members can cast votes |
Tallied | Voting window closed, result computed |
Approved | Quorum and threshold met — ready to execute |
Rejected | Failed quorum or threshold |
Executed | On-chain action completed |
Expired | Approved but not executed within execution window |
Proposal Categories
Each proposal category has its own quorum requirement and pass threshold.
| Category | Quorum | Pass Threshold | Execution |
|---|---|---|---|
General | 10% of members | 51% yes | None (informational) |
TreasuryPayout | 20% | 60% | Calls treasury.execute_payout() |
SetDomPrice | 15% | 55% | Calls governance.update_dom_price() |
MembershipPolicy | 25% | 66% | Updates membership canister config |
ConstitutionalAmendment | 40% | 75% | Updates governance config |
EmergencyAction | 5% | 80% | Bypasses normal window — 24h fast track |
Quorum is calculated over active members (membership SBT not expired) at the time the voting window opens.
Proposal Struct
type ProposalCategory = variant {
General;
TreasuryPayout;
SetDomPrice;
MembershipPolicy;
ConstitutionalAmendment;
EmergencyAction;
};
type Proposal = record {
id : nat64;
title : text;
body : text; // Markdown
category : ProposalCategory;
proposed_by : principal;
created_at : nat64;
voting_opens : nat64;
voting_closes: nat64;
status : ProposalStatus;
payload : opt ProposalPayload; // action to execute on approval
tally : opt VoteTally;
};SetDomPrice Proposal Type (BL-076)
SetDomPrice proposals update the on-chain DOM/USD price used by treasury payouts, donation burns, and Otter Camp USD campaign goals.
Why On-Chain Price?
The Internet Computer cannot make outbound HTTP calls at finalization time (update calls are synchronous to consensus). Price feeds must be pushed onto the chain by a trusted oracle, then ratified by DAO governance before they affect financial operations.
This two-step model — oracle submits price, governance ratifies it — prevents any single party from unilaterally updating the price while keeping the data on-chain.
Proposal Payload
type SetDomPricePayload = record {
price_usd : float64; // new DOM/USD price, e.g. 0.0142
effective_at : opt nat64; // Unix timestamp, default: immediately on execution
source_url : opt text; // optional link to price source for transparency
expires_at : opt nat64; // optional expiry — reverts to previous price after
};
// Included in Proposal.payload as:
type ProposalPayload = variant {
SetDomPrice : SetDomPricePayload;
TreasuryPayout : PayoutPayload;
// ... other variants
};Execution
When a SetDomPrice proposal is approved and executed:
- The governance canister calls
update_dom_price(price_usd)on itself - The new price is stored in stable memory as the current oracle tick
- An
AdminEvent { PriceUpdated }is emitted - Treasury and otter-camp canisters read the price via inter-canister query on their next operation
// Query the current DOM/USD price
get_dom_price : () -> (opt DomPriceTick) query;
type DomPriceTick = record {
price_usd : float64;
set_by : principal; // governance canister principal
proposal_id: nat64;
timestamp : nat64;
};Submitting a SetDomPrice Proposal
# Via oracle-bridge API (authenticated)
curl -X POST https://oracle.helloworlddao.com/api/governance/propose \
-H "Content-Type: application/json" \
-b "session=<token>" \
-d '{
"title": "Update DOM/USD price to $0.0155",
"body": "Aggregate price from 3 sources as of 2026-03-31. Source: oracle-bridge price feed.",
"category": "SetDomPrice",
"payload": {
"price_usd": 0.0155,
"source_url": "https://oracle.helloworlddao.com/api/price/dom-usd"
}
}'Price Feed Mechanism
The oracle-bridge calculates the DOM/USD price from an aggregate of configured data sources and exposes it via REST. A cron job submits price update proposals to governance on a configurable interval.
Oracle Price Endpoint
GET /api/price/dom-usd{
"price_usd": 0.0142,
"timestamp": 1711900800,
"source": "aggregate",
"sources": [
{ "name": "internal", "price": 0.0141, "weight": 0.5 },
{ "name": "partner_feed", "price": 0.0143, "weight": 0.5 }
],
"confidence": "high",
"stale": false
}| Field | Description |
|---|---|
confidence | high / medium / low based on source agreement |
stale | true if no fresh data within configured staleness window |
Price Staleness
If all price sources are stale, the endpoint returns stale: true and the canister's stored price should not be updated. Treasury operations that depend on USD conversion will return PriceStale errors rather than use outdated data.
Automatic Price Proposal Cron
# oracle-bridge admin endpoint — manually trigger price proposal submission
curl -X POST https://oracle.helloworlddao.com/api/admin/price/submit-proposal \
-H "X-API-Token: fos_<token>"The auto-submit cron respects a minimum price change threshold (configurable) to avoid spamming governance with proposals for trivial movements.
Voting
Casting a Vote
type Vote = variant { Yes; No; Abstain };
cast_vote : (proposal_id : nat64, vote : Vote) -> (Result<(), VoteError>);Voters must have an active membership SBT (non-expired ICRC-7 token from the membership canister). Membership is verified via inter-canister call at vote time.
On voting, the governance canister mints a vote-pass NFT (ICRC-7) to the voter as a receipt. These are burned when the proposal is finalized.
Vote Tally
type VoteTally = record {
yes_votes : nat64;
no_votes : nat64;
abstentions : nat64;
total_eligible : nat64; // active members at window open
quorum_met : bool;
threshold_met: bool;
};
// Query tally for an open or closed proposal
get_tally : (proposal_id : nat64) -> (opt VoteTally) query;Voting Windows
| Category | Default Window |
|---|---|
General | 7 days |
TreasuryPayout | 5 days |
SetDomPrice | 3 days |
MembershipPolicy | 7 days |
ConstitutionalAmendment | 14 days |
EmergencyAction | 24 hours |
Windows can be adjusted by ConstitutionalAmendment proposals. The window length at time of proposal creation is fixed — it does not change if governance config is updated mid-vote.
USD Campaign Goals in Otter Camp (BL-078.2)
Otter Camp crowdfunding campaigns can specify goals in USD rather than DOM tokens. The otter-camp canister reads the current price from governance to convert contributions.
Campaign Goal Types
type CampaignGoal = variant {
DomTokens : nat; // fixed DOM amount (e8s)
UsdValue : float64; // USD target, converted at current price
};USD Goal Calculation
When a campaign with UsdValue goal receives a contribution:
- Otter-camp queries
governance.get_dom_price()to get the current price tick - USD contribution is converted:
dom_amount = usd_amount / price_usd - If price is stale or unavailable, the contribution is accepted but tracked in DOM only, and the USD progress is marked
unavailable
// Query campaign progress (includes both DOM and USD views)
get_campaign : (campaign_id : text) -> (opt Campaign) query;
type CampaignProgress = record {
goal_dom : nat; // DOM equivalent of USD goal at current price
raised_dom : nat; // total DOM-equivalent raised
raised_usd : opt float64; // None if price unavailable
goal_usd : opt float64; // None if DomTokens goal type
pct_complete : float64; // 0.0–100.0
price_tick_id : opt nat64; // which proposal set the price used
};Governance Canister API Reference
Proposals
// Submit a new proposal
create_proposal : (CreateProposalArgs) -> (Result<nat64, GovernanceError>);
// Get a single proposal
get_proposal : (nat64) -> (opt Proposal) query;
// List proposals with filters
list_proposals : (ListProposalsArgs) -> (vec Proposal) query;
type ListProposalsArgs = record {
status : opt ProposalStatus;
category : opt ProposalCategory;
from_id : opt nat64;
limit : opt nat32; // default 20, max 100
};Voting
cast_vote : (nat64, Vote) -> (Result<(), VoteError>);
get_tally : (nat64) -> (opt VoteTally) query;
get_my_vote : (nat64) -> (opt Vote) query;
list_votes : (nat64) -> (vec VoteRecord) query; // admin onlyPrice Oracle
get_dom_price : () -> (opt DomPriceTick) query;
list_price_history : (limit : opt nat32) -> (vec DomPriceTick) query;Vote-Pass NFTs
// ICRC-7 methods on the governance canister (vote receipts)
icrc7_balance_of : (vec Account) -> (vec nat) query;
icrc7_owner_of : (vec nat) -> (vec opt Account) query;
icrc7_tokens_of : (Account, opt nat, opt nat32) -> (vec nat) query;Error Types
type GovernanceError = variant {
NotAuthorized;
NotAMember;
ProposalNotFound;
ProposalNotOpen;
AlreadyVoted;
VotingWindowClosed;
InvalidPayload;
PriceStale;
QuorumNotMet;
ExecutionFailed : text;
};Governance Suite Integration
The governance-suite React app (staging: wkep6-mqaaa-aaaao-a6dha-cai) communicates with the governance canister via the oracle-bridge. Direct canister calls from the frontend are not used — all mutations go through POST /api/governance/* endpoints.
Note (BL-061): The governance-suite had an auth mismatch where fetch calls used localStorage Bearer tokens instead of cookies. This is a known bug backlogged as BL-061. Until fixed, governance-suite API calls may return 401 on cross-suite navigation. Use the suite's own login page to establish a fresh session.
Local Development
cd /home/coby/git/governance
# Run PocketIC tests
cargo test
# Deploy to local replica
dfx deploy governance --network local
# Create a test SetDomPrice proposal
dfx canister call governance create_proposal '(record {
title = "Test price update";
body = "Setting test price";
category = variant { SetDomPrice };
payload = opt variant { SetDomPrice = record {
price_usd = 0.015;
effective_at = null;
source_url = null;
expires_at = null;
}}
})' --network local