Repository: vyperlang/vyper

Issue:
Events that take structs have malformed ABI JSON
### Version Information

* vyper Version (output of `vyper --version`): 0.3.0+commit.8a23feb
* OS: Release Linux Mint 20.2 Uma 64-bit
* Python Version (output of `python --version`): Python 3.8.10

### What's your issue about?

If you declare an event that takes a struct, the ABI JSON for it will be malformed.  Note that unlike the previous encoding bug, this bug is not specific to the case where the input is just a single struct alone; all structs used as event arguments appear to exhibit this bug.  Because of this, decoding libraries will fail to decode such events.

As an example, suppose I write a contract with the following declarations:

```vyper
struct Pair:
    x: uint256
    y: uint256

event Stuff:
    pair: Pair
    z: uint256
```

The ABI JSON I get for this event is:

```json
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "name": "pair",
          "type": "(uint256,uint256)"
        },
        {
          "indexed": false,
          "name": "z",
          "type": "uint256"
        }
      ],
      "name": "Stuff",
      "type": "event"
    },
```

As you can see, this ABI item is malformed.  The first input, rather than having type `"tuple"` and an appropriate `components` field, instead has the malformed type `"(uint256,uint256)"`.  Decoding libraries are unable to handle this.

This bug does not apply to function inputs, function outputs, or constructor inputs; those have proper ABI JSON.

### How can it be fixed?

Given that structs are handled properly in the ABI JSON for functions and constructors, both on input and on output, whatever code you use for handling those can presumably be applied to event inputs as well.


Discussion:
Thanks for the bug report!

Resolving patch:
diff --git a/vyper/semantics/types/function.py b/vyper/semantics/types/function.py
--- a/vyper/semantics/types/function.py
+++ b/vyper/semantics/types/function.py
@@ -17,10 +17,10 @@
 from vyper.semantics.namespace import get_namespace
 from vyper.semantics.types.bases import BaseTypeDefinition, DataLocation, StorageSlot
 from vyper.semantics.types.indexable.sequence import TupleDefinition
-from vyper.semantics.types.user.struct import StructDefinition
 from vyper.semantics.types.utils import (
     StringEnum,
     check_constant,
+    generate_abi_type,
     get_type_from_abi,
     get_type_from_annotation,
 )
@@ -482,15 +482,15 @@ def to_abi_dict(self) -> List[Dict]:
             abi_dict["type"] = "function"
             abi_dict["name"] = self.name
 
-        abi_dict["inputs"] = [_generate_abi_type(v, k) for k, v in self.arguments.items()]
+        abi_dict["inputs"] = [generate_abi_type(v, k) for k, v in self.arguments.items()]
 
         typ = self.return_type
         if typ is None:
             abi_dict["outputs"] = []
         elif isinstance(typ, TupleDefinition) and len(typ.value_type) > 1:  # type: ignore
-            abi_dict["outputs"] = [_generate_abi_type(i) for i in typ.value_type]  # type: ignore
+            abi_dict["outputs"] = [generate_abi_type(i) for i in typ.value_type]  # type: ignore
         else:
-            abi_dict["outputs"] = [_generate_abi_type(typ)]
+            abi_dict["outputs"] = [generate_abi_type(typ)]
 
         if self.has_default_args:
             # for functions with default args, return a dict for each possible arg count
@@ -503,21 +503,6 @@ def to_abi_dict(self) -> List[Dict]:
             return [abi_dict]
 
 
-def _generate_abi_type(type_definition, name=""):
-    if isinstance(type_definition, StructDefinition):
-        return {
-            "name": name,
-            "type": "tuple",
-            "components": [_generate_abi_type(v, k) for k, v in type_definition.members.items()],
-        }
-    if isinstance(type_definition, TupleDefinition):
-        return {
-            "type": "tuple",
-            "components": [_generate_abi_type(i) for i in type_definition.value_type],
-        }
-    return {"name": name, "type": type_definition.canonical_abi_type}
-
-
 def _generate_method_id(name: str, canonical_abi_types: List[str]) -> Dict[str, int]:
     function_sig = f"{name}({','.join(canonical_abi_types)})"
     selector = keccak256(function_sig.encode())[:4].hex()
diff --git a/vyper/semantics/types/user/event.py b/vyper/semantics/types/user/event.py
--- a/vyper/semantics/types/user/event.py
+++ b/vyper/semantics/types/user/event.py
@@ -6,7 +6,11 @@
 from vyper.exceptions import EventDeclarationException, NamespaceCollision, StructureException
 from vyper.semantics.namespace import validate_identifier
 from vyper.semantics.types.bases import DataLocation
-from vyper.semantics.types.utils import get_type_from_abi, get_type_from_annotation
+from vyper.semantics.types.utils import (
+    generate_abi_type,
+    get_type_from_abi,
+    get_type_from_annotation,
+)
 from vyper.semantics.validation.utils import validate_expected_type
 from vyper.utils import keccak256
 
@@ -119,7 +123,7 @@ def to_abi_dict(self) -> List[Dict]:
             {
                 "name": self.name,
                 "inputs": [
-                    {"name": name, "type": typ.canonical_abi_type, "indexed": idx}
+                    dict(**generate_abi_type(typ, name), **{"indexed": idx})
                     for (name, typ), idx in zip(self.arguments.items(), self.indexed)
                 ],
                 "anonymous": False,
diff --git a/vyper/semantics/types/utils.py b/vyper/semantics/types/utils.py
--- a/vyper/semantics/types/utils.py
+++ b/vyper/semantics/types/utils.py
@@ -12,7 +12,7 @@
 )
 from vyper.semantics.namespace import get_namespace
 from vyper.semantics.types.bases import BaseTypeDefinition, DataLocation
-from vyper.semantics.types.indexable.sequence import ArrayDefinition
+from vyper.semantics.types.indexable.sequence import ArrayDefinition, TupleDefinition
 from vyper.semantics.validation.utils import get_exact_type_from_node, get_index_value
 
 
@@ -200,3 +200,21 @@ def check_constant(node: vy_ast.VyperNode) -> bool:
 
     value_type = get_exact_type_from_node(node)
     return getattr(value_type, "is_constant", False)
+
+
+def generate_abi_type(type_definition, name=""):
+    # TODO oof fixme
+    from vyper.semantics.types.user.struct import StructDefinition
+
+    if isinstance(type_definition, StructDefinition):
+        return {
+            "name": name,
+            "type": "tuple",
+            "components": [generate_abi_type(v, k) for k, v in type_definition.members.items()],
+        }
+    if isinstance(type_definition, TupleDefinition):
+        return {
+            "type": "tuple",
+            "components": [generate_abi_type(i) for i in type_definition.value_type],
+        }
+    return {"name": name, "type": type_definition.canonical_abi_type}


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
@@ -474,17 +474,25 @@ def test_event_logging_with_multiple_logs_topics_and_data(
     w3, tester, keccak, get_logs, get_contract_with_gas_estimation
 ):
     loggy_code = """
+struct SmallStruct:
+    t: String[5]
+    w: decimal
+struct MyStruct:
+    x: uint256
+    y: Bytes[3]
+    z: SmallStruct
+
 event MyLog:
     arg1: indexed(int128)
     arg2: Bytes[3]
 event YourLog:
     arg1: indexed(address)
-    arg2: Bytes[5]
+    arg2: MyStruct
 
 @external
 def foo():
     log MyLog(1, b'bar')
-    log YourLog(self, b'house')
+    log YourLog(self, MyStruct({x: 1, y: b'abc', z: SmallStruct({t: 'house', w: 13.5})}))
     """
 
     c = get_contract_with_gas_estimation(loggy_code)
@@ -494,7 +502,7 @@ def foo():
     logs1 = receipt["logs"][0]
     logs2 = receipt["logs"][1]
     event_id1 = keccak(bytes("MyLog(int128,bytes)", "utf-8"))
-    event_id2 = keccak(bytes("YourLog(address,bytes)", "utf-8"))
+    event_id2 = keccak(bytes("YourLog(address,(uint256,bytes,(string,fixed168x10)))", "utf-8"))
 
     # Event id is always the first topic
     assert logs1["topics"][0] == event_id1.hex()
@@ -512,8 +520,24 @@ def foo():
     assert c._classic_contract.abi[1] == {
         "name": "YourLog",
         "inputs": [
-            {"type": "address", "name": "arg1", "indexed": True},
-            {"type": "bytes", "name": "arg2", "indexed": False},
+            {"name": "arg1", "type": "address", "indexed": True},
+            {
+                "name": "arg2",
+                "type": "tuple",
+                "components": [
+                    {"name": "x", "type": "uint256"},
+                    {"name": "y", "type": "bytes"},
+                    {
+                        "name": "z",
+                        "type": "tuple",
+                        "components": [
+                            {"name": "t", "type": "string"},
+                            {"name": "w", "type": "fixed168x10"},
+                        ],
+                    },
+                ],
+                "indexed": False,
+            },
         ],
         "anonymous": False,
         "type": "event",
@@ -527,7 +551,7 @@ def foo():
     logs = get_logs(tx_hash, c, "YourLog")
     args = logs[0].args
     assert args.arg1 == c.address
-    assert args.arg2 == b"house"
+    assert args.arg2 == (1, b"abc", ("house", Decimal("13.5")))
 
 
 def test_fails_when_input_is_the_wrong_type(assert_tx_failed, get_contract_with_gas_estimation):