Skip to content

Commit 83091b8

Browse files
committed
Include suggestions
1 parent 86d1e7d commit 83091b8

File tree

1 file changed

+111
-54
lines changed

1 file changed

+111
-54
lines changed

learn_evm/arithmetic-checks.md

Lines changed: 111 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
# A Guide on Performing Arithmetic Checks in the EVM
22

3-
The EVM is a peculiar machine that many of us have come to love and hate for all its quirks.
4-
One such quirk is the absence of native arithmetic checks, which are typically present in most architectures and virtual machines through the use of carry bits or an overflow flag.
5-
The EVM treats all stack values as uint256 types.
6-
Although opcodes for signed integers (such as `sdiv`, `smod`, `slt`, `sgt`, etc.) exist,
7-
arithmetic checks must be implemented within the constraints of the EVM.
3+
The Ethereum Virtual Machine (EVM) distinguishes itself from traditional computer systems and virtual machines through several unique aspects.
4+
One notable variation is its treatment of arithmetic checks.
5+
While most architectures and virtual machines offer access to carry bits or an overflow flag,
6+
these features are not present in the EVM.
7+
As a result, developers must manually incorporate these safeguards within the machine's constraints.
88

9-
> Note: [EIP-1051](https://eips.ethereum.org/EIPS/eip-1051)'s goal is to introduce the opcodes `ovf` and `sovf`.
10-
> These would provide built-in overflow flags. However, the EIP's current status is stagnant.
9+
Starting with Solidity version 0.8.0 the compiler includes over and underflow protection in all arithmetic operations by default.
10+
Prior to version 0.8.0, developers had to implement these checks manually, often using a library known as [SafeMath](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol), originally developed by OpenZeppelin.
11+
Much like how SafeMath works, the compiler inserts arithmetic checks through additional operations.
1112

12-
Since Solidity version 0.8.0 the compiler includes over and underflow protection in all arithmetic operations by default.
13-
Before version 0.8.0, these checks had to be implemented manually - a commonly used library is called [SafeMath](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol), originally developed by OpenZeppelin.
14-
Much like how SafeMath works, arithmetic checks are inserted by the compiler through additional operations.
13+
In this article, we'll explore many ways to perform arithmetic checks within the EVM.
14+
This guide is designed for those with a keen interest in bit manipulations and seek a deeper understanding of the EVM's inner workings.
15+
It is assumed that you have a basic understanding of bitwise arithmetic and Solidity opcodes.
1516

16-
> **Disclaimer:** Please note that this post is for educational purposes.
17+
Some additional references for complementary reading are:
18+
19+
- [evm.codes](https://evm.codes)
20+
- [Understanding Two's Complement](https://www.geeksforgeeks.org/twos-complement/)
21+
22+
> **Disclaimer:** Please note that this article is for educational purposes.
1723
> It is not our intention to encourage micro optimizations in order to save gas,
18-
> as this can potentially lead to the introduction of new bugs that are difficult to detect and may compromise the security and stability of the protocol.
19-
> As a protocol developer, it is important to prioritize the safety and security of the protocol over [premature optimization](https://www.youtube.com/watch?v=tKbV6BpH-C8).
24+
> as this can potentially lead to the introduction of new bugs that are difficult to detect and may compromise the security and stability of a protocol.
25+
> As a developer, it is important to prioritize the safety and security of the protocol over [premature optimization](https://www.youtube.com/watch?v=tKbV6BpH-C8).
2026
> In situations where the code for the protocol is still evolving, including redundant checks for critical operations may be a good practice.
2127
> However, we do encourage experimentation with these operations for educational purposes.
2228
@@ -30,7 +36,7 @@ Alternatively, by using the `--ir` flag, we can examine the Yul code that is gen
3036
> This provides an opportunity to examine the Yul code and gain a better understanding of how arithmetic checks are executed in Solidity.
3137
> However, it's important to keep in mind that the final bytecode may differ slightly when compiler optimizations are turned on.
3238
33-
To illustrate how the compiler detects overflow in unsigned integer addition, consider the following example of Yul code that is produced by the compiler.
39+
To illustrate how the compiler detects overflow in unsigned integer addition, consider the following example of Yul code that is produced by the compiler before version 0.8.16.
3440

3541
```solidity
3642
function checked_add_t_uint256(x, y) -> sum {
@@ -84,7 +90,8 @@ This can be demonstrated by the transformation `~b = ~(0 ^ b) = ~0 ^ b = MAX ^ b
8490
> We also obtain the relation `~b + 1 = 0 - b = -b` if we add `1` mod `2**256` to both sides of the previous equation.
8591
8692
By computing the result of the addition first and then performing a check on the sum,
87-
modern versions of Solidity can eliminate the need for performing extra arithmetic operations in the comparison.
93+
we eliminate the need for performing extra arithmetic operations in the comparison.
94+
This is how the compiler implements arithmetic checks for unsigned integer addition in versions 0.8.16 and later.
8895

8996
```solidity
9097
/// @notice versions >=0.8.16
@@ -104,7 +111,7 @@ An important observation is that `a > a + b` (mod `2**256`) for `b > 0` is only
104111

105112
## Arithmetic checks for int256 addition
106113

107-
The Solidity compiler generates the following (equivalent) code for detecting overflow in signed integer addition:
114+
The Solidity compiler generates the following (equivalent) code for detecting overflow in signed integer addition below version 0.8.16.
108115

109116
```solidity
110117
/// @notice versions >=0.8.0 && <0.8.16
@@ -148,29 +155,43 @@ The first bit of an integer represents the sign, with `0` indicating a positive
148155
For positive integers (those with a sign bit of `0`), their binary representation is the same as their unsigned bit representation.
149156
However, the negative domain is shifted to lie "above" the positive domain.
150157

151-
```
152-
| -------------------------------- uint256 -------------------------------- |
153-
0 --------------------------------------------------------------------- uint256_max
158+
$$uint256 \text{ domain}$$
159+
160+
$$
161+
├\underset{0}{─}────────────────────────────\underset{\hskip -1.5em 2^{256} - 1}{─}┤
162+
$$
154163

155-
| --------- positive int256 --------- | --------- negative int256 --------- |
156-
0 ------------------------ int256_max | int256_min ------------------------ -1
164+
```solidity
165+
0x0000000000000000000000000000000000000000000000000000000000000000 // 0
166+
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // uint256_max
157167
```
158168

169+
$$int256 \text{ domain}$$
170+
171+
$$
172+
\overset{\hskip 1em positive}{
173+
├\underset{0}{─}────────────\underset{\hskip -2em 2^{255} - 1}{─}┤
174+
}
175+
\overset{\hskip 1em negative}{
176+
├────\underset{\hskip -3.5em - 2^{255}}─────────\underset{\hskip -0.4 em -1}{─}┤
177+
}
178+
$$
179+
159180
```solidity
160181
0x0000000000000000000000000000000000000000000000000000000000000000 // 0
161182
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // int256_max
162183
0x8000000000000000000000000000000000000000000000000000000000000000 // int256_min
163184
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // -1
164185
```
165186

166-
The maximum positive integer that can be represented in a two's complement system using int256 is
167-
`0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff` which is equal to half of the maximum value that can be represented using uint256.
187+
The maximum positive integer that can be represented in a two's complement system using 256 bits is
188+
`0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff` which is roughly equal to half of the maximum value that can be represented using uint256.
168189
The most significant bit of this number is `0`, while all other bits are `1`.
169190

170191
On the other hand, all negative numbers start with a `1` as their first bit.
171-
If we look at the underlying hex representation of these numbers, they are all greater than or equal to the smallest integer that can be represented using int256, which is `0x8000000000000000000000000000000000000000000000000000000000000000` (equal to `1` shifted 255 bits to the left).
192+
If we look at the underlying hex representation of these numbers, they are all greater than or equal to the smallest integer that can be represented using int256, which is `0x8000000000000000000000000000000000000000000000000000000000000000`. The integer's binary representation is a `1` followed by 255 `0`'s.
172193

173-
To obtain the negative value of an integer in a two's complement system, we can flip all the underlying bits and add `1`: `-a = ~a + 1`.
194+
To obtain the negative value of an integer in a two's complement system, we flip the underlying bits and add `1`: `-a = ~a + 1`.
174195
An example illustrates this.
175196

176197
```solidity
@@ -201,7 +222,7 @@ Finally, looking at the underlying bit (or hex) representation highlights the im
201222
= 0x0000000000000000000000000000000000000000000000000000000000000000
202223
```
203224

204-
Newer versions of Solidity prevent integer overflow by using the computed result `c = a + b` to check for overflow/underflow.
225+
Starting with Solidity versions 0.8.16, integer overflow is prevented by using the computed result `c = a + b` to check for overflow/underflow.
205226
However, unlike unsigned addition, signed addition requires two separate checks instead of one.
206227

207228
```solidity
@@ -218,8 +239,9 @@ function checkedAddInt2(int256 a, int256 b) public pure returns (int256 c) {
218239
}
219240
```
220241

221-
Nevertheless, using the boolean exclusive-or lets us combine these checks into one step.
222-
Solidity doesn't have a built-in operation for boolean values, but we can still make use of it through inline-assembly. In doing so, we need to take care that both inputs are actually boolean (either 0 or 1), as the xor operation works bitwise and isn't restricted to boolean values.
242+
Nevertheless, by utilizing the boolean exclusive-or, we can combine these checks into a single step.
243+
While Solidity doesn't permit the `xor` operation for boolean values, it can still be employed through inline-assembly.
244+
While doing so, it is crucial to ensure that both inputs are genuinely boolean (either `0` or `1`), as the xor operation functions bitwise and is not limited to only boolean values.
223245

224246
```solidity
225247
function checkedAddInt3(int256 a, int256 b) public pure returns (int256 c) {
@@ -313,9 +335,9 @@ function checkedMulUint1(uint256 a, uint256 b) public pure returns (uint256 c) {
313335
}
314336
```
315337

316-
> It's important to note that the Solidity compiler always includes a division by zero check for all division and modulo operations, regardless of the presence of an unchecked block.
317-
> The EVM itself simply returns `0` when dividing by `0`, and this also applies to inline-assembly.
318-
> If the order of the boolean expressions is evaluated in reverse order, it could cause an arithmetic check to incorrectly revert when `a = 0`.
338+
> The Solidity compiler always includes a zero check for all division and modulo operations, irrespective of whether an unchecked block is present.
339+
> The EVM itself, however, returns `0` when dividing by `0`, which applies to inline-assembly as well.
340+
> Evaluating the boolean expression `a != 0 && b > type(uint256).max / a` in reverse order would cause an incorrect reversion when `a = 0`.
319341
320342
We can compute the maximum value for `b` as long as `a` is non-zero. However, if `a` is zero, we know that the result will be zero as well, and there is no need to check for overflow.
321343
Like before, we can also make use of the result and try to reconstruct one multiplicand from it. This is possible if the product didn't overflow and the first multiplicand is non-zero.
@@ -353,7 +375,7 @@ function checkedMulUint3(uint256 a, uint256 b) public pure returns (uint256 c) {
353375

354376
## Arithmetic checks for int256 multiplication
355377

356-
In older versions, the Solidity compiler uses four separate checks to detect integer multiplication overflow.
378+
In versions before 0.8.17, the Solidity compiler uses four separate checks to detect integer multiplication overflow.
357379
The produced Yul code is equivalent to the following high-level Solidity code.
358380

359381
```solidity
@@ -370,7 +392,7 @@ function checkedMulInt(int256 a, int256 b) public pure returns (int256 c) {
370392
}
371393
```
372394

373-
Newer Solidity versions optimize the process by utilizing the computed product in the check.
395+
Since Solidity version 0.8.17, the check is performed by utilizing the computed product in the check.
374396

375397
```solidity
376398
/// @notice versions >=0.8.17
@@ -414,7 +436,7 @@ otherwise the value won't be interpreted correctly.
414436
It's worth noting that not all operations require clean upper bits.
415437
In fact, even if the upper bits are dirty, we can still get correct results for addition.
416438
However, the sum will usually contain dirty upper bits that will need to be cleaned.
417-
For example, when performing addition without knowing what the upper bits are set to, we get the following result.
439+
For example, we can perform addition without knowledge of the upper bits.
418440

419441
```solidity
420442
0x????????????????????????????????????????????????fffffffffffffffe // int64(-2)
@@ -425,7 +447,7 @@ For example, when performing addition without knowing what the upper bits are se
425447
It is crucial to be mindful of when to clean the bits before and after operations.
426448
By default, Solidity takes care of cleaning the bits before operations on smaller types and lets the optimizer remove any redundant steps.
427449
However, values accessed after operations included by the compiler are not guaranteed to be clean. In particular, this is the case for addition with small data types.
428-
The bit cleaning steps will be removed by the optimizer (even without optimizations enabled) if a variable is only accessed in a subsequent assembly block.
450+
For example, the bit cleaning steps will be removed by the optimizer (even without optimizations enabled) if a variable is only accessed in a subsequent assembly block.
429451
Refer to the [Solidity documentation](https://docs.soliditylang.org/en/v0.8.18/internals/variable_cleanup.html#cleaning-up-variables) for further information on this matter.
430452

431453
When performing arithmetic checks in the same way as before, it is necessary to include a step to clean the bits on the sum.
@@ -491,33 +513,59 @@ We can simplify the expression to a single comparison if we're able to shift the
491513
To accomplish this, we subtract the smallest negative int64 `type(int64).min` from a value (or add the underlying unsigned value).
492514
A better way to understand this is by visualizing the signed integer number domain in relation to the unsigned domain (which is demonstrated here using int128).
493515

494-
```
495-
| -------------------------------- uint256 -------------------------------- |
496-
0 --------------------------------------------------------------------- uint256_max
516+
$$uint256 \text{ domain}$$
497517

498-
| --------- positive int256 --------- | --------- negative int256 --------- |
499-
0 ------------------------ int256_max | int256_min ------------------------ -1
500-
```
518+
$$
519+
├\underset{0}{─}────────────────────────────\underset{\hskip -1.5em 2^{256} - 1}{─}┤
520+
$$
521+
522+
$$int256 \text{ domain}$$
523+
524+
$$
525+
\overset{\hskip 1em positive}{
526+
├\underset{0}{─}────────────\underset{\hskip -2em 2^{255} - 1}{─}┤
527+
}
528+
\overset{\hskip 1em negative}{
529+
├────\underset{\hskip -3.5em - 2^{255}}─────────\underset{\hskip -0.4 em -1}{─}┤
530+
}
531+
$$
501532

502533
The domain for uint128/int128 can be visualized as follows.
503534

504-
```
505-
| ------------ uint128 -------------- | |
506-
0 ----------------------- uint128_max | |
535+
$$uint128 \text{ domain}$$
507536

508-
| -- pos int128 -- | | -- neg int128 -- |
509-
0 ----- int128_max | | int128_min ----- -1
510-
```
537+
$$
538+
├\underset{0}─────────────\underset{\hskip -2em 2^{128}-1}─┤
539+
\phantom{───────────────}┆
540+
$$
541+
542+
$$int128 \text{ domain}$$
543+
544+
$$
545+
\overset{\hskip 1em positive}{
546+
├\underset{0}{─}────\underset{\hskip -2em 2^{127} - 1}{─}┤
547+
}
548+
\phantom{────────────────}
549+
\overset{\hskip 1em negative}{
550+
├────\underset{\hskip -3.5em - 2^{127}}─\underset{\hskip -0.4 em -1}{─}┤
551+
}
552+
$$
553+
554+
Note that the scales of the number ranges above do not accurately depict the magnitude of numbers that are representable with the different types and only serves as a visualization.
555+
We are able to represent twice as many numbers with only one additional bit. Yet, the uint256 domain has twice the number of bits compared to uint128.
511556

512557
After subtracting `type(int128).min` we get the following, connected set of values.
513558

514-
```
515-
| ------------ uint128 -------------- | |
516-
0 ----------------------- uint128_max | |
559+
$$
560+
├\underset{0}─────────────\underset{\hskip -2em 2^{128}-1}─┤
561+
\phantom{───────────────}┆
562+
$$
517563

518-
| -- neg int128 -- | -- pos int128 -- | |
519-
int128_min ----- -1| 0 --- int128_max | |
520-
```
564+
$$
565+
\overset{\hskip 1em negative}{├──────┤}
566+
\overset{\hskip 1em positive}{├──────┤}
567+
\phantom{───────────────}┆
568+
$$
521569

522570
If we interpret the shifted value as an unsigned integer, we only need to check whether it exceeds the maximum unsigned integer `type(uint128).max`.
523571
The corresponding check in Solidity is shown below.
@@ -596,10 +644,12 @@ function checkedAddInt64_2(int64 a, int64 b) public pure returns (int64 c) {
596644
}
597645
```
598646

647+
One further optimization that we could perform is to add `-type(int64).min` instead of subtracting `type(int64).min`. This would not reduce computation costs, however it could end up reducing bytecode size. This is because when we subtract `-type(int64).min`, we need to push 32 bytes (`0xffffffffffffffffffffffffffffffffffffffffffffffff8000000000000000`), whereas when we add `-type(int64).min`, we only end up pushing 8 bytes (`0x8000000000000000`). However, as soon as we turn on compiler optimizations, the produced bytecode ends up being the same.
648+
599649
## Arithmetic checks for multiplication with sub-32-byte types
600650

601651
If the product `c = a * b` can be calculated in 256 bits without the possibility of overflowing, we can once again verify whether the result can fit into the anticipated data type.
602-
This is also the way Solidity handles the check in newer versions.
652+
This is also the way Solidity handles the check in versions 0.8.17 and later.
603653

604654
```solidity
605655
/// @notice version >= 0.8.17
@@ -666,3 +716,10 @@ function checkedMulInt192_2(int192 a, int192 b) public pure returns (int192 c) {
666716
}
667717
}
668718
```
719+
720+
## Conclusion
721+
722+
In conclusion, this article has provided a comprehensive examination of arithmetic checks within the Ethereum Virtual Machine, delving into various Solidity opcodes and optimizations for assembly code. We have explored some of the intricacies of implementing arithmetic checks for both uint256 and int256 addition, subtraction, and multiplication, as well as for sub-32-byte types.
723+
Furthermore, we have highlighted some caveats to be aware of when working with assembly to avoid potential pitfalls.
724+
725+
The purpose of this article is to deepen one's familiarity of low-level arithmetic, thereby improving the security of Solidity code by equipping developers to better assess and grasp the assumptions present in these operations. It is crucial to remember that custom low-level optimizations should be integrated only after rigorous manual analysis, fuzzing, and symbolic verification.

0 commit comments

Comments
 (0)