跳到主要内容

对提案进行投票

协议变更通过 OpenFiat 改进提案(OFIP)决定,投票权重来自你实际质押的资产,而绝不取决于客户端声称的数字。本指南讲解如何创建提案、如何投票,以及结果如何计票。同时也会如实说明尚未完成的部分:被通过的提案会被记录为已授权,但它想要修改的程序目前还无法真正被它修改。

里程碑 3 / 6 · 运行基础设施

开始之前

  • 一个持有少量 devnet SOL 用于支付交易手续费的 Solana 钱包
  • 至少在一种角色下质押了 OPEN——投票权重取自该质押账户,因此没有质押的钱包没有投票权
  • 若你打算创建提案而不只是投票,还需要足够的 OPEN 来支付提案保证金
  • devnet 上 OPEN 铸币权限已被永久移除,任何人都无法增发 OPEN——你质押的 OPEN 必须是你已经持有的

操作步骤

  1. 01

    了解投票能改变什么、不能改变什么

    每个提案都属于六个类别之一:Informational(信息性)、Standards(标准)、Parameter(参数)、Treasury(国库)、ProtocolUpgrade(协议升级)与 Constitutional(宪章性)。类别不只是一个标签——它决定了该提案适用的法定人数与通过门槛,其中 ProtocolUpgrade 与 Constitutional 适用最高标准,Treasury 则有自己的门槛。在撰写提案前请注意一个真实的限制:对于已通过的 Parameter 或 Treasury 提案,程序只会把提案标记为已执行,然后就结束了。它目前不会真的去修改履约托管或质押程序中的数值,治理本身也没有可供支出的国库金库。这两点都是如实存在的缺口,而非疏漏——目标程序仍由 admin 控制,尚未将治理 PDA 识别为有效授权方。

  2. 02

    先质押,再投票

    投票权重等于你所使用角色对应质押账户上的有效质押量——即已锁定的数额,不包含你已申请解绑的部分。因此,一旦申请解绑,你的权重会立即下降,而不是等到解绑期结束才下降。一个钱包可以在多个角色下分别持有质押,但投票记录仅以提案与投票者为键,所以无论你持有多少种角色,每个提案你都只有一票。

  3. 03

    创建提案

    创建提案会把一笔保证金从你的代币账户转入程序的保证金金库,同时把该类别的法定人数与通过门槛快照到提案本身——这样即使之后修改配置,也不会改变一个正在进行中的投票的标准。链上只保存标题与摘要的哈希;可读的正文则通过链下治理层传播。正因为这种拆分,仅读取链上状态的客户端能显示提案的编号、类别与票数,却无法显示它的标题。

    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

    投出你的票

    投票指令只接受你的选择以及你使用哪种角色的质押来投票——它从不接受权重参数。程序会自行读取你的质押账户并记录该数值,因此客户端没有任何可以虚报的空间。创建投票记录本身也起到了防止重复投票的作用:该账户已存在,交易就会失败。参考应用在其治理页面上提供了可用的钱包签名版本,并会列出你实际持有质押的角色。

    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

    理解链下镜像机制

    节点同时也会通过点对点治理层传播投票,而微妙的信任问题正出在这里。被 gossip 传播的投票确实带有一个自报的权重字段,但没有任何节点会相信它。当一票到达时,节点先校验签名,然后把该票排入队列而不是直接应用。在之后的某个周期里,它会去读取该票所指明的质押账户,确认该账户确实由质押程序拥有,确认账户中的 owner 字段与签名的投票者一致,只有到这时才会用它自己解码出的数额记录这一票。它所校验的质押程序地址不可配置:该地址在编译期即已固定,因为若节点运营者能够指定该程序,他们就可以部署自己的程序、给自己铸造任意数额的质押,并让自己的节点按此计票。运营者能够决定的是节点能否连上 Solana——GossipOnly 节点没有任何可供校验的依据,会把这类投票留在队列中而不予信任,这是安全的失败方式,但也意味着它不会计入任何票。

    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

    结算最终结果

    投票期结束后,任何人都可以执行结算——你、提案人,或是一个无关的自动化脚本。这里没有特权关闭者,结果也不存在任何自由裁量空间。当投出的总权重达到该提案快照的法定人数时,即视为达到法定人数,其衡量基准是 OPEN 总供应量,而非投票参与量。在达到法定人数的前提下,若赞成票占实际投出票数的比例达到快照的通过门槛,提案即获通过。未达法定人数与恰好平票这两种情况,都会被确定性地判定为否决。

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

    结算保证金

    保证金的作用是让无人问津的提案付出代价,而不是惩罚失败的一方。因此,只要达到了法定人数,保证金就会退还,无论提案最终是通过还是被实质性否决;只有当投票根本没有达到法定人数时,保证金才会被没收。结算是在最终确定之后的一次独立的、无需许可的调用,并且只能执行一次。

    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,
    );