Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated event topic is incorrect for events with dynamic size tuple inputs #15

Closed
skellet0r opened this issue Feb 12, 2021 · 0 comments

Comments

@skellet0r
Copy link
Contributor

skellet0r commented Feb 12, 2021

Environment information

  • eth-event Version: 1.2.0
  • Python Version: 3.9.1
  • OS: linux

What was wrong?

The generated log topic for events with dynamic size tuple inputs is incorrect. I noticed this issue while testing a contract using the eth-brownie package. Below is a demonstration of the issue.

contracts/Foobar.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;

/// @title A placeholder demo contract
/// @dev Used for demonstrating bug in eth-event
contract Foobar {
    struct Foo {
        address x;
        uint8 y;
        bytes4[] z;
    }

    /// @notice This event gets emitted everytime function main is called
    /// @dev An example of an event with a dynamic tuple/struct for an argument
    /// @dev Topic = keccak("Bar((address,uint8,bytes4[])[],address,string)")
    /// 0x0ec34a86243bfab8deb0e5685fb8ecacabf10ede0975ba4c8c61bbfdaad70a3c
    /// @dev Incorrectly gets encoded as 0x820a1d2c7df65cbffdfe074485ce39f5e4722d7c99abef3883bcc2c68a32b612
    /// @param _foo a Foo type argument
    /// @param _x a address type argument
    /// @param _y a string type argument
    event Bar(Foo[] _foo, address _x, string _y);

    /// @notice The primary entrypoint for this demo
    function main() external {
        // a dynamic bytes4 array of size 1
        bytes4[] memory _z = new bytes4[](1);
        _z[0] = bytes4(0x00c0ffee);

        // a dynamic Foo type array of size 1
        Foo[] memory _foo = new Foo[](1);
        _foo[0] = Foo({x: address(this), y: 0, z: _z});

        address _x = address(this);
        string memory _y = "Hello World!";

        // emit the event
        emit Bar(_foo, _x, _y);
    }
}

build/contracts/Foobar.json

Note: The following is only the ABI of the Foobar contract, and not the full build output.

[
  {
    "anonymous": false,
    "inputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "x",
            "type": "address"
          },
          {
            "internalType": "uint8",
            "name": "y",
            "type": "uint8"
          },
          {
            "internalType": "bytes4[]",
            "name": "z",
            "type": "bytes4[]"
          }
        ],
        "indexed": false,
        "internalType": "struct Foobar.Foo[]",
        "name": "_foo",
        "type": "tuple[]"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "_x",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "_y",
        "type": "string"
      }
    ],
    "name": "Bar",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "main",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

tests/log_topic_tests.py

def test_emitted_event_is_correctly_hashed(accounts, Foobar):
    instance = Foobar.deploy({"from": accounts[0]})
    tx = instance.main({"from": accounts[0]})
    expected_topic_hash = "0x0ec34a86243bfab8deb0e5685fb8ecacabf10ede0975ba4c8c61bbfdaad70a3c"

    assert tx.events[0]["topic1"] == expected_topic_hash

def test_contract_topics(Foobar):
    expected_topic_hash = "0x0ec34a86243bfab8deb0e5685fb8ecacabf10ede0975ba4c8c61bbfdaad70a3c"

    assert Foobar.topics["Bar"] == expected_topic_hash

The first test (test_emitted_event_is_correctly_hashed) passes, verifying that the correct log topic for the Bar event is actually 0x0ec34a86243bfab8deb0e5685fb8ecacabf10ede0975ba4c8c61bbfdaad70a3c. The second test however fails, as the generated log topic via eth-event.get_log_topic is 0x820a1d2c7df65cbffdfe074485ce39f5e4722d7c99abef3883bcc2c68a32b612.

This issue is due to a minor bug in the eth_event/main.py::_params function.

eth_event/main.py::_params

def _params(abi_params: List) -> List:
    types = []
    for i in abi_params:
        if i["type"] != "tuple":
            types.append(i["type"])
            continue
        types.append(f"({','.join(x for x in _params(i['components']))})")

    return types

When given the Bar event's inputs from build/contracts/Foobar.json, the _params function incorrectly enters the if i["type"] != "tuple" branch and simply appends tuple[] to the types list.

How can it be fixed?

A simple fix would change the branching logic of the _params function, to also evaluate whether the input type is a tuple array (whether it be fixed size or dynamic would also matter). Similar to the already written code, some recursion would be necessary to get the types of the components of the tuple array and care needs to be taken to include the array notation at the end of the returned tuple type (i.e. (address,uint8,bytes)[])

Additional Contextual Material

Some stuff I found while researching why this error was occurring for me:

Contract ABI Specification - Types

Most interesting to note is:

Types can be combined to a tuple by enclosing them inside parentheses, separated by commas:
(T1,T2,...,Tn): tuple consisting of the types T1, …, Tn, n >= 0
It is possible to form tuples of tuples, arrays of tuples and so on. It is also possible to form zero-tuples (where n == 0).

Contract ABI Specification - Handling Tuple Types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants