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

Handle Multicall Failures #641

Merged
merged 23 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions fastlane_bot/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@
- ``ConfigProvider`` (``provider``; provider for network access)
- ``Config`` (``config``; main configuration class, integrates the above)

Submodules provide the following

- Constants (``constants`` and ``selectors``; various constants)
- ``MultiCaller`` and related (``multicaller``; TODO: what is this?)
- ``NetworkBase`` and ``EthereumNetwork`` (``connect``; network/chain connection code TODO: details)
- ``Cloaker`` (``cloaker``; deprecated)



---
(c) Copyright Bprotocol foundation 2023-24.
All rights reserved.
Expand Down
169 changes: 25 additions & 144 deletions fastlane_bot/config/multicaller.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,18 @@
"""
This is the multicaller module. TODO: BETTER NAME

TODO-MIKE: What exactly does this do and is it a bona fide config module?
MultiCaller class

---
(c) Copyright Bprotocol foundation 2023-24.
All rights reserved.
Licensed under MIT.
"""
from functools import partial
from typing import List, Callable, ContextManager, Any, Dict
from typing import Callable, Any, List, Dict

import web3
from eth_abi import decode
from web3 import Web3

from fastlane_bot.data.abi import MULTICALL_ABI


def cast(typ, val):
"""Cast a value to a type.

This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val


def collapse_if_tuple(abi: Dict[str, Any]) -> str:
"""
Converts a tuple from a dict to a parenthesized list of its types.
Expand All @@ -46,141 +30,38 @@ def collapse_if_tuple(abi: Dict[str, Any]) -> str:
... )
'(address,uint256,bytes)'
"""
typ = abi["type"]
if not isinstance(typ, str):
raise TypeError(
"The 'type' must be a string, but got %r of type %s" % (typ, type(typ))
)
elif not typ.startswith("tuple"):
return typ

delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
# Whatever comes after "tuple" is the array dims. The ABI spec states that
# this will have the form "", "[]", or "[k]".
array_dim = typ[5:]
collapsed = "({}){}".format(delimited, array_dim)

return collapsed


def get_output_types_from_abi(abi: List[Dict[str, Any]], function_name: str) -> List[str]:
"""
Get the output types from an ABI.

Parameters
----------
abi : List[Dict[str, Any]]
The ABI
function_name : str
The function name

Returns
-------
List[str]
The output types

"""
for item in abi:
if item['type'] == 'function' and item['name'] == function_name:
return [collapse_if_tuple(cast(Dict[str, Any], item)) for item in item['outputs']]
raise ValueError(f"No function named {function_name} found in ABI.")
if abi["type"].startswith("tuple"):
delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
return "({}){}".format(delimited, abi["type"][len("tuple"):])
return abi["type"]


class ContractMethodWrapper:
"""
Wraps a contract method to be used with multicall.
"""
__DATE__ = "2022-09-26"
__VERSION__ = "0.0.2"

def __init__(self, original_method, multicaller):
self.original_method = original_method
self.multicaller = multicaller

def __call__(self, *args, **kwargs):
contract_call = self.original_method(*args, **kwargs)
self.multicaller.add_call(contract_call)
return contract_call


class MultiCaller(ContextManager):
class MultiCaller:
"""
Context manager for multicalls.
"""
__DATE__ = "2022-09-26"
__VERSION__ = "0.0.2"

def __init__(self, web3: Any, multicall_contract_address: str):
self.multicall_contract = web3.eth.contract(abi=MULTICALL_ABI, address=multicall_contract_address)
self.contract_calls: List[Callable] = []
self.output_types_list: List[List[str]] = []

def __init__(self, contract: web3.contract.Contract,
web3: Web3,
block_identifier: Any = 'latest', multicall_address = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"):
self._contract_calls: List[Callable] = []
self.contract = contract
self.block_identifier = block_identifier
self.web3 = web3
self.MULTICALL_CONTRACT_ADDRESS = self.web3.to_checksum_address(multicall_address)

def __enter__(self) -> 'MultiCaller':
return self

def __exit__(self, exc_type, exc_val, exc_tb):
pass

def add_call(self, fn: Callable, *args, **kwargs) -> None:
self._contract_calls.append(partial(fn, *args, **kwargs))

def multicall(self) -> List[Any]:
calls_for_aggregate = []
output_types_list = []
_calls_for_aggregate = {}
_output_types_list = {}
for fn in self._contract_calls:
fn_name = str(fn).split('functools.partial(<Function ')[1].split('>')[0]
output_types = get_output_types_from_abi(self.contract.abi, fn_name)
if fn_name in _calls_for_aggregate:
_calls_for_aggregate[fn_name].append({
'target': self.contract.address,
'callData': fn()._encode_transaction_data()
})
_output_types_list[fn_name].append(output_types)
else:
_calls_for_aggregate[fn_name] = [{
'target': self.contract.address,
'callData': fn()._encode_transaction_data()
}]
_output_types_list[fn_name] = [output_types]

for fn_list in _calls_for_aggregate.keys():
calls_for_aggregate += (_calls_for_aggregate[fn_list])
output_types_list += (_output_types_list[fn_list])

encoded_data = self.web3.eth.contract(
abi=MULTICALL_ABI,
address=self.MULTICALL_CONTRACT_ADDRESS
).functions.aggregate(calls_for_aggregate).call(block_identifier=self.block_identifier)

if not isinstance(encoded_data, list):
raise TypeError(f"Expected encoded_data to be a list, got {type(encoded_data)} instead.")

encoded_data = encoded_data[1]
decoded_data_list = []
for output_types, encoded_output in zip(output_types_list, encoded_data):
decoded_data = decode(output_types, encoded_output)
decoded_data_list.append(decoded_data)

return_data = [i[0] for i in decoded_data_list if len(i) == 1]
return_data += [i[1] for i in decoded_data_list if len(i) > 1]
def add_call(self, call: Callable):
self.contract_calls.append({'target': call.address, 'callData': call._encode_transaction_data()})
self.output_types_list.append([collapse_if_tuple(item) for item in call.abi['outputs']])

# Handling for Bancor POL - combine results into a Tuple
if "tokenPrice" in _calls_for_aggregate and "amountAvailableForTrading" in _calls_for_aggregate:
new_return = []
returned_items = int(len(return_data))
total_pools = int(returned_items / 2)
assert returned_items % 2 == 0, f"[multicaller.py multicall] non-even number of returned calls for Bancor POL {returned_items}"
total_pools = int(total_pools)
def run_calls(self, block_identifier: Any = 'latest') -> List[Any]:
encoded_data = self.multicall_contract.functions.tryAggregate(
False,
self.contract_calls
).call(block_identifier=block_identifier)

for idx in range(total_pools):
new_return.append((return_data[idx][0], return_data[idx][1], return_data[idx + total_pools]))
return_data = new_return
result_list = [
decode(output_types, encoded_output[1]) if encoded_output[0] else (None,)
for output_types, encoded_output in zip(self.output_types_list, encoded_data)
]

return return_data
# Convert every single-value tuple into a single value
return [result if len(result) > 1 else result[0] for result in result_list]
4 changes: 2 additions & 2 deletions fastlane_bot/config/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ class _ConfigNetworkMainnet(ConfigNetwork):
RPC_ENDPOINT = "https://eth-mainnet.alchemyapi.io/v2/"
WEB3_ALCHEMY_PROJECT_ID = os.environ.get("WEB3_ALCHEMY_PROJECT_ID")

MULTICALL_CONTRACT_ADDRESS = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"
MULTICALL_CONTRACT_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"
# NATIVE_GAS_TOKEN_KEY = "ETH-EEeE"
# WRAPPED_GAS_TOKEN_KEY = "WETH-6Cc2"
# STABLECOIN_KEY = "USDC-eB48"
Expand Down Expand Up @@ -810,7 +810,7 @@ class _ConfigNetworkTenderly(ConfigNetwork):
FASTLANE_CONTRACT_ADDRESS = "0x41Eeba3355d7D6FF628B7982F3F9D055c39488cB"
CARBON_CONTROLLER_ADDRESS = "0xC537e898CD774e2dCBa3B14Ea6f34C93d5eA45e1"
CARBON_CONTROLLER_VOUCHER = "0x3660F04B79751e31128f6378eAC70807e38f554E"
MULTICALL_CONTRACT_ADDRESS = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"
MULTICALL_CONTRACT_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"

# Uniswap
UNISWAP_V2_ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
Expand Down
6 changes: 3 additions & 3 deletions fastlane_bot/data/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,10 @@
MULTICALL_ABI = [
{
"type": "function",
"name": "aggregate",
"name": "tryAggregate",
"stateMutability": "view",
"inputs": [{"components": [{"internalType": "address", "name": "target", "type": "address"}, {"internalType": "bytes", "name": "callData", "type": "bytes"}], "internalType": "struct Multicall2.Call[]", "name": "calls", "type": "tuple[]"}],
"outputs": [{"internalType": "uint256", "name": "blockNumber", "type": "uint256"}, {"internalType": "bytes[]", "name": "returnData", "type": "bytes[]"}]
"inputs": [{"internalType": "bool", "name": "requireSuccess", "type": "bool"}, {"components": [{"internalType": "address", "name": "target", "type": "address"}, {"internalType": "bytes", "name": "callData", "type": "bytes"}], "internalType": "struct Multicall3.Call[]", "name": "calls", "type": "tuple[]"}],
"outputs": [{"components": [{"internalType": "bool", "name": "success", "type": "bool"}, {"internalType": "bytes", "name": "returnData", "type": "bytes"}], "internalType": "struct Multicall3.Result[]", "name": "returnData", "type": "tuple[]"}]
}
]

Expand Down
Loading
Loading