From e0fc53a10e69abd55ad563f98477bcf9568d76b0 Mon Sep 17 00:00:00 2001 From: sandbubbles <160503471+sandbubbles@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:42:34 +0000 Subject: [PATCH 1/2] chore[docs]: mention the `--venom` flag in venom docs (#4353) add some notes to the relevant docs that `venom` is an alias for `experimental-codegen` --- docs/compiling-a-contract.rst | 7 ++++++- docs/structure-of-a-contract.rst | 10 ++++++++++ vyper/cli/vyper_compile.py | 6 +++++- vyper/venom/README.md | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/compiling-a-contract.rst b/docs/compiling-a-contract.rst index c2cd3ed22c..7132cff58d 100644 --- a/docs/compiling-a-contract.rst +++ b/docs/compiling-a-contract.rst @@ -31,7 +31,7 @@ Include the ``-f`` flag to specify which output formats to return. Use ``vyper - .. code:: shell - $ vyper -f abi,abi_python,bytecode,bytecode_runtime,blueprint_bytecode,interface,external_interface,ast,annotated_ast,integrity,ir,ir_json,ir_runtime,asm,opcodes,opcodes_runtime,source_map,source_map_runtime,archive,solc_json,method_identifiers,userdoc,devdoc,metadata,combined_json,layout yourFileName.vy + $ vyper -f abi,abi_python,bb,bb_runtime,bytecode,bytecode_runtime,blueprint_bytecode,cfg,cfg_runtime,interface,external_interface,ast,annotated_ast,integrity,ir,ir_json,ir_runtime,asm,opcodes,opcodes_runtime,source_map,source_map_runtime,archive,solc_json,method_identifiers,userdoc,devdoc,metadata,combined_json,layout yourFileName.vy .. note:: The ``opcodes`` and ``opcodes_runtime`` output of the compiler has been returning incorrect opcodes since ``0.2.0`` due to a lack of 0 padding (patched via `PR 3735 `_). If you rely on these functions for debugging, please use the latest patched versions. @@ -134,6 +134,11 @@ In codesize optimized mode, the compiler will try hard to minimize codesize by * out-lining code, and * using more loops for data copies. +Enabling Experimental Code Generation +=========================== + +When compiling, you can use the CLI flag ``--experimental-codegen`` or its alias ``--venom`` to activate the new `Venom IR `_. +Venom IR is inspired by LLVM IR and enables new advanced analysis and optimizations. .. _evm-version: diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index fc817cf4b6..7e599d677b 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -54,6 +54,16 @@ EVM Version The EVM version can be set with the ``evm-version`` pragma, which is documented in :ref:`evm-version`. +Experimental Code Generation +----------------- +The new experimental code generation feature can be activated using the following directive: + +.. code-block:: vyper + + #pragma experimental-codegen + +Alternatively, you can use the alias ``"venom"`` instead of ``"experimental-codegen"`` to enable this feature. + Imports ======= diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index fde35f781e..046cac2c0b 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -34,6 +34,8 @@ layout - Storage layout of a Vyper contract ast - AST (not yet annotated) in JSON format annotated_ast - Annotated AST in JSON format +cfg - Control flow graph of deployable bytecode +cfg_runtime - Control flow graph of runtime bytecode interface - Vyper interface of a contract external_interface - External interface of a contract, used for outside contract calls opcodes - List of opcodes as a string @@ -41,6 +43,8 @@ ir - Intermediate representation in list format ir_json - Intermediate representation in JSON format ir_runtime - Intermediate representation of runtime bytecode in list format +bb - Basic blocks of Venom IR for deployable bytecode +bb_runtime - Basic blocks of Venom IR for runtime bytecode asm - Output the EVM assembly of the deployable bytecode integrity - Output the integrity hash of the source code archive - Output the build as an archive file @@ -177,7 +181,7 @@ def _parse_args(argv): parser.add_argument( "--experimental-codegen", "--venom", - help="The compiler use the new IR codegen. This is an experimental feature.", + help="The compiler uses the new IR codegen. This is an experimental feature.", action="store_true", dest="experimental_codegen", ) diff --git a/vyper/venom/README.md b/vyper/venom/README.md index 4e4f5ca3d1..6f3b318c9b 100644 --- a/vyper/venom/README.md +++ b/vyper/venom/README.md @@ -193,7 +193,7 @@ An operand can be a label, a variable, or a literal. By convention, variables have a `%-` prefix, e.g. `%1` is a valid variable. However, the prefix is not required. ## Instructions -To enable Venom IR in Vyper, use the `--experimental-codegen` flag. To view the Venom IR output, use `-f bb_runtime` for the runtime code, or `-f bb` to see the deploy code. To get a dot file (for use e.g. with `xdot -`), use `-f cfg` or `-f cfg_runtime`. +To enable Venom IR in Vyper, use the `--experimental-codegen` CLI flag or its alias `--venom`, or the corresponding pragma statements (e.g. `#pragma experimental-codegen`). To view the Venom IR output, use `-f bb_runtime` for the runtime code, or `-f bb` to see the deploy code. To get a dot file (for use e.g. with `xdot -`), use `-f cfg` or `-f cfg_runtime`. Assembly can be inspected with `-f asm`, whereas an opcode view of the final bytecode can be seen with `-f opcodes` or `-f opcodes_runtime`, respectively. From f38b61a27a975c80600d54981386e3cf8697edb6 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Thu, 21 Nov 2024 16:54:55 +0000 Subject: [PATCH 2/2] refactor[test]: add some sanity checks to `abi_decode` tests (#4096) QOL improvements for abi_decode tests: - added sanity checks to abi decode tests to ensure that we're never failing on calldatasize - also split the creation of payload into two parts when calling target contract with low-level msg_call to increase readability --------- Co-authored-by: Charles Cooper --- .../builtins/codegen/test_abi_decode.py | 377 ++++++++++-------- 1 file changed, 216 insertions(+), 161 deletions(-) diff --git a/tests/functional/builtins/codegen/test_abi_decode.py b/tests/functional/builtins/codegen/test_abi_decode.py index 9ae869c9cc..475118c7e3 100644 --- a/tests/functional/builtins/codegen/test_abi_decode.py +++ b/tests/functional/builtins/codegen/test_abi_decode.py @@ -8,6 +8,8 @@ TEST_ADDR = "0x" + b"".join(chr(i).encode("utf-8") for i in range(20)).hex() +BUFFER_OVERHEAD = 4 + 2 * 32 + def test_abi_decode_complex(get_contract): contract = """ @@ -474,8 +476,10 @@ def test_abi_decode_length_mismatch(get_contract, assert_compile_failed, bad_cod assert_compile_failed(lambda: get_contract(bad_code), exception) -def _abi_payload_from_tuple(payload: tuple[int | bytes, ...]) -> bytes: - return b"".join(p.to_bytes(32, "big") if isinstance(p, int) else p for p in payload) +def _abi_payload_from_tuple(payload: tuple[int | bytes, ...], max_sz: int) -> bytes: + ret = b"".join(p.to_bytes(32, "big") if isinstance(p, int) else p for p in payload) + assert len(ret) <= max_sz + return ret def _replicate(value: int, count: int) -> tuple[int, ...]: @@ -486,11 +490,12 @@ def test_abi_decode_arithmetic_overflow(env, tx_failed, get_contract): # test based on GHSA-9p8r-4xp4-gw5w: # https://github.com/vyperlang/vyper/security/advisories/GHSA-9p8r-4xp4-gw5w#advisory-comment-91841 # buf + head causes arithmetic overflow - code = """ + buffer_size = 32 * 3 + code = f""" @external -def f(x: Bytes[32 * 3]): +def f(x: Bytes[{buffer_size}]): a: Bytes[32] = b"foo" - y: Bytes[32 * 3] = x + y: Bytes[{buffer_size}] = x decoded_y1: Bytes[32] = _abi_decode(y, Bytes[32]) a = b"bar" @@ -500,39 +505,47 @@ def f(x: Bytes[32 * 3]): """ c = get_contract(code) - data = method_id("f(bytes)") - payload = ( - 0x20, # tuple head - 0x60, # parent array length - # parent payload - this word will be considered as the head of the abi-encoded inner array - # and it will be added to base ptr leading to an arithmetic overflow - 2**256 - 0x60, - ) - data += _abi_payload_from_tuple(payload) + tuple_head_ofst = 0x20 + parent_array_len = 0x60 + msg_call_overhead = (method_id("f(bytes)"), tuple_head_ofst, parent_array_len) + + data = _abi_payload_from_tuple(msg_call_overhead, BUFFER_OVERHEAD) + + # parent payload - this word will be considered as the head of the + # abi-encoded inner array and it will be added to base ptr leading to an + # arithmetic overflow + buffer_payload = (2**256 - 0x60,) + + data += _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): env.message_call(c.address, data=data) -def test_abi_decode_nonstrict_head(env, tx_failed, get_contract): +def test_abi_decode_nonstrict_head(env, get_contract): # data isn't strictly encoded - head is 0x21 instead of 0x20 # but the head + length is still within runtime bounds of the parent buffer - code = """ + buffer_size = 32 * 5 + code = f""" @external -def f(x: Bytes[32 * 5]): - y: Bytes[32 * 5] = x +def f(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x a: Bytes[32] = b"a" decoded_y1: DynArray[uint256, 3] = _abi_decode(y, DynArray[uint256, 3]) + assert len(decoded_y1) == 1 and decoded_y1[0] == 0 a = b"aaaa" decoded_y1 = _abi_decode(y, DynArray[uint256, 3]) + assert len(decoded_y1) == 1 and decoded_y1[0] == 0 """ c = get_contract(code) - data = method_id("f(bytes)") + tuple_head_ofst = 0x20 + parent_array_len = 0xA0 + msg_call_overhead = (method_id("f(bytes)"), tuple_head_ofst, parent_array_len) - payload = ( - 0x20, # tuple head - 0xA0, # parent array length + data = _abi_payload_from_tuple(msg_call_overhead, BUFFER_OVERHEAD) + + buffer_payload = ( # head should be 0x20 but is 0x21 thus the data isn't strictly encoded 0x21, # we don't want to revert on invalid length, so set this to 0 @@ -543,27 +556,30 @@ def f(x: Bytes[32 * 5]): *_replicate(0x03, 2), ) - data += _abi_payload_from_tuple(payload) + data += _abi_payload_from_tuple(buffer_payload, buffer_size) env.message_call(c.address, data=data) def test_abi_decode_child_head_points_to_parent(tx_failed, get_contract): # data isn't strictly encoded and the head for the inner array - # skipts the corresponding payload and points to other valid section of the parent buffer - code = """ + # skips the corresponding payload and points to other valid section of the + # parent buffer + buffer_size = 14 * 32 + code = f""" @external -def run(x: Bytes[14 * 32]): - y: Bytes[14 * 32] = x +def run(x: Bytes[{buffer_size}]) -> DynArray[DynArray[DynArray[uint256, 2], 1], 2]: + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[DynArray[DynArray[uint256, 2], 1], 2] = _abi_decode( y, DynArray[DynArray[DynArray[uint256, 2], 1], 2] ) + return decoded_y1 """ c = get_contract(code) # encode [[[1, 1]], [[2, 2]]] and modify the head for [1, 1] # to actually point to [2, 2] - payload = ( + buffer_payload = ( 0x20, # top-level array head 0x02, # top-level array length 0x40, # head of DAr[DAr[DAr, uint256]]][0] @@ -582,30 +598,33 @@ def run(x: Bytes[14 * 32]): 0x02, # DAr[DAr[DAr, uint256]]][1][0][1] ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) - c.run(data) + res = c.run(data) + assert res == [[[2, 2]], [[2, 2]]] def test_abi_decode_nonstrict_head_oob(tx_failed, get_contract): # data isn't strictly encoded and (non_strict_head + len(DynArray[..][2])) > parent_static_sz # thus decoding the data pointed to by the head would cause an OOB read # non_strict_head + length == parent + parent_static_sz + 1 - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[Bytes[32 * 3], 3] = _abi_decode(y, DynArray[Bytes[32 * 3], 3]) """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x20, # DynArray head 0x03, # DynArray length - # non_strict_head - if the length pointed to by this head is 0x60 (which is valid - # length for the Bytes[32*3] buffer), the decoding function would decode - # 1 byte over the end of the buffer - # we define the non_strict_head as: skip the remaining heads, 1st and 2nd tail + # non_strict_head - if the length pointed to by this head is 0x60 + # (which is valid length for the Bytes[32*3] buffer), the decoding + # function would decode 1 byte over the end of the buffer + # we define the non_strict_head as: + # skip the remaining heads, 1st and 2nd tail # to the third tail + 1B 0x20 * 8 + 0x20 * 3 + 0x01, # inner array0 head 0x20 * 4 + 0x20 * 3, # inner array1 head @@ -622,7 +641,7 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x03, 2), ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) @@ -631,10 +650,11 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): def test_abi_decode_nonstrict_head_oob2(tx_failed, get_contract): # same principle as in Test_abi_decode_nonstrict_head_oob # but adapted for dynarrays - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode( y, DynArray[DynArray[uint256, 3], 3] @@ -642,7 +662,7 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x20, # DynArray head 0x03, # DynArray length (0x20 * 8 + 0x20 * 3 + 0x01), # inner array0 head @@ -658,7 +678,7 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x01, 2), # DynArray[..][2] data ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) @@ -666,33 +686,36 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): def test_abi_decode_head_pointing_outside_buffer(tx_failed, get_contract): # the head points completely outside the buffer - code = """ + buffer_size = 3 * 32 + code = f""" @external -def run(x: Bytes[3 * 32]): - y: Bytes[3 * 32] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: Bytes[32] = _abi_decode(y, Bytes[32]) """ c = get_contract(code) - payload = (0x80, 0x20, 0x01) - data = _abi_payload_from_tuple(payload) + buffer_payload = (0x80, 0x20, 0x01) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) def test_abi_decode_bytearray_clamp(tx_failed, get_contract): - # data has valid encoding, but the length of DynArray[Bytes[96], 3][0] is set to 0x61 + # data has valid encoding, but the length of DynArray[Bytes[96], 3][0] is + # set to 0x61 # and thus the decoding should fail on bytestring clamp - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[Bytes[32 * 3], 3] = _abi_decode(y, DynArray[Bytes[32 * 3], 3]) """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x20, # DynArray head 0x03, # DynArray length 0x20 * 3, # inner array0 head @@ -707,32 +730,38 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x01, 3), # DynArray[Bytes[96], 3][2] data ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) def test_abi_decode_runtimesz_oob(tx_failed, get_contract, env): - # provide enough data, but set the runtime size to be smaller than the actual size - # so after y: [..] = x, y will have the incorrect size set and only part of the - # original data will be copied. This will cause oob read outside the - # runtime sz (but still within static size of the buffer) - code = """ + # provide enough data, but set the runtime size to be smaller than the + # actual size so after y: [..] = x, y will have the incorrect size set and + # only part of the original data will be copied. This will cause oob read + # outside the runtime sz (but still within static size of the buffer) + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def f(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[Bytes[32 * 3], 3] = _abi_decode(y, DynArray[Bytes[32 * 3], 3]) """ c = get_contract(code) - data = method_id("f(bytes)") - - payload = ( + msg_call_overhead = ( + method_id("f(bytes)"), 0x20, # tuple head # the correct size is 0x220 (2*32+3*32+4*3*32) - # therefore we will decode after the end of runtime size (but still within the buffer) + # therefore we will decode after the end of runtime size (but still + # within the buffer) 0x01E4, # top-level bytes array length + ) + + data = _abi_payload_from_tuple(msg_call_overhead, BUFFER_OVERHEAD) + + buffer_payload = ( 0x20, # DynArray head 0x03, # DynArray length 0x20 * 3, # inner array0 head @@ -746,7 +775,7 @@ def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x01, 3), # DynArray[Bytes[96], 3][2] data ) - data += _abi_payload_from_tuple(payload) + data += _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): env.message_call(c.address, data=data) @@ -755,10 +784,11 @@ def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): def test_abi_decode_runtimesz_oob2(tx_failed, get_contract, env): # same principle as in test_abi_decode_runtimesz_oob # but adapted for dynarrays - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def f(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode( y, DynArray[DynArray[uint256, 3], 3] @@ -766,11 +796,15 @@ def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): """ c = get_contract(code) - data = method_id("f(bytes)") - - payload = ( + msg_call_overhead = ( + method_id("f(bytes)"), 0x20, # tuple head 0x01E4, # top-level bytes array length + ) + + data = _abi_payload_from_tuple(msg_call_overhead, BUFFER_OVERHEAD) + + buffer_payload = ( 0x20, # DynArray head 0x03, # DynArray length 0x20 * 3, # inner array0 head @@ -784,7 +818,7 @@ def f(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x01, 3), # DynArray[..][2] data ) - data += _abi_payload_from_tuple(payload) + data += _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): env.message_call(c.address, data=data) @@ -796,11 +830,13 @@ def test_abi_decode_head_roundtrip(tx_failed, get_contract, env): # which are in turn in the y2 buffer # NOTE: the test is memory allocator dependent - we assume that y1 and y2 # have the 800 & 960 addresses respectively - code = """ + buffer_size1 = 4 * 32 + buffer_size2 = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x1: Bytes[4 * 32], x2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y1: Bytes[4*32] = x1 # addr: 800 - y2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x2 # addr: 960 +def run(x1: Bytes[{buffer_size1}], x2: Bytes[{buffer_size2}]): + y1: Bytes[{buffer_size1}] = x1 # addr: 800 + y2: Bytes[{buffer_size2}] = x2 # addr: 960 decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode( y2, DynArray[DynArray[uint256, 3], 3] @@ -808,7 +844,7 @@ def run(x1: Bytes[4 * 32], x2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x03, # DynArray length # distance to y2 from y1 is 160 160 + 0x20 + 0x20 * 3, # points to DynArray[..][0] length @@ -816,9 +852,9 @@ def run(x1: Bytes[4 * 32], x2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): 160 + 0x20 + 0x20 * 8 + 0x20 * 3, # points to DynArray[..][2] length ) - data1 = _abi_payload_from_tuple(payload) + data1 = _abi_payload_from_tuple(buffer_payload, buffer_size1) - payload = ( + buffer_payload = ( # (960 + (2**256 - 160)) % 2**256 == 800, ie will roundtrip to y1 2**256 - 160, # points to y1 0x03, # DynArray length (not used) @@ -833,7 +869,7 @@ def run(x1: Bytes[4 * 32], x2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): *_replicate(0x03, 3), # DynArray[..][2] data ) - data2 = _abi_payload_from_tuple(payload) + data2 = _abi_payload_from_tuple(buffer_payload, buffer_size2) with tx_failed(): c.run(data1, data2) @@ -841,22 +877,23 @@ def run(x1: Bytes[4 * 32], x2: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): def test_abi_decode_merge_head_and_length(get_contract): # compress head and length into 33B - code = """ + buffer_size = 32 * 2 + 8 * 32 + code = f""" @external -def run(x: Bytes[32 * 2 + 8 * 32]) -> uint256: - y: Bytes[32 * 2 + 8 * 32] = x +def run(x: Bytes[{buffer_size}]) -> Bytes[{buffer_size}]: + y: Bytes[{buffer_size}] = x decoded_y1: Bytes[256] = _abi_decode(y, Bytes[256]) - return len(decoded_y1) + return decoded_y1 """ c = get_contract(code) - payload = (0x01, (0x00).to_bytes(1, "big"), *_replicate(0x00, 8)) + buffer_payload = (0x01, (0x00).to_bytes(1, "big"), *_replicate(0x00, 8)) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) - length = c.run(data) + res = c.run(data) - assert length == 256 + assert res == bytes(256) def test_abi_decode_extcall_invalid_head(tx_failed, get_contract): @@ -880,8 +917,8 @@ def foo(): def test_abi_decode_extcall_oob(tx_failed, get_contract): # the head returned from the extcall is 1 byte bigger than expected - # thus we'll take the last 31 0-bytes from tuple[1] and the 1st byte from tuple[2] - # and consider this the length - thus the length is 2**5 + # thus we'll take the last 31 0-bytes from tuple[1] and the 1st byte from + # tuple[2] and consider this the length - thus the length is 2**5 # and thus we'll read 1B over the buffer end (33 + 32 + 32) code = """ @external @@ -902,7 +939,8 @@ def foo(): def test_abi_decode_extcall_runtimesz_oob(tx_failed, get_contract): # the runtime size (33) is bigger than the actual payload (32 bytes) - # thus we'll read 1B over the runtime size - but still within the static size of the buffer + # thus we'll read 1 byte over the runtime size - but still within the + # static size of the buffer code = """ @external def bar() -> (uint256, uint256, uint256): @@ -932,11 +970,13 @@ def bar() -> (uint256, uint256, uint256, uint256): def bar() -> Bytes[32]: nonpayable @external -def foo(): - x:Bytes[32] = extcall A(self).bar() +def foo() -> Bytes[32]: + return extcall A(self).bar() """ c = get_contract(code) - c.foo() + res = c.foo() + + assert res == (36).to_bytes(32, "big") def test_abi_decode_extcall_truncate_returndata2(tx_failed, get_contract): @@ -1053,12 +1093,14 @@ def bar() -> (uint256, uint256): def bar() -> DynArray[Bytes[32], 2]: nonpayable @external -def run(): - x: DynArray[Bytes[32], 2] = extcall A(self).bar() +def run() -> DynArray[Bytes[32], 2]: + return extcall A(self).bar() """ c = get_contract(code) - c.run() + res = c.run() + + assert res == [] def test_abi_decode_extcall_complex_empty_dynarray(get_contract): @@ -1079,13 +1121,14 @@ def bar() -> (uint256, uint256, uint256, uint256, uint256, uint256): def bar() -> DynArray[Point, 2]: nonpayable @external -def run(): - x: DynArray[Point, 2] = extcall A(self).bar() - assert len(x) == 1 and len(x[0].y) == 0 +def run() -> DynArray[Point, 2]: + return extcall A(self).bar() """ c = get_contract(code) - c.run() + res = c.run() + + assert res == [(1, [], 0)] def test_abi_decode_extcall_complex_empty_dynarray2(tx_failed, get_contract): @@ -1124,21 +1167,21 @@ def bar() -> (uint256, uint256): def bar() -> DynArray[Bytes[32], 2]: nonpayable @external -def run() -> uint256: - x: DynArray[Bytes[32], 2] = extcall A(self).bar() - return len(x) +def run() -> DynArray[Bytes[32], 2]: + return extcall A(self).bar() """ c = get_contract(code) - length = c.run() + res = c.run() - assert length == 0 + assert res == [] def test_abi_decode_top_level_head_oob(tx_failed, get_contract): - code = """ + buffer_size = 256 + code = f""" @external -def run(x: Bytes[256], y: uint256): +def run(x: Bytes[{buffer_size}], y: uint256): player_lost: bool = empty(bool) if y == 1: @@ -1150,9 +1193,9 @@ def run(x: Bytes[256], y: uint256): c = get_contract(code) # head points over the buffer end - payload = (0x0100, *_replicate(0x00, 7)) + bufffer_payload = (0x0100, *_replicate(0x00, 7)) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(bufffer_payload, buffer_size) with tx_failed(): c.run(data, 1) @@ -1162,23 +1205,24 @@ def run(x: Bytes[256], y: uint256): def test_abi_decode_dynarray_complex_insufficient_data(env, tx_failed, get_contract): - code = """ + buffer_size = 32 * 8 + code = f""" struct Point: x: uint256 y: uint256 @external -def run(x: Bytes[32 * 8]): - y: Bytes[32 * 8] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[Point, 3] = _abi_decode(y, DynArray[Point, 3]) """ c = get_contract(code) # runtime buffer has insufficient size - we decode 3 points, but provide only # 3 * 32B of payload - payload = (0x20, 0x03, *_replicate(0x03, 3)) + buffer_payload = (0x20, 0x03, *_replicate(0x03, 3)) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) @@ -1187,7 +1231,8 @@ def run(x: Bytes[32 * 8]): def test_abi_decode_dynarray_complex2(env, tx_failed, get_contract): # point head to the 1st 0x01 word (ie the length) # but size of the point is 3 * 32B, thus we'd decode 2B over the buffer end - code = """ + buffer_size = 32 * 8 + code = f""" struct Point: x: uint256 y: uint256 @@ -1195,19 +1240,19 @@ def test_abi_decode_dynarray_complex2(env, tx_failed, get_contract): @external -def run(x: Bytes[32 * 8]): +def run(x: Bytes[{buffer_size}]): y: Bytes[32 * 11] = x decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2]) """ c = get_contract(code) - payload = ( + buffer_payload = ( 0xC0, # points to the 1st 0x01 word (ie the length) *_replicate(0x03, 5), *_replicate(0x01, 2), ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) @@ -1216,7 +1261,8 @@ def run(x: Bytes[32 * 8]): def test_abi_decode_complex_empty_dynarray(env, tx_failed, get_contract): # point head to the last word of the payload # this will be the length, but because it's set to 0, the decoding should succeed - code = """ + buffer_size = 32 * 16 + code = f""" struct Point: x: uint256 y: DynArray[uint256, 2] @@ -1224,14 +1270,13 @@ def test_abi_decode_complex_empty_dynarray(env, tx_failed, get_contract): @external -def run(x: Bytes[32 * 16]): - y: Bytes[32 * 16] = x - decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2]) - assert len(decoded_y1) == 1 and len(decoded_y1[0].y) == 0 +def run(x: Bytes[{buffer_size}]) -> DynArray[Point, 2]: + y: Bytes[{buffer_size}] = x + return _abi_decode(y, DynArray[Point, 2]) """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x20, 0x01, 0x20, @@ -1243,14 +1288,17 @@ def run(x: Bytes[32 * 16]): 0x00, # length is 0, so decoding should succeed ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) + + res = c.run(data) - c.run(data) + assert res == [(1, [], 4)] def test_abi_decode_complex_arithmetic_overflow(tx_failed, get_contract): # inner head roundtrips due to arithmetic overflow - code = """ + buffer_size = 32 * 16 + code = f""" struct Point: x: uint256 y: DynArray[uint256, 2] @@ -1258,13 +1306,13 @@ def test_abi_decode_complex_arithmetic_overflow(tx_failed, get_contract): @external -def run(x: Bytes[32 * 16]): - y: Bytes[32 * 16] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2]) """ c = get_contract(code) - payload = ( + buffer_payload = ( 0x20, 0x01, 0x20, @@ -1276,39 +1324,43 @@ def run(x: Bytes[32 * 16]): 0x00, ) - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) def test_abi_decode_empty_toplevel_dynarray(get_contract): - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def run(x: Bytes[{buffer_size}]) -> DynArray[DynArray[uint256, 3], 3]: + y: Bytes[{buffer_size}] = x assert len(y) == 2 * 32 decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode( y, DynArray[DynArray[uint256, 3], 3] ) - assert len(decoded_y1) == 0 + return decoded_y1 """ c = get_contract(code) - payload = (0x20, 0x00) # DynArray head, DynArray length + buffer_payload = (0x20, 0x00) # DynArray head, DynArray length + + data = _abi_payload_from_tuple(buffer_payload, buffer_size) - data = _abi_payload_from_tuple(payload) + res = c.run(data) - c.run(data) + assert res == [] def test_abi_decode_invalid_toplevel_dynarray_head(tx_failed, get_contract): # head points 1B over the bounds of the runtime buffer - code = """ + buffer_size = 2 * 32 + 3 * 32 + 3 * 32 * 4 + code = f""" @external -def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): - y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x +def run(x: Bytes[{buffer_size}]): + y: Bytes[{buffer_size}] = x decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode( y, DynArray[DynArray[uint256, 3], 3] @@ -1317,33 +1369,34 @@ def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]): c = get_contract(code) # head points 1B over the bounds of the runtime buffer - payload = (0x21, 0x00) # DynArray head, DynArray length + buffer_payload = (0x21, 0x00) # DynArray head, DynArray length - data = _abi_payload_from_tuple(payload) + data = _abi_payload_from_tuple(buffer_payload, buffer_size) with tx_failed(): c.run(data) def test_nested_invalid_dynarray_head(get_contract, tx_failed): - code = """ + buffer_size = 320 + code = f""" @nonpayable @external -def foo(x:Bytes[320]): +def foo(x:Bytes[{buffer_size}]): if True: a: Bytes[320-32] = b'' # make the word following the buffer x_mem dirty to make a potential # OOB revert fake_head: uint256 = 32 - x_mem: Bytes[320] = x + x_mem: Bytes[{buffer_size}] = x y: DynArray[DynArray[uint256, 2], 2] = _abi_decode(x_mem,DynArray[DynArray[uint256, 2], 2]) @nonpayable @external -def bar(x:Bytes[320]): - x_mem: Bytes[320] = x +def bar(x:Bytes[{buffer_size}]): + x_mem: Bytes[{buffer_size}] = x y:DynArray[DynArray[uint256, 2], 2] = _abi_decode(x_mem,DynArray[DynArray[uint256, 2], 2]) """ @@ -1355,7 +1408,7 @@ def bar(x:Bytes[320]): # 0x0, # head2 ) - encoded = _abi_payload_from_tuple(encoded + inner) + encoded = _abi_payload_from_tuple(encoded + inner, buffer_size) with tx_failed(): c.foo(encoded) # revert with tx_failed(): @@ -1363,22 +1416,23 @@ def bar(x:Bytes[320]): def test_static_outer_type_invalid_heads(get_contract, tx_failed): - code = """ + buffer_size = 320 + code = f""" @nonpayable @external -def foo(x:Bytes[320]): - x_mem: Bytes[320] = x +def foo(x:Bytes[{buffer_size}]): + x_mem: Bytes[{buffer_size}] = x y:DynArray[uint256, 2][2] = _abi_decode(x_mem,DynArray[uint256, 2][2]) @nonpayable @external -def bar(x:Bytes[320]): +def bar(x:Bytes[{buffer_size}]): if True: a: Bytes[160] = b'' # write stuff here to make the call revert in case decode do # an out of bound access: fake_head: uint256 = 32 - x_mem: Bytes[320] = x + x_mem: Bytes[{buffer_size}] = x y:DynArray[uint256, 2][2] = _abi_decode(x_mem,DynArray[uint256, 2][2]) """ c = get_contract(code) @@ -1389,7 +1443,7 @@ def bar(x:Bytes[320]): # 0x00, # head of the second dynarray ) - encoded = _abi_payload_from_tuple(encoded + inner) + encoded = _abi_payload_from_tuple(encoded + inner, buffer_size) with tx_failed(): c.foo(encoded) @@ -1402,9 +1456,10 @@ def test_abi_decode_max_size(get_contract, tx_failed): # of abi encoding the type. this can happen when the payload is # "sparse" and has garbage bytes in between the static and dynamic # sections - code = """ + buffer_size = 1000 + code = f""" @external -def foo(a:Bytes[1000]): +def foo(a:Bytes[{buffer_size}]): v: DynArray[uint256, 1] = _abi_decode(a,DynArray[uint256, 1]) """ c = get_contract(code) @@ -1420,7 +1475,7 @@ def foo(a:Bytes[1000]): ) with tx_failed(): - c.foo(_abi_payload_from_tuple(payload)) + c.foo(_abi_payload_from_tuple(payload, buffer_size)) # returndatasize check for uint256