往期回顧:


本文將具體從如下兩個角度來分別介紹Rust智能合約中權限控制的相關事宜:

  • 合約方法(函數)訪問/調用的可見性;

  • 特權函數的訪問控制/權責劃分;

1.合約函數(方法)可見性

在編寫智能合約時,我們可以通過指定合約函數的可見性(Visibility)來控制什麼函數可以被誰調用。藉此我們可以輕鬆地保護合約中的某些關鍵部分不被意外地訪問或操控。

為體現正確設置合約函數可見性的重要性,本文將以Bancor Network交易所為例進行說明。早在2020的06月18日,該交易所便發生了一起由於合約的關鍵函數訪問控制權限設置錯誤,所導致的合約資產安全事件。該合約由Solidity語言編寫而成。在該語言中,合約函數的可見性大致被分為public/externalprivate/internal兩種。前者允許了合約函數可被合外部的調用者調用,即可視為合約接口的一部分。

然而此前Bancor Network交易所在修改某一安全漏洞時,由於疏忽,誤將合約中部分的關鍵轉賬函數設置為了public屬性(如下所示):

基於此,任何人包括普通用戶,都可以從該合約的外部調用這些函數為自己或他人進行相應的轉賬操作。

該關鍵漏洞的存在,致使其用戶的59萬美元資產面臨著嚴重的風險。

同樣的,在Rust智能合約中,也必須重視合約函數的可見性控制問題
在本系列智能合約養成日記Rust 智能合約養成日記(1)中,我們已經為大家介紹了NEAR SDK所定義的宏: #[near_bindgen]

#[near_bindgen]在near-sdk-macros-version包中通過near_bindgen函數定義,這是利用宏自動生成注入代碼的地方(Macros-Auto-GeneratedInjectedCode,簡稱MAGIC)

通過查閱NEAR 官方所提供的描述文檔可知:對於使用#[near_bindgen]宏所修飾定義的Rust智能合約函數中存在有如下多種不同的可見屬性:

  • pub fn:表明該合約方法為public屬於合約接口的一部分,這意味著任何人都可以從合約外部調用它。

  • fn:若合約的方法函數未顯式地指明pub ,則表明無法從合約的外部直接調用該函數,只能在合約中由其他函數內部( internal )調用。

  • pub(crate) fn:相當於pub(in crate) ,類似於fn該可見性修飾符可將具體的合約方法限制在crate內部範圍內被調用

還有一種將合約的方法設置為internal的方式是在合約中定義一個獨立的impl Contract代碼塊。

但需注意的是,該implementation並不被#[near_bindgen]所修飾:

 #[near_bindgen] impl Contract { /// 由于该方法定义于一个被`#[near_bindgen] `所修饰的合约implementation中/// 因此该方法可由外部用户调用 pub fn increment(&mut self ) { self .internal_increment(); } } impl Contract { /// 由于该方法定义于一个并未被`#[near_bindgen] `所修饰的合约implementation中/// 因此该方法仍无法由外部用户调用 pub fn internal_increment(&mut self ) { self .counter += 1 ; } }

回調( Callbacks )函數的訪問控制

回調函數在合約中的定義必須被設置為public屬性,這樣才能通過function call的方式被調用。

當我們在合約中定義回調函數時,還需要確保該回調函數不能被他人隨意調用。即回調函數的調用者env::current_account_id()必須是本合約自己env::current_account_id()

NEAR SDK為我們定義了一個等效的Rust 宏#[private] 。利用該宏,合約的回調函數便能達到上述代碼第4-5行中所實現的相同功能。
 1. #[near_bindgen] 2. impl Contract { 3. #[private] 4. pub fn resolve_transfer(&mut self) { 5. env::log_str('This is a callback'); 6. } 7. }
  • 默認情況下,Rust語言中的所有內容都是private ,例如上述未設置public屬性的函數fn ,其默認可見性為private

    這裡需要solidity區分的是,在某些老版本的solidty編譯器中:如果合約函數的定義中不添加任何修飾符,則會被默認視為public。

    但在Rust語言中,也存在有兩個例外:

    • pub Trait 中的子項目默認都是public的;

    • pub Enum 中的Enum變量默認也是public的;


    2. 特權函數的訪問控制(白名單機制)

    在編寫Rust智能合約時,除了需要了解具體的函數可見性之外,我們還要從合約的語義層面進行深度的思考,即建立一套完整的訪問控制白名單機制。

    類似於Solidity智能合約庫openzeppelin-contracts中所定義使用的contracts/access/Ownable.sol合約那樣,某些函數作為特權函數,例如合約的初始化,合約的開啟/暫停,統一的轉賬等.... ...則只能由合約的擁有者(owner)前來調用,這些函數也通常被稱為only owner函數。

    但是owner本質上也是一個合約的外部調用者,如需調用,這些關鍵函數必須被設置為public屬性。那麼,既然這些函數是public屬性,是否意味著所有的其他普通用戶也都可以前來調用呢?

    答案是肯定的,不過非owner的普通用戶在調用執行時,他們很快就會發現:能調,但不能完全調

    這是因為在智能合約中,可為合約函數定義一些訪問控制規則,必須要滿足相應的規則才能完整地被授權執行。例如,在solidity合約中存在如下常用的modifier:

  •  bstract contract Ownable is Context { address private _owner; .... /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner ( ) { require(owner() == _msgSender(), 'Ownable: caller is not the owner' ); _; } }
  • 由該modifier所修飾的合約函數在被調用時,將首先檢查本次交易的調用者msg.sender是否為合約是初始化時所設置的owner ,若不匹配則後續該函數的執行將abortrevert ,從而阻止非法用戶的訪問執行。

    同樣的,在NEAR Rust的智能合約中,我們也可以實現如下類似的自定義Trait:

     pub trait Ownable { fn assert_owner(& self ) { assert_eq!(env::predecessor_account_id(), self .get_owner()); } fn get_owner(& self ) -> AccountId; fn set_owner(&mut self , owner: AccountId); }

    利用該trait也能實現對於合約中某些特權函數的訪問控制,即本次交易中合約的調用者env::predecessor_account_id()需要等於本合約的owner

  • 以上我們便建立了一個簡單且僅針對ownable特權函數的白名單示例。基於此原理,我們可以通過自定義更為複雜的modifiertrait在白名單中設置多位用戶,或設定多個白名單來達到良好精細的分組訪問控制效果。

    3. 更多訪問控制方法

    有關其他Rust智能合約中訪問控制的方法例如:

    • 合約的調用時機控制

    • 合約函數的多簽調用機制,governance(DAO)的實現

    • ...

    盡請關注本系列智能合約養成日記的後續推送????