往期回顾:Rust智能合约养成日记(1)合约状态数据定义与方法实现 , Rust智能合约养成日记(2)编写Rust智能合约单元测试,Rust智能合约养成日记(3)Rust智能合约部署,函数调用及Explorer的使用
往期回顾:
在大多数编程语言中,一个整数的数值通常保存在一段定长的内存当中。整数可分为两种类型,即无符号数与有符号数。它们之间的区别在于最高位是否被用作符号位,用来表示整数的正负。例如32bit的内存空间可以存储0到4,294,967,295范围之间的无符号整数(uint32),或−2,147,483,648到2,147,483,647范围之间的有符号整数(int32)。
但是,当我们在uint32的范围内,执行计算4,294,967,295 + 1并试图存储大于该整数类型最大值的结果时,会发生什么呢?
尽管该执行的结果取决于特定编程语言和编译器,但在大多数情况下,计算的结果将表现出“溢出”的现象并返回0。同时,大多数编程语言和编译器不会检查该类型的错误,而仅仅执行一个简单的模运算,甚至还存在其他未定义的行为。
整数溢出的存在,往往使得程序在运行时产生意料之外的结果。在区块链智能合约的编写中,尤其是去中心化金融领域,整数数值计算的使用场景十分普遍,因此需格外注意整数溢出漏洞存在的可能性。
假设,某金融机构使用无符号的32位整数来表示股票价格。然而,当使用该整数类型表示一个大于该类型所能表示的最大值数字时,计算机将在32位的内存范围外额外放置一个1或更多的位(即溢出),最终该数字将表示为截断了溢出位以外的值,如可能将$429,496,7296读为0。此时,如果有人使用该数值继续进行交易,股票价格将为 0 ,这将引起各种各样的混乱。因此,整数溢出漏洞的问题值得我们的重视。
如何在使用Rust语言编写智能合约时,避免整数溢出,将是本文后续讨论的重点。
若数值超出了变量类型所能表示的范围,则会导致溢出。溢出主要可分为两种情况,即整数上溢(overflow)和下溢(underflow)。
即类似于上文整数溢出漏洞概述中所描述的那样,例如在Solidity中uint32所能表示的无符号整数范围为:0 至 2^32 - 1,2^32 - 1使用16进制表示为0xFFFFFFFF,2^32 - 1再加上1即会导致上溢。
无符号整数uin32的表示范围也有下界,即最小值0。当0减去1时将导致uint32整数的下溢:
BeautyChain团队2018年4月22日宣布,BEC token在4月22日出现了异常波动。攻击者利用整数溢出造成的漏洞成功获得了10^58 个BECs。
在该合约的攻击事件中,攻击者执行了具有整数溢出漏洞的函数“batchTransfer”进行了交易
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
以下是该该函数的具体实现:
1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {2. uint cnt = _receivers.length;3. uint256 amount = uint256(cnt) * _value;4. require(cnt > 0 && cnt <= 20);5. require(_value > 0 && balances[msg.sender] >= amount);6. 7. balances[msg.sender] = balances[msg.sender].sub(amount);8. for (uint i = 0; i < cnt; i++) {9. balances[_receivers[i]] = balances[_receivers[i]].add(_value);10. Transfer(msg.sender, _receivers[i], _value);11. }12. return true;13. }
该函数用来向多个地址(_receivers)转账, 每个地址的转账金额为_value。
上述代码的第三行 uint256 amount = uint256(cnt) * _value
用来计算整个需要转账的金额,但是该行代码存在整数溢出的可能性。当_value =0x8000000000000000000000000000000000000000000000000000000000000000,同时_receivers的 长度为2. 则在第三行代码乘法运算的时候将发生整数溢出,使得amount = 0。由于amount = 0要比用户的balances[msg.sender]要小,因此第5行中检查合约调用者用户msg.sender的余额是否大于将要转出的amount数额会轻松被通过。从而攻击者可以执行后续的转账操作而获利。
本小节将介绍如何使用一些常用的手段并结合Rust语言的特性来避免整数溢出。
在Rust语言中:当我们编译获得release版本的目标文件时,若不加以配置,Rust将默认不检查整数溢出。当整数溢出时,例如在8位无符号整数(uint8)的情况下,Rust的做法通常是,使值256变成0,257变成1,以此类推。此时Rust并不会触发Panic,但是变量的值可能不是我们所期望的值。因此我们需要对Rust程序的编译选项稍加配置,使得程序在Release模式下也能够检查整数溢出,并能够触发Panic,从而避免因整数溢出而导致的程序异常现象。
配置Cargo.toml
,在release模式下检查整数溢出。
利用该配置我们可以设置程序内整数溢出时的处理策略。
对比于Solidity所能够支持的最大整数类型为u256,Rust目前标准库所能提供的最大整数类型仅为u128。为了更好地在我们的Rust智能合约中支持更大的整数运算,我们可以使用Rust uint crate来帮助拓展。
使用Rust uint
crate可提供大无符号整数类型,并内置支持了与Rust原始整数类型非常相似的API,同时兼顾了性能与跨平台可用性。
首先在Rust项目的<span>Cargo.toml</span>
中添加对uint crate的依赖,并指定版本号为最新的"0.9.1"版本。
随后我们可以在Rust程序中导入使用该crate
use uint::construct_uint;
如下语句可以用于构造自己想要的无符号整数类型:
我们可以使用如下方法首先定义变量<span>p</span>
,并使用uint crate为U1024定义的方法<span>from_dec_str</span>
为变量<span>p</span>
赋值。
// (2^1024)-1 = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215
let p = U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
单元测试一:用于检查uint是否能够支持表示U1024所能表示的最大值。
#[test]
fn test_uint(){
let p = U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
assert_eq!(p,U1024::max_value());
}
单元测试一结果:
running 1 testtest tests::test_uint ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
可见变量p: U1024
准确保存了U1024所能表示的最大值。
单元测试二:整数上溢测试
单元测试的结果如下:
running 1 test115792089237316195423570985008687907852589419931798687112530834793049593217025340282366920938463463374607431768211456thread 'tests::test_overflow' panicked at 'Integer overflow when casting to u128', src/lib.rs:16:1
根据uint crate所提供的类型转换函数.as_u128()
特性可知,当将amount_u256 通过类型转化为u128的时候,由于溢出了u128无符号整数所能表示的范围,因此将触发Painc。可见此时Rust能够检测整数上溢。
Rust语言对于整数运算中可能发生的整数溢出也提供了不同的运算行为。如果需要更精细地控制整数溢出的行为,可以调用标准库中的wrapping_*
、saturating_*
、checked_*
和overflowing_*
系列函数,本节将重点讲述checked_*
函数,读者可以检索上述关键字了解更多的控制整数溢出的方式。
checked_*
返回的类型是Option<_>
,当出现溢出的时候,返回值是None;
如 checked_sub就会进行减法运算,并且检查溢出是否会发生。
单元测试三:使用checked_sub检查整数下溢
单元测试的结果如下:
running 1 testNonetest tests::test_underflow ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s
此时在上述单元测试的结果中可以发现:当执行单元测试的时候尽管发生了整数溢出,并且运算结果返回了None
。但是并没有触发Panic。为此我们需要基于运算结果的返回值来判断是否需要触发Panic.
#[test]
fn test_underflow(){
let amounts= U256::from(0);
- let amount_u256 = amounts.checked_sub(U256::from(1));
+ let amount_u256 = amounts.checked_sub(U256::from(1)).expect("ERR_SUB_INSUFFICIENT");
println!("{:?}",amount_u256);
此时的单元测试结果输出如下:
running 1 testthread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62
即Rust能够利用checked_*
系列函数检测整数下溢。同理我们也可以用上述方式来检测整数的上溢情况,并在适当的时候触发Panic终止程序的运行。
这一期我们讲述了rust智能合约中的整数溢出问题,同时给出了建议,在书写代码时使用uint类型转换函数或者safe math来防止整数溢出问题发生,下一期我们将讲述rust智能合约中的重入问题。敬请关注。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!