diff --git a/tests/parser/features/external_contracts/test_external_contract_calls.py b/tests/parser/features/external_contracts/test_external_contract_calls.py index b9408cf6c4..63ccf27ce1 100644 --- a/tests/parser/features/external_contracts/test_external_contract_calls.py +++ b/tests/parser/features/external_contracts/test_external_contract_calls.py @@ -2452,3 +2452,52 @@ def do_stuff(f: Foo) -> uint256: c2 = get_contract(callee_code) assert c1.do_stuff(c2.address) == 1 + + +TEST_ADDR = b"".join(chr(i).encode("utf-8") for i in range(20)).hex() + + +@pytest.mark.parametrize("typ,val", [("address", TEST_ADDR)]) +def test_calldata_clamp(w3, get_contract, assert_tx_failed, abi_encode, keccak, typ, val): + code = f""" +@external +def foo(a: {typ}): + pass + """ + c1 = get_contract(code) + sig = keccak(f"foo({typ})".encode()).hex()[:10] + encoded = abi_encode(f"({typ})", (val,)).hex() + data = f"{sig}{encoded}" + + # Static size is short by 1 byte + malformed = data[:-2] + assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed})) + + # Static size exceeds by 1 byte + malformed = data + "ff" + assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed})) + + # Static size is exact + w3.eth.send_transaction({"to": c1.address, "data": data}) + + +@pytest.mark.parametrize("typ,val", [("address", ([TEST_ADDR] * 3, "vyper"))]) +def test_dynamic_calldata_clamp(w3, get_contract, assert_tx_failed, abi_encode, keccak, typ, val): + code = f""" +@external +def foo(a: DynArray[{typ}, 3], b: String[5]): + pass + """ + + c1 = get_contract(code) + sig = keccak(f"foo({typ}[],string)".encode()).hex()[:10] + encoded = abi_encode(f"({typ}[],string)", val).hex() + data = f"{sig}{encoded}" + + # Dynamic size is short by 1 byte + malformed = data[:264] + assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed})) + + # Dynamic size is at least minimum (132 bytes * 2 + 2 (for 0x) = 266) + valid = data[:266] + w3.eth.send_transaction({"to": c1.address, "data": valid}) diff --git a/tests/parser/functions/test_default_function.py b/tests/parser/functions/test_default_function.py index 6ee83de13f..6ec3ea0380 100644 --- a/tests/parser/functions/test_default_function.py +++ b/tests/parser/functions/test_default_function.py @@ -126,7 +126,7 @@ def __default__(): logs = get_logs( # call blockHashAskewLimitary - w3.eth.send_transaction({"to": c.address, "value": 0, "data": "0x00000000"}), + w3.eth.send_transaction({"to": c.address, "value": 0, "data": "0x" + "00" * 36}), c, "Sent", ) diff --git a/vyper/codegen/function_definitions/external_function.py b/vyper/codegen/function_definitions/external_function.py index e1b7693819..b340042839 100644 --- a/vyper/codegen/function_definitions/external_function.py +++ b/vyper/codegen/function_definitions/external_function.py @@ -86,6 +86,15 @@ def handler_for(calldata_kwargs, default_kwargs): # a sequence of statements to strictify kwargs into memory ret = ["seq"] + # ensure calldata is at least of minimum length + args_abi_t = calldata_args_t.abi_type + calldata_min_size = args_abi_t.min_size() + 4 + if args_abi_t.is_dynamic(): + ret.append(["assert", ["ge", "calldatasize", calldata_min_size]]) + else: + # stricter for static data + ret.append(["assert", ["eq", "calldatasize", calldata_min_size]]) + # TODO optimize make_setter by using # TupleType(list(arg.typ for arg in calldata_kwargs + default_kwargs)) # (must ensure memory area is contiguous)