Repository: vyperlang/vyper

Issue:
verify calldata length
Vyper doesn't seem to do any verification of the calldata length currently, which can read to a whole range of problems and unintuitive behaviour. For reference, check out:

- https://github.com/ethereum/eth2.0-specs/issues/1357
- https://medium.com/golem-project/how-to-find-10m-by-just-reading-blockchain-6ae9d39fcd95
- https://github.com/ethereum/solidity/issues/2109

Expected behaviour is to REVERT if CALLDATA is smaller than expected


Discussion:
this is an interesting set of behaviors. this issue is slightly related: https://github.com/ethereum/vyper/issues/1602.

so if I understand correctly, in some cases where user input is not validated, an address with fewer than 20 bytes could be input into a contract, and then following values will appear shifted in the calldata. as an example with an erc20 transfer, if the user inputs this address
```
0x12345678901234567890123456789012345678 # 19 bytes
```
then the calldata could look like
```
0x12345678901234567890123456789012345678<amount>
```

it's a bit unclear to me though how to detect that the calldata is smaller than expected. for static data it's pretty clear since the size can only be one value but for dynamic data you could still have this issue.
Meeting Notes: Implement same checks as solidity.

Resolving patch:
diff --git a/vyper/codegen/function_definitions/external_function.py b/vyper/codegen/function_definitions/external_function.py
--- 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)


Test patch:
diff --git a/tests/parser/features/external_contracts/test_external_contract_calls.py b/tests/parser/features/external_contracts/test_external_contract_calls.py
--- 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
--- 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",
     )