Skip to content

Commit

Permalink
Merge pull request #641 from bancorprotocol/handle-multicall-failures
Browse files Browse the repository at this point in the history
Handle Multicall Failures
  • Loading branch information
barakman authored May 12, 2024
2 parents 0259524 + 4e3ae95 commit 279d1c4
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 496 deletions.
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

0 comments on commit 279d1c4

Please sign in to comment.