Rust 智慧合約養成日記(8)合約安全之許可權控制

語言: CN / TW / HK

往期回顧:

本文將具體從如下兩個角度來分別介紹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-Generated Injected Code,簡稱M.A.G.I.C. )

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

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

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

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

#[near_bindgen]
impl Contract {
/// 該方法可由使用者(或其他合約)從本合約外部呼叫
pub fn increment(&mut self) {
self.internal_increment();
}
/// 該方法只能由本合約的函式方法從內部呼叫,例如 pub fn increment 從內部呼叫了 fn internal_increment().
fn internal_increment(&mut self) {
self.counter += 1;
}
}


還有一種將合約的方法設定為 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()

1. #[near_bindgen]
2. impl Contract {
3. pub fn resolve_transfer(&mut self) {
4. if near_sdk::env::current_account_id() != near_sdk::env::predecessor_account_id() {
5. near_sdk::env::panic_str("Method resolve_transfer is private");
6. }
7. env::log_str("This is a callback");
8. }
9. }


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. }


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:


abstract 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

impl Ownable for Upgrade {
fn get_owner(&self) -> AccountId {
self.owner.clone()
}

/// 該函式只能由合約的owner前來執行
fn set_owner(&mut self, owner: AccountId) {
self.assert_owner();
self.owner = owner;
}

/// 該函式只能由合約的owner前來執行
fn stage_code(&mut self, code: Vec<u8>, timestamp: Timestamp) {
self.assert_owner();
assert!(
env::block_timestamp() + self.staging_duration < timestamp,
"Timestamp must be later than staging duration"
);
// Writes directly into storage to avoid serialization penalty by using default struct.
env::storage_write(b"upgrade", &code);
self.staging_timestamp = timestamp;
}
}


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

3. 更多訪問控制方法

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

  • 合約的呼叫時機控制

  • 合約函式的多籤呼叫機制,governance(DAO)的實現

  • ...

盡請關注本系列智慧合約養成日記的後續推送 :blush: