Skip to content

Commit

Permalink
Merge pull request #183 from bancorprotocol/add-multichain-support
Browse files Browse the repository at this point in the history
Add multichain support
  • Loading branch information
mikewcasale authored Nov 9, 2023
2 parents f640152 + 362c948 commit 6310ae8
Show file tree
Hide file tree
Showing 79 changed files with 64,866 additions and 3,226 deletions.
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

0 comments on commit 6310ae8

Please sign in to comment.