-
Notifications
You must be signed in to change notification settings - Fork 269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update overflow-underflow.md #82
base: master
Are you sure you want to change the base?
Conversation
Updated overflow-underflow.md with more examples on using YUL
```solidity | ||
function getSwapQuoteUint128(uint128 amountToSwap) external view returns(uint128 outputTokens) { | ||
assembly { | ||
outputTokens := addmod(amountToSwap, 1, 340282366920938463463374607431768211455) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In most circumstances, we'd rather have execution revert rather than actually overflowing the output. In fact, I can't really think of any circumstances where we wouldn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't think we should suggest allowing the overflow
In the above code we are adding 1 into the variable with inline assembly and then returning the result. The variable result will overflow and 0 will be returned, despite this the contract will NOT throw an error or revert! | ||
|
||
|
||
### Subtle Overflow with Smaller Integers (e.g., `uint128`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda feel like this section just leads to confusion because it's really quite similar to what would happen when not dropping down to assembly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well actually, my initial assumption was that it reverts when we try to return outputTokens as a uint128, but perhaps this isn't the case? If so this would be good to clarify
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda feel like this section just leads to confusion because it's really quite similar to what would happen when not dropping down to assembly
which section specifically are you talking about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well actually, my initial assumption was that it reverts when we try to return outputTokens as a uint128, but perhaps this isn't the case? If so this would be good to clarify
about this @kadenzipfel, it took me a while to wrap my head around it, but here is what I found out:-
-
Function Definition:
function getSwapQuoteUint128(uint128 amountToSwap) external view returns (uint128 outputTokens) { assembly { outputTokens := add(amountToSwap, 1) if lt(outputTokens, amountToSwap) { revert(0, 0) } } }
This function is intended to return
amountToSwap + 1
and detect an overflow ifoutputTokens
is less thanamountToSwap
. -
256-bit Arithmetic:
In the EVM, arithmetic operations within inline assembly (Yul) are performed using 256-bit values. When adding two 128-bit values, the result is treated as a 256-bit value. -
Overflow Check:
The function includes an overflow detection mechanism:if lt(outputTokens, amountToSwap) { revert(0, 0) }
This checks if
outputTokens
is less thanamountToSwap
. -
Overflow Scenario:
WhenamountToSwap
istype(uint128).max
(2^128 - 1
), adding 1 results in2^128
, which fits in a 256-bit register but exceeds the 128-bit range. The result is a valid 256-bit value (2^128
), but it cannot be represented as a 128-bit value. -
Implicit Conversion:
When the 256-bit result is assigned to a 128-bit variable (outputTokens
), it truncates the higher bits, resulting in 0 (since2^128
overflows a 128-bit integer back to 0). -
Assertion:
The assertion confirms that the function indeed returns 0 when given the maximumuint128
value due to the overflow:assertEq(0, dexPair.getSwapQuoteUint128(type(uint128).max));
The key issue here is that the overflow detection mechanism:
if lt(outputTokens, amountToSwap) { revert(0, 0) }
fails because outputTokens
and amountToSwap
are treated as 256-bit values during the addition operation. When outputTokens
is 2^128
, it is not less than amountToSwap
in 256-bit arithmetic. Therefore, the check lt(outputTokens, amountToSwap)
does not trigger a revert, even though the actual result overflows when considering 128-bit constraints.
TLDR;
The inline assembly overflow check fails because the EVM uses 256-bit arithmetic, while the function is supposed to handle 128-bit values. This discrepancy allows an overflow to evade detection, causing the function to return incorrect results without reverting. Specifically, adding 1 to the maximum uint128
value (2^128 - 1
) results in an overflow to 0 instead of triggering the overflow detection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think this section is has too much overlap with the above section for it to be necessary
This check ensures that if the result is less than the input, it indicates an overflow. | ||
|
||
> [!IMPORTANT] | ||
> Be careful not to fall into the pitfall of the aforementioned `uint128` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not clear on what this note is indicating
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was supposed to indicate the pitfall a dev may fall into when implementing an overflow check in assembly using the patterns discussed.
Perhaps it wasn't clear :)
In the above code we are adding 1 into the variable with inline assembly and then returning the result. The variable result will overflow and 0 will be returned, despite this the contract will NOT throw an error or revert! | ||
|
||
|
||
### Subtle Overflow with Smaller Integers (e.g., `uint128`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think this section is has too much overlap with the above section for it to be necessary
} | ||
``` | ||
|
||
3. Add a manual check. The below check for example ensures that if the result is less than the input, it indicates an overflow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not the same as number 2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably simply the mitigation section to just:
- Revert if an over/underflow will occur (will happen regardless with safemath/v>0.8)
- Don't use unchecked blocks unless over/underflow is impossible
```solidity | ||
function getSwapQuoteUint128(uint128 amountToSwap) external view returns(uint128 outputTokens) { | ||
assembly { | ||
outputTokens := addmod(amountToSwap, 1, 340282366920938463463374607431768211455) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't think we should suggest allowing the overflow
Related Issue
Checklist
Describe the changes you've made:
Updated overflow-underflow.md with more examples on using YUL
Type of change
Select the appropriate checkbox: