# Rust智能合约养成日记(10-3):Sputnik DAO核心概念 - 提案(Proposal)解析Sputnik-DAO作为NEAR Protocol提供的基础设施,正有力推动NEAR生态向"去中心化"方向发展。目前该平台已促成众多NEAR项目建立"去中心化"自治社区,同时提供了完整灵活且高效的社区决策治理解决方案。Sputnikdaov2是用于Sputnik-DAO社区治理投票的智能合约。本文将介绍该合约的核心概念:提案(Proposal),后续文章将围绕"提案"介绍相关的DAO社区治理模式(Policy)。## 1. 提案发起(Add Proposal)Sputnik-DAO社区中的每位成员都可以就项目的治理或管理发表意见或提交提案。随后每个在DAO中持股的社区成员都可以对该提案进行审议和投票。换言之,Sputnik-DAO中的每个成员都可以通过对他人提案投票或自己发起新的管理提案来影响项目未来走向。在合约层面,DAO社区成员可调用sputnikdaov2合约提供的add_proposal()方法发起新提案。rustpub fn add_proposal(&mut self, proposal: ProposalInput) -> u64提案者需提供该提案的详细信息(ProposalInput):- 提案的文字描述(Description)。此信息将公开展示在Sputnik-DAO主页前端,帮助社区成员理解提案的目的与意义。- 提案的类型(kind)。提案者需根据对项目管理所提意见类型进行选择(如合约关键特权函数调用需选择FunctionCall类型,合约项目资金转移需选择Transfer类型,合约治理权限控制级别的设置/变更需选择ChangePolicyAddOrUpdateRole类型等)这些ProposalInput信息将作为参数传入add_proposal()方法,该方法将进一步执行相关校验与处理,并生成带完整初始化信息的提案(Proposal)。最终该提案会与唯一的proposal_id绑定,以<key, value="">形式被添加到Sputnik-DAO合约全局维护的Contract.proposals映射中(提案池)。Sputnik-DAO定义的提案拥有以下完整属性信息:rustpub struct Proposal { pub id: u64, pub proposer: AccountId, pub description: String, pub kind: ProposalKind, pub status: ProposalStatus, pub vote_period_end: BlockHeight, pub vote_counts: HashMap<votepolicy, hashmap<accountid,="" balance="">>, pub votes: HashMap<accountid, vote="">, pub submission_time: Timestamp,}该提案中,description与kind属性内容将从proposer创建提案时提供的ProposalInput信息中提取。具体而言,该合约利用Rust语言From trait实现了ProposalInput到Proposal的类型转化。此转化过程绑定了更多提案状态信息:- 新添加提案中的提案者(proposer)属性会被自动赋值为add_proposal()方法的调用者,即env::predecessor_account_id(),该属性真实且不受用户控制;- 新添加的提案状态(status)被默认初始化为ProposalStatus::InProgress,即尚处于投票阶段;- 新添加提案的发起时间(submission_time)被赋值为本区块的时间戳env::block_timestamp();- 由于新提案提交时暂无人投票,因此投票状态(vote_counts, votes)均初始化为空HashMap::default()。需要注意的是:Sputnik-DAO中存在提案押金(proposal_bond)的概念,该押金将依照具体的Sputnik-DAO社区治理模式(Policy)进行管理。阅读相关代码可知,合约要求提案者在调用add_proposal()方法时质押一定数额的NEAR代币作为新提案的保证金。该笔押金将在提案正常结束(社区投票赞成ProposalStatus::Approved | 社区投票反对ProposalStatus::Rejected)时通过调用合约的内部函数internal_return_bonds()退还给提案人。然而,BlockSec此前在解读该处合约代码时发现:Sputnik-DAO在处理提案押金时,并没有为每一位用户单独地维护历史提案押金数额。而当用户发起交易,调用合约方法add_proposal()添加新提案时,可能会给该笔交易附加超过由该DAO治理策略(Policy)所定义的policy.bounty_bond NEAR代币。这将导致多余的部分押金,并不会在后续函数internal_return_bonds执行时返还给提案者。在BlockSec Team及时与项目方取得联系后,最终该Issue#158被确认并及时在PR#160中得到修复。更多Sputnik-DAO内部所执行提案相关的校验与处理策略,将在后续推出的《Rust智能合约养成日记(10-4) Sputnik DAO::社区治理模式剖析》中详细说明。## 2. 提案状态(Proposal Status)Sputnik-DAO中的任何一个标准提案将有可能经历如下多种状态(新的提案状态被初始化为:InProgress)rustpub enum ProposalStatus { InProgress, Approved, Rejected, Removed, Expired, Moved, Failed,}提案池中的提案状态变化由合约的另一方法act_proposal()驱动。Sputnik-DAO成员可调用act_proposal()方法对具体的提案(通过id指定)执行如下操作:rustpub enum Action { AddProposal, RemoveProposal, VoteApprove, VoteReject, VoteRemove, Finalize, MoveToHub,}典型的,对于处于InProgress状态的提案,DAO社区成员可调用act_proposal()执行具体的投票操作:- Action::VoteApprove:表赞成;- Action::VoteReject:表反对; - Action::VoteRemove:认为该提案没有实际意义,需移除;根据上述实现,在内部调用函数update_votes()之后,程序会主动调用policy.proposal_status()进行计票工作。对于满足投票阈值的提案,提案的状态将进行相应的变更。变更后:- 若提案状态为Approved,则该提案将通过调用internal_execute_proposal()被执行;- 若提案状态为Rejected或Removed,则该提案将通过调用internal_reject_proposal()执行后续的收尾操作。值得一提的是,Rejected与Removed状态不同之处在于:最终被确定为Removed状态的提案将直接从提案池中移除,(作为惩罚)并不会退还当初所质押的押金给提案者。而对于Rejected状态的提案而言,该提案将继续保留在提案池中,并退还相应的押金。## 3. 提案执行(Execute Proposal)若某一提案在投票结束后状态匹配为Approved,此时合约方法act_proposal()内部将继续调用internal_execute_proposal()函数执行提案所包含的决策内容。Sputnik-DAO所支持的提案类型列举如下(大多类型的提案涉及到了DAO治理模式的配置更新):- ProposalKind::ChangeConfig- ProposalKind::ChangePolicy- ProposalKind::AddMemberToRole- ProposalKind::RemoveMemberFromRole- ProposalKind::FunctionCall- ProposalKind::UpgradeSelf- ProposalKind::UpgradeRemote- ProposalKind::Transfer- ProposalKind::SetStakingContract- ProposalKind::AddBounty- ProposalKind::BountyDone- ProposalKind::Vote- ProposalKind::FactoryInfoUpdate- ProposalKind::ChangePolicyAddOrUpdateRole- ProposalKind::ChangePolicyRemoveRole- ProposalKind::ChangePolicyUpdateDefaultVotePolicy- ProposalKind::ChangePolicyUpdateParameters以上每一种提案类型在函数internal_execute_proposal()中都实现了相应的处理分支。本小节将深入为大家介绍两种典型的提案类型处理流程:- ProposalKind::FunctionCall - ProposalKind::Transfer### 3.1 合约函数执行提案执行(ProposalKind::FunctionCall)函数internal_execute_proposal()对于匹配ProposalKind为FunctionCall的提案实现了如下处理入口:rustProposalKind::FunctionCall { receiver_id, actions } => { let mut promise = Promise::new(receiver_id.clone()); for action in actions { promise = promise.function_call( action.method_name, action.args, action.deposit, action.gas, ) } promise.into()}FunctionCall类型的提案在提案者调用add_proposal()方法之时,便已经通过ProposalInput参数传入了具体该提案所要执行的函数操作(actions)。NEAR合约允许在一个Promise中绑定多个连续的function_call。因此最初提案者设定的actions内部可以有如下多种个ActionCall对象:rustpub struct ActionCall { pub method_name: String, pub args: Vec<u8>, pub deposit: Balance, pub gas: Gas,}每个ActionCall可指定相应的合约方法名以及方法参数等。综上Sputnik-DAO采用Promise Batch Actions的形式完成了合约函数执行类型提案的执行。### 3.2 合约资金转移提案执行(ProposalKind::Transfer)当部署上线的NEAR智能合约项目运行了较长一段时间后,合约账户本身可能已积累了较多的Fungible Token(包括原生NEAR代币,或其它符合NEP-141标准的代币)。此时Sputnik-DAO社区成员可通过提交合约资金转移提案将这些代币归集到指定的receiver_id账户。同样的internal_execute_proposal()对于匹配ProposalKind为Transfer的提案也实现了相应的处理入口:rustProposalKind::Transfer { token_id, receiver_id, amount } => { self.internal_payout(token_id, receiver_id, amount) .into()}该处理分支底层将调用internal_payout()函数,实现对于不同类型Fungible Token以及不同类型receiver_id(EOA或者合约账户)的转账操作。## 4. 总结与预告本文已为大家介绍了Sputnik DAO合约的核心概念——提案(Proposal),同时也简要说明了如何在Sputnik DAO中创建新的提案并投票执行,及其相关提案基本状态(Status)的变化规则。后续Rust智能合约养成日记将基于提案对Sputnik-DAO中治理模式(Policy)的实现与配置展开更为详细的描述,敬请期待!</u8></accountid,></votepolicy,></key,>
NEAR生态新篇章:深入解析Sputnik DAO提案机制
Rust智能合约养成日记(10-3):Sputnik DAO核心概念 - 提案(Proposal)解析
Sputnik-DAO作为NEAR Protocol提供的基础设施,正有力推动NEAR生态向"去中心化"方向发展。目前该平台已促成众多NEAR项目建立"去中心化"自治社区,同时提供了完整灵活且高效的社区决策治理解决方案。
Sputnikdaov2是用于Sputnik-DAO社区治理投票的智能合约。本文将介绍该合约的核心概念:提案(Proposal),后续文章将围绕"提案"介绍相关的DAO社区治理模式(Policy)。
1. 提案发起(Add Proposal)
Sputnik-DAO社区中的每位成员都可以就项目的治理或管理发表意见或提交提案。随后每个在DAO中持股的社区成员都可以对该提案进行审议和投票。换言之,Sputnik-DAO中的每个成员都可以通过对他人提案投票或自己发起新的管理提案来影响项目未来走向。
在合约层面,DAO社区成员可调用sputnikdaov2合约提供的add_proposal()方法发起新提案。
rust pub fn add_proposal(&mut self, proposal: ProposalInput) -> u64
提案者需提供该提案的详细信息(ProposalInput):
提案的文字描述(Description)。此信息将公开展示在Sputnik-DAO主页前端,帮助社区成员理解提案的目的与意义。
提案的类型(kind)。提案者需根据对项目管理所提意见类型进行选择(如合约关键特权函数调用需选择FunctionCall类型,合约项目资金转移需选择Transfer类型,合约治理权限控制级别的设置/变更需选择ChangePolicyAddOrUpdateRole类型等)
这些ProposalInput信息将作为参数传入add_proposal()方法,该方法将进一步执行相关校验与处理,并生成带完整初始化信息的提案(Proposal)。最终该提案会与唯一的proposal_id绑定,以<key, value="">形式被添加到Sputnik-DAO合约全局维护的Contract.proposals映射中(提案池)。
Sputnik-DAO定义的提案拥有以下完整属性信息:
rust pub struct Proposal { pub id: u64, pub proposer: AccountId, pub description: String, pub kind: ProposalKind, pub status: ProposalStatus, pub vote_period_end: BlockHeight, pub vote_counts: HashMap<votepolicy, hashmap<accountid,="" balance="">>, pub votes: HashMap<accountid, vote="">, pub submission_time: Timestamp, }
该提案中,description与kind属性内容将从proposer创建提案时提供的ProposalInput信息中提取。具体而言,该合约利用Rust语言From trait实现了ProposalInput到Proposal的类型转化。
此转化过程绑定了更多提案状态信息:
新添加提案中的提案者(proposer)属性会被自动赋值为add_proposal()方法的调用者,即env::predecessor_account_id(),该属性真实且不受用户控制;
新添加的提案状态(status)被默认初始化为ProposalStatus::InProgress,即尚处于投票阶段;
新添加提案的发起时间(submission_time)被赋值为本区块的时间戳env::block_timestamp();
由于新提案提交时暂无人投票,因此投票状态(vote_counts, votes)均初始化为空HashMap::default()。
需要注意的是:Sputnik-DAO中存在提案押金(proposal_bond)的概念,该押金将依照具体的Sputnik-DAO社区治理模式(Policy)进行管理。
阅读相关代码可知,合约要求提案者在调用add_proposal()方法时质押一定数额的NEAR代币作为新提案的保证金。该笔押金将在提案正常结束(社区投票赞成ProposalStatus::Approved | 社区投票反对ProposalStatus::Rejected)时通过调用合约的内部函数internal_return_bonds()退还给提案人。
然而,BlockSec此前在解读该处合约代码时发现:
Sputnik-DAO在处理提案押金时,并没有为每一位用户单独地维护历史提案押金数额。而当用户发起交易,调用合约方法add_proposal()添加新提案时,可能会给该笔交易附加超过由该DAO治理策略(Policy)所定义的policy.bounty_bond NEAR代币。这将导致多余的部分押金,并不会在后续函数internal_return_bonds执行时返还给提案者。
在BlockSec Team及时与项目方取得联系后,最终该Issue#158被确认并及时在PR#160中得到修复。
更多Sputnik-DAO内部所执行提案相关的校验与处理策略,将在后续推出的《Rust智能合约养成日记(10-4) Sputnik DAO::社区治理模式剖析》中详细说明。
2. 提案状态(Proposal Status)
Sputnik-DAO中的任何一个标准提案将有可能经历如下多种状态(新的提案状态被初始化为:InProgress)
rust pub enum ProposalStatus { InProgress, Approved, Rejected, Removed, Expired, Moved, Failed, }
提案池中的提案状态变化由合约的另一方法act_proposal()驱动。
Sputnik-DAO成员可调用act_proposal()方法对具体的提案(通过id指定)执行如下操作:
rust pub enum Action { AddProposal, RemoveProposal, VoteApprove, VoteReject, VoteRemove, Finalize, MoveToHub, }
典型的,对于处于InProgress状态的提案,DAO社区成员可调用act_proposal()执行具体的投票操作:
根据上述实现,在内部调用函数update_votes()之后,程序会主动调用policy.proposal_status()进行计票工作。对于满足投票阈值的提案,提案的状态将进行相应的变更。
变更后:
若提案状态为Approved,则该提案将通过调用internal_execute_proposal()被执行;
若提案状态为Rejected或Removed,则该提案将通过调用internal_reject_proposal()执行后续的收尾操作。
值得一提的是,Rejected与Removed状态不同之处在于:最终被确定为Removed状态的提案将直接从提案池中移除,(作为惩罚)并不会退还当初所质押的押金给提案者。而对于Rejected状态的提案而言,该提案将继续保留在提案池中,并退还相应的押金。
3. 提案执行(Execute Proposal)
若某一提案在投票结束后状态匹配为Approved,此时合约方法act_proposal()内部将继续调用internal_execute_proposal()函数执行提案所包含的决策内容。
Sputnik-DAO所支持的提案类型列举如下(大多类型的提案涉及到了DAO治理模式的配置更新):
以上每一种提案类型在函数internal_execute_proposal()中都实现了相应的处理分支。 本小节将深入为大家介绍两种典型的提案类型处理流程:
3.1 合约函数执行提案执行(ProposalKind::FunctionCall)
函数internal_execute_proposal()对于匹配ProposalKind为FunctionCall的提案实现了如下处理入口:
rust ProposalKind::FunctionCall { receiver_id, actions } => { let mut promise = Promise::new(receiver_id.clone()); for action in actions { promise = promise.function_call( action.method_name, action.args, action.deposit, action.gas, ) } promise.into() }
FunctionCall类型的提案在提案者调用add_proposal()方法之时,便已经通过ProposalInput参数传入了具体该提案所要执行的函数操作(actions)。
NEAR合约允许在一个Promise中绑定多个连续的function_call。因此最初提案者设定的actions内部可以有如下多种个ActionCall对象:
rust pub struct ActionCall { pub method_name: String, pub args: Vec, pub deposit: Balance, pub gas: Gas, }
每个ActionCall可指定相应的合约方法名以及方法参数等。
综上Sputnik-DAO采用Promise Batch Actions的形式完成了合约函数执行类型提案的执行。
3.2 合约资金转移提案执行(ProposalKind::Transfer)
当部署上线的NEAR智能合约项目运行了较长一段时间后,合约账户本身可能已积累了较多的Fungible Token(包括原生NEAR代币,或其它符合NEP-141标准的代币)。
此时Sputnik-DAO社区成员可通过提交合约资金转移提案将这些代币归集到指定的receiver_id账户。
同样的internal_execute_proposal()对于匹配ProposalKind为Transfer的提案也实现了相应的处理入口:
rust ProposalKind::Transfer { token_id, receiver_id, amount } => { self.internal_payout(token_id, receiver_id, amount) .into() }
该处理分支底层将调用internal_payout()函数,实现对于不同类型Fungible Token以及不同类型receiver_id(EOA或者合约账户)的转账操作。
4. 总结与预告
本文已为大家介绍了Sputnik DAO合约的核心概念——提案(Proposal),同时也简要说明了如何在Sputnik DAO中创建新的提案并投票执行,及其相关提案基本状态(Status)的变化规则。
后续Rust智能合约养成日记将基于提案对Sputnik-DAO中治理模式(Policy)的实现与配置展开更为详细的描述,敬请期待!