往期回顾:
本文将具体从如下两个角度来分别介绍Rust智能合约中权限控制的相关事宜:
合约方法(函数)访问/调用的可见性;
特权函数的访问控制/权责划分;
1.合约函数(方法)可见性
在编写智能合约时,我们可以通过指定合约函数的可见性(Visibility)来控制什么函数可以被谁调用。借此我们可以轻松地保护合约中的某些关键部分不被意外地访问或操控。
为体现正确设置合约函数可见性的重要性,本文将以Bancor Network交易所为例进行说明。早在2020的06月18日,该交易所便发生了一起由于合约的关键函数访问控制权限设置错误,所导致的合约资产安全事件。该合约由Solidity语言编写而成。在该语言中,合约函数的可见性大致被分为public/external
和private/internal
两种。前者允许了合约函数可被合外部的调用者调用,即可视为合约接口的一部分。
然而此前Bancor Network交易所在修改某一安全漏洞时,由于疏忽,误将合约中部分的关键转账函数设置为了public属性(如下所示):
基于此,任何人包括普通用户,都可以从该合约的外部调用这些函数为自己或他人进行相应的转账操作。
该关键漏洞的存在,致使其用户的59万美元资产面临着严重的风险。
#[near_bindgen]
:#[near_bindgen]在near-sdk-macros-version包中通过near_bindgen函数定义,这是利用宏自动生成注入代码的地方(Macros-Auto-GeneratedInjectedCode,简称M.A.G.I.C.)
通过查阅 NEAR 官方所提供的描述文档可知:对于使用#[near_bindgen]
宏所修饰定义的Rust智能合约函数中存在有如下多种不同的可见属性:
pub fn:
表明该合约方法为public
属于合约接口的一部分,这意味着任何人都可以从合约外部调用它。fn:
若合约的方法函数未显式地指明pub
,则表明无法从合约的外部直接调用该函数,只能在合约中由其他函数内部(internal
)调用。pub(crate) fn:
相当于pub(in crate)
,类似于fn
,该可见性修饰符可将具体的合约方法限制在crate内部范围内被调用。
internal
的方式是在合约中定义一个独立的impl Contract
代码块。但需注意的是,该implementation并不被#[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()
。
#[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
,若不匹配则后续该函数的执行将abort
或revert
,从而阻止非法用户的访问执行。同样的,在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特权函数的白名单示例。基于此原理,我们可以通过自定义更为复杂的
modifier
或trait
在白名单中设置多位用户,或设定多个白名单来达到良好精细的分组访问控制效果。3. 更多访问控制方法
有关其他Rust智能合约中访问控制的方法例如:
合约的调用时机控制
合约函数的多签调用机制,governance(DAO)的实现
...
尽请关注本系列智能合约养成日记的后续推送
????