智能合约变量定义规范,这些坑你踩过吗
智能合约的变量定义,看着简单,写起来全是细节。我审计过不少合约,因为变量定义不规范导致的漏洞,比业务逻辑错误还多。变量类型选错、可见性设错、存储位置搞混,轻则多花gas,重则直接让合约被攻击。这篇文章把最常见的变量定义问题捋一遍,帮你少走弯路。
状态变量和局部变量怎么区分
状态变量存在链上,永久保存,每次读写都要消耗gas。局部变量只在函数执行期间存在,存在内存或栈里,不消耗存储费用。很多新手分不清这两者,把临时计算的结果也存成状态变量,白白浪费用户的手续费。

判断标准很简单:这个数据需要跨函数调用保存吗? 如果只是函数内部用一下,就定义成局部变量。如果多个函数都要访问,比如代币的余额映射、合约的所有者地址,才定义成状态变量。另外,状态变量默认是internal可见性,如果需要外部合约读取,显式加上public,编译器会自动生成getter函数。
还有一个容易忽略的点:状态变量的声明顺序会影响存储布局。Solidity会按声明顺序分配存储槽位,把相同类型的数据放在一起,能减少存储槽位的浪费。比如两个uint128拼一个槽位,比两个uint256各自占一个槽位省一半存储空间。
变量类型选错会有什么后果
整数类型的选择直接影响安全性和gas消耗。uint256能表示的最大值是2的256次方减一,做加减乘除时如果溢出,Solidity 0.8以上版本会自动回滚交易。但如果你用的是uint8,一个简单的循环计数器加到256就溢出了,合约直接罢工。

地址类型也有讲究。address和address payable的区别在于,后者才能接收以太币转账。如果你定义了一个普通address,后面想往这个地址转ETH,编译都过不去,还得做类型转换。在定义变量时就明确是否需要接收ETH,能省去后续一堆麻烦。
枚举类型和结构体也很常用。枚举本质上还是整数,但用枚举定义状态,代码可读性会好很多,比如用枚举表示订单状态比用数字0、1、2清晰得多。结构体则适合打包多个相关字段,比如一笔转账的收款人、金额、时间戳,定义成一个结构体,比三个独立变量好维护得多。
映射和数组的存储位置怎么定
映射只能作为状态变量,不能出现在函数参数或返回值里。数组可以定义成storage、memory或calldata。函数参数里用calldata修饰数组,比用memory省gas,因为calldata是只读的,不需要拷贝到内存。

storage引用类型变量之间的赋值,是引用传递,不是值拷贝。这意味着你给一个storage数组赋值给另一个storage变量,两个变量指向同一块存储,改一个另一个也变。很多bug就是这么来的。如果不想共享存储,用memory拷贝一份。
映射的键类型有限制,只能用内置值类型、bytes或string,不能用结构体或数组。值类型没有限制,可以是任意类型。定义嵌套映射时,比如mapping(address => mapping(address => uint256)),注意外层映射的值是内层映射的引用,操作时先取出来再修改,逻辑上要清晰。
变量定义规范的核心就三条:类型选对、可见性设对、存储位置放对。每一条都直接影响合约的安全性和运行成本。写合约前先花两分钟想清楚这三个问题,比事后debug省太多时间。
文章评论