Перейти к содержимому

Vote on a proposal

Protocol changes are decided by OpenFiat Improvement Proposals — OFIPs — and voting weight comes from stake you have actually bonded, never from what a client claims. This walks through creating a proposal, casting a vote, and how the result is tallied. It is also honest about the part that is not finished: an accepted proposal is recorded as authorized, but the programs it would change cannot yet be changed by it.

Этап 3 из 6 · Run the infrastructure

Прежде чем начать

  • A Solana wallet with some devnet SOL for transaction fees
  • OPEN bonded under at least one role — voting weight is read from that stake account, so a wallet with no stake has no vote
  • OPEN to cover the proposal deposit, if you intend to create a proposal rather than only vote
  • The devnet OPEN mint's mint authority is permanently unset, so nobody can mint OPEN — any OPEN you stake has to be OPEN you already hold

Шаги

  1. 01

    Know what a vote can and cannot change

    Every proposal carries one of six categories: Informational, Standards, Parameter, Treasury, ProtocolUpgrade and Constitutional. The category is not a label — it selects the quorum and approval threshold the proposal will be held to, with ProtocolUpgrade and Constitutional held to the highest bar and Treasury to its own. Be aware of a real limitation before you write one: for an accepted Parameter or Treasury proposal, the program marks the proposal executed and stops there. It does not yet reach into the escrow or staking programs to change a value, and governance holds no treasury vault to disburse from. Both are honest gaps, not oversights — the target programs are still admin-gated and do not recognise the governance PDA as an authority.

  2. 02

    Bond stake before you vote

    Voting weight is the effective stake on your stake account for the role you vote under — the bonded amount, excluding anything you have already requested to unbond. Requesting an unstake therefore reduces your weight immediately, not when the unbonding period ends. A wallet may hold independent stakes under several roles, but the vote record is keyed by proposal and voter only, so you still get exactly one vote per proposal no matter how many roles you hold.

  3. 03

    Create a proposal

    Creating a proposal transfers a deposit from your token account into the program's deposit vault, and snapshots this category's quorum and threshold onto the proposal itself — so a later config change never moves the goalposts on a vote already under way. Only hashes of the title and summary go on chain; the readable text travels over the off-chain governance layer instead. That split is why a client reading chain state alone can show a proposal's id, category and tallies but not its title.

    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    // Category selects the quorum and threshold this proposal is judged by.
    const ix = onchain.governance.createProposalIx(
      proposer,
      mint,
      from,                                 // your OPEN token account, funds the deposit
      42n,                                  // proposal id
      onchain.ProposalCategory.Parameter,
      titleHash,                            // sha256 of the title, 32 bytes
      summaryHash,                          // sha256 of the summary, 32 bytes
      604_800n,                             // voting period, in seconds
    );
    Publishing the readable text
    # The on-chain proposal stores only hashes. Publish the actual title and
    # summary to the off-chain governance layer so people can read what they
    # are voting on.
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getProposals","params":{}}'
  4. 04

    Cast your vote

    The vote instruction takes only your choice and which role's stake you are voting with — it never takes a weight. The program reads your stake account itself and records that number, so there is nothing a client could inflate. Creating the vote record is also what prevents a second vote: the account already exists, and the transaction fails. The reference app has a working wallet-signed version of this on its governance page, which lists the roles you actually hold stake under.

    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    // No weight argument exists — cast_vote reads your StakeAccount directly.
    const ix = onchain.governance.castVoteIx(
      voter,
      42n,                            // proposal id
      true,                           // in favour
      onchain.Role.NodeOperator,      // which role's stake to vote with
    );
  5. 05

    Understand the off-chain mirror

    Nodes also carry votes over the peer-to-peer governance layer, and this is where a subtle trust question lives. A gossiped vote does carry a self-reported weight field, and no node ever believes it. When a vote arrives, the node checks the signature, then queues the vote rather than applying it. On a later tick it fetches the stake account the vote names, confirms that account is genuinely owned by the staking program, confirms the account's owner field matches the voter who signed, and only then records the vote using the amount it decoded itself. The staking program it checks against is not configurable: it is fixed at compile time, because a node operator who could name that program could deploy their own, mint themselves any stake they liked, and have their node count votes weighted by it. What an operator does control is whether the node can reach Solana at all — a GossipOnly node has nothing to verify against and leaves such votes queued rather than trusting them, which is the safe failure but does mean it counts nothing.

    shell
    # A node that cannot reach Solana cannot verify a vote's stake, so it
    # queues gossiped votes rather than trusting them. Give it an endpoint:
    openfiat-node --solana-rpc-url https://api.devnet.solana.com ...
    
    # The staking program it verifies against is pinned at compile time and
    # is deliberately not a setting — see openfiat-core's
    # crates/chain/src/programs.rs for why.
  6. 06

    Finalize the result

    Once the voting period has passed, anyone can finalize — you, the proposer, or an unrelated bot. There is no privileged closer and no discretion in the outcome. Quorum is met when the total weight cast reaches the proposal's snapshotted quorum, measured against total OPEN supply rather than against turnout. If quorum is met, the proposal passes when votes in favour reach its snapshotted threshold as a share of the votes actually cast. A missed quorum and an exact tie both resolve to rejected, deterministically.

    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    // Permissionless: this instruction takes no signer at all.
    const ix = onchain.governance.tallyAndFinalizeIx(42n);
  7. 07

    Settle the deposit

    The deposit exists to make a proposal cost something if nobody turns up, not to punish losing. So it comes back whenever quorum was met, whether the proposal passed or was rejected on its merits; it is forfeited only when the vote failed to draw quorum at all. Settlement is a separate permissionless call after finalization, and it can only run once.

    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    // Refunded to the proposer if quorum was met, forfeited otherwise.
    const ix = onchain.governance.refundOrForfeitDepositIx(
      mint,
      42n,                    // proposal id
      proposerTokenAccount,
      forfeitDestination,
    );