diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index 13b69d6f9..f5ecd81f3 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -34,6 +34,9 @@ jobs: run: | echo TENDERLY_FORK=$TENDERLY_FORK > .env echo WEB3_ALCHEMY_PROJECT_ID=$WEB3_ALCHEMY_PROJECT_ID >> .env + echo WEB3_ALCHEMY_BASE=$WEB3_ALCHEMY_BASE >> .env + echo WEB3_FANTOM=$WEB3_FANTOM >> .env + echo WEB3_MANTLE=$WEB3_MANTLE >> .env echo ETHERSCAN_TOKEN=$ETHERSCAN_TOKEN >> .env echo DEFAULT_MIN_PROFIT_BNT=$DEFAULT_MIN_PROFIT_BNT >> .env echo ETH_PRIVATE_KEY_BE_CAREFUL=$ETH_PRIVATE_KEY_BE_CAREFUL >> .env @@ -41,6 +44,9 @@ jobs: env: TENDERLY_FORK: '${{ secrets.TENDERLY_FORK }}' WEB3_ALCHEMY_PROJECT_ID: '${{ secrets.WEB3_ALCHEMY_PROJECT_ID }}' + WEB3_ALCHEMY_BASE: '${{ secrets.WEB3_ALCHEMY_BASE }}' + WEB3_FANTOM: '${{ secrets.WEB3_FANTOM }}' + WEB3_MANTLE: '${{ secrets.WEB3_MANTLE }}' ETHERSCAN_TOKEN: '${{ secrets.ETHERSCAN_TOKEN }}' DEFAULT_MIN_PROFIT_BNT: '${{ secrets.DEFAULT_MIN_PROFIT_BNT }}' ETH_PRIVATE_KEY_BE_CAREFUL: '${{ secrets.ETH_PRIVATE_KEY_BE_CAREFUL }}' diff --git a/CHANGELOG.md b/CHANGELOG.md index ae62ef084..22543d906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,26 @@ # Changelog -## [Unreleased](https://github.com/bancorprotocol/fastlane-bot/tree/HEAD) +## [v3.1.5](https://github.com/bancorprotocol/fastlane-bot/tree/v3.1.5) (2024-04-09) -[Full Changelog](https://github.com/bancorprotocol/fastlane-bot/compare/v3.1.4...HEAD) +[Full Changelog](https://github.com/bancorprotocol/fastlane-bot/compare/v3.1.4...v3.1.5) - Carbon strategies are duplicated from events [\#521](https://github.com/bancorprotocol/fastlane-bot/issues/521) +- Add support for puffs\_penthouse uniswap\_v3 fork [\#524](https://github.com/bancorprotocol/fastlane-bot/pull/524) ([NIXBNT](https://github.com/NIXBNT)) + ## [v3.1.4](https://github.com/bancorprotocol/fastlane-bot/tree/v3.1.4) (2024-04-08) [Full Changelog](https://github.com/bancorprotocol/fastlane-bot/compare/v3.1.3...v3.1.4) - GPU token external liquidity not found [\#515](https://github.com/bancorprotocol/fastlane-bot/issues/515) +- Hotfix - Carbon strategies duplicated [\#522](https://github.com/bancorprotocol/fastlane-bot/pull/522) ([Lesigh-3100](https://github.com/Lesigh-3100)) ## [v3.1.3](https://github.com/bancorprotocol/fastlane-bot/tree/v3.1.3) (2024-04-04) [Full Changelog](https://github.com/bancorprotocol/fastlane-bot/compare/v3.1.2...v3.1.3) +- gpu external sources added to event mapping [\#517](https://github.com/bancorprotocol/fastlane-bot/pull/517) ([NIXBNT](https://github.com/NIXBNT)) + Closed issues - All files should have doc strings [\#438](https://github.com/bancorprotocol/fastlane-bot/issues/438) diff --git a/fastlane_bot/bot.py b/fastlane_bot/bot.py index a631b5e9c..a64115554 100644 --- a/fastlane_bot/bot.py +++ b/fastlane_bot/bot.py @@ -62,12 +62,10 @@ from fastlane_bot.helpers import ( TxRouteHandler, TxHelpers, - TxHelpersBase, TradeInstruction, Univ3Calculator, add_wrap_or_unwrap_trades_to_route, - split_carbon_trades, - submit_transaction_tenderly + split_carbon_trades ) from fastlane_bot.helpers.routehandler import maximize_last_trade_per_tkn from fastlane_bot.tools.cpc import ConstantProductCurve as CPC, CPCContainer, T @@ -95,7 +93,7 @@ class CarbonBotBase: the database manager. TxRouteHandlerClass ditto (default: TxRouteHandler). - TxHelpersClass: class derived from TxHelpersBase + TxHelpersClass: ditto (default: TxHelpers). """ @@ -119,8 +117,6 @@ def __post_init__(self): if self.ConfigObj is None: self.ConfigObj = Config() - self.c = self.ConfigObj - assert ( self.polling_interval is None ), "polling_interval is now a parameter to run" @@ -129,10 +125,7 @@ def __post_init__(self): self.TxRouteHandlerClass = TxRouteHandler if self.TxHelpersClass is None: - self.TxHelpersClass = TxHelpers(ConfigObj=self.ConfigObj) - assert issubclass( - self.TxHelpersClass.__class__, TxHelpersBase - ), f"TxHelpersClass not derived from TxHelpersBase {self.TxHelpersClass}" + self.TxHelpersClass = TxHelpers(cfg=self.ConfigObj) self.db = QueryInterface(ConfigObj=self.ConfigObj) self.RUN_FLASHLOAN_TOKENS = [*self.ConfigObj.CHAIN_FLASHLOAN_TOKENS.values()] @@ -1042,19 +1035,6 @@ def _handle_trade_instructions( route_struct_maximized = maximize_last_trade_per_tkn(route_struct=route_struct_processed) - # Get the cids - cids = list({ti["cid"] for ti in best_trade_instructions_dic}) - - # Check if the network is tenderly and submit the transaction accordingly - if self.ConfigObj.NETWORK == self.ConfigObj.NETWORK_TENDERLY: - return submit_transaction_tenderly( - cfg=self.ConfigObj, - flashloan_struct=flashloan_struct, - route_struct=route_struct_maximized, - src_amount=flashloan_amount_wei, - src_address=flashloan_token_address, - ) - # Log the route_struct self.handle_logging_for_trade_instructions( 4, # The log id @@ -1065,18 +1045,13 @@ def _handle_trade_instructions( best_trade_instructions_dic=best_trade_instructions_dic, ) - # Get the tx helpers class - tx_helpers = TxHelpers(ConfigObj=self.ConfigObj) - # Return the validate and submit transaction - return tx_helpers.validate_and_submit_transaction( + return self.TxHelpersClass.validate_and_submit_transaction( route_struct=route_struct_maximized, src_amt=flashloan_amount_wei, src_address=flashloan_token_address, expected_profit_gastkn=best_profit_gastkn, expected_profit_usd=best_profit_usd, - safety_override=False, - verbose=True, log_object=log_dict, flashloan_struct=flashloan_struct, ) @@ -1320,7 +1295,7 @@ def run_single_mode( randomizer=randomizer, replay_mode=replay_mode, ) - if tx_hash and tx_hash[0]: + if tx_hash: self.ConfigObj.logger.info( f"[bot.run_single_mode] Arbitrage executed [hash={tx_hash}]" ) @@ -1328,25 +1303,15 @@ def run_single_mode( # Write the tx hash to a file in the logging_path directory if self.logging_path: filename = f"successful_tx_hash_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt" - print(f"Writing tx_hash hash {tx_hash} to {filename}") + self.ConfigObj.logger.info(f"Writing tx hash {tx_hash} to {filename}") with open(f"{self.logging_path}/{filename}", "w") as f: - - # if isinstance(tx_hash[0], AttributeDict): - # f.write(str(tx_hash[0])) - # else: - for record in tx_hash: - f.write("\n") - f.write("\n") - try: - json.dump(record, f, indent=4) - except: - f.write(str(record)) + f.write(tx_hash) except self.NoArbAvailable as e: self.ConfigObj.logger.warning(f"[NoArbAvailable] {e}") except Exception as e: self.ConfigObj.logger.error(f"[bot:run:single] {e}") - raise + raise e def _ensure_connection(self, tenderly_fork: str): """ diff --git a/fastlane_bot/config/constants.py b/fastlane_bot/config/constants.py index c5075dbed..090f2d323 100644 --- a/fastlane_bot/config/constants.py +++ b/fastlane_bot/config/constants.py @@ -26,6 +26,5 @@ FUSIONX_V3_NAME = "fusionx_v3" CLEOPATRA_V3_NAME = "cleopatra_v3" CARBON_V1_NAME = "carbon_v1" -CARBON_V1_FORK1_NAME = "altered_carbon" VELOCIMETER_V2_NAME = "velocimeter_v2" SOLIDLY_V2_NAME = "solidly_v2" diff --git a/fastlane_bot/config/network.py b/fastlane_bot/config/network.py index ee08ac708..6c85d5287 100644 --- a/fastlane_bot/config/network.py +++ b/fastlane_bot/config/network.py @@ -19,7 +19,7 @@ from . import selectors as S from .base import ConfigBase -from .constants import CARBON_V1_FORK1_NAME, CARBON_V1_NAME +from .constants import CARBON_V1_NAME load_dotenv() from decimal import Decimal @@ -216,7 +216,6 @@ class ConfigNetwork(ConfigBase): SUSHISWAP_V2_NAME = "sushiswap_v2" SUSHISWAP_V3_NAME = "sushiswap_v3" CARBON_V1_NAME = CARBON_V1_NAME - CARBON_V1_X2_NAME = CARBON_V1_FORK1_NAME BANCOR_POL_NAME = "bancor_pol" BALANCER_NAME = "balancer" PANCAKESWAP_V2_NAME = "pancakeswap_v2" @@ -244,8 +243,6 @@ class ConfigNetwork(ConfigBase): GAS_ORACLE_ADDRESS = None - CARBON_V1_FORKS = [CARBON_V1_NAME, CARBON_V1_X2_NAME] - MULTICALLABLE_EXCHANGES = [BANCOR_V3_NAME, BANCOR_POL_NAME, BALANCER_NAME] # BANCOR POL BANCOR_POL_START_BLOCK = 18184448 @@ -266,25 +263,10 @@ class ConfigNetwork(ConfigBase): # DEFAULT VALUES SECTION ####################################################################################### - UNIV3_FEE_LIST = [80, 100, 250, 450, 500, 2500, 3000, 10000] - MIN_BNT_LIQUIDITY = 2_000_000_000_000_000_000 - DEFAULT_GAS = 950_000 - DEFAULT_GAS_PRICE = 0 - DEFAULT_GAS_PRICE_OFFSET = 1.09 DEFAULT_GAS_SAFETY_OFFSET = 25_000 - DEFAULT_POLL_INTERVAL = 12 DEFAULT_BLOCKTIME_DEVIATION = 13 * 500 * 100 # 10 block time deviation - DEFAULT_MAX_SLIPPAGE = Decimal("1") # 1% _PROJECT_PATH = os.path.normpath(f"{os.getcwd()}") # TODO: FIX THIS - DEFAULT_CURVES_DATAFILE = os.path.normpath( - f"{_PROJECT_PATH}/carbon/data/curves.csv.gz" - ) - CARBON_STRATEGY_CHUNK_SIZE = 200 Q96 = Decimal("2") ** Decimal("96") - DEFAULT_TIMEOUT = 60 - CARBON_FEE = Decimal("0.002") - BANCOR_V3_FEE = Decimal("0.0") - DEFAULT_REWARD_PERCENT = Decimal("0.5") LIMIT_BANCOR3_FLASHLOAN_TOKENS = True DEFAULT_MIN_PROFIT_GAS_TOKEN = Decimal("0.02") @@ -308,6 +290,15 @@ class ConfigNetwork(ConfigBase): GAS_TKN_IN_FLASHLOAN_TOKENS = None IS_NO_FLASHLOAN_AVAILABLE = False + # HOOKS + ####################################################################################### + @staticmethod + def gas_strategy(web3): + return { + "maxFeePerGas": web3.eth.gas_price, + "maxPriorityFeePerGas": web3.eth.max_priority_fee + } + @classmethod def new(cls, network=None): """ diff --git a/fastlane_bot/config/provider.py b/fastlane_bot/config/provider.py index 32a1666d0..e775b19d8 100644 --- a/fastlane_bot/config/provider.py +++ b/fastlane_bot/config/provider.py @@ -112,51 +112,23 @@ def __init__(self, network: ConfigNetwork, **kwargs): self.connection.connect_network() self.w3 = self.connection.web3 self.w3_async = self.connection.w3_async - self.LOCAL_ACCOUNT = self.w3.eth.account.from_key(ETH_PRIVATE_KEY_BE_CAREFUL) - - - if network.NETWORK in [N.NETWORK_BASE, N.NETWORK_ETHEREUM, N.NETWORK_FANTOM, N.NETWORK_MANTLE]: - self.CARBON_CONTROLLER_CONTRACT = self.w3.eth.contract( - address=network.CARBON_CONTROLLER_ADDRESS, - abi=CARBON_CONTROLLER_ABI, - ) - self.BANCOR_ARBITRAGE_CONTRACT = self.w3.eth.contract( - address=self.w3.to_checksum_address(network.FASTLANE_CONTRACT_ADDRESS), - abi=FAST_LANE_CONTRACT_ABI, - ) - - if network.GAS_ORACLE_ADDRESS: - self.GAS_ORACLE_CONTRACT = self.w3_async.eth.contract( - address=network.GAS_ORACLE_ADDRESS, - abi=GAS_ORACLE_ABI - ) + self.BANCOR_ARBITRAGE_CONTRACT = self.w3.eth.contract( + address=self.w3.to_checksum_address(N.FASTLANE_CONTRACT_ADDRESS), + abi=FAST_LANE_CONTRACT_ABI, + ) - if network.NETWORK in N.NETWORK_ETHEREUM: - self.BANCOR_NETWORK_INFO_CONTRACT = self.w3.eth.contract( - address=network.BANCOR_V3_NETWORK_INFO_ADDRESS, - abi=BANCOR_V3_NETWORK_INFO_ABI, + if N.GAS_ORACLE_ADDRESS: + self.GAS_ORACLE_CONTRACT = self.w3.eth.contract( + address=N.GAS_ORACLE_ADDRESS, + abi=GAS_ORACLE_ABI, ) - self.ARB_CONTRACT_VERSION = self.BANCOR_ARBITRAGE_CONTRACT.caller.version() - else: - self.CARBON_CONTROLLER_CONTRACT = None - self.ARB_CONTRACT_VERSION = 10 + code = self.w3.eth.get_code(self.w3.to_checksum_address(N.FASTLANE_CONTRACT_ADDRESS)) + assert len(code) > 10, f"{N.FASTLANE_CONTRACT_ADDRESS}:\n{code}\n{self.RPC_URL}" - if self.BANCOR_ARBITRAGE_CONTRACT is not None: - try: - ( - reward_percent, - max_profit, - ) = self.BANCOR_ARBITRAGE_CONTRACT.caller.rewards() - self.ARB_REWARD_PERCENTAGE = str(int(reward_percent) / 1000000) - self.ARB_MAX_PROFIT = 1000000 # This is no longer used - except: - self.ARB_REWARD_PERCENTAGE = "0.5" - else: - self.ARB_REWARD_PERCENTAGE = "0.5" - - self.EXPECTED_GAS_MODIFIER = "0.85" + self.ARB_CONTRACT_VERSION = self.BANCOR_ARBITRAGE_CONTRACT.caller.version() + self.ARB_REWARDS_PPM = self.BANCOR_ARBITRAGE_CONTRACT.caller.rewards()[0] class _ConfigProviderTenderly(ConfigProvider): @@ -182,26 +154,20 @@ def __init__(self, network: ConfigNetwork, **kwargs): ) self.connection.connect_network() self.w3 = self.connection.web3 - self.LOCAL_ACCOUNT = self.w3.eth.account.from_key(ETH_PRIVATE_KEY_BE_CAREFUL) - self.BANCOR_NETWORK_INFO_CONTRACT = self.w3.eth.contract( - address=N.BANCOR_V3_NETWORK_INFO_ADDRESS, - abi=BANCOR_V3_NETWORK_INFO_ABI, - ) - self.CARBON_CONTROLLER_CONTRACT = self.w3.eth.contract( - address=N.CARBON_CONTROLLER_ADDRESS, - abi=CARBON_CONTROLLER_ABI, - ) self.BANCOR_ARBITRAGE_CONTRACT = self.w3.eth.contract( address=self.w3.to_checksum_address(N.FASTLANE_CONTRACT_ADDRESS), abi=FAST_LANE_CONTRACT_ABI, ) - self.ARB_CONTRACT_VERSION = self.BANCOR_ARBITRAGE_CONTRACT.caller.version() - reward_percent, max_profit = self.BANCOR_ARBITRAGE_CONTRACT.caller.rewards() + if N.GAS_ORACLE_ADDRESS: + self.GAS_ORACLE_CONTRACT = self.w3.eth.contract( + address=N.GAS_ORACLE_ADDRESS, + abi=GAS_ORACLE_ABI, + ) - self.ARB_REWARD_PERCENTAGE = str(int(reward_percent) / 1000000) - self.ARB_MAX_PROFIT = str(int(max_profit) / (10**18)) + self.ARB_CONTRACT_VERSION = self.BANCOR_ARBITRAGE_CONTRACT.caller.version() + self.ARB_REWARDS_PPM = self.BANCOR_ARBITRAGE_CONTRACT.caller.rewards()[0] class _ConfigProviderInfura(ConfigProvider): @@ -232,6 +198,4 @@ def __init__(self, network: ConfigNetwork, **kwargs): # raise NotImplementedError("Infura not implemented") self.connection = None self.w3 = None - self.BANCOR_NETWORK_INFO_CONTRACT = None - self.CARBON_CONTROLLER_CONTRACT = None self.BANCOR_ARBITRAGE_CONTRACT = None diff --git a/fastlane_bot/data/multichain_addresses.csv b/fastlane_bot/data/multichain_addresses.csv index 7e708bd6e..ce5ed8167 100644 --- a/fastlane_bot/data/multichain_addresses.csv +++ b/fastlane_bot/data/multichain_addresses.csv @@ -81,7 +81,7 @@ agni_v3,mantle,uniswap_v3,0x25780dc8Fc3cfBD75F33bFDAB65e969b603b2035,0x319B69888 puffs_penthouse_v3,mantle,uniswap_v3,0x8f140Fc3e9211b8DC2fC1D7eE3292F6817C5dD5D,0x58b3a3797FF02B934a7d69D00873889F33Fb8829,,59915640, butter_v3,mantle,uniswap_v3,0xEECa0a86431A7B42ca2Ee5F479832c3D4a4c2644,0xAe5c0a73bAE513fe5842DbcB8E32fc7E3e30DA57,,22966090, carbon_v1,mantle,carbon_v1,0x7900f766F06e361FDDB4FdeBac5b138c4EEd8d4A,0x7900f766F06e361FDDB4FdeBac5b138c4EEd8d4A,,, -some_carbon_fork,mantle,carbon_v1,0x3749f6Ef09B0f74CD5324126b02d867A03DEE844,0x3749f6Ef09B0f74CD5324126b02d867A03DEE844,,, +fusionx_supernova,mantle,carbon_v1,0x04FBC7f949326fFf7Fe4D6aE96BAfa3D8e8A8c0a,0x04FBC7f949326fFf7Fe4D6aE96BAfa3D8e8A8c0a,,61955475, cleopatra_v2,mantle,solidly_v2,0xAAA16c016BF556fcD620328f0759252E29b1AB57,0xAAA45c8F5ef92a000a121d102F4e89278a711Faa,,34705175, cleopatra_v3,mantle,uniswap_v3,0xAAA32926fcE6bE95ea2c51cB4Fcb60836D320C42,0xAAAE99091Fbb28D400029052821653C1C752483B,,34705175, fusionx_v2,mantle,uniswap_v2,0xE5020961fA51ffd3662CDf307dEf18F9a87Cce7c,0xDd0840118bF9CCCc6d67b2944ddDfbdb995955FD,0.003,2868, diff --git a/fastlane_bot/events/utils.py b/fastlane_bot/events/utils.py index 7d200774b..500a9cecf 100644 --- a/fastlane_bot/events/utils.py +++ b/fastlane_bot/events/utils.py @@ -578,8 +578,8 @@ def get_config( Parameters ---------- - default_min_profit_bnt : int or Decimal - The default minimum profit in BNT. + default_min_profit_gas_token : int or Decimal + The default minimum profit in the gas token. limit_bancor3_flashloan_tokens : bool Whether to limit the flashloan tokens to Bancor v3 pools. loglevel : str @@ -2001,93 +2001,13 @@ def handle_tokens_csv(mgr, prefix_path, read_only: bool = False): ) -def self_funding_warning_sequence(cfg): - """ - This function initiates a warning sequence if the user has specified to use their own funds. - - :param cfg: the config object - - """ - cfg.logger.info( - f"\n\n*********************************************************************************\n********************************* WARNING *********************************\n\n" - ) - cfg.logger.info( - f"Arbitrage bot is set to use its own funds instead of using Flashloans.\n\n***** This could put your funds at risk. ******\nIf you did not mean to use this mode, cancel the bot now.\n\nOtherwise, the bot will submit token approvals IRRESPECTIVE OF CURRENT GAS PRICE for each token specified in Flashloan tokens.\n\n*********************************************************************************" - ) - time.sleep(5) - cfg.logger.info(f"Submitting approvals in 15 seconds") - time.sleep(5) - cfg.logger.info(f"Submitting approvals in 10 seconds") - time.sleep(5) - cfg.logger.info(f"Submitting approvals in 5 seconds") - time.sleep(5) - cfg.logger.info( - f"*********************************************************************************\n\nSelf-funding mode activated." - ) - cfg.logger.info( - f"""\n\n - _____ - |A . | _____ - | /.\ ||A ^ | _____ - |(_._)|| / \ ||A _ | _____ - | | || \ / || ( ) ||A_ _ | - |____V|| . ||(_'_)||( v )| - |____V|| | || \ / | - |____V|| . | - |____V| - \n\n""" - ) - - -def find_unapproved_tokens(tokens: List, cfg, tx_helpers) -> List: - """ - This function checks if tokens have been previously approved from the wallet address to the Arbitrage contract. - If they are not already approved, it will submit approvals for each token specified in Flashloan tokens. - :param tokens: the list of tokens to check/approve - :param cfg: the config object - :param tx_helpers: the TxHelpers instantiated class - - returns: List of tokens that have not been approved - - """ - unapproved_tokens = [] - for tkn in tokens: - if not tx_helpers.check_if_token_approved(token_address=tkn): - unapproved_tokens.append(tkn) - return unapproved_tokens - - -def check_and_approve_tokens(tokens: List, cfg) -> bool: +def check_and_approve_tokens(cfg: Config, tokens: List): """ This function checks if tokens have been previously approved from the wallet address to the Arbitrage contract. If they are not already approved, it will submit approvals for each token specified in Flashloan tokens. - :param tokens: the list of tokens to check/approve :param cfg: the config object + :param tokens: the list of tokens to check/approve """ - - tokens = [tkn for tkn in tokens if tkn != cfg.NATIVE_GAS_TOKEN_ADDRESS] - - self_funding_warning_sequence(cfg=cfg) - tx_helpers = TxHelpers(ConfigObj=cfg) - unapproved_tokens = find_unapproved_tokens( - tokens=tokens, cfg=cfg, tx_helpers=tx_helpers - ) - - if len(unapproved_tokens) == 0: - return True - - for _tkn in unapproved_tokens: - tx = tx_helpers.approve_token_for_arb_contract(token_address=_tkn) - if tx is not None: - continue - else: - assert ( - False - ), f"Failed to approve token: {_tkn}. This can be fixed by approving manually, or restarting the bot to try again." - - unapproved_tokens = find_unapproved_tokens( - tokens=unapproved_tokens, cfg=cfg, tx_helpers=tx_helpers - ) - return len(unapproved_tokens) == 0 + TxHelpers(cfg=cfg).check_and_approve_tokens(tokens=tokens) diff --git a/fastlane_bot/helpers/__init__.py b/fastlane_bot/helpers/__init__.py index 105359cdc..8427e8c44 100644 --- a/fastlane_bot/helpers/__init__.py +++ b/fastlane_bot/helpers/__init__.py @@ -10,12 +10,7 @@ """ from .tradeinstruction import TradeInstruction from .routehandler import TxRouteHandler, RouteStruct -from .submithandler import submit_transaction_tenderly from .txhelpers import TxHelpers from .univ3calc import Univ3Calculator from .wrap_unwrap_processor import add_wrap_or_unwrap_trades_to_route from .carbon_trade_splitter import split_carbon_trades -TxHelpersBase = TxHelpers - - - diff --git a/fastlane_bot/helpers/submithandler.py b/fastlane_bot/helpers/submithandler.py deleted file mode 100644 index fbabcf713..000000000 --- a/fastlane_bot/helpers/submithandler.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Submit handler for the Fastlane project. - -TODO: despite the naming of the module, it currently ONLY seems to support Tenderly -submission, via the ``submit_transaction_tenderly`` function. Either the module should -be renamed or, better, the code should be consolidated - ---- -(c) Copyright Bprotocol foundation 2023-24. -All rights reserved. -Licensed under MIT. -""" -__VERSION__ = "1.0" -__DATE__ = "01/May/2023" - -from typing import List, Any, Dict -from .routehandler import RouteStruct -from ..data.abi import FAST_LANE_CONTRACT_ABI -from fastlane_bot.config import Config - -# TODO: splitting this into two functions does not seem to be particularly useful -# given how little the wrapping code does - -def submit_transaction_tenderly( - cfg: Config, - route_struct: List[RouteStruct], - src_address: str, - src_amount: int, - flashloan_struct: List[Dict[str, int or str]], -) -> Any: - """ - Submits a transaction to the network. - - Parameters - ---------- - route_struct: the list of RouteStruct objects - flashloan_struct: The list of objects containing Flashloan instructions - src_amount: DEPRECATED. Source amount used in function flashloanAndArb - src_address: DEPRECATED Source token address used in function flashloanAndArb - - Returns - ------- - str - The transaction hash. - """ - tx = _submit_transaction_tenderly(cfg, route_struct, src_address, src_amount, flashloan_struct) - return cfg.w3.eth.wait_for_transaction_receipt(tx) - -def _submit_transaction_tenderly( - cfg: Config, - route_struct: List[RouteStruct], - src_address: str, - src_amount: int, - flashloan_struct: List[Dict[str, int or str]], -) -> Any: - arb_contract = cfg.w3.eth.contract( - address=cfg.w3.to_checksum_address(cfg.network.FASTLANE_CONTRACT_ADDRESS), - abi=FAST_LANE_CONTRACT_ABI - ) - - address = cfg.w3.to_checksum_address(cfg.BINANCE14_WALLET_ADDRESS) - - if cfg.SELF_FUND: - return arb_contract.functions.fundAndArb(route_struct, src_address, src_amount).transact( - { - "gas": cfg.DEFAULT_GAS, - "from": address, - "nonce": cfg.w3.eth.get_transaction_count(address), - "gasPrice": 0, - "value": src_amount if src_address in cfg.NATIVE_GAS_TOKEN_ADDRESS else 0 - } - ) - - elif flashloan_struct is None: - return arb_contract.functions.flashloanAndArb(route_struct, src_address, src_amount).transact( - { - "gas": cfg.DEFAULT_GAS, - "from": address, - "nonce": cfg.w3.eth.get_transaction_count(address), - "gasPrice": 0, - } - ) - - else: - return arb_contract.functions.flashloanAndArbV2(flashloan_struct, route_struct).transact( - { - "gas": cfg.DEFAULT_GAS, - "from": address, - "nonce": cfg.w3.eth.get_transaction_count(address), - "gasPrice": 0, - } - ) diff --git a/fastlane_bot/helpers/txhelpers.py b/fastlane_bot/helpers/txhelpers.py index 45a5680df..df941ebff 100644 --- a/fastlane_bot/helpers/txhelpers.py +++ b/fastlane_bot/helpers/txhelpers.py @@ -5,15 +5,7 @@ and methods for working with transactions: - ``validate_and_submit_transaction``: Validates a transaction and then submits it to the arb contract -- ``get_access_list``: TODO -- ``construct_contract_function``: Builds a transaction using the Arb Contract function -- ``build_transaction_with_gas``: Builds a transaction (how is it different from ``construct_contract_function``?) -- ``get_nonce``: Returns the nonce of the wallet address -- ``build_tx``: Yet another method for building a transaction (TODO) -- ``submit_regular_transaction``: Submits a non-private transaction to the blockchain -- ``submit_private_transaction``: Ditto private -- ``sign_transaction``: Signs a transaction -- ... +- ``check_and_approve_tokens``: Approves every token with zero allowance to the maximum allowance --- (c) Copyright Bprotocol foundation 2023-24. @@ -23,23 +15,20 @@ __VERSION__ = "1.0" __DATE__ = "01/May/2023" -import asyncio -import nest_asyncio from _decimal import Decimal -from json import loads +from json import dumps from dataclasses import dataclass from typing import List, Any, Dict, Optional -import requests -from alchemy import Network, Alchemy from web3.exceptions import TimeExhausted from fastlane_bot.config import Config from fastlane_bot.data.abi import ERC20_ABI -from fastlane_bot.utils import num_format, log_format, num_format_float, int_prefix +from fastlane_bot.utils import num_format, log_format -nest_asyncio.apply() +MAX_UINT256 = 2 ** 256 - 1 +ETH_RESOLUTION = 10 ** 18 @dataclass class TxHelpers: @@ -50,81 +39,20 @@ class TxHelpers: __VERSION__ = __VERSION__ __DATE__ = __DATE__ - ConfigObj: Config - # This is used for the Alchemy SDK - network = Network.ETH_MAINNET + cfg: Config def __post_init__(self): - - if self.ConfigObj.network.DEFAULT_PROVIDER != "tenderly": - self.alchemy = Alchemy( - api_key=self.ConfigObj.WEB3_ALCHEMY_PROJECT_ID, - network=self.network, - max_retries=3, - ) - self.arb_contract = self.ConfigObj.BANCOR_ARBITRAGE_CONTRACT - self.web3 = self.ConfigObj.w3 - # Set the local account - self.local_account = self.web3.eth.account.from_key( - self.ConfigObj.ETH_PRIVATE_KEY_BE_CAREFUL - ) - - # Set the public address - self.wallet_address = str(self.local_account.address) - - self.alchemy_api_url = self.ConfigObj.RPC_URL - self.nonce = self.get_nonce() - - def _get_transaction_info(self) -> (int, int, int, int): - # Get current base fee for pending block - current_gas_price = self.web3.eth.get_block("pending").get("baseFeePerGas") - - # Get the current recommended priority fee from Alchemy, and increase it by our offset - current_max_priority_gas = ( - int( - self.get_max_priority_fee_per_gas_alchemy() - * self.ConfigObj.DEFAULT_GAS_PRICE_OFFSET - ) - if self.ConfigObj.NETWORK in ["ethereum", "coinbase_base"] - else 0 - ) - - # Get current block number - block_number = int(self.web3.eth.get_block("latest")["number"]) - - # Get current nonce for our account - nonce = self.get_nonce() - - return current_gas_price, current_max_priority_gas, block_number, nonce - - def _get_prices_info( - self, - current_gas_price: int, - gas_estimate: int, - expected_profit_usd: Decimal, - expected_profit_eth: Decimal, - raw_transaction: Any - ) -> (int, int, int, int): - # Multiply expected gas by 0.8 to account for actual gas usage vs expected. - gas_cost_eth = ( - Decimal(str(current_gas_price)) - * Decimal(str(gas_estimate)) - * Decimal(self.ConfigObj.EXPECTED_GAS_MODIFIER) - / Decimal("10") ** Decimal("18") - ) - - if self.ConfigObj.network.GAS_ORACLE_ADDRESS: - layer_one_gas_fee = self._get_layer_one_gas_fee(raw_transaction) - gas_cost_eth += layer_one_gas_fee - - # Gas cost in usd can be estimated using the profit usd/eth rate - gas_cost_usd = gas_cost_eth * expected_profit_usd / expected_profit_eth - - # Multiply by reward percentage, taken from the arb contract - adjusted_reward_eth = Decimal(Decimal(expected_profit_eth) * Decimal(self.ConfigObj.ARB_REWARD_PERCENTAGE)) - adjusted_reward_usd = adjusted_reward_eth * expected_profit_usd / expected_profit_eth - - return gas_cost_eth, gas_cost_usd, adjusted_reward_eth, adjusted_reward_usd + self.chain_id = self.cfg.w3.eth.chain_id + self.arb_contract = self.cfg.BANCOR_ARBITRAGE_CONTRACT + self.arb_rewards_portion = Decimal(self.cfg.ARB_REWARDS_PPM) / 1_000_000 + self.wallet_address = self.cfg.w3.eth.account.from_key(self.cfg.ETH_PRIVATE_KEY_BE_CAREFUL).address + + if self.cfg.NETWORK == self.cfg.NETWORK_ETHEREUM: + self.use_access_list = True + self.send_transaction = self._send_private_transaction + else: + self.use_access_list = False + self.send_transaction = self._send_regular_transaction def validate_and_submit_transaction( self, @@ -133,579 +61,145 @@ def validate_and_submit_transaction( src_address: str, expected_profit_gastkn: Decimal, expected_profit_usd: Decimal, - verbose: bool = False, - safety_override: bool = False, - log_object: Dict[str, Any] = None, - flashloan_struct: List[Dict[str, int or str]] = None, - ) -> Optional[Dict[str, Any]]: - """ - Validates and submits a transaction to the arb contract. - - Parameters - ---------- - - """ - - if expected_profit_gastkn < self.ConfigObj.DEFAULT_MIN_PROFIT_GAS_TOKEN: - self.ConfigObj.logger.info( - f"Transaction below minimum profit, reverting... /*_*\\" - ) - return None - - if verbose: - self.ConfigObj.logger.info( - "[helpers.txhelpers.validate_and_submit_transaction] Validating trade..." - ) - self.ConfigObj.logger.debug( - f"[helpers.txhelpers.validate_and_submit_transaction] \nRoute to execute: routes: {route_struct}, sourceAmount: {src_amt}, source token: {src_address}, expected profit in GAS TOKEN: {num_format(expected_profit_gastkn)} \n\n" - ) - - current_gas_price, current_max_priority_gas, block_number, nonce = self._get_transaction_info() - - arb_tx = self.build_transaction_with_gas( - routes=route_struct, - src_address=src_address, - src_amt=src_amt, - gas_price=current_gas_price, - max_priority_fee=current_max_priority_gas, - nonce=nonce, - test_fake_gas=False, - flashloan_struct=flashloan_struct, - ) - - if arb_tx is None: - self.ConfigObj.logger.info( - "[helpers.txhelpers.validate_and_submit_transaction] Failed to construct trade. " - "This is expected to happen occasionally, discarding..." - ) - return None - gas_estimate = arb_tx["gas"] - - if "maxFeePerGas" in arb_tx: - current_gas_price = arb_tx["maxFeePerGas"] - else: - current_gas_price = arb_tx["gasPrice"] - - signed_arb_tx = self.sign_transaction(arb_tx) - - gas_cost_eth, gas_cost_usd, adjusted_reward_eth, adjusted_reward_usd = self._get_prices_info( - current_gas_price, - gas_estimate, - expected_profit_usd, - expected_profit_gastkn, - signed_arb_tx.rawTransaction - ) - - transaction_log = { - "block_number": block_number, - "gas": gas_estimate, - "max_gas_fee_wei": current_gas_price, - "gas_cost_eth": num_format_float(gas_cost_eth), - "gas_cost_usd": +num_format_float(gas_cost_usd), - } - if "maxPriorityFeePerGas" in arb_tx: - transaction_log["base_fee_wei"] = ( - current_gas_price - arb_tx["maxPriorityFeePerGas"] - ) - transaction_log["priority_fee_wei"] = arb_tx["maxPriorityFeePerGas"] - - log_json = {**log_object, **transaction_log} - - self.ConfigObj.logger.info( - log_format(log_data=log_json, log_name="arb_with_gas") - ) - - if adjusted_reward_eth > gas_cost_eth or safety_override: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.validate_and_submit_transaction] Expected reward of {num_format(adjusted_reward_eth)} GAS TOKEN vs cost of {num_format(gas_cost_eth)} GAS TOKEN in gas, executing arb." - ) - self.ConfigObj.logger.info( - f"[helpers.txhelpers.validate_and_submit_transaction] Expected reward of {num_format(adjusted_reward_usd)} USD vs cost of {num_format(gas_cost_usd)} USD in gas, executing arb." - ) - - # Submit the transaction - if "tenderly" in self.web3.provider.endpoint_uri or self.ConfigObj.NETWORK != "ethereum": - tx_hash = self.submit_regular_transaction(signed_arb_tx) - else: - tx_hash = self.submit_private_transaction(signed_arb_tx, block_number) - self.ConfigObj.logger.info( - f"[helpers.txhelpers.validate_and_submit_transaction] Arbitrage executed, tx hash: {tx_hash}" - ) - return tx_hash - else: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.validate_and_submit_transaction] Gas price too expensive! profit of {num_format(adjusted_reward_eth)} GAS TOKEN vs gas cost of {num_format(gas_cost_eth)} GAS TOKEN. Abort, abort!\n\n" - ) - self.ConfigObj.logger.info( - f"[helpers.txhelpers.validate_and_submit_transaction] Gas price too expensive! profit of {num_format(adjusted_reward_usd)} USD vs gas cost of {num_format(gas_cost_usd)} USD. Abort, abort!\n\n" - ) - return None - - def get_access_list(self, transaction_data, expected_gas, eth_input=None): - """TODO: docstring""" - expected_gas = hex(expected_gas) - json_data = ( - { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_createAccessList", - "params": [ - { - "from": self.wallet_address, - "to": self.arb_contract.address, - "gas": expected_gas, - "data": transaction_data, - } - ], - } - if eth_input is None - else { - "id": 1, - "jsonrpc": "2.0", - "method": "eth_createAccessList", - "params": [ - { - "from": self.wallet_address, - "to": self.arb_contract.address, - "gas": expected_gas, - "value": hex(eth_input), - "data": transaction_data, - } - ], - } - ) - response = requests.post(self.alchemy_api_url, json=json_data) - if "failed to apply transaction" in response.text: - return None - else: - access_list = loads(response.text)["result"]["accessList"] - return access_list - - def construct_contract_function( - self, - routes: List[Dict[str, Any]], - src_amt: int, - src_address: str, - flashloan_struct=None, - ): + log_object: Dict[str, Any], + flashloan_struct: List[Dict] + ) -> Optional[str]: """ - Builds the transaction using the Arb Contract function. This version can generate transactions using flashloanAndArb and flashloanAndArbV2. - - routes: the routes to be used in the transaction - src_amt: the amount of the source token to be sent to the transaction - gas_price: the gas price to be used in the transaction - - returns: the transaction function ready to be submitted - """ - if self.ConfigObj.SELF_FUND: - return self.arb_contract.functions.fundAndArb( - routes, src_address, src_amt - ) - return self.arb_contract.functions.flashloanAndArbV2( - flashloan_struct, routes - ) - - def _handle_function_build_exception(self, exception: Exception) -> int or None: - """Handles exceptions that occur during transaction building. - - This method logs the exception and attempts to extract information from it. If the exception - indicates that the maximum fee per gas is less than the block base fee, it extracts and returns - the base fee. Otherwise, it logs a warning and returns None. + This method validates and submits a transaction to the arb contract. Args: - self: The instance of the class. - exception (Exception): The exception raised during transaction building. + route_struct: + src_amt: + src_address: + expected_profit_gastkn: + expected_profit_usd: + log_object: + flashloan_struct: Returns: - int or None: The base fee if it can be extracted from the exception, otherwise None. - + The hash of the transaction if submitted, None otherwise. """ - self.ConfigObj.logger.debug( - f"[helpers.txhelpers.build_transaction_with_gas] Error when building transaction: {exception.__class__.__name__} {exception}" + + self.cfg.logger.info("[helpers.txhelpers.validate_and_submit_transaction] Validating trade...") + self.cfg.logger.debug( + f"[helpers.txhelpers.validate_and_submit_transaction]:\n" + f"- Routes: {route_struct}\n" + f"- Source amount: {src_amt}\n" + f"- Source token: {src_address}\n" + f"- Expected profit: {num_format(expected_profit_gastkn)} GAS token ({num_format(expected_profit_usd)} USD)\n" ) - if "max fee per gas less than block base fee" in str(exception): - message = str(exception) - return int_prefix(message.split("baseFee: ")[1]) + if self.cfg.SELF_FUND: + fn_name = "fundAndArb" + args = [route_struct, src_address, src_amt] + value = src_amt if src_address == self.cfg.NATIVE_GAS_TOKEN_ADDRESS else 0 else: - self.ConfigObj.logger.warning( - f"[helpers.txhelpers.build_transaction_with_gas] (***2***) \n" - f"Error when building transaction, this is expected to happen occasionally, discarding. Exception: {exception.__class__.__name__} {exception}" - ) - return None - - def build_transaction_with_gas( - self, - routes: List[Dict[str, Any]], - src_amt: int, - src_address: str, - gas_price: int, - max_priority_fee: int, - nonce: int, - access_list: bool = True, - test_fake_gas: bool = False, - flashloan_struct: List[Dict[str, int or str]] = None, - ): - """ - Builds the transaction to be submitted to the blockchain. - - routes: the routes to be used in the transaction - src_amt: the amount of the source token to be sent to the transaction - gas_price: the gas price to be used in the transaction + fn_name = "flashloanAndArbV2" + args = [flashloan_struct, route_struct] + value = 0 - returns: the transaction to be submitted to the blockchain - """ - value = src_amt if (src_address == self.ConfigObj.NATIVE_GAS_TOKEN_ADDRESS and self.ConfigObj.SELF_FUND) else 0 - transaction = self.build_transaction_generic( - self.construct_contract_function, - routes, - src_amt, - src_address, - flashloan_struct, - gas_price=gas_price, - max_priority_fee=max_priority_fee, - nonce=nonce, - value=value - ) - if transaction is None: - return None - - if test_fake_gas: - transaction["gas"] = self.ConfigObj.DEFAULT_GAS - return transaction + tx = self._create_transaction(self.arb_contract, fn_name, args, value) try: - estimated_gas = int( - self.web3.eth.estimate_gas(transaction=transaction) - + self.ConfigObj.DEFAULT_GAS_SAFETY_OFFSET - ) + self._update_transaction(tx) except Exception as e: - self.ConfigObj.logger.warning( - f"[helpers.txhelpers.build_transaction_with_gas] Failed to estimate gas for transaction because the " - f"transaction is likely fail. Most often this is due to an arb opportunity already being closed, " - f"but it can include other bugs. This is expected to happen occasionally, discarding. Exception: {e}" - ) + self.cfg.logger.info(f"Transaction {dumps(tx, indent=4)}\nGas estimation failed with {e}") return None - try: - if access_list and self.ConfigObj.NETWORK_NAME in "ethereum": - access_list = self.get_access_list( - transaction_data=transaction["data"], expected_gas=estimated_gas - ) - - if access_list is not None: - transaction_after = transaction - transaction_after["accessList"] = access_list - self.ConfigObj.logger.debug( - f"[helpers.txhelpers.build_transaction_with_gas] Transaction after access list: {transaction}" - ) - estimated_gas_after = ( - self.web3.eth.estimate_gas(transaction=transaction_after) - + self.ConfigObj.DEFAULT_GAS_SAFETY_OFFSET - ) - self.ConfigObj.logger.debug( - f"[helpers.txhelpers.build_transaction_with_gas] gas before access list: {estimated_gas}, after access list: {estimated_gas_after}" - ) - if estimated_gas_after is not None: - if estimated_gas_after < estimated_gas: - transaction = transaction_after - estimated_gas = estimated_gas_after - else: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.build_transaction_with_gas] Failed to apply access list to transaction" - ) - except Exception as e: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.build_transaction_with_gas] Failed to add Access List to transaction. This should not invalidate the transaction. Exception: {e}" - ) - transaction["gas"] = estimated_gas - return transaction - - def get_nonce(self): - """ - Returns the nonce of the wallet address. - """ - return self.web3.eth.get_transaction_count(self.wallet_address) - - def build_tx( - self, - nonce: int, - gas_price: int = 0, - max_priority_fee: int = 0, - value: int = 0 - ) -> Dict[str, Any]: - """ - Builds the transaction to be submitted to the blockchain. - - maxFeePerGas: the maximum gas price to be paid for the transaction - maxPriorityFeePerGas: the maximum miner tip to be given for the transaction - value: The amount of ETH to send - only relevant if not using Flashloans - The following condition must be met: - maxFeePerGas <= baseFee + maxPriorityFeePerGas - - returns: the transaction to be submitted to the blockchain - """ - max_priority_fee = int(max_priority_fee) - base_gas_price = int(gas_price) - max_gas_price = base_gas_price + max_priority_fee - - if self.ConfigObj.NETWORK == self.ConfigObj.NETWORK_TENDERLY: - self.wallet_address = self.ConfigObj.BINANCE14_WALLET_ADDRESS - - # if "tenderly" in self.web3.provider.endpoint_uri: - # print("Tenderly network detected: Manually setting maxFeePerFas and maxPriorityFeePerGas") - # max_gas_price = 3 - # max_priority_fee = 3 - - if self.ConfigObj.NETWORK in ["ethereum", "coinbase_base"]: - tx_details = { - "type": "0x2", - "maxFeePerGas": max_gas_price, - "maxPriorityFeePerGas": max_priority_fee, - "from": self.wallet_address, - "nonce": nonce, - } - else: - tx_details = { - "gasPrice": max_gas_price, - "from": self.wallet_address, - "nonce": nonce, - } - tx_details["value"] = value - return tx_details - - def submit_regular_transaction(self, signed_tx) -> str: - """ - Submits the transaction to the blockchain. - :param signed_tx: the signed transaction to be submitted to the blockchain + tx["gas"] += self.cfg.DEFAULT_GAS_SAFETY_OFFSET - returns: the transaction hash of the submitted transaction - """ + raw_tx = self._sign_transaction(tx) - self.ConfigObj.logger.info( - f"[helpers.txhelpers.submit_regular_transaction] Attempting to submit transaction {signed_tx}" - ) + gas_cost_wei = tx["gas"] * tx["maxFeePerGas"] + if self.cfg.network.GAS_ORACLE_ADDRESS: + gas_cost_wei += self.cfg.GAS_ORACLE_CONTRACT.caller.getL1Fee(raw_tx) - return self._submit_transaction(self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)) + gas_cost_eth = Decimal(gas_cost_wei) / ETH_RESOLUTION + gas_cost_usd = gas_cost_eth * expected_profit_usd / expected_profit_gastkn - def submit_private_transaction(self, signed_tx, block_number: int) -> str: - """ - Submits the transaction privately through Alchemy -> Flashbots RPC to mitigate frontrunning. + gas_gain_eth = self.arb_rewards_portion * expected_profit_gastkn + gas_gain_usd = self.arb_rewards_portion * expected_profit_usd - :param signed_tx: the signed transaction to be submitted to the blockchain - :param block_number: the current block number + self.cfg.logger.info(log_format(log_name="arb_with_gas", log_data={**log_object, "tx": tx})) - returns: The transaction receipt, or None if the transaction failed - """ - - self.ConfigObj.logger.info( - f"[helpers.txhelpers.submit_private_transaction] Attempting to submit transaction to Flashbots" - ) - - params = [ - { - "tx": signed_tx.rawTransaction.hex(), - "maxBlockNumber": hex(block_number + 10), - "preferences": {"fast": True}, - } - ] - - response = self.alchemy.core.provider.make_request( - method="eth_sendPrivateTransaction", - params=params, - method_name="eth_sendPrivateTransaction", - headers=self._get_headers, + self.cfg.logger.info( + f"[helpers.txhelpers.validate_and_submit_transaction]:\n" + f"- Expected cost: {num_format(gas_cost_eth)} GAS token ({num_format(gas_cost_usd)} USD)\n" + f"- Expected gain: {num_format(gas_gain_eth)} GAS token ({num_format(gas_gain_usd)} USD)\n" ) - if response != 400: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.submit_private_transaction] Submitted transaction to Flashbots succeeded" - ) - return self._submit_transaction(response.get("result")) - else: - self.ConfigObj.logger.info( - f"[helpers.txhelpers.submit_private_transaction] Submitted transaction to Flashbots failed with response = {response}" - ) - return None - - def _submit_transaction(self, tx_hash) -> str: - try: - tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash) - assert tx_hash == tx_receipt["transactionHash"] + if gas_gain_eth > gas_cost_eth: + self.cfg.logger.info("Executing profitable arb transaction") + tx_hash = self.send_transaction(raw_tx) + self._wait_for_transaction_receipt(tx_hash) return tx_hash - except TimeExhausted as e: - self.ConfigObj.logger.info( - f"[helpers.txhelpers._submit_transaction] Transaction timeout (stuck in mempool); moving on" - ) + else: + self.cfg.logger.info("Discarding non-profitable arb transaction") return None - def sign_transaction(self, transaction: Dict[str, Any]) -> Dict[str, Any]: + def check_and_approve_tokens(self, tokens: List): """ - Signs the transaction. - - transaction: the transaction to be signed - - returns: the signed transaction - """ - return self.web3.eth.account.sign_transaction( - transaction, self.ConfigObj.ETH_PRIVATE_KEY_BE_CAREFUL - ) + This method checks if tokens have been previously approved from the wallet address to the Arbitrage contract. + If they are not already approved, it will submit approvals for each token specified in the given list of tokens. - @property - def _get_alchemy_url(self): - """ - Returns the Alchemy API URL with attached API key - """ - return self.alchemy_api_url - - @property - def _get_headers(self): - """ - Returns the headers for the API call - """ - return {"accept": "application/json", "content-type": "application/json"} + Args: + tokens: A list of tokens to check/approve + """ + + for token_address in [token for token in tokens if token != self.cfg.NATIVE_GAS_TOKEN_ADDRESS]: + token_contract = self.cfg.w3.eth.contract(address=token_address, abi=ERC20_ABI) + allowance = token_contract.caller.allowance(self.wallet_address, self.arb_contract.address) + self.cfg.logger.info(f"Remaining allowance for token {token_address} = {allowance}") + if allowance == 0: + tx = self._create_transaction(token_contract, "approve", [self.arb_contract.address, MAX_UINT256], 0) + self._update_transaction(tx) + raw_tx = self._sign_transaction(tx) + tx_hash = self._send_regular_transaction(raw_tx) + self._wait_for_transaction_receipt(tx_hash) + + def _create_transaction(self, contract, fn_name: str, args: list, value: int) -> dict: + return { + "type": 2, + "value": value, + "chainId": self.chain_id, + "from": self.wallet_address, + "to": contract.address, + "data": contract.encode_abi(fn_name=fn_name, args=args), + "nonce": self.cfg.w3.eth.get_transaction_count(self.wallet_address) + } - @staticmethod - def _get_payload(method: str, params: [] = None) -> Dict: - """ - Generates the request payload for the API call. If the method is "eth_estimateGas", it attaches the params - :param method: the API method to call - """ + def _update_transaction(self, tx: dict): + tx["gas"] = self.cfg.w3.eth.estimate_gas(tx) # occasionally throws an exception + if self.use_access_list: + result = self.cfg.w3.eth.create_access_list(tx) # rarely throws an exception + if tx["gas"] > result["gasUsed"]: + tx["gas"] = result["gasUsed"] + tx["accessList"] = [ + { + "address": access_item["address"], + "storageKeys": [storage_key.hex() for storage_key in access_item["storageKeys"]] + } + for access_item in result["accessList"] + ] + tx.update(self.cfg.network.gas_strategy(self.cfg.w3)) - if method == "eth_estimateGas" or method == "eth_sendPrivateTransaction": - return {"id": 1, "jsonrpc": "2.0", "method": method, "params": params} - else: - return {"id": 1, "jsonrpc": "2.0", "method": method} + def _sign_transaction(self, tx: dict) -> str: + return self.cfg.w3.eth.account.sign_transaction(tx, self.cfg.ETH_PRIVATE_KEY_BE_CAREFUL).rawTransaction - def _query_alchemy_api_gas_methods(self, method: str, params: list = None): - """ - This queries the Alchemy API for a gas-related call which returns a Hex String. - The Hex String can be decoded by casting it as an int like so: int(hex_str, 16) + def _send_regular_transaction(self, raw_tx: str) -> str: + return self.cfg.w3.eth.send_raw_transaction(raw_tx).hex() - :param method: the API method to call - """ - response = requests.post( - self.alchemy_api_url, - json=self._get_payload(method=method, params=params), - headers=self._get_headers, + def _send_private_transaction(self, raw_tx: str) -> str: + response = self.cfg.w3.provider.make_request( + method="eth_sendPrivateTransaction", + params=[{"tx": raw_tx, "maxBlockNumber": hex(self.cfg.w3.eth.block_number + 10), "preferences": {"fast": True}}] ) - return int(loads(response.text)["result"].split("0x")[1], 16) - - def get_max_priority_fee_per_gas_alchemy(self): - """ - Queries the Alchemy API to get an estimated max priority fee per gas - """ - result = self._query_alchemy_api_gas_methods(method="eth_maxPriorityFeePerGas") - return result - - def get_eth_gas_price_alchemy(self): - """ - Returns an estimated gas price for the upcoming block - """ - return self._query_alchemy_api_gas_methods(method="eth_gasPrice") - - def check_if_token_approved(self, token_address: str, owner_address = None, spender_address = None) -> bool: - """ - This function checks if a token has already been approved. - :param token_address: the token to approve - :param owner_address: Optional param for specific debugging, otherwise it will be automatically set to the wallet address - :param spender_address: Optional param for specific debugging, otherwise it will be set to the arb contract address + assert "result" in response, f"Private transaction failed: {dumps(response, indent=4)}" + return response["result"] - returns: - bool - """ - owner_address = self.wallet_address if owner_address is None else owner_address - if self.ConfigObj.NETWORK == self.ConfigObj.NETWORK_TENDERLY: - owner_address = self.ConfigObj.BINANCE14_WALLET_ADDRESS - - spender_address = self.arb_contract.address if spender_address is None else spender_address - - token_contract = self.web3.eth.contract(address=token_address, abi=ERC20_ABI) - - allowance = token_contract.caller.allowance(owner_address, spender_address) - if type(allowance) == int: - if allowance > 0: - return True - return False - else: - return False - - def build_transaction_generic(self, contract_function, *args, **kwargs): - """Builds a transaction using a contract function with the provided arguments. - - This function attempts to construct a transaction by calling the given contract function - with the provided arguments and keyword arguments. If an exception occurs during the - transaction construction, it adjusts the gas price and retries the function call. - - Args: - self: The instance of the class. - contract_function: The contract function to be called to construct the transaction. - *args: Positional arguments to be passed to the contract function. - **kwargs: Keyword arguments to be passed to the contract function. - - Returns: - The constructed transaction if successful, otherwise None. - - """ + def _wait_for_transaction_receipt(self, tx_hash: str): try: - transaction = contract_function(*args).build_transaction(self.build_tx(**kwargs)) - except Exception as e: - new_base_fee = self._handle_function_build_exception(exception=e) - if new_base_fee is None: - return None - try: - kwargs['gas_price'] = new_base_fee - transaction = contract_function(*args).build_transaction(self.build_tx(**kwargs)) - except Exception as e: - self.ConfigObj.logger.warning( - f" Error when building transaction, this is expected to happen occasionally, discarding. (***1***)\n" - f"Exception: {e.__class__.__name__} {e}" - ) - return None - return transaction - - def approve_token_for_arb_contract(self, token_address: str, approval_amount: int = 115792089237316195423570985008687907853269984665640564039457584007913129639935): - """ - This function submits a token approval to the Arb Contract. The default approval amount is the max approval. - :param token_address: the token to approve - :param approval_amount: the amount to approve. This is set to the max possible by default - - returns: - transaction hash - """ - current_gas_price = self.web3.eth.get_block("pending").get("baseFeePerGas") - max_priority = int(self.get_max_priority_fee_per_gas_alchemy()) if self.ConfigObj.NETWORK in ["ethereum", "coinbase_base"] else 0 - - token_contract = self.web3.eth.contract(address=token_address, abi=ERC20_ABI) - - - approve_tx = self.build_transaction_generic( - token_contract.functions.approve, - self.arb_contract.address, - approval_amount, - gas_price=current_gas_price, - max_priority_fee=max_priority, - nonce=self.get_nonce(), - ) - if approve_tx is None: - self.ConfigObj.logger.info(f"*****Failed to submit approval for token: {token_address}!*****") - return None - self.ConfigObj.logger.info(f"Submitting approval for token: {token_address}") - - return self.submit_regular_transaction(self.sign_transaction(approve_tx)) - - def _get_layer_one_gas_fee(self, raw_transaction) -> Decimal: - """ - Returns the expected layer one gas fee for a Mantle, Base, and Optimism transaction - - Args: - raw_transaction: the raw transaction - - Returns: - Decimal: the expected layer one gas fee - """ - - l1_data_fee = asyncio.get_event_loop().run_until_complete( - asyncio.gather(self.ConfigObj.GAS_ORACLE_CONTRACT.caller.getL1Fee(raw_transaction)))[0] - # Dividing by 10 ** 18 in order to convert from wei resolution to native-token resolution - return Decimal(f"{l1_data_fee}e-18") + tx_receipt = self.cfg.w3.eth.wait_for_transaction_receipt(tx_hash) + self.cfg.logger.info(f"Transaction {tx_hash} completed: {dumps(tx_receipt, indent=4)}") + except TimeExhausted as _: + self.cfg.logger.info(f"Transaction {tx_hash} stuck in mempool; moving on") diff --git a/main.py b/main.py index 5c9c86bdf..645a0df85 100644 --- a/main.py +++ b/main.py @@ -164,7 +164,7 @@ def main(args: argparse.Namespace) -> None: args.flashloan_tokens = handle_flashloan_tokens(cfg, args.flashloan_tokens, tokens) if args.self_fund: - check_and_approve_tokens(tokens=args.flashloan_tokens, cfg=cfg) + check_and_approve_tokens(cfg=cfg, tokens=args.flashloan_tokens) # Search the logging directory for the latest timestamped folder args.logging_path = find_latest_timestamped_folder(args.logging_path) diff --git a/poetry.lock b/poetry.lock index 67c014f6a..fb525f280 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3243,4 +3243,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "9acff65426ac864da069ddc934a0dbf548241df25be1bf4a3ebc126c28367d89" +content-hash = "149e85dd34f992c56480d1882a25ed437269561fb0a0cb2f7d567d4eb7248c5f" diff --git a/pyproject.toml b/pyproject.toml index 027e94502..8eab7d7fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ hexbytes = "^0.3.1" setuptools = "^67.6.1" protobuf = "^4.24.4" tqdm = "^4.64.1" -web3 = "^6.11.2" +web3 = "^6.16.0" nest-asyncio = "^1.5.8" diff --git a/resources/FAQ.md b/resources/FAQ.md index a6d3e31c9..3aa32fe40 100644 --- a/resources/FAQ.md +++ b/resources/FAQ.md @@ -6,7 +6,7 @@ ### **2. Using a Private Node or Infura with the Fastlane Bot** - **Question:** Can a private node or Infura be used with the Fastlane Bot? -- **Answer:** Yes. The bot can use any RPC. This can be edited in the fastlane_bot/config/providers file - search for: self.RPC_URL. Note that some functions require an Alchemy API key, making it necessary unless the functions themselves are modified. These functions are in fastlane_bot/helpers/txhelpers, and include get_access_list, submit_private_transaction, and get_max_priority_fee_per_gas_alchemy. +- **Answer:** Yes. The bot can use any RPC. This can be edited in the fastlane_bot/config/providers file - search for: self.RPC_URL. Note that some functions require an Alchemy API key, making it necessary unless the functions themselves are modified. These functions are in fastlane_bot/helpers/txhelpers. ### **3. Arbitraging Specific Tokens using the Fastlane Bot** - **Question:** How can I search for arbitrage for specific tokens using the Fastlane Bot? diff --git a/resources/How_to_make_your_bot_competitive.md b/resources/How_to_make_your_bot_competitive.md index c158d068d..431dc6ba1 100644 --- a/resources/How_to_make_your_bot_competitive.md +++ b/resources/How_to_make_your_bot_competitive.md @@ -4,18 +4,17 @@ Typically only one bot-operator is able to close each arbitrage opportunity, making bot operation competitive in nature. The purpose of this document is to provide ideas on ways to improve your own bot. -### Gas Priority Fee Optimization -In config/network.py there is a constant: DEFAULT_GAS_PRICE_OFFSET. This is a multiplier that increases the priority fee paid by your TX. The higher it is, the more likely your TX will get executed, but the more the TX costs. It’s set to increase by 9% by default, which is likely insufficient to be competitive. You could also design a custom function to modify the priority fee based on profit - this could likely make you competitive, but would require some work. +### Gas Fee Optimization +Function `gas_strategy` (in config/network.py) allows you to implement your own gas-strategy. +It takes a `Web3` instance as input, and it should return a dictionary specifying `maxFeePerGas` and `maxPriorityFeePerGas` as output. +By default, it returns the network's current values (obtained by sending `eth_gasPrice` and `eth_maxPriorityFeePerGas` requests to the node). +The tradeoff for each transaction is between the expected execution time and the expected profit. +Higher values generally reduce the expected execution time, but they also reduce the expected profit. ### Data Throughput Faster data means faster cycles for the arbitrage bot. The easiest way to achieve this is by using a premium plan from a data provider such as Alchemy. -Another way to achieve this is by running an Ethereum node locally. The bot can be connected to a local Ethereum node fairly easily by changing the RPC_URL in the fastlane_bot/config/providers file. Note that some functions require an Alchemy API key, making Alchemy necessary unless the functions themselves are modified. - -These functions are in fastlane_bot/helpers/txhelpers, and include: -* **get_access_list:** this function is optional - using it saves around 5000 gas on average per transaction. -* **submit_private_transaction:** this function is not optional, however it's possible to submit transactions directly to Flashbots. See the Flashbots Documentation for more details: https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint -* **get_max_priority_fee_per_gas_alchemy:** this function is not optional, but could be replaced with custom priority fee logic. +Another way to achieve this is by running an Ethereum node locally. The bot can be connected to a local Ethereum node fairly easily by changing the RPC_URL in the fastlane_bot/config/providers file. Note that some functions require an Alchemy API key, making Alchemy necessary unless the functions themselves are modified. These functions are in fastlane_bot/helpers/txhelpers. ### New Arbitrage Modes Currently the bot is geared towards closing pariwise & triangular arbitrage on Bancor V3 & Carbon, but it can easily be generalized for all the exchanges it supports. To do this, you would need to create a new Arb Mode file, and design the combinations that are fed into the Optimizer. A lot of the heavy lifting here is already handled, but there may be a few specific changes you would need to make to get it to work. diff --git a/fastlane_bot/tests/test_059_TestNetworkInfoMultichain.py b/resources/NBTest/test_059_TestNetworkInfoMultichain.py similarity index 100% rename from fastlane_bot/tests/test_059_TestNetworkInfoMultichain.py rename to resources/NBTest/test_059_TestNetworkInfoMultichain.py diff --git a/fastlane_bot/tests/test_062_TestRouteHandler.py b/resources/NBTest/test_062_TestRouteHandler.py similarity index 100% rename from fastlane_bot/tests/test_062_TestRouteHandler.py rename to resources/NBTest/test_062_TestRouteHandler.py diff --git a/fastlane_bot/tests/test_069_TestTxHelpers.py b/resources/NBTest/test_069_TestTxHelpers.py similarity index 100% rename from fastlane_bot/tests/test_069_TestTxHelpers.py rename to resources/NBTest/test_069_TestTxHelpers.py