Skip to content

Checking access...

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
StateDescription
CreatedProposal submitted, not yet open for voting
OpenVoting window active — members can cast votes
TalliedVoting window closed, result computed
ApprovedQuorum and threshold met — ready to execute
RejectedFailed quorum or threshold
ExecutedOn-chain action completed
ExpiredApproved but not executed within execution window

Proposal Categories

Each proposal category has its own quorum requirement and pass threshold.

CategoryQuorumPass ThresholdExecution
General10% of members51% yesNone (informational)
TreasuryPayout20%60%Calls treasury.execute_payout()
SetDomPrice15%55%Calls governance.update_dom_price()
MembershipPolicy25%66%Updates membership canister config
ConstitutionalAmendment40%75%Updates governance config
EmergencyAction5%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

rust
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

rust
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:

  1. The governance canister calls update_dom_price(price_usd) on itself
  2. The new price is stored in stable memory as the current oracle tick
  3. An AdminEvent { PriceUpdated } is emitted
  4. Treasury and otter-camp canisters read the price via inter-canister query on their next operation
rust
// 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

bash
# 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

http
GET /api/price/dom-usd
json
{
  "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
}
FieldDescription
confidencehigh / medium / low based on source agreement
staletrue 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

bash
# 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

rust
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

rust
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

CategoryDefault Window
General7 days
TreasuryPayout5 days
SetDomPrice3 days
MembershipPolicy7 days
ConstitutionalAmendment14 days
EmergencyAction24 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

rust
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:

  1. Otter-camp queries governance.get_dom_price() to get the current price tick
  2. USD contribution is converted: dom_amount = usd_amount / price_usd
  3. If price is stale or unavailable, the contribution is accepted but tracked in DOM only, and the USD progress is marked unavailable
rust
// 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

rust
// 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

rust
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 only

Price Oracle

rust
get_dom_price      : () -> (opt DomPriceTick) query;
list_price_history : (limit : opt nat32) -> (vec DomPriceTick) query;

Vote-Pass NFTs

rust
// 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

rust
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

bash
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

Hello World Co-Op DAO