Repository: vyperlang/vyper

Issue:
Array types are not hashed for event topics
Solidity hashes all array types, including `bytes` and `string`s, when they're used as indexed parameters (event topics).

Vyper only accepts `bytes` and `string`s with a maximum length of 32. They're then converted to `bytes32`. This could be an interoperability problem. Vyper doesn't appear to accept other array types as indexed event parameters at all.


Discussion:
This has been pointed out before, but I can't find the issue. I'm not sure if this is covered in the ABI specification, but I feel like it should be.

cc @davesque?
I think https://solidity.readthedocs.io/en/v0.5.12/abi-spec.html#events is the relevant ABI documentation:

> However, for all “complex” types or types of dynamic length, including all arrays, `string`, `bytes` and structs, `EVENT_INDEXED_ARGS` will contain the _Keccak hash_ of a special in-place encoded value (see Encoding of Indexed Event Parameters), rather than the encoded value directly.

Resolving patch:
diff --git a/vyper/parser/events.py b/vyper/parser/events.py
--- a/vyper/parser/events.py
+++ b/vyper/parser/events.py
@@ -1,10 +1,9 @@
 from vyper import ast as vy_ast
-from vyper.exceptions import InvalidLiteral, TypeMismatch
+from vyper.exceptions import TypeMismatch
 from vyper.parser.expr import Expr
 from vyper.parser.lll_node import LLLnode
 from vyper.parser.parser_utils import (
     base_type_conversion,
-    byte_array_to_num,
     getpos,
     make_byte_array_copier,
     make_setter,
@@ -17,7 +16,7 @@
     ListType,
     get_size_of_type,
 )
-from vyper.utils import bytes_to_int, ceil32, string_to_bytes
+from vyper.utils import bytes_to_int, ceil32, keccak256
 
 
 def pack_logging_topics(event_id, args, expected_topics, context, pos):
@@ -34,17 +33,46 @@ def pack_logging_topics(event_id, args, expected_topics, context, pos):
                 raise TypeMismatch(
                     f"Topic input bytes are too big: {arg_type} {expected_type}", code_pos
                 )
-            if isinstance(arg, vy_ast.Str):
-                bytez, bytez_length = string_to_bytes(arg.s)
-                if len(bytez) > 32:
-                    raise InvalidLiteral("Can only log a maximum of 32 bytes at a time.", code_pos)
-                topics.append(bytes_to_int(bytez + b"\x00" * (32 - bytez_length)))
+
+            if isinstance(arg, (vy_ast.Str, vy_ast.Bytes)):
+                # for literals, generate the topic at compile time
+                value = arg.value
+                if isinstance(value, str):
+                    value = value.encode()
+                topics.append(bytes_to_int(keccak256(value)))
+
+            elif value.location == "memory":
+                topics.append(["sha3", ["add", value, 32], ["mload", value]])
+
             else:
-                if value.location == "memory":
-                    size = ["mload", value]
-                elif value.location == "storage":
-                    size = ["sload", ["sha3_32", value]]
-                topics.append(byte_array_to_num(value, arg, "uint256", size))
+                # storage or calldata
+                placeholder = context.new_placeholder(value.typ)
+                placeholder_node = LLLnode.from_list(placeholder, typ=value.typ, location="memory")
+                copier = make_byte_array_copier(
+                    placeholder_node,
+                    LLLnode.from_list("_sub", typ=value.typ, location=value.location),
+                )
+                lll_node = [
+                    "with",
+                    "_sub",
+                    value,
+                    ["seq", copier, ["sha3", ["add", placeholder, 32], ["mload", placeholder]]],
+                ]
+                topics.append(lll_node)
+
+        elif isinstance(arg_type, ListType) and isinstance(expected_type, ListType):
+            size = get_size_of_type(value.typ) * 32
+            if value.location == "memory":
+                topics.append(["sha3", value, size])
+
+            else:
+                # storage or calldata
+                placeholder = context.new_placeholder(value.typ)
+                placeholder_node = LLLnode.from_list(placeholder, typ=value.typ, location="memory")
+                setter = make_setter(placeholder_node, value, "memory", value.pos)
+                lll_node = ["seq", setter, ["sha3", placeholder, size]]
+                topics.append(lll_node)
+
         else:
             if arg_type != expected_type:
                 raise TypeMismatch(
diff --git a/vyper/signatures/event_signature.py b/vyper/signatures/event_signature.py
--- a/vyper/signatures/event_signature.py
+++ b/vyper/signatures/event_signature.py
@@ -46,12 +46,6 @@ def from_declaration(cls, class_node, global_ctx):
                 if isinstance(typ, vy_ast.Call) and typ.get("func.id") == "indexed":
                     indexed_list.append(True)
                     typ = typ.args[0]
-                    if (
-                        isinstance(typ, vy_ast.Subscript)
-                        and getattr(typ.value, "id", None) == "bytes"
-                        and typ.slice.value.n > 32
-                    ):  # noqa: E501
-                        raise EventDeclarationException("Indexed arguments are limited to 32 bytes")
                 else:
                     indexed_list.append(False)
                 check_valid_varname(
diff --git a/vyper/types/types.py b/vyper/types/types.py
--- a/vyper/types/types.py
+++ b/vyper/types/types.py
@@ -154,10 +154,7 @@ def canonicalize_type(t, is_indexed=False):
     if isinstance(t, ByteArrayLike):
         # Check to see if maxlen is small enough for events
         byte_type = "string" if isinstance(t, StringType) else "bytes"
-        if is_indexed:
-            return f"{byte_type}{t.maxlen}"
-        else:
-            return f"{byte_type}"
+        return byte_type
 
     if isinstance(t, ListType):
         if not isinstance(t.subtype, (ListType, BaseType)):


Test patch:
diff --git a/tests/parser/features/test_logging.py b/tests/parser/features/test_logging.py
--- a/tests/parser/features/test_logging.py
+++ b/tests/parser/features/test_logging.py
@@ -1,5 +1,7 @@
 from decimal import Decimal
 
+import eth_abi
+
 from vyper.exceptions import (
     ArgumentException,
     EventDeclarationException,
@@ -41,32 +43,32 @@ def foo():
 
 def test_event_logging_with_topics(w3, tester, keccak, get_logs, get_contract_with_gas_estimation):
     loggy_code = """
+
+a: bytes[3]
+
 event MyLog:
     arg1: indexed(bytes[3])
 
 @public
 def foo():
-    log MyLog(b'bar')
+    self.a = b"bar"
+    log MyLog(self.a)
     """
 
     c = get_contract_with_gas_estimation(loggy_code)
     tx_hash = c.foo(transact={})
     receipt = tester.get_transaction_receipt(tx_hash.hex())
-    event_id = keccak(bytes("MyLog(bytes3)", "utf-8"))
+    event_id = keccak(bytes("MyLog(bytes)", "utf-8"))
 
     # Event id is always the first topic
     assert receipt["logs"][0]["topics"][0] == event_id.hex()
     # Event abi is created correctly
     assert c._classic_contract.abi[0] == {
         "name": "MyLog",
-        "inputs": [{"type": "bytes3", "name": "arg1", "indexed": True}],
+        "inputs": [{"type": "bytes", "name": "arg1", "indexed": True}],
         "anonymous": False,
         "type": "event",
     }
-    # Event is decoded correctly
-    logs = get_logs(tx_hash, c, "MyLog")
-    assert logs[0].event == "MyLog"
-    assert logs[0].args.arg1 == b"bar"
 
 
 def test_event_logging_with_multiple_topics(
@@ -74,28 +76,28 @@ def test_event_logging_with_multiple_topics(
 ):
     loggy_code = """
 event MyLog:
-    arg1: indexed(bytes[3])
-    arg2: indexed(bytes[4])
+    arg1: indexed(int128)
+    arg2: indexed(bool)
     arg3: indexed(address)
 
 @public
 def foo():
-    log MyLog(b'bar', b'home', self)
+    log MyLog(-2, True, self)
     """
 
     c = get_contract_with_gas_estimation(loggy_code)
     tx_hash = c.foo(transact={})
     receipt = tester.get_transaction_receipt(tx_hash.hex())
 
-    event_id = keccak(bytes("MyLog(bytes3,bytes4,address)", "utf-8"))
+    event_id = keccak(bytes("MyLog(int128,bool,address)", "utf-8"))
     # Event id is always the first topic
     assert receipt["logs"][0]["topics"][0] == event_id.hex()
     # Event abi is created correctly
     assert c._classic_contract.abi[0] == {
         "name": "MyLog",
         "inputs": [
-            {"type": "bytes3", "name": "arg1", "indexed": True},
-            {"type": "bytes4", "name": "arg2", "indexed": True},
+            {"type": "int128", "name": "arg1", "indexed": True},
+            {"type": "bool", "name": "arg2", "indexed": True},
             {"type": "address", "name": "arg3", "indexed": True},
         ],
         "anonymous": False,
@@ -104,8 +106,8 @@ def foo():
     # Event is decoded correctly
     logs = get_logs(tx_hash, c, "MyLog")
     assert logs[0].event == "MyLog"
-    assert logs[0].args.arg1 == b"bar"
-    assert logs[0].args.arg2 == b"home"
+    assert logs[0].args.arg1 == -2
+    assert logs[0].args.arg2 is True
     assert logs[0].args.arg3 == c._classic_contract.address
 
 
@@ -114,30 +116,28 @@ def test_event_logging_with_multiple_topics_var_and_store(
 ):
     code = """
 event MyLog:
-    arg1: indexed(bytes[3])
-    arg2: indexed(bytes[4])
+    arg1: indexed(int128)
+    arg2: indexed(bool)
     arg3: indexed(address)
-    arg4: bytes[10]
 
-b: bytes[10]
+b: address
 
 @public
-def foo(arg1: bytes[3]):
-    a: bytes[4] = b'home'
-    self.b = b'hellothere'
-    log MyLog(arg1, a,  self, self.b)
+def foo(arg1: int128):
+    a: bool = True
+    self.b = self
+    log MyLog(arg1, a, self.b)
     """
 
     c = get_contract_with_gas_estimation(code)
-    tx_hash = c.foo(b"hel", transact={})
+    tx_hash = c.foo(31337, transact={})
 
     # Event is decoded correctly
     log = get_logs(tx_hash, c, "MyLog")[0]
 
-    assert log.args.arg1 == b"hel"
-    assert log.args.arg2 == b"home"
+    assert log.args.arg1 == 31337
+    assert log.args.arg2 is True
     assert log.args.arg3 == c.address
-    assert log.args.arg4 == b"hellothere"
 
 
 def test_logging_the_same_event_multiple_times_with_topics(
@@ -284,7 +284,7 @@ def test_logging_with_input_bytes_1(
 ):
     loggy_code = """
 event MyLog:
-    arg1: indexed(bytes[4])
+    arg1: bytes[4]
     arg2: indexed(bytes[29])
     arg3: bytes[31]
 
@@ -297,15 +297,15 @@ def foo(arg1: bytes[29], arg2: bytes[31]):
     tx_hash = c.foo(b"bar", b"foo", transact={})
     receipt = tester.get_transaction_receipt(tx_hash.hex())
 
-    event_id = keccak(bytes("MyLog(bytes4,bytes29,bytes)", "utf-8"))
+    event_id = keccak(bytes("MyLog(bytes,bytes,bytes)", "utf-8"))
     # Event id is always the first topic
     assert receipt["logs"][0]["topics"][0] == event_id.hex()
     # Event abi is created correctly
     assert c._classic_contract.abi[0] == {
         "name": "MyLog",
         "inputs": [
-            {"type": "bytes4", "name": "arg1", "indexed": True},
-            {"type": "bytes29", "name": "arg2", "indexed": True},
+            {"type": "bytes", "name": "arg1", "indexed": False},
+            {"type": "bytes", "name": "arg2", "indexed": True},
             {"type": "bytes", "name": "arg3", "indexed": False},
         ],
         "anonymous": False,
@@ -314,8 +314,8 @@ def foo(arg1: bytes[29], arg2: bytes[31]):
     # Event is decoded correctly
     logs = get_logs(tx_hash, c, "MyLog")
 
-    assert logs[0].args.arg1 == b"bar\x00"
-    assert logs[0].args.arg2 == bytes_helper("bar", 29)
+    assert logs[0].args.arg1 == b"bar"
+    assert logs[0].args.arg2 == keccak(b"bar")
     assert logs[0].args.arg3 == b"foo"
 
 
@@ -597,7 +597,7 @@ def foo(arg1: bytes[4]):
     assert_tx_failed(lambda: get_contract_with_gas_estimation(loggy_code), TypeMismatch)
 
 
-def test_fails_when_topic_is_over_32_bytes(assert_tx_failed, get_contract_with_gas_estimation):
+def test_topic_over_32_bytes(get_contract_with_gas_estimation):
     loggy_code = """
 event MyLog:
     arg1: indexed(bytes[100])
@@ -606,10 +606,7 @@ def test_fails_when_topic_is_over_32_bytes(assert_tx_failed, get_contract_with_g
 def foo():
     pass
     """
-
-    assert_tx_failed(
-        lambda: get_contract_with_gas_estimation(loggy_code), EventDeclarationException
-    )
+    get_contract_with_gas_estimation(loggy_code)
 
 
 def test_logging_fails_with_over_three_topics(assert_tx_failed, get_contract_with_gas_estimation):
@@ -943,16 +940,6 @@ def set_list():
     ]
 
 
-def test_logging_fails_when_declartation_is_too_big(
-    assert_tx_failed, get_contract_with_gas_estimation
-):
-    code = """
-event Bar:
-    _value: indexed(bytes[33])
-"""
-    assert_tx_failed(lambda: get_contract_with_gas_estimation(code), EventDeclarationException)
-
-
 def test_logging_fails_when_input_is_too_big(assert_tx_failed, get_contract_with_gas_estimation):
     code = """
 event Bar:
@@ -1047,3 +1034,174 @@ def set_list():
     assert log.args["arg3"] == b"test"
     assert log.args["arg4"] == [7, 8, 9]
     assert log.args["arg5"] == [1024, 2048]
+
+
+def test_hashed_indexed_topics_calldata(tester, keccak, get_contract):
+    loggy_code = """
+event MyLog:
+    arg1: indexed(bytes[36])
+    arg2: indexed(int128[2])
+    arg3: indexed(string[7])
+
+@public
+def foo(a: bytes[36], b: int128[2], c: string[7]):
+    log MyLog(a, b, c)
+    """
+
+    c = get_contract(loggy_code)
+    tx_hash = c.foo(b"bar", [1, 2], "weird", transact={})
+    receipt = tester.get_transaction_receipt(tx_hash.hex())
+
+    # Event id is always the first topic
+    event_id = keccak(b"MyLog(bytes,int128[2],string)")
+    assert receipt["logs"][0]["topics"][0] == event_id.hex()
+
+    topic1 = f"0x{keccak256(b'bar').hex()}"
+    assert receipt["logs"][0]["topics"][1] == topic1
+
+    topic2 = f"0x{keccak256(eth_abi.encode_single('int128[2]', [1,2])).hex()}"
+    assert receipt["logs"][0]["topics"][2] == topic2
+
+    topic3 = f"0x{keccak256(b'weird').hex()}"
+    assert receipt["logs"][0]["topics"][3] == topic3
+
+    # Event abi is created correctly
+    assert c._classic_contract.abi[0] == {
+        "name": "MyLog",
+        "inputs": [
+            {"type": "bytes", "name": "arg1", "indexed": True},
+            {"type": "int128[2]", "name": "arg2", "indexed": True},
+            {"type": "string", "name": "arg3", "indexed": True},
+        ],
+        "anonymous": False,
+        "type": "event",
+    }
+
+
+def test_hashed_indexed_topics_memory(tester, keccak, get_contract):
+    loggy_code = """
+event MyLog:
+    arg1: indexed(bytes[10])
+    arg2: indexed(int128[3])
+    arg3: indexed(string[44])
+
+@public
+def foo():
+    a: bytes[10] = b"potato"
+    b: int128[3] = [-777, 42, 8008135]
+    c: string[44] = "why hello, neighbor! how are you today?"
+    log MyLog(a, b, c)
+    """
+
+    c = get_contract(loggy_code)
+    tx_hash = c.foo(transact={})
+    receipt = tester.get_transaction_receipt(tx_hash.hex())
+
+    # Event id is always the first topic
+    event_id = keccak(b"MyLog(bytes,int128[3],string)")
+    assert receipt["logs"][0]["topics"][0] == event_id.hex()
+
+    topic1 = f"0x{keccak256(b'potato').hex()}"
+    assert receipt["logs"][0]["topics"][1] == topic1
+
+    topic2 = f"0x{keccak256(eth_abi.encode_single('int128[3]', [-777,42,8008135])).hex()}"
+    assert receipt["logs"][0]["topics"][2] == topic2
+
+    topic3 = f"0x{keccak256(b'why hello, neighbor! how are you today?').hex()}"
+    assert receipt["logs"][0]["topics"][3] == topic3
+
+    # Event abi is created correctly
+    assert c._classic_contract.abi[0] == {
+        "name": "MyLog",
+        "inputs": [
+            {"type": "bytes", "name": "arg1", "indexed": True},
+            {"type": "int128[3]", "name": "arg2", "indexed": True},
+            {"type": "string", "name": "arg3", "indexed": True},
+        ],
+        "anonymous": False,
+        "type": "event",
+    }
+
+
+def test_hashed_indexed_topics_storage(tester, keccak, get_contract):
+    loggy_code = """
+event MyLog:
+    arg1: indexed(bytes[32])
+    arg2: indexed(int128[2])
+    arg3: indexed(string[6])
+
+a: bytes[32]
+b: int128[2]
+c: string[6]
+
+
+@public
+def setter(_a: bytes[32], _b: int128[2], _c: string[6]):
+    self.a = _a
+    self.b = _b
+    self.c = _c
+
+@public
+def foo():
+    log MyLog(self.a, self.b, self.c)
+    """
+
+    c = get_contract(loggy_code)
+    c.setter(b"zonk", [838, -2109], "yessir", transact={})
+    tx_hash = c.foo(transact={})
+    receipt = tester.get_transaction_receipt(tx_hash.hex())
+
+    # Event id is always the first topic
+    event_id = keccak(b"MyLog(bytes,int128[2],string)")
+    assert receipt["logs"][0]["topics"][0] == event_id.hex()
+
+    topic1 = f"0x{keccak256(b'zonk').hex()}"
+    assert receipt["logs"][0]["topics"][1] == topic1
+
+    topic2 = f"0x{keccak256(eth_abi.encode_single('int128[2]', [838,-2109])).hex()}"
+    assert receipt["logs"][0]["topics"][2] == topic2
+
+    topic3 = f"0x{keccak256(b'yessir').hex()}"
+    assert receipt["logs"][0]["topics"][3] == topic3
+
+    # Event abi is created correctly
+    assert c._classic_contract.abi[0] == {
+        "name": "MyLog",
+        "inputs": [
+            {"type": "bytes", "name": "arg1", "indexed": True},
+            {"type": "int128[2]", "name": "arg2", "indexed": True},
+            {"type": "string", "name": "arg3", "indexed": True},
+        ],
+        "anonymous": False,
+        "type": "event",
+    }
+
+
+def test_hashed_indexed_topics_storxxage(tester, keccak, get_contract):
+    loggy_code = """
+event MyLog:
+    arg1: indexed(bytes[64])
+    arg2: indexed(int128[3])
+    arg3: indexed(string[21])
+
+@public
+def foo():
+    log MyLog(b"wow", [6,66,666], "madness!")
+    """
+
+    c = get_contract(loggy_code)
+    tx_hash = c.foo(transact={})
+    receipt = tester.get_transaction_receipt(tx_hash.hex())
+
+    # Event id is always the first topic
+    event_id = keccak(b"MyLog(bytes,int128[3],string)")
+    assert receipt["logs"][0]["topics"][0] == event_id.hex()
+
+    topic1 = f"0x{keccak256(b'wow').hex()}"
+    assert receipt["logs"][0]["topics"][1] == topic1
+
+    topic2 = f"0x{keccak256(eth_abi.encode_single('int128[3]', [6, 66, 666])).hex()}"
+    assert receipt["logs"][0]["topics"][2] == topic2
+
+    topic3 = f"0x{keccak256(b'madness!').hex()}"
+    assert receipt["logs"][0]["topics"][3] == topic3