往期回顧:

前文Rust智能合約養成日記(10-1)Sputnik DAO 概述已為大家介紹了在區塊鏈智能合約中引入DAO社區治理模式的重要性,並簡要描述了SputnikDAO平台的主要功能。

本期摘要:

從本文開始,本系列合約代碼解讀將自頂向下地為大家介紹NEAR生態基礎設施—Sputnik-DAO 平台。首先為大家帶來的是Sputnik_DAOv2::Factory Contract的合約解讀????


1. Sputnik-DAO 工廠合約

Sputnik-DAO 採用創建型工廠設計模式(Factory Pattern)實現了該平台下去中心化自治組織(DAO)的統一創建與管理。

本文將詳細介紹Sputnik-DAO 平台工廠模式(sputnikdao-factory)的設計實現。

對應合約的源代碼倉庫位於:https://github.com/near-daos/sputnik-dao-contract/tree/518ad1d97614fff4b945aba75b6c8bd2483187a2

????為方便讀者理解,以上提供了該合約的架構示意圖供參考。


2. DAPP 模塊功能介紹

打開Sputnik DAO 平台的DAPP頁面,可見已經有不少去中心化自治組織在該平台中創建並定制了屬於自己的DAO實例對象(Sputnikdaov2合約)。

截止2022年03月,該平台下所創建最活躍的DAO為news.sputnik-dao.near ,其中已有3051個提案(proposals)正在公開投票中或狀態已結。

通過在NEAR Explorer中探索,我們不難發現,該平台各DAO實例合約由NEAR賬戶sputnik-dao.near (sputnikdao-factory合約)統一部署。

即所有基於Sputnik DAO 平台創建的DAO實例合約分別被部署在該NEAR賬戶的子賬戶下,例如:

  • pcp .sputnik-dao.near

  • test-dao-bro .sputnik-dao.near

  • blaqkstereo .sputnik-dao.near

  • octopode-dao .sputnik-dao.near

有關NEAR Protocol 中的子賬戶定義,可以在https://docs.near.org/docs/concepts/account#subaccounts ????獲得參考。

如下圖所示,去中心化組織可在NEAR主網中公開發起交易,通過調用sputnikdao-factory合約所提供的create()方法,創建新的DAO實例。

3. sputnikdao-factory 合約代碼解讀

為幫助大家更好地了解Rust工廠模式合約的編寫方法,本文將深入解讀sputnikdao-factory的合約代碼。


3.1 創建DAO


sputnikdao-factory合約狀態主要由如下兩個部分組成:

 pub struct SputnikDAOFactory { factory_manager : FactoryManager, daos: UnorderedSet<AccountId>, }
  • factory_manager:合約主要的內部功能邏輯實現,提供了一系列創建/刪除/更新DAO實例的方法。

  • daos:採用集合數據結構,記錄了該平台歷史上所有已創建DAO實例的NEAR賬戶地址。

創建DAO實例所使用的sputnikdao-factory合約方法create()定義如下:

 1 #[payable] 2 pub fn create(&mut self, name: AccountId, args: Base64VecU8) { 3 let account_id: AccountId = format!('{}.{}', name, env::current_account_id()) 4 .parse() 5 .unwrap(); 6 let callback_args = serde_json::to_vec(&json!({ 7 'account_id': account_id, 8 'attached_deposit': U128(env::attached_deposit()), 9 'predecessor_account_id': env::predecessor_account_id() 10 })) 11 .expect('Failed to serialize'); 12 self.factory_manager.create_contract( 13 self.get_default_code_hash(), 14 account_id, 15 'new', 16 &args.0, 17 'on_create', 18 &callback_args, 19 ); 20}
  • 代碼3-5行的作用是將調用create()方法時函數參數所指定的用戶名name補全,以獲得未來部署DAO合約的NEAR子賬戶地址。此處env::current_account_id()指代了sputnikdao-factory合約的地址,即sputnik-dao.near

  • 代碼6-11行構造了create()方法在調用factory_manager.create_contract後回調函數on_create的函數參數。

  • 代碼12-19行調用了工廠合約中factory_manager所提供的create_contract接口為create()方法調用者新建並部署新的DAO實例合約。同時,對於新部署的DAO實例合約,合約的基本配置信息可通過create_contract參數args以Base64字符串的形式進行傳遞。

  • 如下是NEAR主網中某一去中心化組織在Sputnik-DAO平台中創建DAO實例合約所用的一筆交易:

    FyECaggFxATGaUMrRKkbotRWAPkhjw5SBnZfRHpzSiQ8 ????

    該筆交易調用了sputnikdao-factory合約代碼中的create()方法,實現了multicall.sputnik-dao.near子賬戶的創建,並成功部署了相應DAO實例的合約代碼(具體實現細節將在後文詳細展開說明)。

  • 其中args參數Base64解碼後具體的內容為:

     { 'config' : { 'name' : 'multicall' , 'purpose' : 'governance for near-multicall' , 'metadata' : '' }, 'policy' : [ 'multicall.near' ] }

    該內容正是部署multicall.sputnik-dao.near合約時,執行合約初始化方法new()時所需的合約配置信息。


    下面本文將詳細剖析factory_manager.create_contract的具體實現:

     1 pub fn create_contract( 2 &self, 3 code_hash: Base58CryptoHash, 4 account_id: AccountId, 5 new_method: &str, 6 args: &[u8], 7 callback_method: &str, 8 callback_args: &[u8], 9 ) { 10 let code_hash: CryptoHash = code_hash.into(); 11 let attached_deposit = env::attached_deposit(); 12 let factory_account_id = env::current_account_id().as_bytes().to_vec(); 13 let account_id = account_id.as_bytes().to_vec(); 14 unsafe { 15 // Check that such contract exists. 16 assert_eq!( 17 sys::storage_has_key(code_hash.len() as _, code_hash.as_ptr() as _), 18 1, 19 'Contract doesn't exist' 20 ); 21 // Load input (wasm code) into register 0. 22 sys::storage_read(code_hash.len() as _, code_hash.as_ptr() as _, 0); 23 // schedule a Promise tx to account_id 24 let promise_id = 25 sys::promise_batch_create(account_id.len() as _, account_id.as_ptr() as _); 26 // create account first. 27 sys::promise_batch_action_create_account(promise_id); 28 // transfer attached deposit. 29 sys::promise_batch_action_transfer(promise_id, &attached_deposit as *const u128 as _); 30 // deploy contract (code is taken from register 0). 31 sys::promise_batch_action_deploy_contract(promise_id, u64::MAX as _, 0); 32 // call `new` with given arguments. 33 sys::promise_batch_action_function_call( 34 promise_id, 35 new_method.len() as _, 36 new_method.as_ptr() as _, 37 args.len() as _, 38 args.as_ptr() as _, 39 &NO_DEPOSIT as *const u128 as _, 40 CREATE_CALL_GAS.0, 41 ); 42 // attach callback to the factory. 43 let _ = sys::promise_then( 44 promise_id, 45 factory_account_id.len() as _, 46 factory_account_id.as_ptr() as _, 47 callback_method.len() as _, 48 callback_method.as_ptr() as _, 49 callback_args.len() as _, 50 callback_args.as_ptr() as _, 51 &NO_DEPOSIT as *const u128 as _, 52 ON_CREATE_CALL_GAS.0, 53 ); 54 sys::promise_return(promise_id); 55 } 56 }

    該函數的參數具體說明如下:

    1. code_hash:由Sputnik-DAO 平台所提供標準DAO實例合約模板代碼的哈希值。

    2. account_id:未來新創建DAO實例合約的部署賬戶,例如multicall.sputnik-dao.near ,該參數的內容已在create_contract()的上層函數create()中構造。

    3. new_method:指定了新創建DAO實例合約中的合約初始化函數,一般為new()

    4. args:執行DAO實例合約初始化函數new()時所需的配置信息,同時包括如下兩個方面:

    5. 由去中心化自治組織所提供的DAO基本信息: Config

       pub struct Config { /// Name of the DAO. pub name: String , /// Purpose of this DAO. pub purpose: String , /// Generic metadata. Can be used by specific UI to store additional data. /// This is not used by anything in the contract. pub metadata: Base64VecU8, }
      • 以及未來該DAO內部治理策略的基本配置: Policy

       pub struct Policy { /// List of roles and permissions for them in the current policy. pub roles: Vec<RolePermission>, /// Default vote policy. Used when given proposal kind doesn't have special policy. pub default_vote_policy: VotePolicy, /// Proposal bond. pub proposal_bond: U128, /// Expiration period for proposals. pub proposal_period: U64, /// Bond for claiming a bounty. pub bounty_bond: U128, /// Period in which giving up on bounty is not punished. pub bounty_forgiveness_period: U64, }

      5. callback_method:指定了create_contract()方法執行完畢後的回調函數,用於維護處理新建DAO實例合約在本工廠合約中的信息。

      6. callback_args:回調函數的函數參數。

      該函數的執行主要分為如下幾個步驟:

      1. 代碼15-22行根據code_hash找到並載入工廠合約所提供的DAO實例合約模板代碼(wasm格式)到編號為0的寄存器中。

      2. 代碼23-25行構造一個Promise用於跟踪如下所有步驟(3-6)的處理結果。

      3. 代碼26-27行創建部署DAO實例合約的賬戶。

      4. 代碼28-29行為新創建的賬戶轉送NEAR代幣,這筆代幣源於最初工廠合約create()方法調用者所attached_deposit的數額。

      5. 代碼30-31行從0號寄存器讀取wasm代碼,並部署合約。

      6. 代碼32-41行調用DAO實例合約代碼的初始化函數new()

      最終DAO實例合約部署完畢後,將在factory_manager.create_contract()執行的末尾代碼32-53行回調on_create()函數。

      如下是回調函數on_create的內部代碼實現:

       #[private] pub fn on_create( &mut self , account_id: AccountId, attached_deposit: U128, predecessor_account_id: AccountId, ) -> bool { if near_sdk::is_promise_success() { self .daos.insert(&account_id); true } else { Promise::new(predecessor_account_id).transfer(attached_deposit .0 ); false } }

      該函數具體的處理邏輯為:

      • 若上述步驟(3-6)中存在錯誤無法正常執行,此時在回調函數on_create()中通過調用near_sdk::is_promise_success()查詢獲得的Promise的返回結果將是false 。此時將退還最初工廠合約create()方法調用者所attached_deposit的NEAR代幣數額。

    • 若上述步驟(3-6)執行準確無誤,說明用戶請求的新DAO實例合約(Sputnikdaov2)被正常創建。同時本工廠合約將記錄追踪該DAO實例合約所部屬的子賬戶地址。
    • 如下是一個在工廠合約中實際成功部署新DAO實例的交易執行結果:

      3.2 更新DAO

      在Sputnik-DAO平台中,DAO可通過該工廠合約進行升級(其他方式將在後續文章中進行介紹)。

      如下為工廠合約所提供的合約接口update() ,它將在底層調用factory_manager所提供的update_contract()接口。

      代碼位於:sputnikdao-factory2/src/lib.rs # Line136-149

       /// Tries to update given account created by this factory to the specified code. pub fn update(& self , account_id: AccountId , code_hash: Base58CryptoHash ) { let caller_id = env::predecessor_account_id(); assert !( caller_id == self .get_owner() || caller_id == account_id, 'Must be updated by the factory owner or the DAO itself' ); assert !( self .daos. contains (&account_id), 'Must be contract created by factory' ); self .factory_manager .update_contract(account_id, code_hash, 'update' ); }

      factory_manager.update_contract()處理細節如下:該接口可實現對相應DAO實例合約中update()函數的調用。

       /// Forces update on the given contract. /// Contract must support update by factory for this via permission check. pub fn update_contract( &self, account_id: AccountId, code_hash: Base58CryptoHash, method_name: &str, ) { let code_hash: CryptoHash = code_hash.into(); let account_id = account_id.as_bytes().to_vec(); unsafe { // Check that such contract exists. assert!(env::storage_has_key(&code_hash), 'Contract doesn't exist' ); // Load the hash from storage. sys::storage_read(code_hash.len() as _, code_hash.as_ptr() as _, 0 ); // Create a promise toward given account. let promise_id = sys::promise_batch_create(account_id.len() as _, account_id.as_ptr() as _); // Call `update` method, which should also handle migrations. sys::promise_batch_action_ function _call ( promise_id, method_name.len() as _, method_name.as_ptr() as _, u64::MAX as _, 0, &NO_DEPOSIT as * const u128 as _, (env::prepaid_gas() - env::used_gas() - GAS_UPDATE_LEFTOVER).0, ) ; sys::promise_return(promise_id); } } 

      ️值得一提的是:

      BlockSec在對Sputnik-DAO 代碼進行解析的過程中發現其Factory 合約中存在著一個嚴重的安全問題,影響所有使用了Sputnik-DAO的合約。經與項目方聯繫後,最終該Issue被確認並及時修復。

      ????該安全漏洞具體描述為:

      在先前版本的代碼中,sputinikdao工廠合約所提供的public update()方法缺少瞭如下一個關鍵的斷言檢查。這導致了該方法可以被任何人調用。

       assert!( caller_id == self .get_owner() || caller_id == account_id, 'Must be updated by the factory owner or the DAO itself' );

      而巧合的是,DAO實例合約(Sputnikdaov2合約)默認允許了可由Sputnik-DAO Factory通過跨合約調用實現本合約的升級。

      DAO實例合約中實現的update()方法如下,代碼位於sputnikdao2/src/upgrade.rs # Line 62

       1. #[no_mangle] 2. pub fn update() { 3. env::setup _panic_ hook(); 4. let factory _info = internal_ get _factory_ info(); 5. let current _id = env::current_ account_id(); 6. assert!( 7. env::predecessor _account_ id() == current_id 8. || (env::predecessor _account_ id() == factory _info.factory_ id 9. && factory _info.auto_ update ), 10. '{}', 11. ERR _MUST_ BE _SELF_ OR_FACTORY 12. ); 13. ......

      上述代碼的第9行中, factory_info.auto_update該值在DAO實例合約部署調用new()方法進行初始化時被默認設置為True。

      DAO實例合約new()方法實現如下:代碼位於sputnikdao2/src/lib.rs # Line 83-104

       #[init] pub fn new (config: Config, policy: VersionedPolicy) -> Self { let this = Self { config: LazyOption::new(StorageKeys::Config, Some(&config)), policy: LazyOption::new(StorageKeys::Policy, Some(&policy.upgrade())), staking_id: None, total_delegation_amount: 0 , delegations: LookupMap::new(StorageKeys::Delegations), last_proposal_id: 0 , proposals: LookupMap::new(StorageKeys::Proposals), last_bounty_id: 0 , bounties: LookupMap::new(StorageKeys::Bounties), bounty_claimers: LookupMap::new(StorageKeys::BountyClaimers), bounty_claims_count: LookupMap::new(StorageKeys::BountyClaimCounts), blobs: LookupMap::new(StorageKeys::Blobs), locked_amount: 0 , }; internal_set_factory_info(&FactoryInfo { factory_id: env::predecessor_account_id(), auto_update: true , // 这里factory_info.auto_update该值被默认设置为true. }); this }

      綜上,一位普通用戶(非Factory合約以及DAO合約本身)即可通過Factory合約所提供的pub fn update()方法實現對任意DAO合約的代碼升級(篡改),這會給Sputnik-DAO 平台以及所有依賴於Sputnik-DAO 平台的合約項目帶來極大的安全隱患。

      ????好在,發現此問題時該版本代碼暫未上線NEAR主網,因此沒有造成損失。

      由於項目方響應迅速,目前該漏洞通過增加合理的白名單校驗機制已被正確修復????

      詳見此Fixing Commit: 518ad1d97614fff4b945aba75b6c8bd2483187a2 ????


      4. Sputnik-DAO Factory合約安全性分析

      上述發現並已修復的漏洞之外,Sputnik-DAO Factory合約的安全性主要還從如下幾個方面進行保證:

      • 【權限控制】合約開放的view類方法,不應修改合約的狀態變量,即方法定義中的第一個參數需設置為&self ,而非&mut self

        以下函數均未修改狀態變量:

        • get_owner(&self)

        • get_number_daos(&self)

        • get_default_version(&self)

        • get_default_code_hash(&self)

        • get_daos(&self, from_index: u64, limit: u64)

        • get_dao_list(&self)

        • get_contracts_metadata(&self)

        • get_code(&self, code_hash: Base58CryptoHash)

      • 【權限控制】合約開放的特權函數,這些函數只能由合約owner(或DAO合約賬戶)執行,並在方法中存在相應的assertion,例如:

       fn assert_owner(& self ) { assert_eq!( self .get_owner(), env::predecessor_account_id(), 'Must be owner' ); } 

      • 以下函數均實現添加了assertion:

        • set_owner

        • set_default_code_hash

        • delete_contract

        • store_contract_metadata

        • delete_contract_metadata

        • store

      • 【錯誤處理】Sputnik-DAO Factory合約對可能發生的異常情況都實現了相應合理的錯誤處理機制。例如用戶使用Factory合約創建新DAO實例合約最後會檢查創建所有的步驟時候都已正常完整地執行,否則不應對用戶造成損失。詳見章節3.1 創建DAO

      • 更多的合約安全性Check Points 將在後續的文章中列舉詳細說明。敬請期待。