solidity 安全 未初始化指针的风险——天上不会掉馅饼
引用类型未初始化
引用类型,则必须明确指明数据存储哪种类型的位置里。 有三种位置: 内存memory 、 存储storage 以及 调用数据calldata 。
memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
storage 状态变量保存的位置,只要合约存在就一直存储.
calldata 用来保存函数参数的特殊数据位置,是一个只读位置。
在开发合约时,准确地理解如何使用这个操作至关重要。否则可以因为利用不适当地初始化变量来产生有漏洞的合约。
漏洞分析
在讨论这个漏洞之前,我们需要先了解状态变量在Solidity中是如何存储的。简单来说,状态变量按照合约中出现的顺序保存在slot中,例如第一个变量存储在slot0,第二个在slot1中,依次类推。
Solidity中,结构体,数组和映射等复杂数据结构,都是通过栈上的指针访问实际的存储位置,memory或者storage;当他们做的局部变量,如果没有初始化,则默认是放在 storage 中的,而且默认指向slot0。
这样意味着我们可以操作这个局部变量,间接控制了原来存在slot0上的值。
实例分析
pragma solidity ^0.4.19;
contract CryptoRoulette {
uint256 public secretNumber;
uint256 public betPrice = 0.1 ether;
struct Game {
address player;
uint256 number;
}
function CryptoRoulette() public {
shuffle();
}
function shuffle() internal {
//secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;
secretNumber = 6;
}
function play(uint256 number) payable public {
require(msg.value >= betPrice && number <= 10);
Game game; //问题所在
game.player = msg.sender;
game.number = number;
if (number == secretNumber) {
msg.sender.transfer(this.balance);
}
shuffle();
}
}
该合约设置了一个public(这里是故意的,让玩家认为这是合约的漏洞)属性的随机数 secretNumber,在 shuffle() 函数作用原来是指定blockhash和时间生成一个随机数(为了更加直观,我直接写死为6),玩家可以通过 play() 函数去盲猜这个随机数,如果猜对了就可以将合约中的所有eth取走,每次调用 play() 函数后都会重置随机数。即使玩家知道随机数是6,也不可能拿走合约里的eth,不信大家可以试试;
原因就是结构体 game的初始化对存储数据 secretNumber 的覆盖;其实就是我们上边讲的,secretNumber 存储到了slot0,game由于没有指定位置,所以默认在storage中,且位于slot0;所以我们初始化时,game.player = msg.sender ,由于结构体 game的初始化对存储数据 secretNumber 进行了覆盖,导致 secretNumber 变成了 msg.sender 的 uint256 内容,这样一来就使得后面的 if判断条件不能成立,从而使得玩家不能转走合约中的所有eth。
解决方法
我们在函数里直接初始化结构体必须加 memory 关键字,因为 memory 是使用内存来进行存储,这样一来就可以避免占用 storage 的存储位;该问题在 Solidity 0.5.0 版本以前只是进行了提示,并没有做出错误警告,所以在老版本编译器中要注意该问题。在最新的版本,则不需要担心,直接error。
这段代码其实是基于《加密轮盘赌轮》修改的,感兴趣的可以看看原来的。
GutHub 地址:github.com/smart-contract-honeypots/CryptoRoulette.sol