From 71d361830578a2881e7e816cf504c1b7a6776ea8 Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 02:53:28 +0100 Subject: [PATCH 01/16] add slice evaluate() and tests --- .../functional/builtins/codegen/test_slice.py | 41 +++++++++++++++++++ vyper/builtins/functions.py | 19 +++++++++ 2 files changed, 60 insertions(+) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 53e092019f..1717605c5e 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -432,3 +432,44 @@ def test_slice_bytes32_calldata_extended(get_contract, code, result): c.bar(3, "0x0001020304050607080910111213141516171819202122232425262728293031", 5).hex() == result ) + + +code_comptime = [ + ( + """ +@external +@view +def baz() -> Bytes[16]: + return slice(0x1234567891234567891234567891234567891234567891234567891234567891, 0, 16) + """, + "12345678912345678912345678912345", + ), + ( + """ +@external +@view +def baz() -> String[5]: + return slice("why hello! how are you?", 4, 5) + """, + "hello", + ), + ( + """ +@external +@view +def baz() -> Bytes[6]: + return slice(b'gm sir, how are you ?', 0, 6) + """, + "gm sir".encode("utf-8").hex(), + ), +] + + +@pytest.mark.parametrize("code,result", code_comptime) +def test_comptime(get_contract, code, result): + c = get_contract(code) + ret = c.baz() + if hasattr(ret, "hex"): + assert ret.hex() == result + else: + assert ret == result diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 001939638b..a5bb3d12a6 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -294,6 +294,25 @@ class Slice(BuiltinFunction): ] _return_type = None + def evaluate(self, node): + (lit, st, le) = node.args[:3] + (st_val, le_val) = (st.value, le.value) + if isinstance(lit, vy_ast.Bytes): + sublit = lit.value[st_val : (st_val + le_val)] + return vy_ast.Bytes.from_node(node, value=sublit) + elif isinstance(lit, vy_ast.Str): + sublit = lit.value[st_val : (st_val + le_val)] + return vy_ast.Str.from_node(node, value=sublit) + elif isinstance(lit, vy_ast.Hex): + length = len(lit.value) // 2 - 1 + if length != 32: + # TODO unreachable? + raise UnfoldableNode + sublit = lit.value[st_val : (2 + st_val + (le_val * 2))] + return vy_ast.Bytes.from_node(node, value=sublit) + else: + raise UnfoldableNode + def fetch_call_return(self, node): arg_type, _, _ = self.infer_arg_types(node) From 42188f68f1160097da32d52e9c40fc259e82652e Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 08:28:15 +0100 Subject: [PATCH 02/16] add comp/run time equivalence check --- .../functional/builtins/codegen/test_slice.py | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 1717605c5e..222be24784 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -434,42 +434,70 @@ def test_slice_bytes32_calldata_extended(get_contract, code, result): ) -code_comptime = [ +code_compruntime = [ ( + "bytes32", """ @external @view def baz() -> Bytes[16]: return slice(0x1234567891234567891234567891234567891234567891234567891234567891, 0, 16) """, + """ +@external +@view +def baz(val: bytes32) -> Bytes[16]: + return slice(val, 0, 16) + """, + "0x1234567891234567891234567891234567891234567891234567891234567891", "12345678912345678912345678912345", ), ( + "string", """ @external @view def baz() -> String[5]: return slice("why hello! how are you?", 4, 5) """, + """ +@external +@view +def baz(val: String[100]) -> String[5]: + return slice(val, 4, 5) + """, + "why hello! how are you?", "hello", ), ( + "bytes", """ @external @view def baz() -> Bytes[6]: return slice(b'gm sir, how are you ?', 0, 6) """, + """ +@external +@view +def baz(val: Bytes[100]) -> Bytes[6]: + return slice(val, 0, 6) + """, + b'gm sir, how are you ?', "gm sir".encode("utf-8").hex(), ), ] -@pytest.mark.parametrize("code,result", code_comptime) -def test_comptime(get_contract, code, result): - c = get_contract(code) - ret = c.baz() - if hasattr(ret, "hex"): - assert ret.hex() == result +@pytest.mark.parametrize("name,compcode,runcode,arg,result", code_compruntime, ids=[el[0] for el in code_compruntime]) +def test_comptime_runtime(get_contract, name, compcode, runcode, arg, result): + c1 = get_contract(compcode) + c2 = get_contract(runcode) + ret1 = c1.baz() + ret2 = c2.baz(arg) + if hasattr(ret1, "hex"): + assert ret1.hex() == result + assert ret2.hex() == result else: - assert ret == result + assert ret1 == result + assert ret2 == result From 3614409b132b9c637a7312f7cb769e03e4172096 Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 08:41:07 +0100 Subject: [PATCH 03/16] lint --- tests/functional/builtins/codegen/test_slice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 222be24784..d8c23a898e 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -483,13 +483,15 @@ def baz() -> Bytes[6]: def baz(val: Bytes[100]) -> Bytes[6]: return slice(val, 0, 6) """, - b'gm sir, how are you ?', + b"gm sir, how are you ?", "gm sir".encode("utf-8").hex(), ), ] -@pytest.mark.parametrize("name,compcode,runcode,arg,result", code_compruntime, ids=[el[0] for el in code_compruntime]) +@pytest.mark.parametrize( + "name,compcode,runcode,arg,result", code_compruntime, ids=[el[0] for el in code_compruntime] +) def test_comptime_runtime(get_contract, name, compcode, runcode, arg, result): c1 = get_contract(compcode) c2 = get_contract(runcode) From a68d78386b8033063df50a829cf2a987536805ec Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 09:16:14 +0100 Subject: [PATCH 04/16] add compile-time strict type check --- vyper/builtins/functions.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index a5bb3d12a6..f5ef5a1046 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -296,20 +296,25 @@ class Slice(BuiltinFunction): def evaluate(self, node): (lit, st, le) = node.args[:3] - (st_val, le_val) = (st.value, le.value) - if isinstance(lit, vy_ast.Bytes): - sublit = lit.value[st_val : (st_val + le_val)] - return vy_ast.Bytes.from_node(node, value=sublit) - elif isinstance(lit, vy_ast.Str): - sublit = lit.value[st_val : (st_val + le_val)] - return vy_ast.Str.from_node(node, value=sublit) - elif isinstance(lit, vy_ast.Hex): - length = len(lit.value) // 2 - 1 - if length != 32: - # TODO unreachable? - raise UnfoldableNode - sublit = lit.value[st_val : (2 + st_val + (le_val * 2))] - return vy_ast.Bytes.from_node(node, value=sublit) + if ( + isinstance(lit, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) + and isinstance(st, vy_ast.Int) + and isinstance(le, vy_ast.Int) + ): + (st_val, le_val) = (st.value, le.value) + if isinstance(lit, vy_ast.Bytes): + sublit = lit.value[st_val : (st_val + le_val)] + return vy_ast.Bytes.from_node(node, value=sublit) + elif isinstance(lit, vy_ast.Str): + sublit = lit.value[st_val : (st_val + le_val)] + return vy_ast.Str.from_node(node, value=sublit) + else: + length = len(lit.value) // 2 - 1 + if length != 32: + # TODO unreachable? + raise UnfoldableNode + sublit = lit.value[st_val : (2 + st_val + (le_val * 2))] + return vy_ast.Bytes.from_node(node, value=sublit) else: raise UnfoldableNode From a782cbe3f653dc36be03d966bfab8dd43b839389 Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 16:55:23 +0100 Subject: [PATCH 05/16] add fuzz tests, harden Slice --- .../functional/builtins/folding/test_slice.py | 63 +++++++++++++++++++ vyper/builtins/functions.py | 19 ++++-- 2 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/functional/builtins/folding/test_slice.py diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py new file mode 100644 index 0000000000..1f8edad211 --- /dev/null +++ b/tests/functional/builtins/folding/test_slice.py @@ -0,0 +1,63 @@ +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from vyper import ast as vy_ast +from vyper.builtins import functions as vy_fn +from vyper.exceptions import ArgumentException + + +@pytest.mark.fuzzing +@settings(max_examples=50) +@given( + a=st.integers(min_value=0, max_value=2**256 - 1), + s=st.integers(min_value=0, max_value=31), + le=st.integers(min_value=1, max_value=32), +) +def test_slice_bytes32(get_contract, a, s, le): + a = hex(a) + while len(a) < 66: + a = f"0x0{a[2:]}" + le = min(32, 32 - s, le) + + source = f""" +@external +def foo(a: bytes32) -> Bytes[{le}]: + return slice(a, {s}, {le}) + """ + contract = get_contract(source) + + vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") + old_node = vyper_ast.body[0].value + new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) + + s *= 2 + le *= 2 + assert ( + int.from_bytes(contract.foo(a), "big") + == int.from_bytes(new_node.value, "big") + == int(a[2:][s : (s + le)], 16) + ) + + +@pytest.mark.fuzzing +@settings(max_examples=50) +@given( + a=st.integers(min_value=0, max_value=2**256 - 1), + s=st.integers(min_value=0, max_value=31), + le=st.integers(min_value=1, max_value=32), +) +def test_slice_bytesnot32(a, s, le): + a = hex(a) + if len(a) == 3: + a = f"0x0{a[2:]}" + elif len(a) == 66: + a = a[:-2] + elif len(a) % 2 == 1: + a = a[:-1] + le = min(32, 32 - s, le) + + vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") + old_node = vyper_ast.body[0].value + with pytest.raises(ArgumentException): + vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index f5ef5a1046..164e5b2942 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -295,6 +295,7 @@ class Slice(BuiltinFunction): _return_type = None def evaluate(self, node): + validate_call_args(node, 3) (lit, st, le) = node.args[:3] if ( isinstance(lit, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) @@ -302,7 +303,15 @@ def evaluate(self, node): and isinstance(le, vy_ast.Int) ): (st_val, le_val) = (st.value, le.value) + if not 0 <= st_val <= 31: + raise ArgumentException("Start cannot take that value", st) + if not 1 <= le_val <= 32: + raise ArgumentException("Length cannot take that value", le) + if st_val + le_val > 32: + raise ArgumentException("Slice is out of bounds", st) if isinstance(lit, vy_ast.Bytes): + st_val *= 2 + le_val *= 2 sublit = lit.value[st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=sublit) elif isinstance(lit, vy_ast.Str): @@ -311,10 +320,12 @@ def evaluate(self, node): else: length = len(lit.value) // 2 - 1 if length != 32: - # TODO unreachable? - raise UnfoldableNode - sublit = lit.value[st_val : (2 + st_val + (le_val * 2))] - return vy_ast.Bytes.from_node(node, value=sublit) + # raise UnfoldableNode + raise ArgumentException("Length can only be of 32", lit) + st_val *= 2 + le_val *= 2 + sublit = lit.value[2:][st_val : (st_val + le_val)] + return vy_ast.Bytes.from_node(node, value=f"0x{sublit}") else: raise UnfoldableNode From b3ad481bd785fca1cb25d7025fe5f5c06d27b908 Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 16:59:18 +0100 Subject: [PATCH 06/16] revert unit tests --- .../functional/builtins/codegen/test_slice.py | 46 ++++--------------- vyper/builtins/functions.py | 2 - 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index d8c23a898e..1717605c5e 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -434,72 +434,42 @@ def test_slice_bytes32_calldata_extended(get_contract, code, result): ) -code_compruntime = [ +code_comptime = [ ( - "bytes32", """ @external @view def baz() -> Bytes[16]: return slice(0x1234567891234567891234567891234567891234567891234567891234567891, 0, 16) """, - """ -@external -@view -def baz(val: bytes32) -> Bytes[16]: - return slice(val, 0, 16) - """, - "0x1234567891234567891234567891234567891234567891234567891234567891", "12345678912345678912345678912345", ), ( - "string", """ @external @view def baz() -> String[5]: return slice("why hello! how are you?", 4, 5) """, - """ -@external -@view -def baz(val: String[100]) -> String[5]: - return slice(val, 4, 5) - """, - "why hello! how are you?", "hello", ), ( - "bytes", """ @external @view def baz() -> Bytes[6]: return slice(b'gm sir, how are you ?', 0, 6) """, - """ -@external -@view -def baz(val: Bytes[100]) -> Bytes[6]: - return slice(val, 0, 6) - """, - b"gm sir, how are you ?", "gm sir".encode("utf-8").hex(), ), ] -@pytest.mark.parametrize( - "name,compcode,runcode,arg,result", code_compruntime, ids=[el[0] for el in code_compruntime] -) -def test_comptime_runtime(get_contract, name, compcode, runcode, arg, result): - c1 = get_contract(compcode) - c2 = get_contract(runcode) - ret1 = c1.baz() - ret2 = c2.baz(arg) - if hasattr(ret1, "hex"): - assert ret1.hex() == result - assert ret2.hex() == result +@pytest.mark.parametrize("code,result", code_comptime) +def test_comptime(get_contract, code, result): + c = get_contract(code) + ret = c.baz() + if hasattr(ret, "hex"): + assert ret.hex() == result else: - assert ret1 == result - assert ret2 == result + assert ret == result diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 164e5b2942..dce3b6ad8e 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -310,8 +310,6 @@ def evaluate(self, node): if st_val + le_val > 32: raise ArgumentException("Slice is out of bounds", st) if isinstance(lit, vy_ast.Bytes): - st_val *= 2 - le_val *= 2 sublit = lit.value[st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=sublit) elif isinstance(lit, vy_ast.Str): From 162d6b3a87c4ac88beef68724506dce34a2fc873 Mon Sep 17 00:00:00 2001 From: francois Date: Fri, 10 Nov 2023 23:19:45 +0100 Subject: [PATCH 07/16] add more robust fuzzing, better slice errors --- .../functional/builtins/codegen/test_slice.py | 20 ++++++ .../functional/builtins/folding/test_slice.py | 67 +++++++++++++++++-- vyper/builtins/functions.py | 1 - 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 1717605c5e..6369b39c7e 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -2,6 +2,8 @@ import pytest from hypothesis import given, settings +from vyper import ast as vy_ast +from vyper.builtins import functions as vy_fn from vyper.compiler.settings import OptimizationLevel from vyper.exceptions import ArgumentException, TypeMismatch @@ -473,3 +475,21 @@ def test_comptime(get_contract, code, result): assert ret.hex() == result else: assert ret == result + + +error_slice = [ + "slice(0x00, 0, 1)", + 'slice("why hello! how are you?", 32, 1)', + 'slice("why hello! how are you?", -1, 1)', + 'slice("why hello! how are you?", 4, 0)', + 'slice("why hello! how are you?", 0, 33)', + 'slice("why hello! how are you?", 16, 17)', +] + + +@pytest.mark.parametrize("code", error_slice) +def test_slice_error(code): + vyper_ast = vy_ast.parse_to_ast(code) + old_node = vyper_ast.body[0].value + with pytest.raises(ArgumentException): + vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py index 1f8edad211..8ad759a483 100644 --- a/tests/functional/builtins/folding/test_slice.py +++ b/tests/functional/builtins/folding/test_slice.py @@ -1,3 +1,5 @@ +import string + import pytest from hypothesis import given, settings from hypothesis import strategies as st @@ -7,6 +9,10 @@ from vyper.exceptions import ArgumentException +def normalize_bytes(data): + return bytes(int.from_bytes(data, "big")) + + @pytest.mark.fuzzing @settings(max_examples=50) @given( @@ -33,11 +39,7 @@ def foo(a: bytes32) -> Bytes[{le}]: s *= 2 le *= 2 - assert ( - int.from_bytes(contract.foo(a), "big") - == int.from_bytes(new_node.value, "big") - == int(a[2:][s : (s + le)], 16) - ) + assert normalize_bytes(contract.foo(a)) == new_node.value == bytes.fromhex(a[2:][s : (s + le)]) @pytest.mark.fuzzing @@ -61,3 +63,58 @@ def test_slice_bytesnot32(a, s, le): old_node = vyper_ast.body[0].value with pytest.raises(ArgumentException): vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) + + +@pytest.mark.fuzzing +@settings(max_examples=50) +@given( + a=st.binary(min_size=1, max_size=100), + s=st.integers(min_value=0, max_value=99), + le=st.integers(min_value=1, max_value=100), +) +def test_slice_dynbytes(get_contract, a, s, le): + s = s % len(a) + le = min(len(a), len(a) - s, le) + + source = f""" +@external +def foo(a: Bytes[100]) -> Bytes[{le}]: + return slice(a, {s}, {le}) + """ + contract = get_contract(source) + + vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") + old_node = vyper_ast.body[0].value + new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) + + assert contract.foo(a) == new_node.value == a[s : (s + le)] + + +valid_char = [ + char for char in string.printable if char not in (string.whitespace.replace(" ", "") + '"\\') +] + + +@pytest.mark.fuzzing +@settings(max_examples=50) +@given( + a=st.text(alphabet=valid_char, min_size=1, max_size=100), + s=st.integers(min_value=0, max_value=99), + le=st.integers(min_value=1, max_value=100), +) +def test_slice_string(get_contract, a, s, le): + s = s % len(a) + le = min(len(a), len(a) - s, le) + + source = f""" +@external +def foo(a: String[100]) -> String[{le}]: + return slice(a, {s}, {le}) + """ + contract = get_contract(source) + + vyper_ast = vy_ast.parse_to_ast(f'slice("{a}", {s}, {le})') + old_node = vyper_ast.body[0].value + new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) + + assert contract.foo(a) == new_node.value == a[s : (s + le)] diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index dce3b6ad8e..747e95ea98 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -318,7 +318,6 @@ def evaluate(self, node): else: length = len(lit.value) // 2 - 1 if length != 32: - # raise UnfoldableNode raise ArgumentException("Length can only be of 32", lit) st_val *= 2 le_val *= 2 From 7c011ea8e6d2ed8c38e08afc1ea19305781f6379 Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 11 Nov 2023 00:58:31 +0100 Subject: [PATCH 08/16] remove bytes conversion --- tests/functional/builtins/folding/test_slice.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py index 8ad759a483..330165c152 100644 --- a/tests/functional/builtins/folding/test_slice.py +++ b/tests/functional/builtins/folding/test_slice.py @@ -9,10 +9,6 @@ from vyper.exceptions import ArgumentException -def normalize_bytes(data): - return bytes(int.from_bytes(data, "big")) - - @pytest.mark.fuzzing @settings(max_examples=50) @given( @@ -39,7 +35,7 @@ def foo(a: bytes32) -> Bytes[{le}]: s *= 2 le *= 2 - assert normalize_bytes(contract.foo(a)) == new_node.value == bytes.fromhex(a[2:][s : (s + le)]) + assert contract.foo(a) == new_node.value == bytes.fromhex(a[2:][s : (s + le)]) @pytest.mark.fuzzing From ea35be9330a7c82813a6b5d13016139bb52c340a Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 11 Nov 2023 10:59:54 +0100 Subject: [PATCH 09/16] fix slice bound for dynamic types --- tests/functional/builtins/codegen/test_slice.py | 4 +++- vyper/builtins/functions.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 6369b39c7e..8ada4fc6a6 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -479,11 +479,13 @@ def test_comptime(get_contract, code, result): error_slice = [ "slice(0x00, 0, 1)", + "slice(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10', 10, 1)", + "slice(b'', 0, 1)", 'slice("why hello! how are you?", 32, 1)', 'slice("why hello! how are you?", -1, 1)', 'slice("why hello! how are you?", 4, 0)', 'slice("why hello! how are you?", 0, 33)', - 'slice("why hello! how are you?", 16, 17)', + 'slice("why hello! how are you?", 16, 10)', ] diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 747e95ea98..48d2b5a6ba 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -307,7 +307,7 @@ def evaluate(self, node): raise ArgumentException("Start cannot take that value", st) if not 1 <= le_val <= 32: raise ArgumentException("Length cannot take that value", le) - if st_val + le_val > 32: + if st_val + le_val > len(lit.value): raise ArgumentException("Slice is out of bounds", st) if isinstance(lit, vy_ast.Bytes): sublit = lit.value[st_val : (st_val + le_val)] From 1dd6af59cd3b81a34b5465f77c69a34e8d293c00 Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 11 Nov 2023 14:20:46 +0100 Subject: [PATCH 10/16] add dynamic bounds check --- vyper/builtins/functions.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 48d2b5a6ba..be54feebe5 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -303,28 +303,36 @@ def evaluate(self, node): and isinstance(le, vy_ast.Int) ): (st_val, le_val) = (st.value, le.value) - if not 0 <= st_val <= 31: - raise ArgumentException("Start cannot take that value", st) - if not 1 <= le_val <= 32: - raise ArgumentException("Length cannot take that value", le) + + if st_val < 0: + raise ArgumentException("Start cannot be negative", st) + elif le_val <= 0: + raise ArgumentException("Length cannot be negative", le) + + if isinstance(lit, vy_ast.Hex): + if st_val >= 32: + raise ArgumentException("Start cannot take that value", st) + if le_val > 32: + raise ArgumentException("Length cannot take that value", le) + length = len(lit.value) // 2 - 1 + if length != 32: + raise ArgumentException("Length can only be of 32", lit) + st_val *= 2 + le_val *= 2 + if st_val + le_val > len(lit.value): raise ArgumentException("Slice is out of bounds", st) + if isinstance(lit, vy_ast.Bytes): sublit = lit.value[st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=sublit) elif isinstance(lit, vy_ast.Str): sublit = lit.value[st_val : (st_val + le_val)] return vy_ast.Str.from_node(node, value=sublit) - else: - length = len(lit.value) // 2 - 1 - if length != 32: - raise ArgumentException("Length can only be of 32", lit) - st_val *= 2 - le_val *= 2 + elif isinstance(lit, vy_ast.Hex): sublit = lit.value[2:][st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=f"0x{sublit}") - else: - raise UnfoldableNode + raise UnfoldableNode def fetch_call_return(self, node): arg_type, _, _ = self.infer_arg_types(node) From 16cac7d955df23ac73287967fc80203356155e4b Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 25 Nov 2023 20:58:58 +0100 Subject: [PATCH 11/16] rename variables and use a tuple --- vyper/builtins/functions.py | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index be54feebe5..c36690de6c 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -296,41 +296,41 @@ class Slice(BuiltinFunction): def evaluate(self, node): validate_call_args(node, 3) - (lit, st, le) = node.args[:3] + literal_value, start, length = node.args if ( - isinstance(lit, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) - and isinstance(st, vy_ast.Int) - and isinstance(le, vy_ast.Int) + isinstance(literal_value, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) + and isinstance(start, vy_ast.Int) + and isinstance(length, vy_ast.Int) ): - (st_val, le_val) = (st.value, le.value) + (st_val, le_val) = (start.value, length.value) if st_val < 0: - raise ArgumentException("Start cannot be negative", st) + raise ArgumentException("Start cannot be negative", start) elif le_val <= 0: - raise ArgumentException("Length cannot be negative", le) + raise ArgumentException("Length cannot be negative", length) - if isinstance(lit, vy_ast.Hex): + if isinstance(literal_value, vy_ast.Hex): if st_val >= 32: - raise ArgumentException("Start cannot take that value", st) + raise ArgumentException("Start cannot take that value", start) if le_val > 32: - raise ArgumentException("Length cannot take that value", le) - length = len(lit.value) // 2 - 1 + raise ArgumentException("Length cannot take that value", length) + length = len(literal_value.value) // 2 - 1 if length != 32: - raise ArgumentException("Length can only be of 32", lit) + raise ArgumentException("Length can only be of 32", literal_value) st_val *= 2 le_val *= 2 - if st_val + le_val > len(lit.value): - raise ArgumentException("Slice is out of bounds", st) + if st_val + le_val > len(literal_value.value): + raise ArgumentException("Slice is out of bounds", start) - if isinstance(lit, vy_ast.Bytes): - sublit = lit.value[st_val : (st_val + le_val)] + if isinstance(literal_value, vy_ast.Bytes): + sublit = literal_value.value[st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=sublit) - elif isinstance(lit, vy_ast.Str): - sublit = lit.value[st_val : (st_val + le_val)] + elif isinstance(literal_value, vy_ast.Str): + sublit = literal_value.value[st_val : (st_val + le_val)] return vy_ast.Str.from_node(node, value=sublit) - elif isinstance(lit, vy_ast.Hex): - sublit = lit.value[2:][st_val : (st_val + le_val)] + elif isinstance(literal_value, vy_ast.Hex): + sublit = literal_value.value[2:][st_val : (st_val + le_val)] return vy_ast.Bytes.from_node(node, value=f"0x{sublit}") raise UnfoldableNode From 5faddf80c18c269ca31851ffc8227729dd0d7fd5 Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 9 Dec 2023 23:49:50 +0100 Subject: [PATCH 12/16] better naming, use hypothesis bytes --- .../functional/builtins/folding/test_slice.py | 91 +++++++++---------- vyper/builtins/functions.py | 22 ++--- 2 files changed, 54 insertions(+), 59 deletions(-) diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py index 330165c152..f8ebf998f0 100644 --- a/tests/functional/builtins/folding/test_slice.py +++ b/tests/functional/builtins/folding/test_slice.py @@ -12,50 +12,45 @@ @pytest.mark.fuzzing @settings(max_examples=50) @given( - a=st.integers(min_value=0, max_value=2**256 - 1), - s=st.integers(min_value=0, max_value=31), - le=st.integers(min_value=1, max_value=32), + bytes_in=st.binary(max_size=32), + start=st.integers(min_value=0, max_value=31), + length=st.integers(min_value=1, max_value=32), ) -def test_slice_bytes32(get_contract, a, s, le): - a = hex(a) - while len(a) < 66: - a = f"0x0{a[2:]}" - le = min(32, 32 - s, le) +def test_slice_bytes32(get_contract, bytes_in, start, length): + as_hex = "0x" + str.join("", ["00" for _ in range(32 - len(bytes_in))]) + bytes_in.hex() + length = min(32 - start, length) source = f""" @external -def foo(a: bytes32) -> Bytes[{le}]: - return slice(a, {s}, {le}) +def foo(bytes_in: bytes32) -> Bytes[{length}]: + return slice(bytes_in, {start}, {length}) """ contract = get_contract(source) - vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") + vyper_ast = vy_ast.parse_to_ast(f"slice({as_hex}, {start}, {length})") old_node = vyper_ast.body[0].value new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) - s *= 2 - le *= 2 - assert contract.foo(a) == new_node.value == bytes.fromhex(a[2:][s : (s + le)]) + start *= 2 + length *= 2 + assert contract.foo(as_hex) == new_node.value == bytes.fromhex(as_hex[2:][start : (start + length)]) @pytest.mark.fuzzing @settings(max_examples=50) @given( - a=st.integers(min_value=0, max_value=2**256 - 1), - s=st.integers(min_value=0, max_value=31), - le=st.integers(min_value=1, max_value=32), + bytes_in=st.binary(max_size=31), + start=st.integers(min_value=0, max_value=31), + length=st.integers(min_value=1, max_value=32), ) -def test_slice_bytesnot32(a, s, le): - a = hex(a) - if len(a) == 3: - a = f"0x0{a[2:]}" - elif len(a) == 66: - a = a[:-2] - elif len(a) % 2 == 1: - a = a[:-1] - le = min(32, 32 - s, le) - - vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") +def test_slice_bytesnot32(bytes_in, start, length): + if not len(bytes_in): + as_hex = "0x00" + else: + as_hex = "0x" + bytes_in.hex() + length = min(32, 32 - start, length) + + vyper_ast = vy_ast.parse_to_ast(f"slice({as_hex}, {start}, {length})") old_node = vyper_ast.body[0].value with pytest.raises(ArgumentException): vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) @@ -64,26 +59,26 @@ def test_slice_bytesnot32(a, s, le): @pytest.mark.fuzzing @settings(max_examples=50) @given( - a=st.binary(min_size=1, max_size=100), - s=st.integers(min_value=0, max_value=99), - le=st.integers(min_value=1, max_value=100), + bytes_in=st.binary(min_size=1, max_size=100), + start=st.integers(min_value=0, max_value=99), + length=st.integers(min_value=1, max_value=100), ) -def test_slice_dynbytes(get_contract, a, s, le): - s = s % len(a) - le = min(len(a), len(a) - s, le) +def test_slice_dynbytes(get_contract, bytes_in, start, length): + start = start % len(bytes_in) + length = min(len(bytes_in), len(bytes_in) - start, length) source = f""" @external -def foo(a: Bytes[100]) -> Bytes[{le}]: - return slice(a, {s}, {le}) +def foo(bytes_in: Bytes[100]) -> Bytes[{length}]: + return slice(bytes_in, {start}, {length}) """ contract = get_contract(source) - vyper_ast = vy_ast.parse_to_ast(f"slice({a}, {s}, {le})") + vyper_ast = vy_ast.parse_to_ast(f"slice({bytes_in}, {start}, {length})") old_node = vyper_ast.body[0].value new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) - assert contract.foo(a) == new_node.value == a[s : (s + le)] + assert contract.foo(bytes_in) == new_node.value == bytes_in[start : (start + length)] valid_char = [ @@ -94,23 +89,23 @@ def foo(a: Bytes[100]) -> Bytes[{le}]: @pytest.mark.fuzzing @settings(max_examples=50) @given( - a=st.text(alphabet=valid_char, min_size=1, max_size=100), - s=st.integers(min_value=0, max_value=99), - le=st.integers(min_value=1, max_value=100), + string_in=st.text(alphabet=valid_char, min_size=1, max_size=100), + start=st.integers(min_value=0, max_value=99), + length=st.integers(min_value=1, max_value=100), ) -def test_slice_string(get_contract, a, s, le): - s = s % len(a) - le = min(len(a), len(a) - s, le) +def test_slice_string(get_contract, string_in, start, length): + start = start % len(string_in) + length = min(len(string_in), len(string_in) - start, length) source = f""" @external -def foo(a: String[100]) -> String[{le}]: - return slice(a, {s}, {le}) +def foo(string_in: String[100]) -> String[{length}]: + return slice(string_in, {start}, {length}) """ contract = get_contract(source) - vyper_ast = vy_ast.parse_to_ast(f'slice("{a}", {s}, {le})') + vyper_ast = vy_ast.parse_to_ast(f'slice("{string_in}", {start}, {length})') old_node = vyper_ast.body[0].value new_node = vy_fn.DISPATCH_TABLE["slice"].evaluate(old_node) - assert contract.foo(a) == new_node.value == a[s : (s + le)] + assert contract.foo(string_in) == new_node.value == string_in[start : (start + length)] diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index c36690de6c..497a4d10c5 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -302,35 +302,35 @@ def evaluate(self, node): and isinstance(start, vy_ast.Int) and isinstance(length, vy_ast.Int) ): - (st_val, le_val) = (start.value, length.value) + (start_val, length_val) = (start.value, length.value) - if st_val < 0: + if start_val < 0: raise ArgumentException("Start cannot be negative", start) - elif le_val <= 0: + elif length_val <= 0: raise ArgumentException("Length cannot be negative", length) if isinstance(literal_value, vy_ast.Hex): - if st_val >= 32: + if start_val >= 32: raise ArgumentException("Start cannot take that value", start) - if le_val > 32: + if length_val > 32: raise ArgumentException("Length cannot take that value", length) length = len(literal_value.value) // 2 - 1 if length != 32: raise ArgumentException("Length can only be of 32", literal_value) - st_val *= 2 - le_val *= 2 + start_val *= 2 + length_val *= 2 - if st_val + le_val > len(literal_value.value): + if start_val + length_val > len(literal_value.value): raise ArgumentException("Slice is out of bounds", start) if isinstance(literal_value, vy_ast.Bytes): - sublit = literal_value.value[st_val : (st_val + le_val)] + sublit = literal_value.value[start_val : (start_val + length_val)] return vy_ast.Bytes.from_node(node, value=sublit) elif isinstance(literal_value, vy_ast.Str): - sublit = literal_value.value[st_val : (st_val + le_val)] + sublit = literal_value.value[start_val : (start_val + length_val)] return vy_ast.Str.from_node(node, value=sublit) elif isinstance(literal_value, vy_ast.Hex): - sublit = literal_value.value[2:][st_val : (st_val + le_val)] + sublit = literal_value.value[2:][start_val : (start_val + length_val)] return vy_ast.Bytes.from_node(node, value=f"0x{sublit}") raise UnfoldableNode From 261fb62c726f5f78197663a421b895753ba7cc29 Mon Sep 17 00:00:00 2001 From: francois Date: Sat, 9 Dec 2023 23:51:34 +0100 Subject: [PATCH 13/16] lint --- tests/functional/builtins/folding/test_slice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py index f8ebf998f0..6c47c98cff 100644 --- a/tests/functional/builtins/folding/test_slice.py +++ b/tests/functional/builtins/folding/test_slice.py @@ -33,7 +33,11 @@ def foo(bytes_in: bytes32) -> Bytes[{length}]: start *= 2 length *= 2 - assert contract.foo(as_hex) == new_node.value == bytes.fromhex(as_hex[2:][start : (start + length)]) + assert ( + contract.foo(as_hex) + == new_node.value + == bytes.fromhex(as_hex[2:][start : (start + length)]) + ) @pytest.mark.fuzzing From 4ea5d0e469ef99b1a481e530709a4a969f5e1b2c Mon Sep 17 00:00:00 2001 From: francois Date: Sun, 10 Dec 2023 17:20:41 +0100 Subject: [PATCH 14/16] use `zfill` --- tests/functional/builtins/folding/test_slice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/builtins/folding/test_slice.py b/tests/functional/builtins/folding/test_slice.py index 6c47c98cff..da5d1166dc 100644 --- a/tests/functional/builtins/folding/test_slice.py +++ b/tests/functional/builtins/folding/test_slice.py @@ -17,7 +17,7 @@ length=st.integers(min_value=1, max_value=32), ) def test_slice_bytes32(get_contract, bytes_in, start, length): - as_hex = "0x" + str.join("", ["00" for _ in range(32 - len(bytes_in))]) + bytes_in.hex() + as_hex = "0x" + str(bytes_in.hex()).zfill(64) length = min(32 - start, length) source = f""" From e585b77b1e2e683ad7b874d92526ab74cdccb833 Mon Sep 17 00:00:00 2001 From: francois Date: Wed, 20 Dec 2023 11:03:08 +0100 Subject: [PATCH 15/16] reduce nesting, better variables --- .../functional/builtins/codegen/test_slice.py | 2 +- vyper/builtins/functions.py | 74 ++++++++++--------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/tests/functional/builtins/codegen/test_slice.py b/tests/functional/builtins/codegen/test_slice.py index 8ada4fc6a6..1a57025410 100644 --- a/tests/functional/builtins/codegen/test_slice.py +++ b/tests/functional/builtins/codegen/test_slice.py @@ -471,7 +471,7 @@ def baz() -> Bytes[6]: def test_comptime(get_contract, code, result): c = get_contract(code) ret = c.baz() - if hasattr(ret, "hex"): + if isinstance(ret, bytes): assert ret.hex() == result else: assert ret == result diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 497a4d10c5..038989724b 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -296,43 +296,45 @@ class Slice(BuiltinFunction): def evaluate(self, node): validate_call_args(node, 3) - literal_value, start, length = node.args - if ( - isinstance(literal_value, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) - and isinstance(start, vy_ast.Int) - and isinstance(length, vy_ast.Int) + bytestring, start_node, length_node = node.args + if not ( + isinstance(bytestring, (vy_ast.Bytes, vy_ast.Str, vy_ast.Hex)) + and isinstance(start_node, vy_ast.Int) + and isinstance(length_node, vy_ast.Int) ): - (start_val, length_val) = (start.value, length.value) - - if start_val < 0: - raise ArgumentException("Start cannot be negative", start) - elif length_val <= 0: - raise ArgumentException("Length cannot be negative", length) - - if isinstance(literal_value, vy_ast.Hex): - if start_val >= 32: - raise ArgumentException("Start cannot take that value", start) - if length_val > 32: - raise ArgumentException("Length cannot take that value", length) - length = len(literal_value.value) // 2 - 1 - if length != 32: - raise ArgumentException("Length can only be of 32", literal_value) - start_val *= 2 - length_val *= 2 - - if start_val + length_val > len(literal_value.value): - raise ArgumentException("Slice is out of bounds", start) - - if isinstance(literal_value, vy_ast.Bytes): - sublit = literal_value.value[start_val : (start_val + length_val)] - return vy_ast.Bytes.from_node(node, value=sublit) - elif isinstance(literal_value, vy_ast.Str): - sublit = literal_value.value[start_val : (start_val + length_val)] - return vy_ast.Str.from_node(node, value=sublit) - elif isinstance(literal_value, vy_ast.Hex): - sublit = literal_value.value[2:][start_val : (start_val + length_val)] - return vy_ast.Bytes.from_node(node, value=f"0x{sublit}") - raise UnfoldableNode + raise UnfoldableNode + + (start, length) = (start_node.value, length_node.value) + + if start < 0: + raise ArgumentException("Start cannot be negative", start_node) + elif length <= 0: + raise ArgumentException("Length must be positive", length_node) + + if isinstance(bytestring, vy_ast.Hex): + bytes_value = bytes.fromhex(bytestring.value.removeprefix("0x")) + if start >= 32: + raise ArgumentException("Start cannot take that value", start_node) + if length > 32: + raise ArgumentException("Length cannot take that value", length_node) + if len(bytes_value) != 32: + raise ArgumentException("Length can only be of 32", bytestring) + elif isinstance(bytestring, vy_ast.Str): + bytes_value = bytestring.value + else: + bytes_value = bytes.fromhex(bytestring.value.hex().removeprefix("0x")) + + if start + length > len(bytes_value): + raise ArgumentException("Slice is out of bounds", start_node) + + end = start + length + res = bytes_value[start : end] + if isinstance(bytestring, vy_ast.Bytes): + return vy_ast.Bytes.from_node(node, value=res) + if isinstance(bytestring, vy_ast.Str): + return vy_ast.Str.from_node(node, value=res) + if isinstance(bytestring, vy_ast.Hex): + return vy_ast.Bytes.from_node(node, value=res) def fetch_call_return(self, node): arg_type, _, _ = self.infer_arg_types(node) From 5b959fe2648ad516d28abff614314ada9d0c1460 Mon Sep 17 00:00:00 2001 From: francois Date: Wed, 20 Dec 2023 11:03:47 +0100 Subject: [PATCH 16/16] lint --- vyper/builtins/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 038989724b..3167cd816a 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -328,7 +328,7 @@ def evaluate(self, node): raise ArgumentException("Slice is out of bounds", start_node) end = start + length - res = bytes_value[start : end] + res = bytes_value[start:end] if isinstance(bytestring, vy_ast.Bytes): return vy_ast.Bytes.from_node(node, value=res) if isinstance(bytestring, vy_ast.Str):