往期回顾:
本期摘要:
Sputnik-DAO 作为 NEAR Protocol 所提供的基础设施,正有力地推动着NEAR生态朝向“去中心化”的目标发展🚀。
目前该平台已促成了众多的NEAR项目“去中心化”自治社区,同时也提供了完整灵活且高效的社区决策治理解决方案。
Sputnikdaov2 🔗
是用于 Sputnik-DAO 社区治理投票的智能合约。本期合约代码解读将为大家介绍该合约的核心概念:提案(Proposal),并将在后续的文章中围绕提案
介绍相关的DAO社区治理模式(Policy)。
注:本系列智能合约源代码解析,文章的所有内容均不构成任何数字货币投资理财建议。
1. 提案发起(Add Proposal)
Sputnik-DAO 社区中的每位成员都有权利就所属项目的治理或管理发表意见或提交提案(Proposal)。随后每个在DAO中持股的社区成员都可以对该提案进行审议和投票。换句话说,Sputnik-DAO 中的每个成员都可以通过对其他成员的提案进行投票或自己发起新的管理提案来影响有关项目未来的走向。
深入合约层面,在 Sputnik-DAO 中,DAO社区成员可调用sputnikdaov2
合约所提供的add_proposal()
方法来发起一个新的提案。
pub fn add_proposal(&mut self, proposal: ProposalInput) -> u64
此时提案者需提供该提案的详细信息(ProposalInput
):
pub struct ProposalInput {
/// Description of this proposal.
pub description: String,
/// Kind of proposal with relevant information.
pub kind: ProposalKind,
}
具体含义为:
-
提案的文字描述(
Description
)。此段文字信息将公开展示在 Sputnik-DAO 主页前端,帮助社区成员理解该提案的目的与意义。 -
提案的类型(
kind
)。提案者需依照对项目管理所提的意见类型进行相应的选择(例如:合约关键特权函数的调用需选择FunctionCall
类型,合约项目资金的转移需选择Transfer
类型,合约治理权限控制级别"RolePermission"
的设置/变更需选择ChangePolicyAddOrUpdateRole
等类型)
以上ProposalInput
信息将以参数的形式传入add_proposal()
方法,随后该方法将进一步执行相关的校验与处理,并生成一个带有完整初始化信息的提案(Proposal
)。最终该提案会与唯一的proposal_id
相绑定,以<Key, Value>的形式被添加到 Sputnik-DAO 合约全局所维护的 Contract.proposals
映射中(提案池)。
pub struct Contract {
...
/// Last available id for the proposals.
pub last_proposal_id: u64,
/// Proposal map from ID to proposal information.
pub proposals: LookupMap<u64, VersionedProposal>
...
}
如下是 Sputnik-DAO 所定义提案所拥有的完整属性信息:
pub struct Proposal {
/// Original proposer.
pub proposer: AccountId,
/// Description of this proposal.
pub description: String,
/// Kind of proposal with relevant information.
pub kind: ProposalKind,
/// Current status of the proposal.
pub status: ProposalStatus,
/// Count of votes per role per decision: yes / no / spam.
pub vote_counts: HashMap<String, [Balance; 3]>,
/// Map of who voted and how.
pub votes: HashMap<AccountId, Vote>,
/// Submission time (for voting period).
pub submission_time: U64,
}
在该提案中,description
与kind
属性内容将从proposer
在创建该提案所提供的ProposalInput
信息中提取。具体为,该合约利用Rust语言 From trait
实现了ProposalInput
到Proposal
的类型转化:
impl From<ProposalInput> for Proposal {
fn from(input: ProposalInput) -> Self {
Self {
proposer: env::predecessor_account_id(),
description: input.description,
kind: input.kind,
status: ProposalStatus::InProgress,
vote_counts: HashMap::default(),
votes: HashMap::default(),
submission_time: U64::from(env::block_timestamp()),
}
}
}
上述转化过程绑定了更多的提案状态信息:
-
新添加提案中的提案者(
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 代币作为新提案的保证金。
#[payable]
pub fn add_proposal(&mut self, proposal: ProposalInput) -> u64 {
// 0. validate bond attached.
// TODO: consider bond in the token of this DAO.
let policy = self.policy.get().unwrap().to_policy();
assert!(
env::attached_deposit() >= policy.proposal_bond.0,
"ERR_MIN_BOND"
);
该笔押金将在提案正常结束(社区投票赞成ProposalStatus::Approved
| 社区投票反对ProposalStatus::Rejected
)时通过调用合约的内部函数internal_return_bonds()
退还给提案人。
fn internal_return_bonds(&mut self, policy: &Policy, proposal: &Proposal) -> Promise {
match &proposal.kind {
ProposalKind::BountyDone { .. } => {
self.locked_amount -= policy.bounty_bond.0;
Promise::new(proposal.proposer.clone()).transfer(policy.bounty_bond.0);
}
_ => {}
}
self.locked_amount -= policy.proposal_bond.0;
Promise::new(proposal.proposer.clone()).transfer(policy.proposal_bond.0)
}
然而,BlockSec 此前在解读该处合约代码时发现:
Sputnik-DAO 在处理提案押金时,并没有为每一位用户单独地维护历史提案押金数额。而当用户发起交易,调用合约方法add_proposal()
添加新提案时,可能会给该笔交易附加超过由该 DAO治理策略(Policy)所定义的 policy.bounty_bond
NEAR代币。这将导致多余的部分押金,并不会在后续函数internal_return_bonds
执行时返还给提案者。
在 BlockSec Team 及时与项目方取得联系后,最终该 Issue#158 (https://github.com/near-daos/sputnik-dao-contract/issues/158) 被确认并及时在 PR#160 (https://github.com/near-daos/sputnik-dao-contract/pull/160) 中得到修复🙂。
更多 Sputnik-DAO 内部所执行提案相关的校验与处理策略,将在后续推出的《Rust 智能合约养成日记(10-4) Sputnik DAO::社区治理模式剖析》中详细说明。
2. 提案状态(Proposal Status)
Sputnik-DAO 中的任何一个标准提案将有可能经历如下多种状态(新的提案状态被初始化为:InProgress
)
pub enum ProposalStatus {
InProgress,
/// If quorum voted yes, this proposal is successfully approved.
Approved,
/// If quorum voted no, this proposal is rejected. Bond is returned.
Rejected,
/// If quorum voted to remove (e.g. spam), this proposal is rejected and bond is not returned.
/// Interfaces shouldn't show removed proposals.
Removed,
/// Expired after period of time.
Expired,
/// If proposal was moved to Hub or somewhere else.
Moved,
/// If proposal has failed when finalizing. Allowed to re-finalize again to either expire or approved.
Failed,
}
具体状态的变化如下图所示:
提案池中的提案状态变化由合约的另一方法act_proposal()
驱动。
Sputnik-DAO 成员可调用act_proposal()
方法对具体的提案(通过id指定)执行如下操作:
pub enum Action {
/// Action to add proposal. Used internally.
AddProposal,
/// Action to remove given proposal. Used for immediate deletion in special cases.
RemoveProposal,
/// Vote to approve given proposal or bounty.
VoteApprove,
/// Vote to reject given proposal or bounty.
VoteReject,
/// Vote to remove given proposal or bounty (because it's spam).
VoteRemove,
/// Finalize proposal, called when it's expired to return the funds
/// (or in the future can be used for early proposal closure).
Finalize,
/// Move a proposal to the hub to shift into another DAO.
MoveToHub,
}
典型的,对于处于InProgress
状态的提案,DAO社区成员可调用act_proposal()
执行具体的投票操作:
-
Action::VoteApprove:表赞成;
-
Action::VoteReject:表反对;
-
Action::VoteRemove:认为该提案没有实际意义,需移除;
pub fn act_proposal(&mut self, id: u64, action: Action, memo: Option<String>) {
...
Action::VoteApprove | Action::VoteReject | Action::VoteRemove => {
assert!(
matches!(proposal.status, ProposalStatus::InProgress),
"ERR_PROPOSAL_NOT_READY_FOR_VOTE"
);
proposal.update_votes(
&sender_id,
&roles,
Vote::from(action),
&policy,
self.get_user_weight(&sender_id),
);
// Updates proposal status with new votes using the policy.
proposal.status =
policy.proposal_status(&proposal, roles, self.total_delegation_amount);
if proposal.status == ProposalStatus::Approved {
self.internal_execute_proposal(&policy, &proposal, id);
true
} else if proposal.status == ProposalStatus::Removed {
self.internal_reject_proposal(&policy, &proposal, false);
self.proposals.remove(&id);
false
} else if proposal.status == ProposalStatus::Rejected {
self.internal_reject_proposal(&policy, &proposal, true);
true
} else {
// Still in progress or expired.
true
}
}
....
根据上述实现,在内部调用函数update_votes()
之后,程序会主动调用 policy.proposal_status()
进行计票工作。该函数的实现内容如下:
pub fn proposal_status(
.....
if vote_counts[Vote::Approve as usize] >= threshold {
return ProposalStatus::Approved;
} else if vote_counts[Vote::Reject as usize] >= threshold {
return ProposalStatus::Rejected;
} else if vote_counts[Vote::Remove as usize] >= threshold {
return ProposalStatus::Removed;
} else {
// continue to next role.
}
在该函数中,对于满足投票阈值的提案,提案的状态将进行相应的变更。
变更后:
-
若提案状态为
Approved
,则该提案将通过调用internal_execute_proposal()
被执行; -
若提案状态为
Rejected
或Removed
,则该提案将通过调用internal_reject_proposal()
执行后续的收尾操作。
值得一提的是,Rejected
与 Removed
状态不同之处在于:最终被确定为Removed
状态的提案将直接从提案池中移除,(作为惩罚)并不会退还当初所质押的押金给提案者。而对于Rejected
状态的提案而言,该提案将继续保留在提案池中,并退还相应的押金。
更多提案状态将在后续推出的《Rust 智能合约养成日记(10-4) Sputnik DAO::社区治理模式剖析》中进一步说明。
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
的提案实现了如下处理入口:
ProposalKind::FunctionCall {
receiver_id,
actions,
} => {
let mut promise = Promise::new(receiver_id.clone().into());
for action in actions {
promise = promise.function_call(
action.method_name.clone().into(),
action.args.clone().into(),
action.deposit.0,
Gas(action.gas.0),
)
}
promise.into()
}
FunctionCall
类型的提案在提案者调用add_proposal()
方法之时,便已经通过ProposalInput
参数传入了具体该提案所要执行的函数操作(actions
)。
NEAR 合约允许在一个
Promise
中绑定多个连续的function_call
。
因此最初提案者设定的actions
内部可以有如下多种个ActionCall
对象:
pub struct ActionCall {
method_name: String, // 方法名
args: Base64VecU8, // 方法调用参数
deposit: U128, // 调用该方法所attach的NEAR代币
gas: U64, // 调用该方法的预付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
的提案也实现了相应的处理入口:
ProposalKind::Transfer {
token_id,
receiver_id,
amount,
msg,
} => self.internal_payout(
&convert_old_to_new_token(token_id),
&receiver_id,
amount.0,
proposal.description.clone(),
msg.clone(),
),
该处理分支底层将调用internal_payout()
函数,实现对于不同类型 Fungible Token 以及不同类型receiver_id
(EOA或者合约账户)的转账操作。
impl Contract {
/// Execute payout of given token to given user.
pub(crate) fn internal_payout(
&mut self,
token_id: &Option<AccountId>,
receiver_id: &AccountId,
amount: Balance,
memo: String,
msg: Option<String>,
) -> PromiseOrValue<()> {
if token_id.is_none() {
Promise::new(receiver_id.clone()).transfer(amount).into()
} else {
if let Some(msg) = msg {
ext_fungible_token::ft_transfer_call(
receiver_id.clone(),
U128(amount),
Some(memo),
msg,
token_id.as_ref().unwrap().clone(),
ONE_YOCTO_NEAR,
GAS_FOR_FT_TRANSFER,
)
} else {
ext_fungible_token::ft_transfer(
receiver_id.clone(),
U128(amount),
Some(memo),
token_id.as_ref().unwrap().clone(),
ONE_YOCTO_NEAR,
GAS_FOR_FT_TRANSFER,
)
}
.into()
}
}
4. 总结与预告
本文已为大家介绍了 Sputnik DAO 合约的核心概念——提案(Proposal),同时也为大家简要说明了如何在 Sputnik DAO 中创建新的提案并投票执行,及其相关提案基本状态(Status)的变化规则。
后续Rust智能合约养成日记将基于提案对 Sputnik-DAO 中治理模式(Policy)的实现与配置展开更为详细的描述,敬请期待!
原文始发于微信公众号(BlockSec Team):Rust 智能合约养成日记(10-3)Sputnik DAO::提案介绍
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论