前言
早上做题的时候遇到了这个存储指针未初始化导致的状态变量覆盖的问题,因此来学习一下。
参考文章:
警惕!Solidity缺陷易使合约状态失控
Uninitialized Storage Pointer
原理
solidity中未初始化的storage pointer有点类似C语言中的空指针。
在传统编程语言中(如C, C++),对空指针(Null Pointer)的访问,通常会引起程序的报错或崩溃。空指针的值等于零,但是语言和底层系统也同时保证内存中地址为 0 的位置是不能存放有意义的值。而在例如 Java 或者 C# 中有 引用 的概念,但是它们都定义了一个空引用的值,“null”。空引用是一个引用的安全保护值,保证这个引用不会指向任何数据。
而solidity中的“空指针”就不是这样的了。一个简单的例子:
pragma solidity ^0.4.25;
contract bet {
uint public num1;
uint public num2;
uint public num3;
struct Feng{
uint nn1;
uint nn2;
uint nn3;
}
function number(uint _num1, uint _num2, uint _num3) public {
Feng feng;
feng.nn1=_num1;
feng.nn2=_num2;
feng.nn3=_num3;
}
}
定义了一个Feng结构体,在number函数中传入三个uint值,然后分别覆盖函数中的feng变量。这些代码乍一看确实没啥问题,其实问题很大。
放在Remix编译的话会报一个warning:
但是并不是Error,可以正常的Deploy,然后试试传个值:
发现变成了这样:
这是怎么回事呢?原因就在于:
在函数内部申明一个变量,通常默认是局部变量。但是Solidity的处理有些问题,在此处反直觉地默认让引用类型(Reference Type)变量 feng 存储位置为 storage,因此对变量 feng 的修改,作用范围是“全局”的。并且对于未初始化的storage 指针(类似传统语言中的空指针),Solidity 默认其指向 storage 的起始地址,即指向合约开头定义的状态变量。
联想一下solidity插槽的知识:
因此也就相当于,对feng.nn1指向slot0,feng.nn2指向slot1,feng.nn3指向slot2。
而在这个例子中,slot0是num1,slot1是num2,slot2是num3,因此改变了这三个状态变量的值。
数组同样有这样的问题:
pragma solidity ^0.4.26;
contract bet {
uint public num1;
uint[] public feng;
function number(uint _num) public {
uint[] tmp;
tmp.push(_num);
feng=tmp;
}
}
这里的tmp默认还是storage,指向slot0,导致了num1的值被覆盖。
修复
实际上,这个问题只存在于solidity0.5.0之前的版本,编译器版本为0.4.26的话,报的还只是一个warning,不影响deploy;在下一个版本,0.5.0里面就变成了报error:
至于在0.5.0之前的版本,对于结构体的话,是使用mapping进行结构体的初始化,并使用storage进行拷贝:
pragma solidity ^0.4.26;
contract bet {
uint public num1;
uint public num2;
uint public num3;
struct Feng{
uint nn1;
uint nn2;
uint nn3;
}
mapping (uint => Feng) fengs;
function number(uint _id, uint _num1, uint _num2, uint _num3) public {
Feng storage feng = fengs[_id];
feng.nn1=_num1;
feng.nn2=_num2;
feng.nn3=_num3;
}
}
对于数组的话,类似上面的那个例子,就是在函数中声明的时候进行初始化:
pragma solidity ^0.4.26;
contract bet {
uint public num1;
uint public num2;
uint public num3;
uint[] public feng;
function number(uint _num) public {
uint[] storage tmp= feng;
tmp.push(_num);
}
}