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

Add multichain support #183

Merged
merged 39 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
37083f5
Add multichain support - work in progress
Lesigh-3100 Oct 25, 2023
b4c2569
Finding Arbs Multichain
Lesigh-3100 Oct 26, 2023
8f8689a
Merge branch 'main' into add-multichain-support
Lesigh-3100 Oct 26, 2023
1a6f5fb
conversion to profit calculations in ETH
NIXBNT Oct 24, 2023
3cf9a52
cleanup print statements
NIXBNT Oct 24, 2023
ab5593a
Full data gathering through arb finding
Lesigh-3100 Oct 29, 2023
903af63
Merge branch 'main' into add-multichain-support
Lesigh-3100 Oct 29, 2023
04e2975
Updates & Fixes to multichain
Lesigh-3100 Oct 29, 2023
b88cd2b
Merge branch 'main' into add-multichain-support
Lesigh-3100 Oct 31, 2023
b9b8dc7
Update tests
Lesigh-3100 Oct 31, 2023
d2b8cc1
Fix tests
Lesigh-3100 Oct 31, 2023
71711e8
Fix test formatting
Lesigh-3100 Oct 31, 2023
f2aeef5
Move import in test
Lesigh-3100 Oct 31, 2023
0b88fd2
Update many tests
Lesigh-3100 Oct 31, 2023
788fbd5
edited token.csv handling
mikewcasale Oct 31, 2023
53b71f5
additional tokens.csv troubleshooting plus fix broken test 906
mikewcasale Oct 31, 2023
d1bfaa8
fixes for tests
mikewcasale Oct 31, 2023
23e6248
fixes broken test
mikewcasale Oct 31, 2023
2d90d00
Fix typo
Lesigh-3100 Nov 1, 2023
e947409
Update token list and git ignore
Lesigh-3100 Nov 1, 2023
f45483a
Additional token.csv updates
Lesigh-3100 Nov 1, 2023
dc58310
fixes issue where tokens.csv had extra columns added
mikewcasale Nov 1, 2023
ebe75b4
fixes proper default flashloan tokens
mikewcasale Nov 1, 2023
d1cefbe
Merge branch 'main' into add-multichain-support
mikewcasale Nov 1, 2023
fd86be3
cleanup
NIXBNT Nov 1, 2023
60ee3bc
rearrange configuration logging
NIXBNT Nov 1, 2023
98766c1
Update Uni V3 Fee List in network.py
Lesigh-3100 Nov 2, 2023
17d172f
Update tokens.csv & remove commented code
Lesigh-3100 Nov 2, 2023
75b36bf
Update .gitignore
Lesigh-3100 Nov 2, 2023
22eefde
Cleanup network.py
Lesigh-3100 Nov 2, 2023
fedcd95
Fix Sushi fee, naming conventions, and static pool data
Lesigh-3100 Nov 2, 2023
9d40706
Update static_pool_data.csv
Lesigh-3100 Nov 2, 2023
6edd0d4
Update network.py
Lesigh-3100 Nov 2, 2023
c01e706
Fix tests
Lesigh-3100 Nov 2, 2023
18a981c
Skip/remove Balancer pools with a 0.01 token weight
Lesigh-3100 Nov 2, 2023
1fd8b59
Update network.py
Lesigh-3100 Nov 6, 2023
142c226
Fix Multicaller, & token handling for multi-token pools
Lesigh-3100 Nov 9, 2023
5463ffa
Fix Tests
Lesigh-3100 Nov 9, 2023
362c948
Update to support transactions on L2
Lesigh-3100 Nov 9, 2023
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ latest_pool_data.json
missing_events.json
*.log
logs/*
/token_details.csv
/fastlane_bot/data/blockchain_data/*/token_detail/
tx_log.txt
71 changes: 34 additions & 37 deletions fastlane_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from fastlane_bot.tools.optimizer import CPCArbOptimizer
from .events.interface import QueryInterface
from .modes.pairwise_multi import FindArbitrageMultiPairwise
from .modes.pairwise_multi_all import FindArbitrageMultiPairwiseAll
from .modes.pairwise_multi_bal import FindArbitrageMultiPairwiseBalancer
from .modes.pairwise_multi_pol import FindArbitrageMultiPairwisePol
from .modes.pairwise_single import FindArbitrageSinglePairwise
Expand Down Expand Up @@ -150,6 +151,7 @@ def __post_init__(self):
), f"TxHelpersClass not derived from TxHelpersBase {self.TxHelpersClass}"

self.db = QueryInterface(ConfigObj=self.ConfigObj)
self.RUN_FLASHLOAN_TOKENS = self.ConfigObj.CHAIN_FLASHLOAN_TOKENS

@property
def C(self) -> Any:
Expand Down Expand Up @@ -239,7 +241,6 @@ class CarbonBot(CarbonBotBase):
AM_MULTI = "multi"
AM_MULTI_TRIANGLE = "multi_triangle"
AM_BANCOR_V3 = "bancor_v3"
RUN_FLASHLOAN_TOKENS = [T.WETH, T.DAI, T.USDC, T.USDT, T.WBTC, T.BNT, T.NATIVE_ETH]
RUN_SINGLE = "single"
RUN_CONTINUOUS = "continuous"
RUN_POLLING_INTERVAL = 60 # default polling interval in seconds
Expand Down Expand Up @@ -388,7 +389,7 @@ class ArbCandidate:

result: any
constains_carbon: bool = None
profit_usd: float = None
best_profit_usd: float = None

@property
def r(self):
Expand Down Expand Up @@ -429,6 +430,8 @@ def _get_arb_finder(arb_mode: str) -> Callable:
return FindArbitrageMultiPairwisePol
elif arb_mode in {"multi_pairwise_bal"}:
return FindArbitrageMultiPairwiseBalancer
elif arb_mode in {"multi_pairwise_all"}:
return FindArbitrageMultiPairwiseAll

def _run(
self,
Expand Down Expand Up @@ -750,26 +753,29 @@ def calculate_profit(
Tuple[Decimal, Decimal, Decimal]
The updated best_profit, flt_per_bnt, and profit_usd.
"""
flt_per_bnt = Decimal(1)
if fl_token_with_weth != T.BNT:
bnt_flt_curves = CCm.bypair(pair=f"{T.BNT}/{fl_token_with_weth}")
bnt_flt = [
x for x in bnt_flt_curves if x.params["exchange"] == "bancor_v3"
][0]
flt_per_bnt = Decimal(str(bnt_flt.x_act / bnt_flt.y_act))
best_profit = Decimal(str(flt_per_bnt * best_profit))

bnt_usdc_curve = CCm.bycid(self.BNT_ETH_CID)
usd_bnt = bnt_usdc_curve.y / bnt_usdc_curve.x
profit_usd = Decimal(str(best_profit)) * Decimal(str(usd_bnt))
best_profit_fl_token = best_profit
if fl_token_with_weth != self.ConfigObj.WRAPPED_GAS_TOKEN_KEY:
try:
fltkn_eth_conversion_rate = Decimal(str(CCm.bytknb(f"{self.ConfigObj.WRAPPED_GAS_TOKEN_KEY}").bytknq(f"{fl_token_with_weth}")[0].p))
best_profit_eth = best_profit_fl_token * fltkn_eth_conversion_rate
except:
try:
fltkn_eth_conversion_rate = 1/Decimal(str(CCm.bytknb(f"{fl_token_with_weth}").bytknq(f"{self.ConfigObj.WRAPPED_GAS_TOKEN_KEY}")[0].p))
best_profit_eth = best_profit_fl_token * fltkn_eth_conversion_rate
except Exception as e:
raise str(e)
else:
best_profit_eth = best_profit_fl_token

return best_profit, flt_per_bnt, profit_usd
usd_eth_conversion_rate = Decimal(str(CCm.bypair(pair=f"{self.ConfigObj.WRAPPED_GAS_TOKEN_KEY}/{self.ConfigObj.STABLECOIN_KEY}")[0].p))
best_profit_usd = best_profit_eth * usd_eth_conversion_rate
return best_profit_fl_token, best_profit_eth, best_profit_usd

@staticmethod
def update_log_dict(
arb_mode: str,
best_profit: Decimal,
profit_usd: Decimal,
best_profit_eth: Decimal,
best_profit_usd: Decimal,
flashloan_tkn_profit: Decimal,
calculated_trade_instructions: List[Any],
fl_token: str,
Expand All @@ -783,7 +789,7 @@ def update_log_dict(
The arbitrage mode.
best_profit: Decimal
The best profit.
profit_usd: Decimal
best_profit_usd: Decimal
The profit in USD.
flashloan_tkn_profit: Decimal
The profit from flashloan token.
Expand All @@ -806,8 +812,8 @@ def update_log_dict(
]
log_dict = {
"type": arb_mode,
"profit_bnt": num_format_float(best_profit),
"profit_usd": num_format_float(profit_usd),
"profit_gas_token": num_format_float(best_profit_eth),
"profit_usd": num_format_float(best_profit_usd),
"flashloan": flashloans,
"trades": [],
}
Expand Down Expand Up @@ -920,20 +926,20 @@ def _handle_trade_instructions(
)

# Use helper function to calculate profit
best_profit, flt_per_bnt, profit_usd = self.calculate_profit(
best_profit_fl_token, best_profit_eth, best_profit_usd = self.calculate_profit(
CCm, best_profit, fl_token, fl_token_with_weth
)

# Log the best trade instructions
self.handle_logging_for_trade_instructions(
1, best_profit=best_profit # The log id
1, best_profit=best_profit_eth # The log id
)

# Use helper function to update the log dict
log_dict = self.update_log_dict(
arb_mode,
best_profit,
profit_usd,
best_profit_eth,
best_profit_usd,
flashloan_tkn_profit,
calculated_trade_instructions,
fl_token,
Expand All @@ -943,9 +949,9 @@ def _handle_trade_instructions(
self.handle_logging_for_trade_instructions(2, log_dict=log_dict) # The log id

# Check if the best profit is greater than the minimum profit
if best_profit < self.ConfigObj.DEFAULT_MIN_PROFIT:
if best_profit_eth < self.ConfigObj.DEFAULT_MIN_PROFIT_GAS_TOKEN:
self.ConfigObj.logger.info(
f"Opportunity with profit: {num_format(best_profit)} does not meet minimum profit: {self.ConfigObj.DEFAULT_MIN_PROFIT}, discarding."
f"Opportunity with profit: {num_format(best_profit_eth)} does not meet minimum profit: {self.ConfigObj.DEFAULT_MIN_PROFIT_GAS_TOKEN}, discarding."
)
return None, None

Expand Down Expand Up @@ -1007,15 +1013,6 @@ def _handle_trade_instructions(
best_trade_instructions_dic=best_trade_instructions_dic,
)

# Get the bnt_eth pool
pool = self.db.get_pool(
exchange_name=self.ConfigObj.BANCOR_V3_NAME,
pair_name=f"{T.BNT}/{T.NATIVE_ETH}",
)

# Get the bnt_eth price
bnt_eth = (int(pool.tkn0_balance), int(pool.tkn1_balance))

# Get the tx helpers class
tx_helpers = TxHelpers(ConfigObj=self.ConfigObj)

Expand All @@ -1025,8 +1022,8 @@ def _handle_trade_instructions(
route_struct=route_struct,
src_amt=flashloan_amount,
src_address=flashloan_token_address,
bnt_eth=bnt_eth,
expected_profit=best_profit,
expected_profit_eth=best_profit_eth,
expected_profit_usd=best_profit_usd,
safety_override=False,
verbose=True,
log_object=log_dict,
Expand Down
66 changes: 12 additions & 54 deletions fastlane_bot/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import os
from dataclasses import dataclass, field, InitVar, asdict

# from .base import ConfigBase
from . import network as network_, db as db_, logger as logger_, provider as provider_
from .cloaker import CloakerL
Expand All @@ -19,23 +18,13 @@
load_dotenv()
TENDERLY_FORK_ID = os.environ.get("TENDERLY_FORK_ID")
if TENDERLY_FORK_ID is None:
TENDERLY_FORK_ID = ""
WEB3_ALCHEMY_PROJECT_ID = os.environ.get("WEB3_ALCHEMY_PROJECT_ID")
PROVIDER_URL = (
f"https://rpc.tenderly.co/fork/{TENDERLY_FORK_ID}"
if TENDERLY_FORK_ID != ""
else f"https://eth-mainnet.alchemyapi.io/v2/{WEB3_ALCHEMY_PROJECT_ID}"
)
NETWORK_ID = "mainnet" if TENDERLY_FORK_ID == "" else "tenderly"
NETWORK_NAME = "Ethereum Mainnet" if TENDERLY_FORK_ID == "" else "Tenderly (Alchemy)"

TENDERLY_FORK_ID = ''

@dataclass
class Config:
class Config():
"""
Fastlane bot configuration object
"""

__VERSION__ = __VERSION__
__DATE__ = __DATE__

Expand All @@ -58,34 +47,13 @@ class Config:
LL_WARN = S.LOGLEVEL_WARNING
LL_ERR = S.LOGLEVEL_ERROR

SUPPORTED_EXCHANGES = [
"carbon_v1",
"bancor_v2",
"bancor_v3",
"uniswap_v2",
"uniswap_v3",
"sushiswap_v2",
"bancor_pol",
"pancakeswap_v2",
"pancakeswap_v3",
]
connection = EthereumNetwork(
network_id=NETWORK_ID,
network_name=NETWORK_NAME,
provider_url=PROVIDER_URL,
provider_name="alchemy",
)
connection.connect_network()
w3 = connection.web3

UNI_V2_FORKS = ["uniswap_v2", "sushiswap_v2", "pancakeswap_v2"]
UNI_V3_FORKS = ["uniswap_v3", "pancakeswap_v3"]
SUPPORTED_EXCHANGES = ['carbon_v1', 'bancor_v2', 'bancor_v3', 'uniswap_v2', 'uniswap_v3', 'sushiswap_v2', 'bancor_pol', 'pancakeswap_v2', 'pancakeswap_v3']

@classmethod
def new(cls, *, config=None, loglevel=None, logging_path=None, **kwargs):
def new(cls, *, config=None, loglevel=None, logging_path=None, blockchain=None, **kwargs):
"""
Alternative constructor: create and return new Config object

:config: CONFIG_MAINNET(default), CONFIG_TENDERLY, CONFIG_UNITTEST
:loglevel: LOGLEVEL_DEBUG, LOGLEVEL_INFO (default), LOGLEVEL_WARNING, LOGLEVEL_ERROR
"""
Expand All @@ -98,7 +66,7 @@ def new(cls, *, config=None, loglevel=None, logging_path=None, **kwargs):
C_log = logger_.ConfigLogger.new(loglevel=loglevel, logging_path=logging_path)

if config == cls.CONFIG_MAINNET:
C_nw = network_.ConfigNetwork.new(network=S.NETWORK_MAINNET)
C_nw = network_.ConfigNetwork.new(network=blockchain)
return cls(network=C_nw, logger=C_log, **kwargs)
elif config == cls.CONFIG_TENDERLY:
C_db = db_.ConfigDB.new(db=S.DATABASE_POSTGRES, POSTGRES_DB="tenderly")
Expand All @@ -107,9 +75,7 @@ def new(cls, *, config=None, loglevel=None, logging_path=None, **kwargs):
elif config == cls.CONFIG_UNITTEST:
C_db = db_.ConfigDB.new(db=S.DATABASE_UNITTEST, POSTGRES_DB="unittest")
C_nw = network_.ConfigNetwork.new(network=S.NETWORK_MAINNET)
C_pr = provider_.ConfigProvider.new(
network=C_nw, provider=S.PROVIDER_DEFAULT
)
C_pr = provider_.ConfigProvider.new(network=C_nw, provider=S.PROVIDER_DEFAULT)
return cls(db=C_db, logger=C_log, network=C_nw, provider=C_pr, **kwargs)
raise ValueError(f"Invalid config: {config}")

Expand All @@ -134,28 +100,22 @@ def get_attribute_from_config(self, name: str):
for obj in [self.network, self.db, self.provider, self.logger]:
if hasattr(obj, name):
return getattr(obj, name)
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
)
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

def __getattr__(self, name: str):
"""
If of type attribute, return it.
"""
if self.is_config_item(name):
return self.get_attribute_from_config(name)
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
)
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

def __post_init__(self):
"""
Post-initialization initialization.
"""
if self.network is None:
self.network = network_.ConfigNetwork.new(
network_.ConfigNetwork.NETWORK_ETHEREUM
)
self.network = network_.ConfigNetwork.new(network_.ConfigNetwork.NETWORK_ETHEREUM)
assert issubclass(type(self.network), network_.ConfigNetwork)

if self.db is None:
Expand All @@ -174,16 +134,14 @@ def __post_init__(self):
self.provider = provider_.ConfigProvider.new(self.network)
assert issubclass(type(self.provider), provider_.ConfigProvider)

assert (
self.network is self.provider.network
), f"Network mismatch: {self.network} != {self.provider.network}"
assert self.network is self.provider.network, f"Network mismatch: {self.network} != {self.provider.network}"

VISIBLE_FIELDS = "network, db, logger, provider, w3, ZERO_ADDRESS"

def cloaked(self, incl=None, excl=None):
"""
returns a cloaked version of the object

:incl: fields to _include_ in the cloaked version (plus those in VISIBLE_FIELDS)
:excl: fields to _exclude_ from the cloaked version
"""
Expand Down
5 changes: 3 additions & 2 deletions fastlane_bot/config/multicaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,14 @@ class MultiCaller(ContextManager):
"""
__DATE__ = "2022-09-26"
__VERSION__ = "0.0.2"
MULTICALL_CONTRACT_ADDRESS = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"


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

def __enter__(self) -> 'MultiCaller':
return self
Expand Down
Loading