权限是指为了保证职责的有效履行,任职者必须具备的对某事项进行决策的范围和程度。它常常用“具有xxxxxxx的权利”来进行表达,比如:公司的CEO具有否定某项提议的权利。站在合约的管理角度来讲,智能合约中的用户可以分为合约的owner、合约的普通用户两类。合约的owner是合约的拥有者,他可以执行合约中所有的函数;合约的普通用户可以执行合约中约定范围内的函数,对于一些对权限有校验或对执行者身份有要求的函数(比如:用onlyowner修饰器修饰的函数)是无法执行的。
solidity使用了public、private、internal、external来对函数的可见性进行限定,下面进行简单的介绍:
使用public限定的函数可以被合约内部函数、继承合约、外部合约调用
pragma solidity ^0.4.26;
contract FunctionTest {
//合约的构造函数
constructor() public {
}
//函数默认的可见性状态为public
function Func () {}
//使用public修饰的函数可以被合约内部的其他函数调用
function callFunc1 () {
Func();
}
}
contract FunctionTest2 is FunctionTest{
//使用public修饰的函数可以被外部合约调用
function callFunc2 () {
Func();
}
}
使用private限定的函数只能被合约内部函数调用
pragma solidity ^0.4.26;
contract FunctionTest1 {
//使用private限定的函数
function Func1 () private {}
//使用private修饰的函数只能被合约内部的其他函数调用
function callFunc1 () {
Func1();
}
}
当继承合约中的函数调用父合约中用private限定的函数时会编译报错:
pragma solidity ^0.4.26;
contract FunctionTest1 {
//使用private限定的函数
function Func1 () private {}
//使用private修饰的函数只能被合约内部的其他函数调用
function callFunc1 () {
Func1();
}
}
/**
* The FunctionTest2 contract does this and that...
*/
contract FunctionTest2 is FunctionTest1{
//使用private修饰的函数不可以在继承合约中调用
function callFunc2 () {
Func1();
}
}
使用internal限定的函数可以被合约内部函数以及继承合约调用:
pragma solidity ^0.4.26;
contract FunctionTest1 {
//使用internal限定的函数
function Func1 () internal {}
//使用internal限定的函数可以被合约内部的其他函数调用
function callFunc1 () {
Func1();
}
}
/**
* The FunctionTest2 contract does this and that...
*/
contract FunctionTest2 is FunctionTest1{
//使用internal限定的函数可以在继承合约调用
function callFunc2 () {
Func1();
}
}
使用external限定的函数只能外部合约调用
pragma solidity ^0.4.26;
contract FunctionTest1 {
//使用external限定的函数
function Func1 () external {}
//使用external限定的函数只能外部合约调用
function callFunc1 () {
Func1();
}
}
注:函数的默认可见性为public
Solidity编写合约和面向对象编程语言非常相似,我们可以用构造函数(constructor)来初始化合约对象,Solidity中构造函数就是方法名和合约名字相同的函数,创建合约时会调用构造函数对状态变量进行数据初始化操作。
构造函数可用的函数类型为public或internal,如果有payable修饰,就只能是public类型。而大部分人的写法都是 public或者不写。不写类型则由函数可见性默认为public类型。同时,如果构造函数带参数,则一定要放在合约下的第一个函数。
从0.4.22版本开始,solidity编译器引入了constructor关键字,以替代低版本的将合约名作为构造函数名的语法,避免程序员容易出现的编码错误,使用旧写法会出现warning 信息,新版本写法为:
构造函数之所以区别于普通函数,是因为构造函数它主要用户初始化整个合约对象,而且不能被任意用户所调用,所以一旦构造函数可以被任意用户调用时,调用者就可以获得初始化合约的权限,带来安全隐患,下面举几个之前引发的案例作为简要分析:
在编译器0.4.22之前构造函数的函数名默认是和合约名一致的,如果智能合约的开发者在开发过程中出现"构造函数名与合约名不一致"的现象(大小写、多加了一个s等情况),那么构造函数将不再是"构造函数",而变为一个任意用户可以调用的普通函数,任意用户可以通过调用该函数实现对合约的初始化操作,例如ReaperCoin11合约:
合约地址:
https://cn.etherscan.com/address/0x1b7cd071187ec0b2995b96ee82296cfa639572f1#code
如上图所示,根据注释可以知晓合约中的reaper11函数是"构造函数",但是细细看该函数名与合约名——ReaperCoin11不一致,所以此处的构造函数变成了一个public修饰的普通函数,我们可以通过Remix来看看区别:
a.构造函数名与合约名不一致时:
可以看到构造函数可以被任意用户调用
b.修改构造函数名为ReaperCoin11之后,重新编译:
此时,你会发现构造函数不可被用户调用,即不可被任意用户用于初始化合约,这就是所谓的区别!
在编译器0.4.22之后使用了constructor来替代原先的"构造函数名与合约名必须一致"的代码编写规范,但是一些合约开发者在开发工程中往往还是会出现各种错误,例如:在constructor前面加function,或者加了function然后开头的C写成了大写,即"function Constructor(){}",这样便使得构造函数变成了公有函数,可被人任意调用,下面举例来说明:
加入function变成普通函数形式:
MDOT合约:
https://cn.etherscan.com/address/0xef7d906fd1c0eb5234df32f40c6a1cb0328d7279#code
我们使用Remix编译一下,看看是否真的是这样(口说无凭嘛!) 在编译时,发现会给出“警告”哦!但是因为“警告”在合约开发中很常见,一般不是什么致命错误所以很多合约开发者在开发合约以及调试过程中会忽略这一点!
在最新版本的Remix IDE部署阶段会给出warning警告无法部署:
而在旧版Remix会直接部署,导致合约中的constructor函数缺失成为了一个"普通函数",不再是"构造函数":
constructor拼写错误
TOGToken合约:
https://cn.etherscan.com/address/0xb9d5c2548266428795fd8b1f12aedbdeb417fe54#code
在新版Remix IDE中部署时会给出警告提示,同时无法部署:
在原先的旧版本中可以正确部署:
对于一些普通函数,我们一般会使用一些修饰器来进行修饰,同时有时候也会使用public、private、internal、external来进行修饰,在笔者审计合约的时候发现有一些合约的开发者为自己留下的传说中的“后门”,下面简单的举几个例子:
Token合约:
https://cn.etherscan.com/address/0xc42209aCcC14029c1012fB5680D95fBd6036E2a0#code
如上图所示,在该合约中的burn函数被onlyAuthorized修饰器修饰限定,通过查看可以发现onlyAuthorized修饰器限定了msg.sender必须要为合约的owner,所以只有合约的owner可以调用burn函数,那么此处不是很正常吗?又有什么风险呢?答案是此处的burn函数的功能是用于销毁代币的,而burn函数中一共有两个参数,第一个参数_member为要销毁代币的用户地址,第二个参数为要销毁的代币的数量,虽然后面使用了SafeMath函数库,但是合约的owner依旧可以通过调用burn函数然后将要销毁代币的用户地址传给第一个参数,然后将要销毁代币的数量传给第二个参数即可,合约的owner可销毁任意用户的代币,甚至归0。
sacToken合约:
https://cn.etherscan.com/address/0xabc1280a0187a2020cc675437aed400185f86db6#code
如上图所示,合约中的melt函数用于销毁用户的代币,但该合约只能被合约的CFO调用,那么我们看看onlyCFO修饰器的具体细节:
可以从上图中看到,CFO其实就是合约的owner,那么我们现在可以确定melt函数真正的调用者应该是合约的owner,下面我们继续分析melt函数:在melt函数中,一共有两个参数,第一个参数是要销毁代币的目标地址,第二个参数是要销毁的代币金额数量,故合约的owner可以通过传入任意用户的地址,之后传入要销毁的代币数量,通过sub减法操作,走正常的逻辑流程之后达到销毁目标用户的代币的目的。
1、根据官方编写规范正确编写构造函数
2、对业务逻辑函数中的权限进行严格的权限设计与划分