Skip to content

Commit

Permalink
feat[lang]!: change ABI type of decimal to int168 (#3696)
Browse files Browse the repository at this point in the history
this commit changes how decimals are exposed in the ABI - changing
the ABI type from `fixed168x10` to `int168`. this is done for
compatibility reaosns, since `fixedMxN` types are not widely supported.
it is also a preparatory breaking change for supporting more decimal
types, in the future, we might expand the set of decimal types, e.g.
something along the lines of `Decimal[bits, places]`. in such a future,
if we add `UDecimal[256, 18]` (colloquially known as `"wad"`), it would
be useful to users if it had an ABI type of uint256, for compatibility
with common ERCs.

to distinguish from plain `int168` in the JSON ABI, an `"internalType"`
field is added, which includes the decimal info. in the future, if we
add more decimal types, it would be distinguished from other decimal
types according to the metadata, ex.:
`{"internalType": {"decimal": {"bits": 168, "places": 10}}}`.

misc/refactor:
- remove `FixedMxN` abi types
- add a `decimal_to_int()` utility function to make test migration
  cleaner. it essentially emulates what the compiler does internally during
  codegen, which is that it takes the arguments to a `Decimal` and then
  bitcasts the resulting `Decimal` to an int.

---------

Co-authored-by: tserg <[email protected]>
  • Loading branch information
charles-cooper and tserg authored Apr 10, 2024
1 parent eb81c26 commit b43ffac
Show file tree
Hide file tree
Showing 37 changed files with 303 additions and 225 deletions.
2 changes: 1 addition & 1 deletion docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ A value with a precision of 10 decimal places between -1870722095783555735300716

In order for a literal to be interpreted as ``decimal`` it must include a decimal point.

The ABI type (for computing method identifiers) of ``decimal`` is ``fixed168x10``.
The ABI type (for computing method identifiers) of ``decimal`` is ``int168``.

Operators
*********
Expand Down
21 changes: 13 additions & 8 deletions tests/functional/builtins/codegen/test_abi_decode.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from decimal import Decimal

import pytest
from eth.codecs import abi

from tests.utils import decimal_to_int
from vyper.exceptions import ArgumentException, StackTooDeep, StructureException

TEST_ADDR = "0x" + b"".join(chr(i).encode("utf-8") for i in range(20)).hex()
Expand Down Expand Up @@ -54,22 +53,28 @@ def abi_decode_struct(x: Bytes[544]) -> Human:
c = get_contract(contract)

test_bytes32 = b"".join(chr(i).encode("utf-8") for i in range(32))
args = (TEST_ADDR, -1, True, Decimal("-123.4"), test_bytes32)
encoding = "(address,int128,bool,fixed168x10,bytes32)"
args = (TEST_ADDR, -1, True, decimal_to_int("-123.4"), test_bytes32)
encoding = "(address,int128,bool,int168,bytes32)"
encoded = abi.encode(encoding, args)
assert tuple(c.abi_decode(encoded)) == (TEST_ADDR, -1, True, Decimal("-123.4"), test_bytes32)
assert tuple(c.abi_decode(encoded)) == (
TEST_ADDR,
-1,
True,
decimal_to_int("-123.4"),
test_bytes32,
)

test_bytes32 = b"".join(chr(i).encode("utf-8") for i in range(32))
human_tuple = (
"foobar",
("vyper", TEST_ADDR, 123, True, Decimal("123.4"), [123, 456, 789], test_bytes32),
("vyper", TEST_ADDR, 123, True, decimal_to_int("123.4"), [123, 456, 789], test_bytes32),
)
args = tuple([human_tuple[0]] + list(human_tuple[1]))
human_t = "((string,(string,address,int128,bool,fixed168x10,uint256[3],bytes32)))"
human_t = "((string,(string,address,int128,bool,int168,uint256[3],bytes32)))"
human_encoded = abi.encode(human_t, (human_tuple,))
assert tuple(c.abi_decode_struct(human_encoded)) == (
"foobar",
("vyper", TEST_ADDR, 123, True, Decimal("123.4"), [123, 456, 789], test_bytes32),
("vyper", TEST_ADDR, 123, True, decimal_to_int("123.4"), [123, 456, 789], test_bytes32),
)


Expand Down
7 changes: 3 additions & 4 deletions tests/functional/builtins/codegen/test_abi_encode.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from decimal import Decimal

import pytest
from eth.codecs import abi

from tests.utils import decimal_to_int
from vyper.exceptions import StackTooDeep


Expand Down Expand Up @@ -109,10 +108,10 @@ def abi_encode3(x: uint256, ensure_tuple: bool, include_method_id: bool) -> Byte
test_bytes32 = b"".join(chr(i).encode("utf-8") for i in range(32))
human_tuple = (
"foobar",
("vyper", test_addr, 123, True, Decimal("123.4"), [123, 456, 789], test_bytes32),
("vyper", test_addr, 123, True, decimal_to_int("123.4"), [123, 456, 789], test_bytes32),
)
args = tuple([human_tuple[0]] + list(human_tuple[1]))
human_t = "(string,(string,address,int128,bool,fixed168x10,uint256[3],bytes32))"
human_t = "(string,(string,address,int128,bool,int168,uint256[3],bytes32))"
human_encoded = abi.encode(human_t, human_tuple)
assert c.abi_encode(*args, False, False).hex() == human_encoded.hex()
assert c.abi_encode(*args, False, True).hex() == (method_id + human_encoded).hex()
Expand Down
15 changes: 10 additions & 5 deletions tests/functional/builtins/codegen/test_as_wei_value.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from decimal import Decimal

import pytest

from tests.utils import decimal_to_int
from vyper.semantics.types import DecimalT
from vyper.utils import quantize, round_towards_zero

wei_denoms = {
"femtoether": 3,
"kwei": 3,
Expand Down Expand Up @@ -61,11 +63,14 @@ def test_wei_decimal(get_contract, tx_failed, denom, multiplier):
def foo(a: decimal) -> uint256:
return as_wei_value(a, "{denom}")
"""

c = get_contract(code)
value = Decimal((2**127 - 1) / (10**multiplier))

assert c.foo(value) == value * (10**multiplier)
denom_int = 10**multiplier
# TODO: test with more values
_, hi = DecimalT().ast_bounds
value = quantize(hi / denom_int)

assert c.foo(decimal_to_int(value)) == round_towards_zero(value * denom_int)


@pytest.mark.parametrize("value", (-1, -(2**127)))
Expand Down
6 changes: 4 additions & 2 deletions tests/functional/builtins/codegen/test_ceil.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import math
from decimal import Decimal

from tests.utils import decimal_to_int


def test_ceil(get_contract_with_gas_estimation):
code = """
Expand Down Expand Up @@ -102,8 +104,8 @@ def ceil_param(p: decimal) -> int256:
assert c.fos() == -5472
assert c.fot() == math.ceil(-(Decimal(2**167 - 1)) / 10**10)
assert c.fou() == -3
assert c.ceil_param(Decimal("-0.5")) == 0
assert c.ceil_param(Decimal("-7777777.7777777")) == -7777777
assert c.ceil_param(decimal_to_int("-0.5")) == 0
assert c.ceil_param(decimal_to_int("-7777777.7777777")) == -7777777


def test_ceil_ext_call(w3, side_effects_contract, assert_side_effects_invoked, get_contract):
Expand Down
29 changes: 23 additions & 6 deletions tests/functional/builtins/codegen/test_convert.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import enum
import itertools

# import random
import math
from decimal import Decimal

import eth.codecs.abi as abi
import eth.codecs.abi.exceptions
import pytest

from tests.utils import decimal_to_int
from vyper.compiler import compile_code
from vyper.exceptions import InvalidLiteral, InvalidType, TypeMismatch
from vyper.semantics.types import AddressT, BoolT, BytesM_T, BytesT, DecimalT, IntegerT, StringT
Expand Down Expand Up @@ -249,13 +249,20 @@ def _padconvert(val_bits, direction, n, padding_byte=None):
def _from_bits(val_bits, o_typ):
# o_typ: the type to convert to
try:
return abi.decode(o_typ.abi_type.selector_name(), val_bits)
ret = abi.decode(o_typ.abi_type.selector_name(), val_bits)
if isinstance(o_typ, DecimalT):
return Decimal(ret) / o_typ.divisor
return ret
except eth.codecs.abi.exceptions.DecodeError:
raise _OutOfBounds() from None


def _to_bits(val, i_typ):
# i_typ: the type to convert from
if isinstance(i_typ, DecimalT):
val = val * i_typ.divisor
assert math.ceil(val) == math.floor(val)
val = int(val)
return abi.encode(i_typ.abi_type.selector_name(), val)


Expand Down Expand Up @@ -430,6 +437,13 @@ def test_convert_passing(
# web3 has special formatter for zero address
expected_val = None

if isinstance(o_typ, DecimalT):
expected_val = decimal_to_int(expected_val)

input_val = val
if isinstance(i_typ, DecimalT):
input_val = decimal_to_int(val)

contract_1 = f"""
@external
def test_convert() -> {o_typ}:
Expand Down Expand Up @@ -461,7 +475,7 @@ def test_input_convert(x: {i_typ}) -> {o_typ}:
"""

c2 = get_contract_with_gas_estimation(contract_2)
assert c2.test_input_convert(val) == expected_val
assert c2.test_input_convert(input_val) == expected_val

contract_3 = f"""
bar: {i_typ}
Expand All @@ -483,7 +497,7 @@ def test_memory_variable_convert(x: {i_typ}) -> {o_typ}:
"""

c4 = get_contract_with_gas_estimation(contract_4)
assert c4.test_memory_variable_convert(val) == expected_val
assert c4.test_memory_variable_convert(input_val) == expected_val


@pytest.mark.parametrize("typ", ["uint8", "int128", "int256", "uint256"])
Expand Down Expand Up @@ -715,5 +729,8 @@ def foo(bar: {i_typ}) -> {o_typ}:
"""

c3 = get_contract_with_gas_estimation(contract_3)
input_val = val
if isinstance(i_typ, DecimalT):
input_val = decimal_to_int(input_val)
with tx_failed():
c3.foo(val)
c3.foo(input_val)
6 changes: 4 additions & 2 deletions tests/functional/builtins/codegen/test_floor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import math
from decimal import Decimal

from tests.utils import decimal_to_int


def test_floor(get_contract_with_gas_estimation):
code = """
Expand Down Expand Up @@ -106,8 +108,8 @@ def floor_param(p: decimal) -> int256:
assert c.fos() == -1
assert c.fot() == math.floor(-Decimal(2**167) / 10**10)
assert c.fou() == -4
assert c.floor_param(Decimal("-5.6")) == -6
assert c.floor_param(Decimal("-0.0000000001")) == -1
assert c.floor_param(decimal_to_int("-5.6")) == -6
assert c.floor_param(decimal_to_int("-0.0000000001")) == -1


def test_floor_ext_call(w3, side_effects_contract, assert_side_effects_invoked, get_contract):
Expand Down
5 changes: 2 additions & 3 deletions tests/functional/builtins/codegen/test_minmax.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from decimal import Decimal

import pytest

from tests.utils import decimal_to_int
from vyper.semantics.types import IntegerT


Expand All @@ -17,7 +16,7 @@ def goo() -> uint256:
"""

c = get_contract_with_gas_estimation(minmax_test)
assert c.foo() == Decimal("58223.123")
assert c.foo() == decimal_to_int("58223.123")
assert c.goo() == 83

print("Passed min/max test")
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/builtins/codegen/test_minmax_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def foo() -> {typ}:
"""
c = get_contract(code)

lo, hi = typ.ast_bounds
lo, hi = typ.int_bounds
if op == "min_value":
assert c.foo() == lo
elif op == "max_value":
Expand Down
7 changes: 3 additions & 4 deletions tests/functional/builtins/codegen/test_unary.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from decimal import Decimal

import pytest

from tests.utils import decimal_to_int
from vyper.exceptions import InvalidOperation


Expand Down Expand Up @@ -63,8 +62,8 @@ def bar() -> decimal:
"""

c = get_contract(code)
assert c.foo() == Decimal("-18707220957835557353007165858768422651595.9365500927")
assert c.bar() == Decimal("18707220957835557353007165858768422651595.9365500927")
assert c.foo() == decimal_to_int("-18707220957835557353007165858768422651595.9365500927")
assert c.bar() == decimal_to_int("18707220957835557353007165858768422651595.9365500927")


def test_negation_int128(get_contract):
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/builtins/folding/test_epsilon.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from tests.utils import parse_and_fold
from tests.utils import decimal_to_int, parse_and_fold


@pytest.mark.parametrize("typ_name", ["decimal"])
Expand All @@ -16,4 +16,4 @@ def foo() -> {typ_name}:
old_node = vyper_ast.body[0].value
new_node = old_node.get_folded_value()

assert contract.foo() == new_node.value
assert contract.foo() == decimal_to_int(new_node.value)
5 changes: 3 additions & 2 deletions tests/functional/builtins/folding/test_floor_ceil.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from hypothesis import example, given, settings
from hypothesis import strategies as st

from tests.utils import parse_and_fold
from tests.utils import decimal_to_int, parse_and_fold

st_decimals = st.decimals(
min_value=-(2**32), max_value=2**32, allow_nan=False, allow_infinity=False, places=10
Expand All @@ -31,4 +31,5 @@ def foo(a: decimal) -> int256:
old_node = vyper_ast.body[0].value
new_node = old_node.get_folded_value()

assert contract.foo(value) == new_node.value
assert isinstance(new_node.value, int)
assert contract.foo(decimal_to_int(value)) == new_node.value
5 changes: 3 additions & 2 deletions tests/functional/builtins/folding/test_fold_as_wei_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hypothesis import given, settings
from hypothesis import strategies as st

from tests.utils import parse_and_fold
from tests.utils import decimal_to_int, parse_and_fold
from vyper.builtins import functions as vy_fn
from vyper.utils import SizeLimits

Expand Down Expand Up @@ -34,7 +34,8 @@ def foo(a: decimal) -> uint256:
old_node = vyper_ast.body[0].value
new_node = old_node.get_folded_value()

assert contract.foo(value) == new_node.value
assert isinstance(new_node.value, int)
assert contract.foo(decimal_to_int(value)) == new_node.value


@pytest.mark.fuzzing
Expand Down
5 changes: 3 additions & 2 deletions tests/functional/builtins/folding/test_min_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hypothesis import given, settings
from hypothesis import strategies as st

from tests.utils import parse_and_fold
from tests.utils import decimal_to_int, parse_and_fold
from vyper.utils import SizeLimits

st_decimals = st.decimals(
Expand Down Expand Up @@ -32,7 +32,8 @@ def foo(a: decimal, b: decimal) -> decimal:
old_node = vyper_ast.body[0].value
new_node = old_node.get_folded_value()

assert contract.foo(left, right) == new_node.value
l, r = [decimal_to_int(t) for t in (left, right)]
assert contract.foo(l, r) == decimal_to_int(new_node.value)


@pytest.mark.fuzzing
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from decimal import Decimal

import pytest
from eth.codecs import abi

from tests.utils import decimal_to_int
from vyper import compile_code
from vyper.exceptions import (
ArgumentException,
Expand Down Expand Up @@ -517,7 +516,7 @@ def bar(arg1: address) -> decimal:
"""

c2 = get_contract(contract_2)
assert c2.bar(c.address) == Decimal("1e-10")
assert c2.bar(c.address) == decimal_to_int("1e-10")


def test_decimal_too_long(get_contract, tx_failed):
Expand Down Expand Up @@ -570,7 +569,7 @@ def bar(arg1: address) -> (decimal, Bytes[3], decimal):
c2 = get_contract(contract_2)
assert c.foo() == [0, b"dog", 1]
result = c2.bar(c.address)
assert result == [Decimal("0.0"), b"dog", Decimal("1e-10")]
assert result == [decimal_to_int("0.0"), b"dog", decimal_to_int("1e-10")]


@pytest.mark.parametrize("a,b", [(8, 256), (256, 8), (256, 256)])
Expand Down
Loading

0 comments on commit b43ffac

Please sign in to comment.