# 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)的實現與配置展開更爲詳細的描述,敬請期待!