You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: learn_evm/arithmetic-checks.md
+40-43Lines changed: 40 additions & 43 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,7 +7,7 @@ these features are absent in the EVM.
7
7
Consequently, these safeguards must be incorporated within the machine's constraints.
8
8
9
9
Starting with Solidity version 0.8.0 the compiler automatically includes over and underflow protection in all arithmetic operations.
10
-
Prior to version 0.8.0, developers were required 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.
10
+
Prior to version 0.8.0, developers were required to implement these checks manually, often using a library known as [SafeMath](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.2/contracts/utils/math/SafeMath.sol), originally developed by OpenZeppelin.
11
11
The compiler incorporates arithmetic checks in a manner similar to SafeMath, through additional operations.
12
12
13
13
As the Solidity language has evolved, the compiler has generated increasingly optimized code for arithmetic checks. This trend is also observed in smart contract development in general, where highly optimized arithmetic code written in low-level assembly is becoming more common. However, there is still a lack of comprehensive resources explaining the nuances of how the EVM handles arithmetic for signed and unsigned integers of 256 bits and less.
@@ -154,25 +154,25 @@ The first bit of an integer represents the sign, with `0` indicating a positive
154
154
For positive integers (those with a sign bit of `0`), their binary representation is the same as their unsigned bit representation.
155
155
However, the negative domain is shifted to lie "above" the positive domain.
├────\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}{─}┤
176
176
}
177
177
$$
178
178
@@ -184,11 +184,11 @@ $$
184
184
```
185
185
186
186
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`.
188
188
The most significant bit of this number is `0`, while all other bits are `1`.
189
189
190
190
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.
192
192
193
193
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`.
194
194
An example illustrates this.
@@ -421,7 +421,7 @@ On a 64-bit system, integer addition works in the same way as before.
421
421
= 0x0000000000000001 // int64(1)
422
422
```
423
423
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,
425
425
otherwise the value won't be interpreted correctly.
426
426
427
427
```solidity
@@ -505,60 +505,56 @@ function overflowInt64(int256 value) public pure returns (bool overflow) {
505
505
```
506
506
507
507
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`).
├──\underset{\hskip -2.1em - 2^{127}}───\underset{\hskip -1 em -1}{─}\overset{\hskip -3em negative}{┤}
547
543
$$
548
544
549
545
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.
550
546
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.
= 0x0000000000000000800000000000000000000000000000000000000000000000 // type(int192).min (when seen as a int192)
684
+
= 0x0000000000000000800000000000000000000000000000000000000000000000 // type(int192).min (when looking at the first 192 bits)
689
685
```
690
686
691
687
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) {
715
711
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.
716
712
We have explored:
717
713
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`
722
719
723
720
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.
The lack of checks on the OnComplete field of the application calls might allow an attacker to execute the clear state program instead of the approval program, breaking core validations.
4
+
5
+
## Description
6
+
7
+
Algorand applications make use of group transactions to realize operations that may not be possible using a single transaction model. Some operations require that other transactions in the group call certain methods and applications. These requirements are asserted by validating that the transactions are ApplicationCall transactions. However, the OnComplete field of these transactions is not always validated, allowing an attacker to submit ClearState ApplicationCall transactions. The ClearState transaction invokes the clear state program instead of the intended approval program of the application.
8
+
9
+
## Exploit Scenario
10
+
11
+
A protocol offers flash loans from a liquidity pool. The flash loan operation is implemented using two methods: `take_flash_loan` and `pay_flash_loan`. `take_flash_loan` method transfers the assets to the user and `pay_flash_loan` verifies that the user has returned the borrowed assets. `take_flash_loan` verifies that a later transaction in the group calls the `pay_flash_loan` method. However, It does not validate the OnComplete field.
# Perform other validations, transfer assets to the user, update the global state
24
+
# [...]
25
+
])
26
+
27
+
@router.method(no_op=CallConfig.CALL)
28
+
defpay_flash_loan(offset: abi.Uint64) -> Expr:
29
+
return Seq([
30
+
# Validate the "take_flash_loan" transaction at `Txn.group_index() - offset.get()`
31
+
# Ensure the user has returned the funds to the pool along with the fee. Fail the transaction otherwise
32
+
# [...]
33
+
])
34
+
```
35
+
36
+
An attacker constructs a valid group transaction for flash loan but sets the OnComplete field of `pay_flash_loan` call to ClearState. The clear state program is executed for complete_flash_loan call, which does not validate that the attacker has returned the funds. The attacker steals all the assets in the pool.
37
+
38
+
## Recommendations
39
+
40
+
Validate the OnComplete field of the ApplicationCall transactions.
0 commit comments