Medium Risk
https://github.com/vyperlang/vyper/blob/3b310d5292c4d1448e673d7b3adb223f9353260e/vyper/ir/optimizer.py#L54-L55
https://github.com/vyperlang/vyper/assets/51274081/3f619c79-88e0-4d15-9ace-7d9ba02d16bc
At compile-time, division is using the same logic for both signed and unsigned integers. This is causing some corectness issues.
Compile time div operation for both signed and unsigned are defined by evm_div
def evm_div(x, y):
if y == 0:
return 0
# NOTE: should be same as: round_towards_zero(Decimal(x)/Decimal(y))
sign = -1 if (x * y) < 0 else 1
return sign * (abs(x) // abs(y)) # adapted from py-evm
But there should be an edge case according to the Ethereum yellow paper:
As you can see, DIV
and SDIV
are not purely equivalent. There is an edge case when \mu[0] = -2^{255} and \mu[1] = -1.
If we evaluate the expression with the Python engine, this is what we get for this function:
>>> def evm_div(x, y):
... if y == 0:
... return 0
... # NOTE: should be same as: round_towards_zero(Decimal(x)/Decimal(y))
... sign = -1 if (x * y) < 0 else 1
... return sign * (abs(x) // abs(y)) # adapted from py-evm
...
>>> evm_div(-2**255, -1)
57896044618658097711785492504343953926634992332820282019728792003956564819968
>>> assert evm_div(-2**255, -1) == 2**255
It's 2**255
, while it should be -2**255
.
Here are some examples at looking how this could be exploited:
@external
def div_bug() -> int256:
return -2**255 / -1
Doesn't work, caught by the type checker:
vyper.exceptions.InvalidType: Expected int256 but literal can only be cast as uint256.
contract "src/div.vy:3", function "div_bug", line 3:11
2 def div_bug() -> int256:
---> 3 return -2**255 / -1
------------------^
4
While it should compile.
But we can for instance make it compile this way, while it should revert since as_wei_value
does not support negative values.
@external
def div_bug() -> uint256:
return as_wei_value(-2**255 / -1, "wei")
This compiles while the value should evaluate to a negative value, and returns 0x8000000000000000000000000000000000000000000000000000000000000000
.
Another example:
@external
def div_bug() -> uint256:
return max(-2**255 / -1, 0)
returns 0x8000000000000000000000000000000000000000000000000000000000000000
because max
is evaluated at compile-time with the wrong computation of -2**255 / -1
. The expected result should be 0
.
@external
def div_bug() -> int256:
return min(-2**255 / -1, 0)
returns 0
Other things that compile while it shouldn't:
@external
def div_bug() -> String[100]:
return uint2str(-2**255 / -1)
@external
def div_bug() -> uint256:
return uint256_addmod(-2**255 / -1, -2**255 / -1, -2**255 / -1)
@external
def div_bug() -> uint256:
return uint256_mulmod(-2**255 / -1, -2**255 / -1, -2**255 / -1)
@external
def div_bug() -> uint256:
return pow_mod256(-2**255 / -1, -2**255 / -1)
Manual review
def evm_div(x, y):
if y == 0:
return 0
elif x == -2**255 and y == -1:
return -2**255
sign = -1 if (x / y) < 0 else 1
return sign * abs(x // y)
(might be better to create a evm_sdiv
to make sure that it won't cause any issue in the future)