Skip to content

Commit a193204

Browse files
authored
Merge pull request #298 from crytic/arithmetic-checks-small-fixes
Include small fixes to arithmetic checks article
2 parents 0aa2581 + 0a0ef3e commit a193204

File tree

1 file changed

+39
-42
lines changed

1 file changed

+39
-42
lines changed

learn_evm/arithmetic-checks.md

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -154,25 +154,25 @@ The first bit of an integer represents the sign, with `0` indicating a positive
154154
For positive integers (those with a sign bit of `0`), their binary representation is the same as their unsigned bit representation.
155155
However, the negative domain is shifted to lie "above" the positive domain.
156156

157-
$$uint256 \text{ domain}$$
157+
$$\text{uint256 domain}$$
158158

159159
$$
160-
├\underset{0}{─}────────────────────────────\underset{\hskip -2em 2^{256} - 1}{─}┤
160+
├\underset{\hskip -0.5em 0}{─}────────────────────────────\underset{\hskip -3em 2^{256} - 1}{─}┤
161161
$$
162162

163163
```solidity
164164
0x0000000000000000000000000000000000000000000000000000000000000000 // 0
165165
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // uint256_max
166166
```
167167

168-
$$int256 \text{ domain}$$
168+
$$\text{int256 domain}$$
169169

170170
$$
171-
\overset{\hskip 1em positive}{
172-
├\underset{0}{─}────────────\underset{\hskip -2em 2^{255} - 1}{─}┤
171+
\overset{positive}{
172+
├\underset{\hskip -0.5em 0}{─}────────────\underset{\hskip -3em 2^{255} - 1}{─}┤
173173
}
174-
\overset{\hskip 1em negative}{
175-
├────\underset{\hskip -3.5em - 2^{255}}────────\underset{\hskip -0.4 em -1}{─}┤
174+
\overset{negative}{
175+
├──\underset{\hskip -2.1em - 2^{255}}{─}──────────\underset{\hskip -1 em -1}{─}┤
176176
}
177177
$$
178178

@@ -184,11 +184,11 @@ $$
184184
```
185185

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

190190
On the other hand, all negative numbers start with a `1` as their first bit.
191-
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.
191+
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.
192192

193193
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`.
194194
An example illustrates this.
@@ -421,7 +421,7 @@ On a 64-bit system, integer addition works in the same way as before.
421421
= 0x0000000000000001 // int64(1)
422422
```
423423

424-
However, when performing the same calculations on a 256-bit machine, we need to extend the sign of the int64 value over all unused bits,
424+
However, when performing the same calculations on a 256-bit machine, we need to extend the sign of the `int64` value over all unused bits,
425425
otherwise the value won't be interpreted correctly.
426426

427427
```solidity
@@ -505,60 +505,56 @@ function overflowInt64(int256 value) public pure returns (bool overflow) {
505505
```
506506

507507
We can simplify the expression to a single comparison if we can shift the disjointed number domain back so that it's connected.
508-
To accomplish this, we subtract the smallest negative int64 `type(int64).min` from a value (or add the underlying unsigned value).
509-
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).
508+
To accomplish this, we subtract the smallest negative `int64` (`type(int64).min`) from a value (or add the underlying unsigned value).
509+
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`).
510510

511-
$$uint256 \text{ domain}$$
511+
$$\text{uint256 domain}$$
512512

513513
$$
514-
├\underset{0}{─}────────────────────────────\underset{\hskip -2em 2^{256} - 1}{─}┤
514+
├\underset{\hskip -0.5em 0}{─}────────────────────────────\underset{\hskip -3em 2^{256} - 1}{─}┤
515515
$$
516516

517-
$$int256 \text{ domain}$$
517+
$$\text{int256 domain}$$
518518

519519
$$
520-
\overset{\hskip 1em positive}{
521-
├\underset{0}{─}────────────\underset{\hskip -2em 2^{255} - 1}{─}┤
520+
\overset{positive}{
521+
├\underset{\hskip -0.5em 0}{─}────────────\underset{\hskip -3em 2^{255} - 1}{─}┤
522522
}
523-
\overset{\hskip 1em negative}{
524-
├────\underset{\hskip -3.5em - 2^{255}}────────\underset{\hskip -0.4 em -1}{─}┤
523+
\overset{negative}{
524+
├──\underset{\hskip -2.1em - 2^{255}}{─}──────────\underset{\hskip -1 em -1}{─}┤
525525
}
526526
$$
527527

528-
The domain for uint128/int128 can be visualized as follows.
528+
The domain for `uint128`/`int128` can be visualized as follows.
529529

530-
$$uint128 \text{ domain}$$
530+
$$\text{uint128 domain}$$
531531

532532
$$
533-
├\underset{0}─────────────\underset{\hskip -2em 2^{128}-1}─┤
534-
\phantom{───────────────}
533+
├\underset{\hskip -0.5em 0}─────────────\underset{\hskip -3em 2^{128}-1}─┤
534+
\hskip 7em
535535
$$
536536

537-
$$int128 \text{ domain}$$
537+
$$\text{int128 domain}$$
538538

539539
$$
540-
\overset{\hskip 1em positive}{
541-
├\underset{0}{─}────\underset{\hskip -2em 2^{127} - 1}{─}┤
542-
}
543-
\phantom{────────────────}
544-
\overset{\hskip 1em negative}{
545-
├────\underset{\hskip -3.5em - 2^{127}}─\underset{\hskip -0.4 em -1}{─}┤
546-
}
540+
├\underset{\hskip -0.5em 0}{─}────\underset{\hskip -3em 2^{127} - 1}─\overset{\hskip -3em positive}{┤}
541+
\hskip 7em
542+
├──\underset{\hskip -2.1em - 2^{127}}───\underset{\hskip -1 em -1}{─}\overset{\hskip -3em negative}{┤}
547543
$$
548544

549545
Note that the scales of the number ranges in the previous section do not accurately depict the magnitude of numbers that are representable with the different types and only serve as a visualization. We can represent twice as many numbers with only one additional bit, yet the uint256 domain has twice the number of bits compared to uint128.
550546

551-
After subtracting `type(int128).min` (or adding $2^{127}$) and essentially shifting the domains to the right, we get the following, connected set of values.
547+
After subtracting `type(int128).min` (or adding `2**127`) and essentially shifting the domains to the right, we get the following, connected set of values.
552548

553549
$$
554-
├\underset{0}─────────────\underset{\hskip -2em 2^{128}-1}─┤
555-
\phantom{───────────────}
550+
├\underset{\hskip -0.5em 0}{─}────────────\underset{\hskip -3em 2^{128}-1}─┤
551+
\hskip 7em
556552
$$
557553

558554
$$
559-
\overset{\hskip 1em negative}{├──────┤}
560-
\overset{\hskip 1em positive}{├──────┤}
561-
\phantom{───────────────}
555+
├──────\overset{\hskip -3em negative}{┤}
556+
├──────\overset{\hskip -3em positive}{┤}
557+
\hskip 7em
562558
$$
563559

564560
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`.
@@ -685,7 +681,7 @@ An example might help explain the second case.
685681
```solidity
686682
0xffffffffffffffff800000000000000000000000000000000000000000000000 // type(int192).min
687683
* 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // -1
688-
= 0x0000000000000000800000000000000000000000000000000000000000000000 // type(int192).min (when seen as a int192)
684+
= 0x0000000000000000800000000000000000000000000000000000000000000000 // type(int192).min (when looking at the first 192 bits)
689685
```
690686

691687
A method to address this issue is to always start by sign-extending or cleaning the result before attempting to reconstruct the other multiplicand.
@@ -715,9 +711,10 @@ function checkedMulInt192_2(int192 a, int192 b) public pure returns (int192 c) {
715711
In conclusion, we hope this article has served as an informative guide on signed integer arithmetic within the EVM and the two's complement system.
716712
We have explored:
717713

718-
- the added complexity from handling signed over unsigned integers
719-
- the intricacies involved in managing sub 32-byte types
720-
- the significance of `signextend` and opcodes related to signed integers
721-
- the importance of bit-cleaning
714+
- How the EVM makes use of the two's complement representation
715+
- How integer values are interpreted as signed or unsigned depending on the opcodes used
716+
- The added complexity from handling arithmetic for signed vs. unsigned integers
717+
- The intricacies involved in managing sub 32-byte types
718+
- The importance of bit-cleaning and the significance of `signextend`
722719

723720
While low-level optimizations are attractive, they are also heavily error-prone. This article aims to deepen one's understanding of low-level arithmetic, to reduce these risks. Nevertheless, it is crucial to integrate custom low-level optimizations only after thorough manual analysis, automated testing, and to document any non-obvious assumptions.

0 commit comments

Comments
 (0)