Skip to content

Commit

Permalink
fix: per-method calldatasize checks (vyperlang#2911)
Browse files Browse the repository at this point in the history
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 <cooper.charles.m@gmail.com>
  • Loading branch information
tserg and charles-cooper authored Jun 17, 2022
1 parent 58a5ae5 commit a37bbbc
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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})
2 changes: 1 addition & 1 deletion tests/parser/functions/test_default_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
9 changes: 9 additions & 0 deletions vyper/codegen/function_definitions/external_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit a37bbbc

Please sign in to comment.