往期回顧:


本期摘要:

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 ):

具體含義為:

  • 提案的文字描述( Description )。此段文字信息將公開展示在Sputnik-DAO 主頁前端,幫助社區成員理解該提案的目的與意義。

  • 提案的類型( kind )。提案者需依照對項目管理所提的意見類型進行相應的選擇(例如:合約關鍵特權函數的調用需選擇FunctionCall類型,合約項目資金的轉移需選擇Transfer類型,合約治理權限控制FunctionCall ' Transfer ' 'RolePermission'設置/變更需選擇ChangePolicyAddOrUpdateRole等類型)

以上ProposalInput信息將以參數的形式傳入add_proposal()方法,隨後該方法將進一步執行相關的校驗與處理,並生成一個帶有完整初始化信息的提案( Proposal )。最終該提案會與唯一的proposal_id相綁定,以<Key, Value>的形式被添加到Sputnik-DAO 合約全局所維護的Contract.proposals映射中(提案池)。

如下是Sputnik-DAO 所定義提案所擁有的完整屬性信息:

在該提案中, descriptionkind屬性內容將從proposer在創建該提案所提供的ProposalInput信息中提取。具體為,該合約利用Rust語言From trait實現了ProposalInputProposal的類型轉化:

上述轉化過程綁定了更多的提案狀態信息:

  • 新添加提案中的提案者( 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 (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

具體狀態的變化如下圖所示:

提案池中的提案狀態變化由合約的另一方法act_proposal()驅動。

Sputnik-DAO 成員可調用act_proposal()方法對具體的提案(通過id指定)執行如下操作:

典型的,對於處於InProgress狀態的提案,DAO社區成員可調用act_proposal()執行具體的投票操作:

  • Action::VoteApprove:表贊成;

  • Action::VoteReject:表反對;

  • Action::VoteRemove:認為該提案沒有實際意義,需移除;

根據上述實現,在內部調用函數update_votes()之後,程序會主動調用policy.proposal_status()進行計票工作。該函數的實現內容如下:

在該函數中,對於滿足投票閾值的提案,提案的狀態將進行相應的變更。

變更後:

  • 若提案狀態為Approved ,則該提案將通過調用internal_execute_proposal()被執行;

  • 若提案狀態為RejectedRemoved ,則該提案將通過調用internal_reject_proposal()執行後續的收尾操作。

值得一提的是, RejectedRemoved狀態不同之處在於:最終被確定為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()對於匹配ProposalKindFunctionCall的提案實現瞭如下處理入口:

FunctionCall類型的提案在提案者調用add_proposal()方法之時,便已經通過ProposalInput參數傳入了具體該提案所要執行的函數操作( actions )。

NEAR 合約允許在一個Promise中綁定多個連續的function_call

因此最初提案者設定的actions內部可以有如下多種個ActionCall對象:

每個ActionCall可指定相應的合約方法名以及方法參數等。

綜上Sputnik-DAO 採用Promise Batch Actions的形式完成了合約函數執行類型提案的執行。

3.2 合約資金轉移提案執行(ProposalKind::Transfer)

當部署上線的NEAR智能合約項目運行了較長一段時間後,合約賬戶本身可能已積累了較多的Fungible Token(包括原生NEAR代幣,或其它符合NEP-141標準的代幣NEP-141

此時Sputnik-DAO 社區成員可通過提交合約資金轉移提案將這些代幣歸集到指定的receiver_id賬戶。

同樣的internal_execute_proposal()對於匹配ProposalKindTransfer的提案也實現了相應的處理入口:

該處理分支底層將調用internal_payout()函數,實現對於不同類型Fungible Token 以及不同類型receiver_id (EOA或者合約賬戶)的轉賬操作。

4. 總結與預告

本文已為大家介紹了Sputnik DAO 合約的核心概念——提案(Proposal),同時也為大家簡要說明瞭如何在Sputnik DAO 中創建新的提案並投票執行,及其相關提案基本狀態(Status)的變化規則。

後續Rust智能合約養成日記將基於提案對Sputnik-DAO 中治理模式(Policy)的實現與配置展開更為詳細的描述,敬請期待!