Solidity作为一种图灵完备的高级语言,可以支持逻辑比较复杂的智能合约的编写。一般情况下,Solidity开发者可以根据自己的意愿和预期开发一个智能合约项目。同时也不能保证,没有黑客攻击的存在。因此呢,确保安全性在Solidity开发中极为重要。
今天我们要介绍的常见智能合约安全陷阱包括如下:
- 函数可重入
- 跨函数资源共享
- 整数溢出漏洞
- gas限制和循环
- tx.orign和msg.sender引发安全漏洞
- 依赖时间戳漏洞
- 利用revert发送dos攻击
- 短地址攻击
智能合约安全陷阱举例
漏洞1:函数可重入
**可重入性:**函数被调用,没有执行完成,又一次被调用
示例:
pragma solidity ^0.5.0;
contract Game{
//记录各个玩家余额
mapping(address => uint256) private balance;
constructor()payable public{
}
//玩家下注函数,向游戏合约充值
function deposit()public payable{
balance[msg.sender] += msg.value;
}
//玩家提款函数
function withdraw()public{
bool ret;
bytes memory data;
uint256 amount = balance[msg.sender];
(ret, data) = msg.sender.call.value(amount)("");
if (ret){
balance[msg.sender] = 0;
}
}
//获取合约总余额函数
function gameBalance() public view returns(uint256){
return address(this).balance;
}
}
contract GamePlayer{
//记录游戏合约地址
address payable target;
constructor()payable public{
}
//攻击函数,外部账户调用这个合约对游戏合约进行攻击
function attack(address payable addr) public{
target = addr;
Game g = Game(target);
//先向合约充值1个以太币
g.deposit.value(1000000000000000000)();
//然后替换
g.withdraw();
}
//fallback函数中进行重入提款
function()payable external{
Game g = Game(target);
g.withdraw();
}
//查看玩家合约余额
function palyerBalance() public view returns(uint256){
return address(this).balance;
}
}
从Game游戏合约中我们可以看出,它的withdraw函数有下面几方面的安全隐患:
-1. 使用了底层的call函数进行转账,同时没有指定gas消耗
-2. 提款操作时,没有将用户的余额状态变量清空,就向用户转账
由于游戏合约的withdraw函数,使用了call函数向玩家进行转账。call函数默认是将剩余的gas全部传入, 也就是说call函数默认允许使用全部剩余的gas。这样的话,如果call函数引发了重入的话,会执行很多代码,无法及时终止。另外withdraw函数是在call调用之后再将用户的余额的状态变量清空,如果call函数引发了重入再次调用withdraw函数,代码会判断用户余额状态,发现还能进行转账。
解决方法:
- 对于第1点安全隐患的解决方案是使用send或者transfer函数进行转账,transfer函数只传入2300个gas,即便发生重入,也无法调用修改状态变量等操作,因为一个修改状态变量的操作所消耗的gas至少是5000。这样可以保证本次转账重入是安全的。
- 对于第2点安全隐患的解决方案是使用CEI模式,全称是Checks-Effects-Interactions,先检查,再生效,最后交互,对于这个例子,游戏合约的withdraw函数应该先检查用户的状态变量是否是大于0,如果等于0就不用转账,如果是大于0的,将用户的状态余额先清空,最后进行进行转账。
- 使用互斥锁对balance资源进行保护,如下:
pragma solidity ^0.5.0;
contract Game{
mapping(address => uint256) private balance;
bool balanceLock = false;
constructor()payable public{
}
function deposit()public payable{
balance[msg.sender] += msg.value;
}
function withdraw()public{
require(!balanceLock);
balanceLock = false;
bool ret;
bytes memory data;
uint256 amount = balance[msg.sender];