Skip to content

Commit a37bbbc

Browse files
fix: per-method calldatasize checks (#2911)
added a calldatasize check to ensure its length is equal to or greater than the minimum ABI size (+ 4 bytes for method ID). in the case where calldata args are all static, use a strict equals check. Co-authored-by: Charles Cooper <[email protected]>
1 parent 58a5ae5 commit a37bbbc

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

tests/parser/features/external_contracts/test_external_contract_calls.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,3 +2452,52 @@ def do_stuff(f: Foo) -> uint256:
24522452
c2 = get_contract(callee_code)
24532453

24542454
assert c1.do_stuff(c2.address) == 1
2455+
2456+
2457+
TEST_ADDR = b"".join(chr(i).encode("utf-8") for i in range(20)).hex()
2458+
2459+
2460+
@pytest.mark.parametrize("typ,val", [("address", TEST_ADDR)])
2461+
def test_calldata_clamp(w3, get_contract, assert_tx_failed, abi_encode, keccak, typ, val):
2462+
code = f"""
2463+
@external
2464+
def foo(a: {typ}):
2465+
pass
2466+
"""
2467+
c1 = get_contract(code)
2468+
sig = keccak(f"foo({typ})".encode()).hex()[:10]
2469+
encoded = abi_encode(f"({typ})", (val,)).hex()
2470+
data = f"{sig}{encoded}"
2471+
2472+
# Static size is short by 1 byte
2473+
malformed = data[:-2]
2474+
assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed}))
2475+
2476+
# Static size exceeds by 1 byte
2477+
malformed = data + "ff"
2478+
assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed}))
2479+
2480+
# Static size is exact
2481+
w3.eth.send_transaction({"to": c1.address, "data": data})
2482+
2483+
2484+
@pytest.mark.parametrize("typ,val", [("address", ([TEST_ADDR] * 3, "vyper"))])
2485+
def test_dynamic_calldata_clamp(w3, get_contract, assert_tx_failed, abi_encode, keccak, typ, val):
2486+
code = f"""
2487+
@external
2488+
def foo(a: DynArray[{typ}, 3], b: String[5]):
2489+
pass
2490+
"""
2491+
2492+
c1 = get_contract(code)
2493+
sig = keccak(f"foo({typ}[],string)".encode()).hex()[:10]
2494+
encoded = abi_encode(f"({typ}[],string)", val).hex()
2495+
data = f"{sig}{encoded}"
2496+
2497+
# Dynamic size is short by 1 byte
2498+
malformed = data[:264]
2499+
assert_tx_failed(lambda: w3.eth.send_transaction({"to": c1.address, "data": malformed}))
2500+
2501+
# Dynamic size is at least minimum (132 bytes * 2 + 2 (for 0x) = 266)
2502+
valid = data[:266]
2503+
w3.eth.send_transaction({"to": c1.address, "data": valid})

tests/parser/functions/test_default_function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def __default__():
126126

127127
logs = get_logs(
128128
# call blockHashAskewLimitary
129-
w3.eth.send_transaction({"to": c.address, "value": 0, "data": "0x00000000"}),
129+
w3.eth.send_transaction({"to": c.address, "value": 0, "data": "0x" + "00" * 36}),
130130
c,
131131
"Sent",
132132
)

vyper/codegen/function_definitions/external_function.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ def handler_for(calldata_kwargs, default_kwargs):
8686
# a sequence of statements to strictify kwargs into memory
8787
ret = ["seq"]
8888

89+
# ensure calldata is at least of minimum length
90+
args_abi_t = calldata_args_t.abi_type
91+
calldata_min_size = args_abi_t.min_size() + 4
92+
if args_abi_t.is_dynamic():
93+
ret.append(["assert", ["ge", "calldatasize", calldata_min_size]])
94+
else:
95+
# stricter for static data
96+
ret.append(["assert", ["eq", "calldatasize", calldata_min_size]])
97+
8998
# TODO optimize make_setter by using
9099
# TupleType(list(arg.typ for arg in calldata_kwargs + default_kwargs))
91100
# (must ensure memory area is contiguous)

0 commit comments

Comments
 (0)