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

Multicall context manager #1011

Closed
iamdefinitelyahuman opened this issue Mar 21, 2021 · 6 comments
Closed

Multicall context manager #1011

iamdefinitelyahuman opened this issue Mar 21, 2021 · 6 comments
Labels
enhancement New feature or request

Comments

@iamdefinitelyahuman
Copy link
Member

Overview

Brownie should offer first-class support for multicall via a context manager:

with multicall:
    a = foo.bar()
    b = foo.barfoo()
    c = bar.foobar(a)
    d = bar.otherbar()

Specification

While the context manager is open, a middleware intercepts all eth_call requests and returns objects which represent the result of the call once it completes. When the context manager closes, or there is an attempt to use one of the pending call objects, a single multicall is made to get the actual values.

In the above example a and b would end up joined as a single multicall that executes when a is needed for c. Then c and d would happen as a multicall when the context manager exits.

We can bundle the MultiCall2 implementation as used by yearn, which uses try/catch to prevent calls from failing. Attempting to interact with object for the specific result that failed in the multicall should raise a ValueError. (Or possibly the object simply returns None? Maybe this behavior is determined via a kwarg in the context manager?)

It is important to consider thread safety during the implementation. The context manager must introspect to be aware of which thread it is running on, and not also batch eth_call operations coming from unrelated threads.

To ensure consistent behaviour across network, we can use Geth's state override feature to simulate the existence of a multicall contract for the purposes of the call. This is badass in so many ways I'm not even sure where to begin 😈 For dev networks where this isn't possible, deploy multicall silently the first time it's required.

@iamdefinitelyahuman iamdefinitelyahuman added the enhancement New feature or request label Mar 21, 2021
@iamdefinitelyahuman iamdefinitelyahuman pinned this issue Mar 21, 2021
@banteg
Copy link
Collaborator

banteg commented Mar 21, 2021

This is an amazing idea! A few caveats:

@omarish
Copy link
Contributor

omarish commented Mar 23, 2021

I've been thinking about this over the past couple days and wanted to float an idea. Would we definitely want it to automatically resolve calls (i.e know to evaluate a and b before getting c)?

One possible design alternative, just to be exhaustive:

# (A) This works
with multicall:
    a = foo.bar()
    b = foo.barfoo()

# (B) This raises an error
with multicall:
    a = foo.bar()
    b = foo.barfoo()
    c = bar.foobar(a)

# (C) This is how we'd want to write (B)
with multicall:
    a = foo.bar()
    b = foo.barfoo()
with multicall:
    c = bar.foobar(a)
    d = bar.otherbar()

(C) Has the advantage of being more explicit, but I agree, is sort of annoying to have to think about re-opening the context manager.

@iamdefinitelyahuman
Copy link
Member Author

I agree that the automatic execution mid-context manager is a bit magical, but when I think about how I personally would feel writing this code, it'd drive me mad having to endlessly close and reopen it.

A middleground here might be a kwarg to enable or disable this behavior?

@omarish
Copy link
Contributor

omarish commented Mar 25, 2021

For sure.

I think a clean implementation would have at least two pieces: a private function to commit a single transaction block (like scenario A above), and then something higher level (probably public) that calls that function every time (1) the multicall needs to be executed, and (2) when the context manager __exit__s.

Perhaps something like this:

def _exec_multicall(*args, **kwargs):
   # used to run a series of calls and will error out 

@contextmanager
def multicall(*args, **kwargs):
   # this is the actual context manager that's exposed, it has some logic and uses `_exec_multicall` for batches.

We could start with the simplest thing, which would be the context manager as initially proposed, then we could see how it feels and go from there. How does that sound?

@skellet0r
Copy link
Collaborator

Battle of the Mulitcalls !!!

@iamdefinitelyahuman
Copy link
Member Author

Implemented in #1125

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

No branches or pull requests

4 participants