Solidity 中的数学(第 2 部分:溢出)

本文是关于在 Solidity 中进行数学运算的系列文章中的第二篇。这次的主题是:溢出

介绍

每次我看到+***审计另一个 Solidity 智能合约时,我都会开始写以下评论:“这里可能会溢出”。我需要几秒钟来写这四个字,在这几秒钟内,我观察附近的行,试图找出原因,为什么不可能溢出,或者为什么在这种特殊情况下应该允许溢出。如果找到原因,我会删除评论,但大多数情况下评论会保留在最终审计报告中。

事情不应该是这样的。算术运算符应该允许编写紧凑且易于阅读的公式,例如a**2 + 2*a*b + b**2. 然而,这个表达式几乎肯定会引起一堆安全问题,真正的代码更可能是这样的:

add (add (pow (a, 2), mul (mul (2, a), b)), pow (b, 2))

这里addmulpow分别是实现 、 和 的“安全”版本+*函数**

不鼓励简洁方便的语法,很少使用简单的算术运算符(一次不超过一个),到处都是繁琐且不可读的函数语法。在这篇文章中,我们分析了这个问题,它让事情变得如此奇怪,它臭名昭著的名字是:溢出

我们在某处走错了路

有人会说,溢出总是存在的,所有的编程语言都深受其害。但这真的是真的吗?您是否见过类似为 C++、Python 或 JavaScript 实现的SafeMath库?在证明相反的情况之前,您真的认为每个+*都是安全漏洞吗?很可能,您对这两个问题的回答都是“否”。所以,

为什么 Solidity 溢出会如此痛苦?

剧透:无处可逃,无处可藏。

数字在纯数学中不会溢出。可以将两个任意大数相加并得到精确的结果。在 JavaScript 和 Python 等高级编程语言中,数字不会溢出。在某些情况下,结果可能会落入无穷大,但至少将两个正数相加可能永远不会产生负结果。在 C++ 和 Java 中,整数会溢出,但浮点数不会。

在那些整数类型确实会溢出的语言中,纯整数主要用于索引、计数器和缓冲区大小,即用于受正在处理的数据大小限制的值。对于可能超出普通整数范围的值,有浮点数、大整数和大十进制数据类型,它们是内置的或通过库实现的。

基本上,当算术运算的结果不适合参数的类型时,编译器可能会做一些选择:i)使用更宽的结果类型;ii) 返回截断的结果并使用侧通道通知程序溢出;iii) 抛出异常;和 iv) 只是默默地返回截断的结果。

int在处理类型溢出时,第一个选项在 Python 2 中实现。第二个选项是 CPU 中的进位/溢出标志的用途。第三个选项由 SafeMath 库为 Solidity 实现。第四个选项是 Solidity 自己实现的。

第四个选项可能是最糟糕的一个,因为它使算术运算容易出错,同时使溢出检测非常昂贵,特别是对于乘法情况。为了安全起见,需要在每次乘法后执行额外的除法。

因此,Solidity 既没有安全类型,可以跑到,也没有安全操作,可以躲在后面。无处可逃,无处可躲,开发人员不得不面对溢出并在整个代码中与它们作斗争。

那么,下一个问题是:

为什么 Solidity 没有安全类型和操作?

剧透:因为 EVM 没有它们。

智能合约必须是安全的。它们中的错误和漏洞会造成数百万美元的损失,因为我们已经从惨痛的教训中吸取了教训。作为智能合约开发的主要语言,Solidity 非常重视安全性。如果有许多功能应该可以防止开发人员搬起石头砸自己的脚。我们指的是payable关键字、类型转换限制等功能。每个主要版本都会添加此类功能,通常会破坏向后兼容性,但社区为了更好的安全性而容忍这种情况。

然而,基本的算术运算非常不安全,现在几乎没有人直接使用它们,而且情况也没有改善。唯一变得更安全的操作是除法:除以零以前返回零,但现在它抛出异常,但即使是除法也没有变得完全安全,因为它仍然可能溢出。是的,在intSolidity 类型中,当 -2¹²⁷ 除以 -1 时会溢出,因为正确答案 (2¹²⁷) 不适合int. 所有其他操作,即+-***仍然容易发生溢出或下溢,因此本质上是不安全的。

Solidity 中的算术运算复制相应的 EVM 操作码的行为,并且使这些运算在编译器级别安全会增加数倍的气体消耗。普通ADD操作码需要 3 个 gas。作者设法找到的用于实现安全添加的最便宜的操作码序列是:

DUP2(3) DUP2(3) NOT(3) LT(3) <overflow>(3) JUMPI(10) ADD(3)

<overflow>是溢出时跳转的地址。括号中的数字是操作的 gas 成本,这些数字总共给我们 28 gas。几乎是 plain 的 10 倍ADD。太多了,对吧?这取决于你比较的是什么。比如说,从 SafeMath 库调用add函数将花费大约 88 gas。

因此,库或编译器级别的安全算术成本很高,但是

为什么 EVM 没有安全的算术操作码?

剧透:没有充分的理由。

出于性能原因,有人会说 EVM 中的算术语义复制了 CPU 的算术语义。是的,一些现代 CPU 有256 位运算的操作码,但主流 EVM 实现似乎不使用这些操作码。Geth 使用big.IntGo 编程语言标准库中的类型。这种类型实现了由原生单词数组支持的任意宽大整数。Parity 使用自己的库在本机 64 位字之上实现固定宽度的大整数。

对于这两种实现,算术溢出检测的额外成本几乎为零。因此,一旦 EVM 拥有算术操作码版本,在溢出时恢复,它们的 gas 成本可以与现有的不安全版本相同,或者略高。

更有用的是完全不溢出的操作码,而是返回整个结果。这样的操作码将允许在编译器或库级别有效地实现任意宽的大整数。

我们不知道为什么 EVM 没有上述操作码。也许只是因为其他主流虚拟机没有它们?

到目前为止,我们讲述的是真正的溢出:计算结果太大而不适合结果数据类型的情况。现在是时候发现问题的另一面了:

幻影溢出

如何计算 Solidity 中x的 3%?在主流语言中,人们只是写0.03*x,但 Solidity 不支持分数。怎么样x*3/100?好吧,这在大多数情况下都有效,但是如果x太大以至于x*3会溢出怎么办?从上一节我们知道该怎么做,对吧?只需使用mulSafeMath 并确保安全:mul (x, 3) / 100......没那么快。

后一个版本更安全一些,因为它会还原前一个版本返回错误结果的位置。这很好,但是……到底为什么计算 3% 的东西可能会溢出?某物的 3% 保证低于原始价值:无论是名义价值还是绝对价值。所以,只要x适合 256 位字,那么x的 3%也应该适合,不是吗?

好吧,我称之为“幻影溢出”:最终计算结果适合结果数据类型,但某些中间操作溢出的情况。

虚拟溢出比真实溢出更难检测和解决。一种解决方案是对中间值使用更宽的整数类型甚至浮点类型。另一个是重构表达式以使幻像溢出成为不可能。让我们尝试用我们的表达式来做后者。

算术定律告诉我们,以下公式应该产生相同的结果:

(x * 3) / 100
(3 * x) / 100
(x / 100) * 3
(3 / 100) * x

但是,Solidity 中的整数除法与纯数学中的除法不同,因为在 Solidity 中它将结果四舍五入为零。前两个变体基本上是等价的,并且都存在幻象溢出。第三种变体没有幻象溢出问题,但不太精确,尤其是对于小x。第四个变体更有趣,因为它会令人惊讶地导致编译错误:

browser/Junk.sol:5:18: TypeError: Operator * not compatible with types rational_const 3 / 10 and uint256
browser/Junk.sol:5:18: TypeError: Operator * not compatible with types rational_const 3 / 10 and uint256

我们已经在上一篇文章中中描述了这种行为。为了使第四个表达式编译,我们需要像这样更改它:

(uint (3) / 100) * x

然而,这并没有多大帮助,因为更正后的表达式的结果始终为零,因为3 / 100向零四舍五入就是零。

通过第三个变体,我们设法以精度为代价解决了幻影溢出问题。实际上,精度损失仅对小x显着,而对于大x则可以忽略不计。请记住,对于原始表达式,只有大x才会出现幻像溢出问题,因此我们似乎可以像这样组合这两种变体:

x > SOME_LARGE_NUMBER ? x / 100 * 3 : x * 3 / 100

这里SOME_LARGE_NUMBER可以计算为 (2²⁵⁶-1)/3 并将该值向下舍入。现在对于小x我们使用原始公式,而对于大x我们使用不允许幻像溢出的修改公式。看起来我们现在解决了幻像溢出问题,而没有显着降低精度。干得好,对吧?

在这种特殊情况下,可能是的。但是,如果我们需要计算的不是 3%,而是 3.1415926535% 怎么办?公式为:

x > SOME_LARGE_NUMBER ?
x / 1000000000000 * 31415926535 :
x * 31415926535 / 1000000000000

我们的SOME_LARGE_NUMBER意愿变为 (2²⁵⁶-1)/31415926535。那时没有那么大。那么 3.141592653589793238462643383279% 呢?这种方法适用于简单的情况,但似乎不能很好地扩展。

结论

EVM 不提供溢出保护操作码。Solidity 在编译器级别不提供任何溢出保护。因此,智能合约开发人员必须在代码级别解决溢出问题,这使得代码变得繁琐且 gas 效率较低。

幻象溢出更难检测和解决。直截了当会导致权衡取舍,而且通常无法扩展。

在我们的下一篇文章中,我们将提出解决幻影溢出问题的更好方法

猜你喜欢

转载自blog.csdn.net/weixin_39842528/article/details/128728149
今日推荐