本次给大家带来的是“智能合约审计系列”的第三篇——“变量覆盖&不一致性检查”。
在这篇文章中将会给大家介绍一些智能合约开发者在合约开发过程中经常会忽略的变量初始化与赋值问题,包括智能合约开发者在开发中因粗心导致的“一致性检查”问题。
在智能合约语言 Solidity当中,存在Storage(存储器)和 Memory(内存)两个不同的概念。Storage变量是指永久存储在区块链中的变量。Memory变量是临时的,这些变量在外部调用结束后会被移除。
Solidity中struct和数组在局部变量中默认是存放在storage中的,因此可以利用未定义的存储指针的问题,p会被当成一个指针,并默认指向slot[0]和slot[1],因此在进行p.name和p.mappedAddress赋值的时候,实际上会修改变量testA,testB的值:
数组也是类似的情况:
结构体 Unintialised Storage Pointers 问题的正确的解决方法是将声明的 struct 进行赋值初始化,通过创建一 个新的临时 memory 结构体,然后将它拷贝到 storage 中。
数组Unintialised Storage Pointers 问题的正确解决方法是在声明局部变量 x 的时候,同时对 x 进行初始化操作。
Solidity 编译器开发团队在Solidity 0.4.25中对存在 Unintialised Storage Pointers问题的代码进行了修复,否则将无法正常通过编译。开发人员需要关注 Solidity版本,并且使用 Solidity >=0.4.25 编写代码。
未初始化的结构体局部变量
pragma solidity ^0.4.22; contract NameRegistrar { bool public unlocked = false; // registrar locked, no name updates struct NameRecord { // map hashes to addresses bytes32 name; address mappedAddress; } mapping(address => NameRecord) public registeredNameRecord; // records who registered names mapping(bytes32 => address) public resolve; // resolves hashes to addresses function register(bytes32 _name, address _mappedAddress) public { // set up the new NameRecord NameRecord newRecord; newRecord.name = _name; newRecord.mappedAddress = _mappedAddress; resolve[_name] = _mappedAddress; registeredNameRecord[msg.sender] = newRecord; require(unlocked); // only allow registrations if contract is unlocked } }
第一次部署后检查合约的unluck状态:
当输入name="0x0000000000000000000000000000000000000000000000000000000000000001"(63个0),地址任意地址时,会覆盖unlocked的值,使其变为true。
未初始化的数组局部变量
pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; contract UnfixedArr { bool public frozen = false; function wrongArr(bytes[] elements) public { bytes[1] storage arr; arr[0] = elements[0]; } }
当输入elements= "0x0000000000000000000000000000000000000000000000000000000000000001",会覆盖frozen的值,使其变为true。
在进行转账操作时我们需要对转出账号的资产是否足够、被授权账户的转账额度是否小于指定的转账额度、接受转账的账户总额度在转账后是否大于在转账前的额度等一系列检查,但是有时候合约的设计者在检查时因为疏忽会将检查对象搞错导致出现逻辑设计问题,最终造成合约的巨额损失。
allowed不一致性检查漏洞
如上面代码所示:
require(_value <= allowed[_from][msg.sender]);
这一句的条件检测与
allowed[_from][_to] -= _value;
这一语句的操作很明显存在不相符,其中require检测的是当前被授权代为转账的账号可操作转账额度是否小于授权的额度,而下面的allowed语句为更新代为转账的额度,但是这里的更新对象却“不一致”,最终导致设置转账额度权限后,攻击者能够持续转账,直到转完所有余额。
balances 不一致性检查漏洞
如上面代码所示:
require(balances[msg.sender] >= _value);
这一句的条件检测与
balances[_from] -= _value;
这一语句的操作不相符,攻击者能够通过"整型溢出",让"_from"账户余额获得极大的token 数量。
同时,有一些合约中使用了safeMath 安全方法进行计算"balances[_from] = balances[_from].sub(_value);",所以暂时没有溢出问题,但是条件检查部分是冗余的。
在智能合约的开发过程中开发人员应该要十分留意功能函数的的设计问题,尤其是对于赋值、转账这样的敏感操作,有时候一个笔误就会引发巨额损失,同时对于安全审计人员在审计过程中也应该是否留意各个功能函数的设计以及操作对象的一致性和变量的引用的恰当性等安全问题。