Solana Realms 治理框架实战SPL Governance 合约部署与提案执行的完整方案一、引言当大多数人讨论 DAO 治理时场景默认在 EVM 生态。但 Solana 作为高性能 L1其治理框架 Realms SPL Governance 拥有完全不同的设计哲学委托型治理架构、Proposal链上状态机、GoverningToken的灵活注册机制以及原生支持多 DAO 治理的 Realms 前端。EVM 治理以合约存储为中心状态在合约内部而 Solana 的无状态程序模型要求所有治理状态以独立账户形式存在。这种差异不是技术偏好——它深刻影响了治理的 Gas 模型Solana 状态租金 vs EVM 存储 slot 成本、权限管理粒度每个 PDA 独享签名权限以及治理合约的升级策略无需 Proxy 模式直接替换程序 ID 并在新程序与旧账户之间建立映射。理解这些基础差异是开发者从 EVM 切换到 Solana 治理栈的第一课。本文从开发者视角拆解 SPL Governance 的部署流程治理代币的铸造与注册、Governance 实例的初始化、提案创建到执行的全链路交易构造以及 Solana 特有的 PDAProgram Derived Address在治理账户寻址中的应用。二、SPL Governance 架构解析2.1 账户关系模型与 EVM 的合约存储模式不同Solana 程序是无状态的——所有数据存储在外部账户中。SPL Governance 通过一组 PDA 构建提案生命周期的状态链。2.2 PDA 寻址规则每个账户的地址由种子seeds 程序 ID 确定性地派生Realm PDA [bgovernance, name_hash] Governance PDA [bgovernance, realm, governing_token_mint] Proposal PDA [bgovernance, governance, governing_token_mint, proposal_index] TokenOwnerRecord [bgovernance, realm, governing_token_mint, governing_token_owner] VoteRecord [bgovernance, proposal, token_owner_record]这种确定性寻址意味着给定 Realm 姓名、治理代币和提案索引客户端可以独立计算出任何一个 DAO 账户的地址无需链上查找。三、代码实现3.1 TypeScript SDK: 创建 Realm 与治理代币// create-dao.ts // 关键设计决策: // 1. 使用 solana/spl-governance SDK 而非手动构造 instruction // — SDK 已处理 PDA 寻址、账户序列化和交易组装 // 2. 社区代币 理事会代币双治理模式 // — 日常提案走社区代币(1 token 1 vote), // 紧急/安全提案走理事会代币(1 member 1 vote) // 3. 所有 Governance 实例共享同一个 Realm, // 但有不同的投票配置(法定人数、投票期) import { Connection, Keypair, PublicKey, clusterApiUrl, sendAndConfirmTransaction, } from solana/web3.js; import { createMint, mintTo, getOrCreateAssociatedTokenAccount, } from solana/spl-token; import { createRealm, createGovernance, GovernanceConfig, VoteThreshold, VoteTipping, getRealmAddress, getGovernanceAddress, PROGRAM_VERSION_V2, } from solana/spl-governance; import BN from bn.js; async function createRichDAO() { // 连接到 devnet(生产环境使用 mainnet-beta) const connection new Connection(clusterApiUrl(devnet), confirmed); // 部署者钱包 — 生产环境使用 Ledger 或 Squads 多签 const payer Keypair.generate(); console.log(Payer:, payer.publicKey.toBase58()); // ---- Step 1: 铸造社区治理代币 ---- // 设计依据: 使用标准 SPL Token 而非 NFT, // 因为投票权重 token amount, 需要可分割性 const communityMint await createMint( connection, payer, payer.publicKey, // mint authority null, // freeze authority — 无冻结权,保证去中心化 9 // decimals — 与 SOL 一致 ); // 铸造初始供应量 const payerTokenAccount await getOrCreateAssociatedTokenAccount( connection, payer, communityMint, payer.publicKey ); await mintTo( connection, payer, communityMint, payerTokenAccount.address, payer, 1_000_000_000_000_000 // 1M tokens with 9 decimals ); // ---- Step 2: 铸造理事会治理代币(NFT 型,不可分割) ---- const councilMint await createMint( connection, payer, payer.publicKey, null, 0 // 0 decimals — 每个理事会成员 1 票 ); // ---- Step 3: 创建 Realm ---- const realmAddress await getRealmAddress( PROGRAM_VERSION_V2, RichDAO // realm name — 用作 PDA 种子 ); await createRealm( connection, payer, PROGRAM_VERSION_V2, RichDAO, // name payer.publicKey, // realmAuthority communityMint, // communityMint payer.publicKey, // payer undefined, // communityMintMaxVoterWeightSource new BN(1), // minCommunityTokensToCreateGovernance Rich DAO Governance // communityMintSupplyFactor // councilMint 可选,传入即启用双治理模式 ); console.log(Realm created:, realmAddress.toBase58()); // ---- Step 4: 创建 Governance 实例 ---- // Governance 代表一种治理规则集,一个 Realm 可以有多个 Governance // 例如: Protocol Governance(参数修改), Treasury Governance(资金分配) const governanceConfig: GovernanceConfig { voteThreshold: VoteThreshold.YesVotePercentage, minCommunityTokensToCreateProposal: new BN(1000), // 0.1% 供应量 minInstructionHoldUpTime: 86400 * 2, // 2 days 时间锁 maxVotingTime: 86400 * 5, // 5 days 投票期 voteTipping: VoteTipping.Strict, proposalCoolOffTime: 0, minCouncilTokensToCreateProposal: new BN(1), }; const governanceAddress await getGovernanceAddress( PROGRAM_VERSION_V2, realmAddress, communityMint ); await createGovernance( connection, payer, PROGRAM_VERSION_V2, realmAddress, communityMint, governanceConfig, payer.publicKey, // tokenOwner payer.publicKey // payer ); console.log(Governance created:, governanceAddress.toBase58()); return { realm: realmAddress, governance: governanceAddress, communityMint, councilMint, }; }3.2 提案创建与投票// proposal-voting.ts // 关键设计决策: // 1. 提案使用 Multi-choice 投票类型 — 允许选项超过二元 For/Against // 2. 每次投票铸造 VoteRecord — 不可篡改的链上投票记录 // 3. useVoteWeight 和 maxVoterWeight 分离 — 支持权重衰减机制 import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, } from solana/web3.js; import { createProposal, castVote, executeTransaction, getProposalAddress, ProposalOption, Vote, PROGRAM_VERSION_V2, withCreateProposal, withCastVote, getTokenOwnerRecordAddress, } from solana/spl-governance; import BN from bn.js; async function createAndVoteOnProposal( connection: Connection, payer: Keypair, realm: PublicKey, governance: PublicKey, communityMint: PublicKey, proposalIndex: number ) { // ---- Step 1: 获取提案人 TokenOwnerRecord ---- const tokenOwnerRecord await getTokenOwnerRecordAddress( PROGRAM_VERSION_V2, realm, communityMint, payer.publicKey ); // ---- Step 2: 计算 Proposal PDA ---- const proposalAddress await getProposalAddress( PROGRAM_VERSION_V2, governance, communityMint, new BN(proposalIndex) ); // ---- Step 3: 创建提案 ---- // 提案类型: 包含要执行的指令列表 const instructionToExecute new TransactionInstruction({ keys: [], programId: new PublicKey(11111111111111111111111111111111), data: Buffer.from([]), }); await createProposal( connection, payer, PROGRAM_VERSION_V2, realm, governance, tokenOwnerRecord, RDAO-1: 金库多元化配置提案, // name 将 30% 金库资产从 SOL 转换为 USDC 以降低波动风险\n## 动机\n...\n## 方案\n..., // descriptionLink communityMint, payer.publicKey, // governanceAuthority new BN(proposalIndex), // 投票选项设计: 允许分级批准,而非简单的 是/否 [ { label: 方案A: 30%转USDC,分3个月执行, voteWeight: new BN(0) }, { label: 方案B: 20%转USDC,分6个月执行, voteWeight: new BN(0) }, { label: 方案C: 维持现状, voteWeight: new BN(0) }, { label: 否决: 不执行多元化, voteWeight: new BN(0) }, ], true, // useDenyOption — 允许否决权 [instructionToExecute] ); console.log(Proposal created:, proposalAddress.toBase58()); // ---- Step 4: 投票 ---- // Vote 枚举: Approve(0), Deny(1), Abstain(2), Veto(3) await castVote( connection, payer, PROGRAM_VERSION_V2, realm, governance, proposalAddress, tokenOwnerRecord, governance, // voterGovernance payer.publicKey, // voter payer.publicKey, // voterAuthority Vote.Approve, // vote 0, // proposalOption — 方案A [new BN(0)] // voteDenyWeight ); console.log(Vote cast successfully); } // ---- 提案执行(投票通过后) ---- async function executeProposal( connection: Connection, payer: Keypair, governance: PublicKey, proposal: PublicKey ) { // 提案通过后,executeTransaction 会调用被提案的 instruction // 注意: 需要满足 minInstructionHoldUpTime(时间锁)才能执行 await executeTransaction( connection, payer, PROGRAM_VERSION_V2, governance, proposal, // 提案中的第 0 条 instruction 0 ); console.log(Proposal executed); }3.3 Anchor 框架的 SPL Governance 集成// programs/rich-dao/src/lib.rs // 如果使用 Anchor 框架构建治理相关的合约逻辑, // 可以通过 CPI 调用 SPL Governance 程序 use anchor_lang::prelude::*; use anchor_spl::governance::{ self, cpi::accounts::CastVote, program::SplGovernance, state::Vote, }; declare_id!(RichDAO111111111111111111111111111111111111); #[program] pub mod rich_dao { use super::*; /// 自定义投票逻辑: 仅在满足条件时允许投票 /// 例如: 需要持有 NFT 才能对特定提案投票 pub fn conditional_vote( ctx: ContextConditionalVoteContext, proposal: Pubkey, proposal_option: u8, ) - Result() { // 自定义条件检查 require!( ctx.accounts.nft_holder.key() ctx.accounts.voter.key(), MyError::NotNFTHolder ); // CPI 调用 SPL Governance 的 cast_vote let vote_cpi_accounts CastVote { realm: ctx.accounts.realm.to_account_info(), governance: ctx.accounts.governance.to_account_info(), proposal: ctx.accounts.proposal.to_account_info(), proposal_owner_record: ctx.accounts.proposal_owner_record.to_account_info(), voter_token_owner_record: ctx.accounts.voter_token_owner_record.to_account_info(), governance_authority: ctx.accounts.governance_authority.to_account_info(), vote_record: ctx.accounts.vote_record.to_account_info(), governing_token_mint: ctx.accounts.governing_token_mint.to_account_info(), payer: ctx.accounts.payer.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), // 可选: 委托投票的 spl-token 账户 voter_weight_record: None, max_voter_weight_record: None, }; let vote_cpi_ctx CpiContext::new( ctx.accounts.governance_program.to_account_info(), vote_cpi_accounts, ); governance::cpi::cast_vote( vote_cpi_ctx, Vote::Approve, proposal_option, vec![], // deny weight (multi-choice) )?; Ok(()) } } #[derive(Accounts)] pub struct ConditionalVoteContextinfo { pub realm: AccountInfoinfo, pub governance: AccountInfoinfo, pub proposal: AccountInfoinfo, pub proposal_owner_record: AccountInfoinfo, pub voter_token_owner_record: AccountInfoinfo, pub governance_authority: AccountInfoinfo, pub vote_record: AccountInfoinfo, pub governing_token_mint: AccountInfoinfo, pub payer: Signerinfo, pub system_program: Programinfo, System, pub governance_program: Programinfo, SplGovernance, /// CHECK: 验证持有者地址 pub voter: AccountInfoinfo, /// CHECK: NFT 持有者验证 pub nft_holder: AccountInfoinfo, } #[error_code] pub enum MyError { #[msg(Voter does not hold required NFT)] NotNFTHolder, }四、边界与 Solana 特有约束PDA 种子冲突如果两个 Realm 使用相同的名称字符串它们的 PDA 会相同种子相同。解决方案在名称后添加创建者公钥的 base58 前缀作为后缀如MyDAO-7EcD...。交易大小限制Solana 单笔交易最多 1232 bytes。当提案包含多条 instruction 时创建提案的巨量交易可能超限。解决方案使用 Versioned Transactions Address Lookup TablesALTs压缩账户引用或分多个交易提交。投票权重的快照时机SPL Governance 在用户首次创建TokenOwnerRecord时锁定投票权重。如果用户在投票期间增持代币新增部分不会自动计入。需要使用depositGoverningTokens指令手动更新权重。治理代币的委托SPL Governance 的委托通过SetGovernanceDelegate指令实现与 EVM 的delegate不同——它委托的是治理权限而非代币所有权委托人保留代币的转移权。网络拥堵下的投票延迟Solana 在 meme 币热潮期间可能出现 10-30 秒的确认延迟。在投票截止时间附近提交的交易可能因为网络拥堵而错过截止区块。建议在截止前至少 30 分钟提交投票。治理分叉与一币多治SPL Governance 允许在同一 Realm 下创建多个 Governance 实例共用同一套治理代币但拥有独立的投票参数和提案队列。当社区对治理规则产生根本分歧时可以创建新 Governance 实例实现软分叉——新旧治理并行运行由代币持有者自行选择参与哪个治理实例。这种模式在 EVM 生态中没有直接对应物是 Solana 账户模型在治理场景的独特优势。五、总结SPL Governance 通过 PDA 寻址将 Solana 的无状态程序模型映射到治理状态机。与 OpenZeppelin Governor 相比其核心差异在于状态不存储在合约内部而是分散在多个 PDA 账户中多选投票Multi-choice机制天然支持分级方案投票VoteTipping策略Strict/Early/Disabled提供了更灵活的法定人数计算方式。从部署视角看createRealm → createGovernance → createProposal → castVote → executeTransaction五个步骤覆盖了从零搭建 DAO 的完整链路。Anchor 框架的 CPI 集成允许开发者在此基础之上构建自定义投票逻辑如 NFT 门控、时间加权投票等实现治理的可编程性叠加。