Updates
160,000 USDC
View results
Submission Details
Severity: low
Valid

Compile-time division for signed integer edge case

Summary

At compile-time, division is using the same logic for both signed and unsigned integers. This is causing some corectness issues.

Vulnerability Details

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:
image

As you can see, DIV and SDIV are not purely equivalent. There is an edge case when and .
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.

Impact

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)

Tools Used

Manual review

Recommendations

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)

Updates

Lead Judging Commences

patrickalphac Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

division doesn't follow yellow paper

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.