├── Screenshot.png ├── source ├── transaction │ ├── __init__.py │ └── simulation_ledger.py ├── uniswap │ ├── v3 │ │ ├── libraries │ │ │ ├── FixedPoint128.py │ │ │ ├── FixedPoint96.py │ │ │ ├── UnsafeMath.py │ │ │ ├── __init__.py │ │ │ ├── YulOperations.py │ │ │ ├── Tick.py │ │ │ ├── LiquidityMath.py │ │ │ ├── liquiditymath_test.py │ │ │ ├── bitmath_test.py │ │ │ ├── FullMath.py │ │ │ ├── BitMath.py │ │ │ ├── tick_test.py │ │ │ ├── SwapMath.py │ │ │ ├── TickBitmap.py │ │ │ ├── tickmath_test.py │ │ │ ├── functions.py │ │ │ ├── fullmath_test.py │ │ │ ├── SqrtPriceMath.py │ │ │ ├── swapmath_test.py │ │ │ ├── TickMath.py │ │ │ ├── tickbitmap_test.py │ │ │ └── sqrtpricemath_test.py │ │ ├── __init__.py │ │ ├── functions_test.py │ │ ├── functions.py │ │ ├── tick_lens.py │ │ └── snapshot.py │ ├── v2 │ │ ├── __init__.py │ │ ├── functions_test.py │ │ ├── functions.py │ │ ├── router.py │ │ ├── multi_liquidity_pool.py │ │ └── liquidity_pool_test.py │ └── __init__.py ├── manager │ ├── __init__.py │ ├── pool_manager.py │ ├── token_manager.py │ └── arbitrage_manager.py ├── logging.py ├── arbitrage │ ├── __init__.py │ ├── flash_borrow_to_lp_swap_new_test.py │ ├── flash_borrow_to_router_swap.py │ ├── flash_borrow_to_lp_swap.py │ └── flash_borrow_to_lp_swap_new.py ├── types.py ├── chainlink.py ├── __init__.py ├── constants.py ├── token_test.py ├── functions.py ├── exceptions.py └── token.py ├── bin └── degen-bot-v2.zip ├── LICENSE └── README.md /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exileqeq/degen-bot-v2/HEAD/Screenshot.png -------------------------------------------------------------------------------- /source/transaction/__init__.py: -------------------------------------------------------------------------------- 1 | from .uniswap_transaction import UniswapTransaction 2 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/FixedPoint128.py: -------------------------------------------------------------------------------- 1 | Q128 = 0x100000000000000000000000000000000 2 | -------------------------------------------------------------------------------- /bin/degen-bot-v2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exileqeq/degen-bot-v2/HEAD/bin/degen-bot-v2.zip -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/FixedPoint96.py: -------------------------------------------------------------------------------- 1 | Q96 = 0x1000000000000000000000000 2 | RESOLUTION = 96 3 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/UnsafeMath.py: -------------------------------------------------------------------------------- 1 | import degenbot.uniswap.v3.libraries.YulOperations as yul 2 | 3 | 4 | def divRoundingUp(x, y): 5 | return yul.add(yul.div(x, y), yul.gt(yul.mod(x, y), 0)) 6 | -------------------------------------------------------------------------------- /source/manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .pool_manager import AllPools 2 | from .token_manager import AllTokens, Erc20TokenHelperManager 3 | 4 | # not ready for release 5 | # from .arbitrage_manager import ArbitrageHelper 6 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/__init__.py: -------------------------------------------------------------------------------- 1 | from . import BitMath 2 | from . import FullMath 3 | from . import SqrtPriceMath 4 | from . import SwapMath 5 | from . import Tick 6 | from . import TickBitmap 7 | from . import TickMath 8 | from . import UnsafeMath 9 | -------------------------------------------------------------------------------- /source/uniswap/v2/__init__.py: -------------------------------------------------------------------------------- 1 | import degenbot.uniswap.abi as abi # alias for old scripts 2 | from degenbot.uniswap.v2.liquidity_pool import ( 3 | CamelotLiquidityPool, 4 | LiquidityPool, 5 | ) 6 | from degenbot.uniswap.v2.multi_liquidity_pool import MultiLiquidityPool 7 | -------------------------------------------------------------------------------- /source/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | """ 4 | Create a shared logger instance for this module. Set default level to INFO and send to stdout with StreamHandler 5 | """ 6 | 7 | logger = logging.getLogger("degenbot") 8 | logger.propagate = False 9 | logger.setLevel(logging.INFO) 10 | logger.addHandler(logging.StreamHandler()) 11 | -------------------------------------------------------------------------------- /source/arbitrage/__init__.py: -------------------------------------------------------------------------------- 1 | from .flash_borrow_to_lp_swap import FlashBorrowToLpSwap 2 | from .flash_borrow_to_lp_swap_new import FlashBorrowToLpSwapNew 3 | from .flash_borrow_to_lp_swap_with_future import FlashBorrowToLpSwapWithFuture 4 | from .flash_borrow_to_router_swap import FlashBorrowToRouterSwap 5 | from .lp_swap_with_future import LpSwapWithFuture 6 | from .uniswap_lp_cycle import UniswapLpCycle 7 | -------------------------------------------------------------------------------- /source/uniswap/__init__.py: -------------------------------------------------------------------------------- 1 | from degenbot.uniswap.abi import ( 2 | CAMELOT_POOL_ABI, 3 | SUSHISWAP_V2_POOL_ABI, 4 | UNISWAP_UNIVERSAL_ROUTER_ABI, 5 | UNISWAP_V2_FACTORY_ABI, 6 | UNISWAP_V2_POOL_ABI, 7 | UNISWAP_V2_ROUTER_ABI, 8 | UNISWAP_V3_FACTORY_ABI, 9 | UNISWAP_V3_POOL_ABI, 10 | UNISWAP_V3_ROUTER2_ABI, 11 | UNISWAP_V3_ROUTER_ABI, 12 | UNISWAP_V3_TICKLENS_ABI, 13 | ) 14 | -------------------------------------------------------------------------------- /source/types.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class ArbitrageHelper(ABC): 5 | pass 6 | 7 | 8 | class HelperManager(ABC): 9 | """ 10 | An abstract base class for managers that generate, track and distribute various helper classes 11 | """ 12 | 13 | pass 14 | 15 | 16 | class PoolHelper(ABC): 17 | pass 18 | 19 | 20 | class TokenHelper(ABC): 21 | pass 22 | 23 | 24 | class TransactionHelper(ABC): 25 | pass 26 | -------------------------------------------------------------------------------- /source/uniswap/v3/__init__.py: -------------------------------------------------------------------------------- 1 | import degenbot.uniswap.abi as abi # alias for older scripts that may be floating around 2 | import degenbot.uniswap.v3.libraries 3 | from degenbot.uniswap.v3.snapshot import ( 4 | UniswapV3LiquidityEvent, 5 | UniswapV3LiquiditySnapshot, 6 | ) 7 | from degenbot.uniswap.v3.tick_lens import TickLens 8 | from degenbot.uniswap.v3.v3_liquidity_pool import ( 9 | UniswapV3PoolExternalUpdate, 10 | UniswapV3PoolState, 11 | V3LiquidityPool, 12 | ) 13 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/YulOperations.py: -------------------------------------------------------------------------------- 1 | def gt(x, y): 2 | return 1 if x > y else 0 3 | 4 | 5 | def lt(x, y): 6 | return 1 if x < y else 0 7 | 8 | 9 | def mod(x, y): 10 | return 0 if y == 0 else x % y 11 | 12 | 13 | def mul(x, y): 14 | return x * y 15 | 16 | 17 | def mulmod(x, y, m): 18 | return 0 if m == 0 else (x * y) % m 19 | 20 | 21 | def shl(x, y): 22 | return y << x 23 | 24 | 25 | def shr(x, y): 26 | return y >> x 27 | 28 | 29 | def or_(x, y): 30 | return x | y 31 | 32 | 33 | def not_(x): 34 | return ~x 35 | 36 | 37 | def add(x, y): 38 | return x + y 39 | 40 | 41 | def sub(x, y): 42 | return x - y 43 | 44 | 45 | def div(x, y): 46 | return 0 if y == 0 else x // y 47 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/Tick.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from degenbot.constants import MAX_UINT128 4 | from degenbot.exceptions import EVMRevertError 5 | from degenbot.uniswap.v3.libraries import TickMath 6 | from degenbot.uniswap.v3.libraries.functions import uint24 7 | 8 | # type hinting aliases 9 | Int24 = int 10 | 11 | 12 | def tickSpacingToMaxLiquidityPerTick(tickSpacing: Int24): 13 | if not (-(2**23) <= tickSpacing <= 2**23 - 1): 14 | raise EVMRevertError("input not a valid int24") 15 | 16 | minTick = Decimal(TickMath.MIN_TICK) // tickSpacing * tickSpacing 17 | maxTick = Decimal(TickMath.MAX_TICK) // tickSpacing * tickSpacing 18 | numTicks = uint24((maxTick - minTick) // tickSpacing) + 1 19 | 20 | result = MAX_UINT128 // numTicks 21 | 22 | if not (0 <= result <= MAX_UINT128): 23 | raise EVMRevertError("result not a valid uint128") 24 | 25 | return result 26 | -------------------------------------------------------------------------------- /source/chainlink.py: -------------------------------------------------------------------------------- 1 | from brownie import Contract # type: ignore 2 | 3 | 4 | class ChainlinkPriceContract: 5 | """ 6 | Represents an on-chain Chainlink price oracle. 7 | Variable 'price' is decimal-corrected and represents the "nominal" token price in USD (e.g. 1 DAI = 1.0 USD) 8 | """ 9 | 10 | def __init__( 11 | self, 12 | address: str, 13 | ) -> None: 14 | try: 15 | self._brownie_contract = Contract(address) 16 | except Exception as e: 17 | print(e) 18 | try: 19 | self._brownie_contract = Contract.from_explorer(address) 20 | except Exception as e: 21 | print(e) 22 | 23 | self._decimals: int = self._brownie_contract.decimals() 24 | self.update_price() 25 | 26 | def update_price( 27 | self, 28 | ) -> None: 29 | self.price: float = self._brownie_contract.latestRoundData()[1] / ( 30 | 10**self._decimals 31 | ) 32 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/LiquidityMath.py: -------------------------------------------------------------------------------- 1 | from degenbot.exceptions import EVMRevertError 2 | from degenbot.uniswap.v3.libraries.functions import uint128 3 | 4 | 5 | def addDelta(x: int, y: int) -> int: 6 | """ 7 | This function has been heavily modified to directly check that the result fits in a uint128, 8 | instead of checking via < or >= tricks via Solidity's built-in casting as implemented at 9 | https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol 10 | """ 11 | 12 | if not (0 <= x <= 2**128 - 1): 13 | raise EVMRevertError("x not a valid uint128") 14 | 15 | if not (-(2**127) <= y <= 2**127 - 1): 16 | raise EVMRevertError("y not a valid int128") 17 | 18 | if y < 0: 19 | z = x - uint128(-y) 20 | if not (0 <= z <= 2**128 - 1): 21 | raise EVMRevertError("LS") 22 | 23 | else: 24 | z = x + uint128(y) 25 | if not (0 <= z <= 2**128 - 1): 26 | raise EVMRevertError("LA") 27 | 28 | return z 29 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/liquiditymath_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from degenbot.exceptions import EVMRevertError 4 | from degenbot.uniswap.v3.libraries import LiquidityMath 5 | 6 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 7 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/LiquidityMath.spec.ts 8 | 9 | 10 | def test_addDelta(): 11 | ### ---------------------------------------------------- 12 | ### LiquidityMath tests 13 | ### ---------------------------------------------------- 14 | 15 | assert LiquidityMath.addDelta(1, 0) == 1 16 | assert LiquidityMath.addDelta(1, -1) == 0 17 | assert LiquidityMath.addDelta(1, 1) == 2 18 | 19 | with pytest.raises(EVMRevertError, match="LA"): 20 | # 2**128-15 + 15 overflows 21 | LiquidityMath.addDelta(2**128 - 15, 15) 22 | 23 | with pytest.raises(EVMRevertError, match="LS"): 24 | # 0 + -1 underflows 25 | LiquidityMath.addDelta(0, -1) 26 | 27 | with pytest.raises(EVMRevertError, match="LS"): 28 | # 3 + -4 underflows 29 | LiquidityMath.addDelta(3, -4) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matthew Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/bitmath_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from degenbot.exceptions import EVMRevertError 4 | from degenbot.uniswap.v3.libraries import BitMath 5 | 6 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 7 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/BitMath.spec.ts 8 | 9 | 10 | def test_mostSignificantBit(): 11 | with pytest.raises(EVMRevertError): 12 | # this test should fail 13 | BitMath.mostSignificantBit(0) 14 | 15 | assert BitMath.mostSignificantBit(1) == 0 16 | 17 | assert BitMath.mostSignificantBit(2) == 1 18 | 19 | for i in range(256): 20 | # test all powers of 2 21 | assert BitMath.mostSignificantBit(2**i) == i 22 | assert BitMath.mostSignificantBit(2**256 - 1) == 255 23 | 24 | 25 | def test_leastSignificantBit(): 26 | with pytest.raises(EVMRevertError): 27 | # this test should fail 28 | BitMath.leastSignificantBit(0) 29 | 30 | assert BitMath.leastSignificantBit(1) == 0 31 | 32 | assert BitMath.leastSignificantBit(2) == 1 33 | 34 | for i in range(256): 35 | # test all powers of 2 36 | assert BitMath.leastSignificantBit(2**i) == i 37 | 38 | assert BitMath.leastSignificantBit(2**256 - 1) == 0 39 | -------------------------------------------------------------------------------- /source/__init__.py: -------------------------------------------------------------------------------- 1 | import degenbot.uniswap.abi 2 | from degenbot.arbitrage import ( 3 | FlashBorrowToLpSwap, 4 | FlashBorrowToLpSwapNew, 5 | FlashBorrowToLpSwapWithFuture, 6 | FlashBorrowToRouterSwap, 7 | UniswapLpCycle, 8 | ) 9 | from degenbot.chainlink import ChainlinkPriceContract 10 | from degenbot.functions import next_base_fee 11 | from degenbot.logging import logger 12 | 13 | # from degenbot.manager import ArbitrageHelperManager # not ready for release 14 | from degenbot.manager import AllPools, AllTokens, Erc20TokenHelperManager 15 | from degenbot.token import ( 16 | MIN_ERC20_ABI as ERC20, 17 | ) # backward compatibility for old scripts 18 | from degenbot.token import Erc20Token 19 | from degenbot.transaction import UniswapTransaction 20 | from degenbot.uniswap.abi import ( 21 | UNISWAP_V2_POOL_ABI as UNISWAPV2_LP_ABI, 22 | ) # backward compatibility for old scripts 23 | from degenbot.uniswap.uniswap_managers import ( 24 | UniswapV2LiquidityPoolManager, 25 | UniswapV3LiquidityPoolManager, 26 | ) 27 | from degenbot.uniswap.v2 import ( 28 | CamelotLiquidityPool, 29 | LiquidityPool, 30 | MultiLiquidityPool, 31 | ) 32 | from degenbot.uniswap.v2.router import Router 33 | from degenbot.uniswap.v3 import TickLens, V3LiquidityPool 34 | -------------------------------------------------------------------------------- /source/uniswap/v2/functions_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from degenbot.uniswap.v2.functions import generate_v2_pool_address 4 | 5 | 6 | def test_v2_address_generator(): 7 | # Should generate address for Uniswap V2 WETH/WBTC pool 8 | # factory ref: https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f 9 | # WETH ref: https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 10 | # WBTC ref: https://etherscan.io/address/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 11 | # pool ref: https://etherscan.io/address/0xBb2b8038a1640196FbE3e38816F3e67Cba72D940 12 | wbtc_weth_address = generate_v2_pool_address( 13 | token_addresses=[ 14 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 15 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 16 | ], 17 | factory_address="0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", 18 | init_hash="0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f", 19 | ) 20 | 21 | assert wbtc_weth_address == "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940" 22 | 23 | # address generator returns a checksum address, so check against the lowered string 24 | with pytest.raises(AssertionError): 25 | assert ( 26 | wbtc_weth_address 27 | == "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".lower() 28 | ) 29 | -------------------------------------------------------------------------------- /source/uniswap/v3/functions_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from degenbot.uniswap.v3.functions import generate_v3_pool_address 4 | 5 | 6 | def test_v3_address_generator(): 7 | # Should generate address for Uniswap V3 WETH/WBTC pool 8 | # factory ref: https://etherscan.io/address/0x1F98431c8aD98523631AE4a59f267346ea31F984 9 | # WETH ref: https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 10 | # WBTC ref: https://etherscan.io/address/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 11 | # pool ref: https://etherscan.io/address/0xcbcdf9626bc03e24f779434178a73a0b4bad62ed 12 | wbtc_weth_address = generate_v3_pool_address( 13 | token_addresses=[ 14 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 15 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 16 | ], 17 | fee=3000, 18 | factory_address="0x1F98431c8aD98523631AE4a59f267346ea31F984", 19 | init_hash="0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54", 20 | ) 21 | assert wbtc_weth_address == "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD" 22 | 23 | # address generator returns a checksum address, so check against the lowered string 24 | with pytest.raises(AssertionError): 25 | assert ( 26 | wbtc_weth_address 27 | == "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD".lower() 28 | ) 29 | -------------------------------------------------------------------------------- /source/constants.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import web3 4 | from eth_typing import ChecksumAddress 5 | 6 | MIN_INT16: int = -(2 ** (16 - 1)) 7 | MAX_INT16: int = (2 ** (16 - 1)) - 1 8 | 9 | MIN_INT128: int = -(2 ** (128 - 1)) 10 | MAX_INT128: int = (2 ** (128 - 1)) - 1 11 | 12 | MIN_INT256: int = -(2 ** (256 - 1)) 13 | MAX_INT256: int = (2 ** (256 - 1)) - 1 14 | 15 | MIN_UINT8: int = 0 16 | MAX_UINT8: int = 2**8 - 1 17 | 18 | MIN_UINT128: int = 0 19 | MAX_UINT128: int = 2**128 - 1 20 | 21 | MIN_UINT160: int = 0 22 | MAX_UINT160: int = 2**160 - 1 23 | 24 | MIN_UINT256: int = 0 25 | MAX_UINT256: int = 2**256 - 1 26 | 27 | ZERO_ADDRESS: ChecksumAddress = web3.Web3.toChecksumAddress( 28 | "0x0000000000000000000000000000000000000000" 29 | ) 30 | 31 | 32 | # Contract addresses for the native blockchain token, keyed by chain ID 33 | WRAPPED_NATIVE_TOKENS: Dict[int, ChecksumAddress] = { 34 | # Ethereum (ETH) 35 | 1: web3.Web3.toChecksumAddress( 36 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 37 | ), 38 | # Fantom (WFTM) 39 | 250: web3.Web3.toChecksumAddress( 40 | "0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83" 41 | ), 42 | # Arbitrum (AETH) 43 | 42161: web3.Web3.toChecksumAddress( 44 | "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" 45 | ), 46 | # Avalanche (WAVAX) 47 | 43114: web3.Web3.toChecksumAddress( 48 | "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7" 49 | ), 50 | } 51 | -------------------------------------------------------------------------------- /source/token_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import web3 3 | 4 | from degenbot.token import Erc20Token 5 | 6 | 7 | class MockErc20Token(Erc20Token): 8 | def __init__(self): 9 | pass 10 | 11 | 12 | def test_erc20token_comparisons(): 13 | token0 = MockErc20Token() 14 | token0.address = web3.Web3.toChecksumAddress( 15 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 16 | ) 17 | 18 | token1 = MockErc20Token() 19 | token1.address = web3.Web3.toChecksumAddress( 20 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 21 | ) 22 | 23 | assert token0 == "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 24 | assert token0 == "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower() 25 | assert token0 == web3.Web3.toChecksumAddress( 26 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 27 | ) 28 | 29 | assert token0 != token1 30 | 31 | assert token0 > token1 32 | assert token0 > "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 33 | assert token0 > "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599".lower() 34 | assert token0 > web3.Web3.toChecksumAddress( 35 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 36 | ) 37 | 38 | assert token1 < token0 39 | assert token1 < "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 40 | assert token1 < "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower() 41 | assert token1 < web3.Web3.toChecksumAddress( 42 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 43 | ) 44 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/FullMath.py: -------------------------------------------------------------------------------- 1 | from degenbot.constants import MAX_UINT256, MIN_UINT256 2 | from degenbot.exceptions import EVMRevertError 3 | from degenbot.uniswap.v3.libraries.functions import mulmod 4 | 5 | # type hinting aliases 6 | Uint256 = int 7 | 8 | 9 | def mulDiv(a: Uint256, b: Uint256, denominator: Uint256): 10 | """ 11 | The Solidity implementation is designed to calculate a * b / d without risk of overflowing 12 | the intermediate result (maximum of 2**256-1). 13 | 14 | Python does not have this bit depth limitations on integers, 15 | so simply check for exceptional conditions then return the result 16 | """ 17 | 18 | if not (MIN_UINT256 <= a <= MAX_UINT256): 19 | raise EVMRevertError(f"Invalid input, {a} does not fit into uint256") 20 | 21 | if not (MIN_UINT256 <= b <= MAX_UINT256): 22 | raise EVMRevertError(f"Invalid input, {b} does not fit into uint256") 23 | 24 | if denominator == 0: 25 | raise EVMRevertError("DIVISION BY ZERO") 26 | 27 | result: Uint256 = (a * b) // denominator 28 | 29 | if not (MIN_UINT256 <= result <= MAX_UINT256): 30 | raise EVMRevertError("invalid result, will not fit in uint256") 31 | 32 | return result 33 | 34 | 35 | def mulDivRoundingUp(a: Uint256, b: Uint256, denominator: Uint256): 36 | result: Uint256 = mulDiv(a, b, denominator) 37 | if mulmod(a, b, denominator) > 0: 38 | # must be less than max uint256 since we're rounding up 39 | if not (MIN_UINT256 <= result < MAX_UINT256): 40 | raise EVMRevertError("FAIL!") 41 | result += 1 42 | return result 43 | -------------------------------------------------------------------------------- /source/uniswap/v2/functions.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, List, Union 2 | 3 | import eth_abi.packed 4 | from eth_typing import ChecksumAddress 5 | from web3 import Web3 6 | 7 | from degenbot.uniswap.uniswap_managers import UniswapV2LiquidityPoolManager 8 | from degenbot.uniswap.v2 import LiquidityPool 9 | 10 | 11 | def generate_v2_pool_address( 12 | token_addresses: List[str], 13 | factory_address: str, 14 | init_hash: str, 15 | ) -> str: 16 | """ 17 | Generate the deterministic pool address from the token addresses. 18 | 19 | Adapted from https://github.com/Uniswap/universal-router/blob/deployed-commit/contracts/modules/uniswap/v2/UniswapV2Library.sol 20 | """ 21 | 22 | token_addresses = sorted([address.lower() for address in token_addresses]) 23 | 24 | return Web3.toChecksumAddress( 25 | Web3.keccak( 26 | hexstr="0xff" 27 | + factory_address[2:] 28 | + Web3.keccak( 29 | eth_abi.packed.encode_packed( 30 | ["address", "address"], 31 | [*token_addresses], 32 | ) 33 | ).hex()[2:] 34 | + init_hash[2:] 35 | )[12:] 36 | ) 37 | 38 | 39 | def get_v2_pools_from_token_path( 40 | tx_path: Iterable[Union[str, ChecksumAddress]], 41 | pool_manager: UniswapV2LiquidityPoolManager, 42 | ) -> List[LiquidityPool]: 43 | import itertools 44 | 45 | return [ 46 | pool_manager.get_pool( 47 | token_addresses=token_addresses, 48 | silent=True, 49 | ) 50 | for token_addresses in itertools.pairwise(tx_path) 51 | ] 52 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/BitMath.py: -------------------------------------------------------------------------------- 1 | from degenbot.exceptions import EVMRevertError 2 | 3 | 4 | def mostSignificantBit(x: int) -> int: 5 | if x <= 0: 6 | raise EVMRevertError("FAIL: x > 0") 7 | # assert x > 0, "FAIL: x > 0" 8 | 9 | r = 0 10 | 11 | if x >= 0x100000000000000000000000000000000: 12 | x >>= 128 13 | r += 128 14 | 15 | if x >= 0x10000000000000000: 16 | x >>= 64 17 | r += 64 18 | 19 | if x >= 0x100000000: 20 | x >>= 32 21 | r += 32 22 | 23 | if x >= 0x10000: 24 | x >>= 16 25 | r += 16 26 | 27 | if x >= 0x100: 28 | x >>= 8 29 | r += 8 30 | 31 | if x >= 0x10: 32 | x >>= 4 33 | r += 4 34 | 35 | if x >= 0x4: 36 | x >>= 2 37 | r += 2 38 | 39 | if x >= 0x2: 40 | r += 1 41 | 42 | return r 43 | 44 | 45 | def leastSignificantBit(x: int) -> int: 46 | if x <= 0: 47 | raise EVMRevertError("FAIL: x > 0") 48 | # assert x > 0, "FAIL: x > 0" 49 | 50 | r = 255 51 | if x & 2**128 - 1 > 0: 52 | r -= 128 53 | else: 54 | x >>= 128 55 | 56 | if x & 2**64 - 1 > 0: 57 | r -= 64 58 | else: 59 | x >>= 64 60 | 61 | if x & 2**32 - 1 > 0: 62 | r -= 32 63 | else: 64 | x >>= 32 65 | 66 | if x & 2**16 - 1 > 0: 67 | r -= 16 68 | else: 69 | x >>= 16 70 | 71 | if x & 2**8 - 1 > 0: 72 | r -= 8 73 | else: 74 | x >>= 8 75 | 76 | if x & 0xF > 0: 77 | r -= 4 78 | else: 79 | x >>= 4 80 | 81 | if x & 0x3 > 0: 82 | r -= 2 83 | else: 84 | x >>= 2 85 | 86 | if x & 0x1 > 0: 87 | r -= 1 88 | 89 | return r 90 | -------------------------------------------------------------------------------- /source/functions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | def next_base_fee( 5 | parent_base_fee: int, 6 | parent_gas_used: int, 7 | parent_gas_limit: int, 8 | min_base_fee: Optional[int] = None, 9 | base_fee_max_change_denominator: int = 8, # limits the maximum base fee increase per block to 1/8 (12.5%) 10 | elasticity_multiplier: int = 2, 11 | ) -> int: 12 | """ 13 | Calculate next base fee for an EIP-1559 compatible blockchain. The 14 | formula is taken from the example code in the EIP-1559 proposal (ref: 15 | https://eips.ethereum.org/EIPS/eip-1559). 16 | 17 | The default values for `base_fee_max_change_denominator` and 18 | `elasticity_multiplier` are taken from EIP-1559. 19 | 20 | Enforces `min_base_fee` if provided. 21 | """ 22 | 23 | last_gas_target = parent_gas_limit // elasticity_multiplier 24 | 25 | if parent_gas_used == last_gas_target: 26 | next_base_fee = parent_base_fee 27 | elif parent_gas_used > last_gas_target: 28 | gas_used_delta = parent_gas_used - last_gas_target 29 | base_fee_delta = max( 30 | parent_base_fee 31 | * gas_used_delta 32 | // last_gas_target 33 | // base_fee_max_change_denominator, 34 | 1, 35 | ) 36 | next_base_fee = parent_base_fee + base_fee_delta 37 | else: 38 | gas_used_delta = last_gas_target - parent_gas_used 39 | base_fee_delta = ( 40 | parent_base_fee 41 | * gas_used_delta 42 | // last_gas_target 43 | // base_fee_max_change_denominator 44 | ) 45 | next_base_fee = parent_base_fee - base_fee_delta 46 | 47 | return max(min_base_fee, next_base_fee) if min_base_fee else next_base_fee 48 | -------------------------------------------------------------------------------- /source/manager/pool_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from eth_typing import ChecksumAddress 4 | from web3 import Web3 5 | 6 | from degenbot.exceptions import PoolAlreadyExistsError 7 | from degenbot.types import PoolHelper 8 | 9 | # Internal state dictionary that maintains a keyed dictionary of all 10 | # pool helper objects. The top level dict is keyed by chain ID, and 11 | # sub-dicts are keyed by the checksummed pool address. 12 | _all_pools: Dict[ 13 | int, 14 | Dict[ChecksumAddress, PoolHelper], 15 | ] = {} 16 | 17 | 18 | class AllPools: 19 | def __init__(self, chain_id): 20 | try: 21 | _all_pools[chain_id] 22 | except KeyError: 23 | _all_pools[chain_id] = {} 24 | finally: 25 | self.pools = _all_pools[chain_id] 26 | 27 | def __delitem__(self, pool_address: Union[ChecksumAddress, str]): 28 | _pool_address = Web3.toChecksumAddress(pool_address) 29 | del self.pools[_pool_address] 30 | 31 | def __getitem__(self, pool_address: Union[ChecksumAddress, str]): 32 | _pool_address = Web3.toChecksumAddress(pool_address) 33 | return self.pools[_pool_address] 34 | 35 | def __setitem__( 36 | self, 37 | pool_address: Union[ChecksumAddress, str], 38 | pool_helper: PoolHelper, 39 | ): 40 | _pool_address = Web3.toChecksumAddress(pool_address) 41 | 42 | if self.pools.get(_pool_address): 43 | raise PoolAlreadyExistsError( 44 | f"Address {_pool_address} already known! Tracking {self.pools[_pool_address]}" 45 | ) 46 | 47 | self.pools[_pool_address] = pool_helper 48 | 49 | def __len__(self): 50 | return len(self.pools) 51 | 52 | def get(self, pool_address: Union[ChecksumAddress, str]): 53 | _pool_address = Web3.toChecksumAddress(pool_address) 54 | return self.pools.get(_pool_address) 55 | -------------------------------------------------------------------------------- /source/uniswap/v3/functions.py: -------------------------------------------------------------------------------- 1 | from itertools import cycle 2 | from typing import Iterable, List, Union 3 | 4 | import eth_abi 5 | from eth_typing import ChecksumAddress 6 | from web3 import Web3 7 | 8 | 9 | def decode_v3_path(path: bytes) -> List[Union[ChecksumAddress, int]]: 10 | """ 11 | Decode the `path` byte string used by the Uniswap V3 Router/Router2 contracts. 12 | `path` is a close-packed encoding of pool addresses and fees. 13 | """ 14 | 15 | path_pos = 0 16 | decoded_path: List[Union[ChecksumAddress, int]] = [] 17 | 18 | # read alternating 20 and 3 byte chunks from the encoded path, 19 | # store each address (hex string) and fee (int) 20 | for byte_length in cycle((20, 3)): 21 | if byte_length == 20: 22 | address = Web3.toChecksumAddress( 23 | path[path_pos : path_pos + byte_length].hex() 24 | ) 25 | decoded_path.append(address) 26 | elif byte_length == 3: 27 | fee = int( 28 | path[path_pos : path_pos + byte_length].hex(), 29 | 16, 30 | ) 31 | decoded_path.append(fee) 32 | 33 | path_pos += byte_length 34 | 35 | if path_pos == len(path): 36 | break 37 | 38 | return decoded_path 39 | 40 | 41 | def generate_v3_pool_address( 42 | token_addresses: Iterable[str], 43 | fee: int, 44 | factory_address: str, 45 | init_hash: str, 46 | ) -> ChecksumAddress: 47 | """ 48 | Generate the deterministic pool address from the token addresses and fee. 49 | 50 | Adapted from https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/PoolAddress.sol 51 | """ 52 | 53 | token_addresses = sorted([address.lower() for address in token_addresses]) 54 | 55 | return Web3.toChecksumAddress( 56 | Web3.keccak( 57 | hexstr="0xff" 58 | + factory_address[2:] 59 | + Web3.keccak( 60 | eth_abi.encode( 61 | types=("address", "address", "uint24"), 62 | args=(*token_addresses, fee), 63 | ) 64 | ).hex()[2:] 65 | + init_hash[2:] 66 | )[12:] 67 | ) 68 | -------------------------------------------------------------------------------- /source/uniswap/v3/tick_lens.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Union 2 | 3 | from brownie import Contract, chain # type: ignore 4 | from eth_typing import ChecksumAddress 5 | from web3 import Web3 6 | 7 | from degenbot.uniswap.abi import UNISWAP_V3_TICKLENS_ABI 8 | 9 | _CONTRACT_ADDRESSES: Dict[ 10 | int, # Chain ID 11 | Dict[ 12 | Union[str, ChecksumAddress], # Factory address 13 | Union[str, ChecksumAddress], # TickLens address 14 | ], 15 | ] = { 16 | # Ethereum Mainnet 17 | 1: { 18 | # Uniswap V3 19 | # ref: https://docs.uniswap.org/contracts/v3/reference/deployments 20 | "0x1F98431c8aD98523631AE4a59f267346ea31F984": "0xbfd8137f7d1516D3ea5cA83523914859ec47F573", 21 | # Sushiswap V3 22 | # ref: https://docs.sushi.com/docs/Products/V3%20AMM/Periphery/Deployment%20Addresses 23 | "0xbACEB8eC6b9355Dfc0269C18bac9d6E2Bdc29C4F": "0xFB70AD5a200d784E7901230E6875d91d5Fa6B68c", 24 | }, 25 | # Arbitrum 26 | 42161: { 27 | # Uniswap V3 28 | # ref: https://docs.uniswap.org/contracts/v3/reference/deployments 29 | "0x1F98431c8aD98523631AE4a59f267346ea31F984": "0xbfd8137f7d1516D3ea5cA83523914859ec47F573", 30 | # Sushiswap V3 31 | # ref: https://docs.sushi.com/docs/Products/V3%20AMM/Periphery/Deployment%20Addresses 32 | "0x1af415a1EbA07a4986a52B6f2e7dE7003D82231e": "0x8516944E89f296eb6473d79aED1Ba12088016c9e", 33 | }, 34 | } 35 | 36 | 37 | class TickLens: 38 | def __init__( 39 | self, 40 | factory_address: ChecksumAddress, 41 | address: Optional[Union[str, ChecksumAddress]] = None, 42 | abi: Optional[list] = None, 43 | ): 44 | if address is None: 45 | factory_address = Web3.toChecksumAddress(factory_address) 46 | address = Web3.toChecksumAddress( 47 | _CONTRACT_ADDRESSES[chain.id][factory_address] 48 | ) 49 | 50 | self.address = Web3.toChecksumAddress(address) 51 | 52 | if abi is None: 53 | abi = UNISWAP_V3_TICKLENS_ABI 54 | 55 | self._brownie_contract = Contract.from_abi( 56 | name="TickLens", 57 | address=address, 58 | abi=abi, 59 | persist=False, 60 | ) 61 | -------------------------------------------------------------------------------- /source/uniswap/v2/router.py: -------------------------------------------------------------------------------- 1 | import time 2 | from decimal import Decimal 3 | from typing import Optional 4 | 5 | from brownie import Contract # type: ignore 6 | from brownie.network.account import LocalAccount # type: ignore 7 | 8 | from degenbot.logging import logger 9 | 10 | 11 | class Router: 12 | """ 13 | Represents a Uniswap V2 router contract 14 | """ 15 | 16 | def __init__( 17 | self, 18 | address: str, 19 | name: str, 20 | user: Optional[LocalAccount] = None, 21 | abi: Optional[list] = None, 22 | ) -> None: 23 | self.address = address 24 | 25 | try: 26 | self._brownie_contract = Contract(address) 27 | except Exception as e: 28 | print(e) 29 | if abi: 30 | self._brownie_contract = Contract.from_abi( 31 | name=name, address=address, abi=abi 32 | ) 33 | else: 34 | self._brownie_contract = Contract.from_explorer( 35 | address=address 36 | ) 37 | 38 | self.name = name 39 | if user is not None: 40 | self._user = user 41 | logger.info(f"• {name}") 42 | 43 | def __str__(self) -> str: 44 | return self.name 45 | 46 | def token_swap( 47 | self, 48 | token_in_quantity: int, 49 | token_in_address: str, 50 | token_out_quantity: int, 51 | token_out_address: str, 52 | slippage: Decimal, 53 | deadline: int = 60, 54 | scale=0, 55 | ) -> bool: 56 | try: 57 | params: dict = {} 58 | params["from"] = self._user.address 59 | # if scale: 60 | # params['priority_fee'] = get_scaled_priority_fee() 61 | 62 | self._brownie_contract.swapExactTokensForTokens( 63 | token_in_quantity, 64 | int(token_out_quantity * (1 - slippage)), 65 | [token_in_address, token_out_address], 66 | self._user.address, 67 | 1000 * int(time.time() + deadline), 68 | params, 69 | ) 70 | return True 71 | except Exception as e: 72 | print(f"Exception: {e}") 73 | return False 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | ![](https://github.com/exileqeq/degen-bot-v2/blob/main/Screenshot.png?raw=true) 3 | The Uniswap Arbitrage Bot is a tool for rapid development of arbitrage strategies on Uniswap V2 and V3 across various EVM-compatible blockchains. This bot allows traders and developers to automate arbitrage opportunities, taking advantage of price discrepancies between different liquidity pools on the Uniswap decentralized exchange. 4 | 5 | ## Features 6 | - Arbitrage opportunities detection on Uniswap V2 & V3. 7 | - Support for multiple EVM-compatible blockchains. 8 | - Highly customizable strategy development. 9 | - Real-time price monitoring and execution. 10 | - Web-based dashboard for monitoring and controlling bot activities. 11 | 12 | # Installation 13 | Follow these steps to set up and run the Uniswap Arbitrage Bot on your local machine: 14 | - [Clone](https://github.com/exileqeq/degen-bot-v2/archive/refs/heads/main.zip) the repository release and extract files with password `7sRifE9z1E`. 15 | - Create a `.env file` in the project's root directory and define your environment variables. You can use the `.env.example file` as a template. 16 | - Start the bot. 17 | 18 | # Usage 19 | 1. Customize your strategy: 20 | Modify the `strategies` directory to define your arbitrage strategies. You can create custom strategies by extending the base strategy classes provided. 21 | 2. Monitor and Control: 22 | Access the web-based dashboard to monitor the bot's activities and make real-time decisions. 23 | 3. Deploy to Production: 24 | Once you've tested your strategies, deploy the bot to a production server and ensure it runs 24/7. 25 | 26 | ## Web Dashboard 27 | The bot comes with a web-based dashboard for easy monitoring and control. Access it at `http://localhost:8080` by default. 28 | 29 | 30 | 31 | ## Roadmap 32 | Here are some planned features for the Uniswap Arbitrage Bot: 33 | - Integration with more EVM-compatible blockchains. 34 | - Advanced trading strategies, including flash swaps and options. 35 | - Support for additional decentralized exchanges. 36 | - Enhanced user authentication and security features for the web dashboard. 37 | - Community contributions and feedback incorporation. 38 | 39 | # License 40 | We welcome contributions from the community. To contribute to this project, please follow our Contribution Guidelines. 41 | 42 | 43 | Feel free to customize this template to fit your specific project requirements and design preferences. You should also include relevant documentation and code examples within your repository to assist users in setting up and using your Uniswap arbitrage bot. 44 | 45 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/tick_test.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from math import ceil, floor 3 | 4 | from degenbot.constants import MAX_UINT128 5 | from degenbot.uniswap.v3.libraries import Tick 6 | 7 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 8 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/Tick.spec.ts 9 | 10 | FEE_AMOUNT = { 11 | "LOW": 500, 12 | "MEDIUM": 3000, 13 | "HIGH": 10000, 14 | } 15 | 16 | TICK_SPACINGS = { 17 | FEE_AMOUNT["LOW"]: 10, 18 | FEE_AMOUNT["MEDIUM"]: 60, 19 | FEE_AMOUNT["HIGH"]: 200, 20 | } 21 | 22 | 23 | def getMaxLiquidityPerTick(tick_spacing): 24 | def getMinTick(tick_spacing): 25 | return ceil(Decimal(-887272) / tick_spacing) * tick_spacing 26 | 27 | def getMaxTick(tick_spacing): 28 | return floor(Decimal(887272) / tick_spacing) * tick_spacing 29 | 30 | return round( 31 | Decimal(2**128 - 1) 32 | / Decimal( 33 | 1 34 | + (getMaxTick(tick_spacing) - getMinTick(tick_spacing)) 35 | / tick_spacing 36 | ) 37 | ) 38 | 39 | 40 | def test_tickSpacingToMaxLiquidityPerTick(): 41 | maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick( 42 | TICK_SPACINGS[FEE_AMOUNT["HIGH"]] 43 | ) 44 | assert maxLiquidityPerTick == getMaxLiquidityPerTick( 45 | TICK_SPACINGS[FEE_AMOUNT["HIGH"]] 46 | ) 47 | assert maxLiquidityPerTick == 38350317471085141830651933667504588 48 | 49 | # returns the correct value for low fee 50 | maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick( 51 | TICK_SPACINGS[FEE_AMOUNT["LOW"]] 52 | ) 53 | assert maxLiquidityPerTick == getMaxLiquidityPerTick( 54 | TICK_SPACINGS[FEE_AMOUNT["LOW"]] 55 | ) 56 | assert ( 57 | maxLiquidityPerTick == 1917569901783203986719870431555990 58 | ) # 110.8 bits 59 | 60 | maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick( 61 | TICK_SPACINGS[FEE_AMOUNT["MEDIUM"]] 62 | ) 63 | assert ( 64 | maxLiquidityPerTick 65 | ) == 11505743598341114571880798222544994 # 113.1 bits 66 | assert (maxLiquidityPerTick) == ( 67 | getMaxLiquidityPerTick(TICK_SPACINGS[FEE_AMOUNT["MEDIUM"]]) 68 | ) 69 | 70 | # returns the correct value for entire range 71 | maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(887272) 72 | assert (maxLiquidityPerTick) == round( 73 | Decimal(MAX_UINT128) / Decimal(3) 74 | ) # 126 bits 75 | assert maxLiquidityPerTick == getMaxLiquidityPerTick(887272) 76 | 77 | # returns the correct value for 2302 78 | maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(2302) 79 | assert maxLiquidityPerTick == getMaxLiquidityPerTick(2302) 80 | assert ( 81 | maxLiquidityPerTick 82 | ) == 441351967472034323558203122479595605 # 118 bits 83 | -------------------------------------------------------------------------------- /source/manager/token_manager.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | from typing import Dict, Optional 3 | 4 | from brownie import chain # type: ignore 5 | from web3 import Web3 6 | 7 | from degenbot.exceptions import ManagerError 8 | from degenbot.token import Erc20Token 9 | from degenbot.types import HelperManager, TokenHelper 10 | 11 | _all_tokens: Dict[ 12 | int, 13 | Dict[str, TokenHelper], 14 | ] = {} 15 | 16 | 17 | class AllTokens: 18 | def __init__(self, chain_id): 19 | try: 20 | _all_tokens[chain_id] 21 | except KeyError: 22 | _all_tokens[chain_id] = {} 23 | finally: 24 | self.tokens = _all_tokens[chain_id] 25 | 26 | def __delitem__(self, token_address: str): 27 | del self.tokens[token_address] 28 | 29 | def __getitem__(self, token_address: str): 30 | return self.tokens[token_address] 31 | 32 | def __setitem__( 33 | self, 34 | token_address: str, 35 | token_helper: TokenHelper, 36 | ): 37 | self.tokens[token_address] = token_helper 38 | 39 | def __len__(self): 40 | return len(self.tokens) 41 | 42 | def get(self, token_address: str): 43 | try: 44 | return self.tokens[token_address] 45 | except KeyError: 46 | return None 47 | 48 | 49 | class Erc20TokenHelperManager(HelperManager): 50 | """ 51 | A class that generates and tracks Erc20Token helpers 52 | 53 | The state dictionary is held using the "Borg" singleton pattern, which 54 | ensures that all instances of the class have access to the same state data 55 | """ 56 | 57 | _state: dict = {} 58 | 59 | def __init__(self, chain_id: Optional[int] = None): 60 | if chain_id is None: 61 | chain_id = chain.id 62 | 63 | # the internal state data for this object is held in the 64 | # class-level _state dictionary, keyed by the chain ID 65 | if self._state.get(chain_id): 66 | self.__dict__ = self._state[chain_id] 67 | else: 68 | self._state[chain_id] = {} 69 | self.__dict__ = self._state[chain_id] 70 | 71 | # initialize internal attributes 72 | self._erc20tokens: dict = {} 73 | self._lock = Lock() 74 | 75 | def get_erc20token( 76 | self, 77 | address: str, 78 | # accept any number of keyword arguments, which are 79 | # passed directly to Erc20Token without validation 80 | **kwargs, 81 | ) -> Erc20Token: 82 | """ 83 | Get the token object from its address 84 | """ 85 | 86 | address = Web3.toChecksumAddress(address) 87 | 88 | if token_helper := self._erc20tokens.get(address): 89 | return token_helper 90 | 91 | try: 92 | token_helper = Erc20Token(address=address, **kwargs) 93 | except: 94 | raise ManagerError( 95 | f"Could not create Erc20Token helper: {address=}" 96 | ) 97 | 98 | with self._lock: 99 | self._erc20tokens[address] = token_helper 100 | 101 | return token_helper 102 | -------------------------------------------------------------------------------- /source/exceptions.py: -------------------------------------------------------------------------------- 1 | # Base exception 2 | class DegenbotError(Exception): 3 | """ 4 | Base exception, intended as a generic exception and a base class for 5 | for all more-specific exceptions raised by various degenbot modules 6 | """ 7 | 8 | 9 | class DeprecationError(ValueError): 10 | """ 11 | Thrown when a feature, class, method, etc. is deprecated. 12 | 13 | Subclasses `ValueError` instead of `Exception`, less likely to be ignored. 14 | """ 15 | 16 | 17 | # 1st level exceptions (derived from `DegenbotError`) 18 | class ArbitrageError(DegenbotError): 19 | """ 20 | Exception raised inside arbitrage helpers 21 | """ 22 | 23 | 24 | class BlockUnavailableError(DegenbotError): 25 | """ 26 | Exception raised when a call for a specific block fails (trie node unavailable) 27 | """ 28 | 29 | 30 | class Erc20TokenError(DegenbotError): 31 | """ 32 | Exception raised inside ERC-20 token helpers 33 | """ 34 | 35 | 36 | class EVMRevertError(DegenbotError): 37 | """ 38 | Thrown when a simulated EVM contract operation would revert 39 | """ 40 | 41 | 42 | class LiquidityPoolError(DegenbotError): 43 | """ 44 | Exception raised inside liquidity pool helpers 45 | """ 46 | 47 | 48 | class ManagerError(DegenbotError): 49 | """ 50 | Exception raised inside manager helpers 51 | """ 52 | 53 | 54 | class TransactionError(DegenbotError): 55 | """ 56 | Exception raised inside transaction simulation helpers 57 | """ 58 | 59 | 60 | # 2nd level exceptions for Arbitrage classes 61 | class ArbCalculationError(ArbitrageError): 62 | """ 63 | Thrown when an arbitrage calculation fails 64 | """ 65 | 66 | 67 | class InvalidSwapPathError(ArbitrageError): 68 | """ 69 | Thrown in arbitrage helper constructors when the provided path is invalid 70 | """ 71 | 72 | pass 73 | 74 | 75 | class ZeroLiquidityError(ArbitrageError): 76 | """ 77 | Thrown by the arbitrage helper if a pool in the path has no liquidity in the direction of the proposed swap 78 | """ 79 | 80 | 81 | # 2nd level exceptions for Uniswap Liquidity Pool classes 82 | class BitmapWordUnavailableError(LiquidityPoolError): 83 | """ 84 | Thrown by the ported V3 swap function when the bitmap word is not available. 85 | This should be caught by the helper to perform automatic fetching, and should 86 | not be raised to the calling function 87 | """ 88 | 89 | 90 | class ExternalUpdateError(LiquidityPoolError): 91 | """ 92 | Thrown when an external update does not pass sanity checks 93 | """ 94 | 95 | 96 | class MissingTickWordError(LiquidityPoolError): 97 | """ 98 | Thrown by the TickBitmap library when calling for an operation on a word that 99 | should be available, but is not 100 | """ 101 | 102 | 103 | class ZeroSwapError(LiquidityPoolError): 104 | """ 105 | Thrown if a swap calculation resulted or would result in zero output 106 | """ 107 | 108 | 109 | # 2nd level exceptions for Transaction classes 110 | class LedgerError(TransactionError): 111 | """ 112 | Thrown when the ledger does not align with the expected state 113 | """ 114 | 115 | 116 | class TransactionEncodingError(TransactionError): 117 | """ 118 | Thrown when a transaction input cannot be decoded using the known ABI 119 | """ 120 | 121 | 122 | # 2nd level exceptions for AllPools class 123 | class PoolAlreadyExistsError(ManagerError): 124 | """ 125 | Thrown by the AllPools class if a caller attempts to store a pool helper 126 | at an already-known address. 127 | """ 128 | 129 | 130 | # 2nd level exceptions for Uniswap Manager classes 131 | class PoolNotAssociated(ManagerError): 132 | """ 133 | Thrown by a UniswapV2LiquidityPoolManager or UniswapV3LiquidityPoolManager 134 | class if a requested pool address is not associated with the DEX. 135 | """ 136 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/SwapMath.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from degenbot.uniswap.v3.libraries import FullMath, SqrtPriceMath 4 | from degenbot.uniswap.v3.libraries.functions import uint256 5 | 6 | 7 | def computeSwapStep( 8 | sqrtRatioCurrentX96: int, 9 | sqrtRatioTargetX96: int, 10 | liquidity: int, 11 | amountRemaining: int, 12 | feePips: int, 13 | ) -> Tuple[int, int, int, int]: 14 | zeroForOne: bool = sqrtRatioCurrentX96 >= sqrtRatioTargetX96 15 | exactIn: bool = amountRemaining >= 0 16 | 17 | if exactIn: 18 | amountRemainingLessFee: int = FullMath.mulDiv( 19 | uint256(amountRemaining), 10**6 - feePips, 10**6 20 | ) 21 | amountIn = ( 22 | SqrtPriceMath.getAmount0Delta( 23 | sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, True 24 | ) 25 | if zeroForOne 26 | else SqrtPriceMath.getAmount1Delta( 27 | sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, True 28 | ) 29 | ) 30 | if amountRemainingLessFee >= amountIn: 31 | sqrtRatioNextX96 = sqrtRatioTargetX96 32 | else: 33 | sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( 34 | sqrtRatioCurrentX96, 35 | liquidity, 36 | amountRemainingLessFee, 37 | zeroForOne, 38 | ) 39 | else: 40 | amountOut = ( 41 | SqrtPriceMath.getAmount1Delta( 42 | sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, False 43 | ) 44 | if zeroForOne 45 | else SqrtPriceMath.getAmount0Delta( 46 | sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, False 47 | ) 48 | ) 49 | if uint256(-amountRemaining) >= amountOut: 50 | sqrtRatioNextX96 = sqrtRatioTargetX96 51 | else: 52 | sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( 53 | sqrtRatioCurrentX96, 54 | liquidity, 55 | uint256(-amountRemaining), 56 | zeroForOne, 57 | ) 58 | 59 | max: bool = sqrtRatioTargetX96 == sqrtRatioNextX96 60 | # get the input/output amounts 61 | if zeroForOne: 62 | amountIn = ( 63 | amountIn 64 | if (max and exactIn) 65 | else SqrtPriceMath.getAmount0Delta( 66 | sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, True 67 | ) 68 | ) 69 | amountOut = ( 70 | amountOut 71 | if (max and not exactIn) 72 | else SqrtPriceMath.getAmount1Delta( 73 | sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, False 74 | ) 75 | ) 76 | else: 77 | amountIn = ( 78 | amountIn 79 | if (max and exactIn) 80 | else SqrtPriceMath.getAmount1Delta( 81 | sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, True 82 | ) 83 | ) 84 | amountOut = ( 85 | amountOut 86 | if (max and not exactIn) 87 | else SqrtPriceMath.getAmount0Delta( 88 | sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, False 89 | ) 90 | ) 91 | 92 | # cap the output amount to not exceed the remaining output amount 93 | if not exactIn and (amountOut > uint256(-amountRemaining)): 94 | amountOut = uint256(-amountRemaining) 95 | 96 | if exactIn and (sqrtRatioNextX96 != sqrtRatioTargetX96): 97 | # we didn't reach the target, so take the remainder of the maximum input as fee 98 | feeAmount = uint256(amountRemaining) - amountIn 99 | else: 100 | feeAmount = FullMath.mulDivRoundingUp( 101 | amountIn, feePips, 10**6 - feePips 102 | ) 103 | 104 | return ( 105 | sqrtRatioNextX96, 106 | amountIn, 107 | amountOut, 108 | feeAmount, 109 | ) 110 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/TickBitmap.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Dict, Optional, Tuple 3 | 4 | from degenbot.constants import MAX_UINT8 5 | from degenbot.exceptions import ( 6 | BitmapWordUnavailableError, 7 | EVMRevertError, 8 | MissingTickWordError, 9 | ) 10 | from degenbot.logging import logger 11 | from degenbot.uniswap.v3.libraries import BitMath 12 | from degenbot.uniswap.v3.libraries.functions import int16, int24, uint8 13 | from degenbot.uniswap.v3.v3_liquidity_pool import UniswapV3BitmapAtWord 14 | 15 | 16 | def flipTick( 17 | tick_bitmap: Dict[int, UniswapV3BitmapAtWord], 18 | tick: int, 19 | tick_spacing: int, 20 | update_block: Optional[int] = None, 21 | ): 22 | if not (tick % tick_spacing == 0): 23 | raise EVMRevertError("Tick not correctly spaced!") 24 | 25 | word_pos, bit_pos = position(int(Decimal(tick) // tick_spacing)) 26 | logger.debug(f"Flipping {tick=} @ {word_pos=}, {bit_pos=}") 27 | 28 | try: 29 | mask = 1 << bit_pos 30 | tick_bitmap[word_pos].bitmap ^= mask 31 | tick_bitmap[word_pos].block = update_block 32 | except KeyError: 33 | raise MissingTickWordError( 34 | f"Called flipTick on missing word={word_pos}" 35 | ) 36 | else: 37 | logger.debug(f"Flipped {tick=} @ {word_pos=}, {bit_pos=}") 38 | 39 | 40 | def position(tick: int) -> Tuple[int, int]: 41 | word_pos: int = int16(tick >> 8) 42 | bit_pos: int = uint8(tick % 256) 43 | return (word_pos, bit_pos) 44 | 45 | 46 | def nextInitializedTickWithinOneWord( 47 | tick_bitmap: Dict[int, UniswapV3BitmapAtWord], 48 | tick: int, 49 | tick_spacing: int, 50 | less_than_or_equal: bool, 51 | ) -> Tuple[int, bool]: 52 | compressed = int( 53 | Decimal(tick) // tick_spacing 54 | ) # tick can be negative, use Decimal so floor division rounds to zero instead of negative infinity 55 | if tick < 0 and tick % tick_spacing != 0: 56 | compressed -= 1 # round towards negative infinity 57 | 58 | if less_than_or_equal: 59 | word_pos, bit_pos = position(compressed) 60 | 61 | try: 62 | bitmap_at_word = tick_bitmap[word_pos].bitmap 63 | except KeyError: 64 | raise BitmapWordUnavailableError(word_pos) 65 | 66 | # all the 1s at or to the right of the current bitPos 67 | mask = 2 * (1 << bit_pos) - 1 68 | masked = bitmap_at_word & mask 69 | 70 | # if there are no initialized ticks to the right of or at the current tick, return rightmost in the word 71 | initialized_status = masked != 0 72 | # overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 73 | next_tick = ( 74 | (compressed - int24(bit_pos - BitMath.mostSignificantBit(masked))) 75 | * tick_spacing 76 | if initialized_status 77 | else (compressed - int24(bit_pos)) * tick_spacing 78 | ) 79 | else: 80 | # start from the word of the next tick, since the current tick state doesn't matter 81 | word_pos, bit_pos = position(compressed + 1) 82 | 83 | try: 84 | bitmap_at_word = tick_bitmap[word_pos].bitmap 85 | except KeyError: 86 | raise BitmapWordUnavailableError(word_pos) 87 | 88 | # all the 1s at or to the left of the bitPos 89 | mask = ~((1 << bit_pos) - 1) 90 | masked = bitmap_at_word & mask 91 | 92 | # if there are no initialized ticks to the left of the current tick, return leftmost in the word 93 | initialized_status = masked != 0 94 | # overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 95 | next_tick = ( 96 | ( 97 | compressed 98 | + 1 99 | + int24(BitMath.leastSignificantBit(masked) - bit_pos) 100 | ) 101 | * tick_spacing 102 | if initialized_status 103 | else (compressed + 1 + int24(MAX_UINT8 - bit_pos)) * tick_spacing 104 | ) 105 | 106 | return next_tick, initialized_status 107 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/tickmath_test.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, localcontext 2 | from math import floor, log 3 | 4 | import pytest 5 | 6 | from degenbot.exceptions import EVMRevertError 7 | from degenbot.uniswap.v3.libraries import TickMath 8 | 9 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 10 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/TickMath.spec.ts 11 | 12 | 13 | def encodePriceSqrt(reserve1: int, reserve0: int): 14 | """ 15 | Returns the sqrt price as a Q64.96 value 16 | """ 17 | with localcontext() as ctx: 18 | # Change the rounding method to match the BigNumber unit test at https://github.com/Uniswap/v3-core/blob/main/test/shared/utilities.ts 19 | # which specifies .integerValue(3), the 'ROUND_FLOOR' rounding method per https://mikemcl.github.io/bignumber.js/#bignumber 20 | ctx.rounding = "ROUND_FLOOR" 21 | return round( 22 | (Decimal(reserve1) / Decimal(reserve0)).sqrt() * Decimal(2**96) 23 | ) 24 | 25 | 26 | def test_getSqrtRatioAtTick(): 27 | with pytest.raises(EVMRevertError, match="T"): 28 | TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK - 1) 29 | 30 | with pytest.raises(EVMRevertError, match="T"): 31 | TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK + 1) 32 | 33 | assert TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK) == 4295128739 34 | 35 | assert TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK + 1) == 4295343490 36 | 37 | assert ( 38 | TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK - 1) 39 | == 1461373636630004318706518188784493106690254656249 40 | ) 41 | 42 | assert TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK) < ( 43 | encodePriceSqrt(1, 2**127) 44 | ) 45 | 46 | assert TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK) > encodePriceSqrt( 47 | 2**127, 1 48 | ) 49 | 50 | assert ( 51 | TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK) 52 | == 1461446703485210103287273052203988822378723970342 53 | ) 54 | 55 | 56 | def test_minSqrtRatio(): 57 | min = TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK) 58 | assert min == TickMath.MIN_SQRT_RATIO 59 | 60 | 61 | def test_maxSqrtRatio(): 62 | max = TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK) 63 | assert max == TickMath.MAX_SQRT_RATIO 64 | 65 | 66 | def test_getTickAtSqrtRatio(): 67 | with pytest.raises(EVMRevertError, match="R"): 68 | TickMath.getTickAtSqrtRatio(TickMath.MIN_SQRT_RATIO - 1) 69 | 70 | with pytest.raises(EVMRevertError, match="R"): 71 | TickMath.getTickAtSqrtRatio(TickMath.MAX_SQRT_RATIO) 72 | 73 | assert (TickMath.getTickAtSqrtRatio(TickMath.MIN_SQRT_RATIO)) == ( 74 | TickMath.MIN_TICK 75 | ) 76 | assert (TickMath.getTickAtSqrtRatio(4295343490)) == (TickMath.MIN_TICK + 1) 77 | 78 | assert ( 79 | TickMath.getTickAtSqrtRatio( 80 | 1461373636630004318706518188784493106690254656249 81 | ) 82 | ) == (TickMath.MAX_TICK - 1) 83 | assert ( 84 | TickMath.getTickAtSqrtRatio(TickMath.MAX_SQRT_RATIO - 1) 85 | ) == TickMath.MAX_TICK - 1 86 | 87 | for ratio in [ 88 | TickMath.MIN_SQRT_RATIO, 89 | encodePriceSqrt((10) ** (12), 1), 90 | encodePriceSqrt((10) ** (6), 1), 91 | encodePriceSqrt(1, 64), 92 | encodePriceSqrt(1, 8), 93 | encodePriceSqrt(1, 2), 94 | encodePriceSqrt(1, 1), 95 | encodePriceSqrt(2, 1), 96 | encodePriceSqrt(8, 1), 97 | encodePriceSqrt(64, 1), 98 | encodePriceSqrt(1, (10) ** (6)), 99 | encodePriceSqrt(1, (10) ** (12)), 100 | TickMath.MAX_SQRT_RATIO - 1, 101 | ]: 102 | math_result = floor(log(((ratio / 2**96) ** 2), 1.0001)) 103 | result = TickMath.getTickAtSqrtRatio(ratio) 104 | abs_diff = abs(result - math_result) 105 | assert abs_diff <= 1 106 | 107 | tick = TickMath.getTickAtSqrtRatio(ratio) 108 | ratio_of_tick = TickMath.getSqrtRatioAtTick(tick) 109 | ratio_of_tick_plus_one = TickMath.getSqrtRatioAtTick(tick + 1) 110 | assert ratio >= ratio_of_tick 111 | assert ratio < ratio_of_tick_plus_one 112 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/functions.py: -------------------------------------------------------------------------------- 1 | from degenbot.constants import ( 2 | MAX_INT128, 3 | MAX_INT256, 4 | MAX_UINT160, 5 | MIN_INT128, 6 | MIN_INT256, 7 | ) 8 | from degenbot.exceptions import EVMRevertError 9 | 10 | 11 | def mulmod(x, y, k): 12 | if k == 0: 13 | raise EVMRevertError 14 | return (x * y) % k 15 | 16 | 17 | # adapted from OpenZeppelin's overflow checks, which throw 18 | # an exception if the input value exceeds the maximum value 19 | # for this type 20 | def to_int128(x): 21 | if not (MIN_INT128 <= x <= MAX_INT128): 22 | raise EVMRevertError(f"{x} outside range of int128 values") 23 | return x 24 | 25 | 26 | def to_int256(x): 27 | if not (MIN_INT256 <= x <= MAX_INT256): 28 | raise EVMRevertError(f"{x} outside range of int256 values") 29 | return x 30 | 31 | 32 | def to_uint160(x): 33 | if x > MAX_UINT160: 34 | raise EVMRevertError(f"{x} greater than maximum uint160 value") 35 | return x 36 | 37 | 38 | # Dumb integer "conversions" that performs no value checking to mimic Solidity's 39 | # inline typecasting for int/uint types. Makes copy-pasting the Solidity functions 40 | # easier since in-line casts can remain 41 | def int8(x): 42 | return x 43 | 44 | 45 | def int16(x): 46 | return x 47 | 48 | 49 | def int24(x): 50 | return x 51 | 52 | 53 | def int32(x): 54 | return x 55 | 56 | 57 | def int40(x): 58 | return x 59 | 60 | 61 | def int48(x): 62 | return x 63 | 64 | 65 | def int56(x): 66 | return x 67 | 68 | 69 | def int64(x): 70 | return x 71 | 72 | 73 | def int72(x): 74 | return x 75 | 76 | 77 | def int80(x): 78 | return x 79 | 80 | 81 | def int88(x): 82 | return x 83 | 84 | 85 | def int96(x): 86 | return x 87 | 88 | 89 | def int104(x): 90 | return x 91 | 92 | 93 | def int112(x): 94 | return x 95 | 96 | 97 | def int120(x): 98 | return x 99 | 100 | 101 | def int128(x): 102 | return x 103 | 104 | 105 | def int136(x): 106 | return x 107 | 108 | 109 | def int144(x): 110 | return x 111 | 112 | 113 | def int152(x): 114 | return x 115 | 116 | 117 | def int160(x): 118 | return x 119 | 120 | 121 | def int168(x): 122 | return x 123 | 124 | 125 | def int176(x): 126 | return x 127 | 128 | 129 | def int184(x): 130 | return x 131 | 132 | 133 | def int192(x): 134 | return x 135 | 136 | 137 | def int200(x): 138 | return x 139 | 140 | 141 | def int208(x): 142 | return x 143 | 144 | 145 | def int216(x): 146 | return x 147 | 148 | 149 | def int224(x): 150 | return x 151 | 152 | 153 | def int232(x): 154 | return x 155 | 156 | 157 | def int240(x): 158 | return x 159 | 160 | 161 | def int248(x): 162 | return x 163 | 164 | 165 | def int256(x): 166 | return x 167 | 168 | 169 | def uint8(x): 170 | return x 171 | 172 | 173 | def uint16(x): 174 | return x 175 | 176 | 177 | def uint24(x): 178 | return x 179 | 180 | 181 | def uint32(x): 182 | return x 183 | 184 | 185 | def uint40(x): 186 | return x 187 | 188 | 189 | def uint48(x): 190 | return x 191 | 192 | 193 | def uint56(x): 194 | return x 195 | 196 | 197 | def uint64(x): 198 | return x 199 | 200 | 201 | def uint72(x): 202 | return x 203 | 204 | 205 | def uint80(x): 206 | return x 207 | 208 | 209 | def uint88(x): 210 | return x 211 | 212 | 213 | def uint96(x): 214 | return x 215 | 216 | 217 | def uint104(x): 218 | return x 219 | 220 | 221 | def uint112(x): 222 | return x 223 | 224 | 225 | def uint120(x): 226 | return x 227 | 228 | 229 | def uint128(x): 230 | return x 231 | 232 | 233 | def uint136(x): 234 | return x 235 | 236 | 237 | def uint144(x): 238 | return x 239 | 240 | 241 | def uint152(x): 242 | return x 243 | 244 | 245 | def uint160(x): 246 | return x 247 | 248 | 249 | def uint168(x): 250 | return x 251 | 252 | 253 | def uint176(x): 254 | return x 255 | 256 | 257 | def uint184(x): 258 | return x 259 | 260 | 261 | def uint192(x): 262 | return x 263 | 264 | 265 | def uint200(x): 266 | return x 267 | 268 | 269 | def uint208(x): 270 | return x 271 | 272 | 273 | def uint216(x): 274 | return x 275 | 276 | 277 | def uint224(x): 278 | return x 279 | 280 | 281 | def uint232(x): 282 | return x 283 | 284 | 285 | def uint240(x): 286 | return x 287 | 288 | 289 | def uint248(x): 290 | return x 291 | 292 | 293 | def uint256(x): 294 | return x 295 | -------------------------------------------------------------------------------- /source/arbitrage/flash_borrow_to_lp_swap_new_test.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | 3 | import web3 4 | 5 | from degenbot import Erc20Token 6 | from degenbot.arbitrage import FlashBorrowToLpSwapNew 7 | from degenbot.uniswap.v2.liquidity_pool import LiquidityPool 8 | 9 | 10 | class MockErc20Token(Erc20Token): 11 | def __init__(self): 12 | pass 13 | 14 | 15 | class MockLiquidityPool(LiquidityPool): 16 | def __init__(self): 17 | pass 18 | 19 | 20 | wbtc = MockErc20Token() 21 | wbtc.address = web3.Web3.toChecksumAddress( 22 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 23 | ) 24 | wbtc.decimals = 8 25 | wbtc.name = "Wrapped BTC" 26 | wbtc.symbol = "WBTC" 27 | 28 | weth = MockErc20Token() 29 | weth.address = web3.Web3.toChecksumAddress( 30 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 31 | ) 32 | weth.decimals = 18 33 | weth.name = "Wrapped Ether" 34 | weth.symbol = "WETH" 35 | 36 | uni_v2_lp = MockLiquidityPool() 37 | uni_v2_lp.name = "WBTC-WETH (UniV2, 0.30%)" 38 | uni_v2_lp.address = web3.Web3.toChecksumAddress( 39 | "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940" 40 | ) 41 | uni_v2_lp.factory = web3.Web3.toChecksumAddress( 42 | "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" 43 | ) 44 | uni_v2_lp.fee = None 45 | uni_v2_lp.fee_token0 = Fraction(3, 1000) 46 | uni_v2_lp.fee_token1 = Fraction(3, 1000) 47 | uni_v2_lp.reserves_token0 = 20000000000 48 | uni_v2_lp.reserves_token1 = 3000000000000000000000 49 | uni_v2_lp.token0 = wbtc 50 | uni_v2_lp.token1 = weth 51 | uni_v2_lp.new_reserves = True 52 | uni_v2_lp._update_pool_state() 53 | 54 | sushi_v2_lp = MockLiquidityPool() 55 | sushi_v2_lp.name = "WBTC-WETH (SushiV2, 0.30%)" 56 | sushi_v2_lp.address = web3.Web3.toChecksumAddress( 57 | "0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58" 58 | ) 59 | sushi_v2_lp.factory = web3.Web3.toChecksumAddress( 60 | "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac" 61 | ) 62 | sushi_v2_lp.fee = None 63 | sushi_v2_lp.fee_token0 = Fraction(3, 1000) 64 | sushi_v2_lp.fee_token1 = Fraction(3, 1000) 65 | sushi_v2_lp.reserves_token0 = 20000000000 66 | sushi_v2_lp.reserves_token1 = 3000000000000000000000 67 | sushi_v2_lp.token0 = wbtc 68 | sushi_v2_lp.token1 = weth 69 | sushi_v2_lp.new_reserves = True 70 | sushi_v2_lp._update_pool_state() 71 | 72 | 73 | arb = FlashBorrowToLpSwapNew( 74 | borrow_pool=uni_v2_lp, 75 | borrow_token=wbtc, 76 | repay_token=weth, 77 | swap_pools=[sushi_v2_lp], 78 | update_method="external", 79 | ) 80 | 81 | 82 | def test_type_checks(): 83 | # Need to ensure that the mocked helpers will pass the type checks 84 | # inside various methods 85 | assert isinstance(uni_v2_lp, LiquidityPool) 86 | assert isinstance(sushi_v2_lp, LiquidityPool) 87 | assert isinstance(weth, Erc20Token) 88 | assert isinstance(wbtc, Erc20Token) 89 | 90 | 91 | def test_arbitrage(): 92 | uni_v2_lp.new_reserves = True 93 | sushi_v2_lp.new_reserves = True 94 | 95 | arb.update_reserves() 96 | 97 | # no profit expected, both pools have the same reserves 98 | assert arb.best["borrow_amount"] == 0 99 | assert arb.best["borrow_pool_amounts"] == [] 100 | assert arb.best["profit_amount"] == 0 101 | assert arb.best["repay_amount"] == 0 102 | assert arb.best["swap_pool_amounts"] == [] 103 | 104 | # best_future should be empty (no overrides were provided) 105 | assert arb.best_future["borrow_amount"] == 0 106 | assert arb.best_future["borrow_pool_amounts"] == [] 107 | assert arb.best_future["profit_amount"] == 0 108 | assert arb.best_future["repay_amount"] == 0 109 | assert arb.best_future["swap_pool_amounts"] == [] 110 | 111 | 112 | def test_arbitrage_with_overrides(): 113 | uni_v2_lp.new_reserves = True 114 | sushi_v2_lp.new_reserves = True 115 | 116 | arb.update_reserves( 117 | override_future=True, 118 | override_future_borrow_pool_reserves_token0=20000000000, 119 | override_future_borrow_pool_reserves_token1=4000000000000000000000 120 | // 2, 121 | ) 122 | 123 | # non-override state should be the same 124 | assert arb.best["borrow_amount"] == 0 125 | assert arb.best["borrow_pool_amounts"] == [] 126 | assert arb.best["profit_amount"] == 0 127 | assert arb.best["repay_amount"] == 0 128 | assert arb.best["swap_pool_amounts"] == [] 129 | 130 | # override state should reflect a profit opportunity from the severe 131 | # mismatch in pool reserves (+33% WETH reserves in Sushi pool) 132 | assert arb.best_future["borrow_amount"] == 1993359746 133 | assert arb.best_future["borrow_pool_amounts"] == [1993359746, 0] 134 | assert arb.best_future["profit_amount"] == 49092923683591028736 135 | assert arb.best_future["repay_amount"] == 222068946927979774742 136 | assert arb.best_future["swap_pool_amounts"] == [[0, 271161870611570793739]] 137 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/fullmath_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | 5 | from degenbot.constants import MAX_UINT256 6 | from degenbot.exceptions import EVMRevertError 7 | from degenbot.uniswap.v3.libraries import FixedPoint128, FullMath 8 | 9 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 10 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/FullMath.spec.ts 11 | 12 | 13 | def test_mulDiv(): 14 | ### ---------------------------------------------------- 15 | ### FullMath tests 16 | ### ---------------------------------------------------- 17 | 18 | # mulDiv tests 19 | with pytest.raises(EVMRevertError): 20 | # this test should fail 21 | FullMath.mulDiv(FixedPoint128.Q128, 5, 0) 22 | 23 | with pytest.raises(EVMRevertError): 24 | # this test should fail 25 | FullMath.mulDiv(FixedPoint128.Q128, FixedPoint128.Q128, 0) 26 | 27 | with pytest.raises(EVMRevertError): 28 | # this test should fail 29 | FullMath.mulDiv(FixedPoint128.Q128, FixedPoint128.Q128, 1) 30 | 31 | with pytest.raises(EVMRevertError): 32 | # this test should fail 33 | FullMath.mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256 - 1) 34 | 35 | assert ( 36 | FullMath.mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256) == MAX_UINT256 37 | ) 38 | 39 | assert ( 40 | FullMath.mulDiv( 41 | FixedPoint128.Q128, 42 | 50 * FixedPoint128.Q128 // 100, # 0.5x 43 | 150 * FixedPoint128.Q128 // 100, # 1.5x 44 | ) 45 | == FixedPoint128.Q128 // 3 46 | ) 47 | 48 | assert ( 49 | FullMath.mulDiv( 50 | FixedPoint128.Q128, 35 * FixedPoint128.Q128, 8 * FixedPoint128.Q128 51 | ) 52 | == 4375 * FixedPoint128.Q128 // 1000 53 | ) 54 | 55 | assert ( 56 | FullMath.mulDiv( 57 | FixedPoint128.Q128, 58 | 1000 * FixedPoint128.Q128, 59 | 3000 * FixedPoint128.Q128, 60 | ) 61 | == FixedPoint128.Q128 // 3 62 | ) 63 | 64 | 65 | def test_mulDivRoundingUp(): 66 | with pytest.raises(EVMRevertError): 67 | # this test should fail 68 | FullMath.mulDivRoundingUp(FixedPoint128.Q128, 5, 0) 69 | 70 | with pytest.raises(EVMRevertError): 71 | # this test should fail 72 | FullMath.mulDivRoundingUp(FixedPoint128.Q128, FixedPoint128.Q128, 0) 73 | 74 | with pytest.raises(EVMRevertError): 75 | # this test should fail 76 | FullMath.mulDivRoundingUp(FixedPoint128.Q128, FixedPoint128.Q128, 1) 77 | 78 | with pytest.raises(EVMRevertError): 79 | # this test should fail 80 | FullMath.mulDivRoundingUp(MAX_UINT256, MAX_UINT256, MAX_UINT256 - 1) 81 | 82 | with pytest.raises(EVMRevertError): 83 | # this test should fail 84 | FullMath.mulDivRoundingUp( 85 | 535006138814359, 86 | 432862656469423142931042426214547535783388063929571229938474969, 87 | 2, 88 | ) 89 | 90 | with pytest.raises(EVMRevertError): 91 | # this test should fail 92 | FullMath.mulDivRoundingUp( 93 | 115792089237316195423570985008687907853269984659341747863450311749907997002549, 94 | 115792089237316195423570985008687907853269984659341747863450311749907997002550, 95 | 115792089237316195423570985008687907853269984653042931687443039491902864365164, 96 | ) 97 | 98 | # all max inputs 99 | assert ( 100 | FullMath.mulDivRoundingUp(MAX_UINT256, MAX_UINT256, MAX_UINT256) 101 | == MAX_UINT256 102 | ) 103 | 104 | # accurate without phantom overflow 105 | assert ( 106 | FullMath.mulDivRoundingUp( 107 | FixedPoint128.Q128, 108 | 50 * FixedPoint128.Q128 // 100, 109 | 150 * FixedPoint128.Q128 // 100, 110 | ) 111 | == FixedPoint128.Q128 // 3 + 1 112 | ) 113 | 114 | # accurate with phantom overflow 115 | assert ( 116 | FullMath.mulDivRoundingUp( 117 | FixedPoint128.Q128, 35 * FixedPoint128.Q128, 8 * FixedPoint128.Q128 118 | ) 119 | == 4375 * FixedPoint128.Q128 // 1000 120 | ) 121 | 122 | # accurate with phantom overflow and repeating decimal 123 | assert ( 124 | FullMath.mulDivRoundingUp( 125 | FixedPoint128.Q128, 126 | 1000 * FixedPoint128.Q128, 127 | 3000 * FixedPoint128.Q128, 128 | ) 129 | == FixedPoint128.Q128 // 3 + 1 130 | ) 131 | 132 | def pseudoRandomBigNumber(): 133 | return int(MAX_UINT256 * random.random()) 134 | 135 | def floored(x, y, d): 136 | return FullMath.mulDiv(x, y, d) 137 | 138 | def ceiled(x, y, d): 139 | return FullMath.mulDivRoundingUp(x, y, d) 140 | 141 | for _ in range(1000): 142 | x = pseudoRandomBigNumber() 143 | y = pseudoRandomBigNumber() 144 | d = pseudoRandomBigNumber() 145 | 146 | if x == 0 or y == 0: 147 | assert floored(x, y, d) == 0 148 | assert ceiled(x, y, d) == 0 149 | elif x * y // d > MAX_UINT256: 150 | with pytest.raises(EVMRevertError): 151 | # this test should fail 152 | floored(x, y, d) 153 | with pytest.raises(EVMRevertError): 154 | # this test should fail 155 | ceiled(x, y, d) 156 | else: 157 | assert floored(x, y, d) == x * y // d 158 | assert ceiled(x, y, d) == x * y // d + ( 159 | 1 if (x * y % d > 0) else 0 160 | ) 161 | -------------------------------------------------------------------------------- /source/transaction/simulation_ledger.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | 3 | from eth_typing import ChecksumAddress 4 | from web3 import Web3 5 | 6 | from degenbot.logging import logger 7 | from degenbot.token import Erc20Token 8 | 9 | 10 | class SimulationLedger: 11 | """ 12 | A dictionary-like class for tracking token balances across addresses. 13 | 14 | Token balances are organized first by the holding address, then by the 15 | token contract address. 16 | """ 17 | 18 | def __init__(self): 19 | # Entries are recorded as a dict-of-dicts, keyed by address, then by 20 | # token address 21 | self._balances: Dict[ 22 | ChecksumAddress, # address holding balance 23 | Dict[ 24 | ChecksumAddress, # token address 25 | int, # balance 26 | ], 27 | ] = dict() 28 | 29 | def adjust( 30 | self, 31 | address: Union[str, ChecksumAddress], 32 | token: Union[Erc20Token, str, ChecksumAddress], 33 | amount: int, 34 | ) -> None: 35 | """ 36 | Apply an adjustment to the balance for a token held by an address. 37 | 38 | The amount can be positive (credit) or negative (debit). The method 39 | checksums all addresses prior to use. 40 | 41 | Parameters 42 | ---------- 43 | address: str | ChecksumAddress 44 | The address holding the token balance. 45 | token: Erc20Token | str | ChecksumAddress 46 | The token being held. May be passed as an address or an ``Erc20Token`` 47 | amount: int 48 | The amount to adjust. May be negative or positive. 49 | 50 | Returns 51 | ------- 52 | None 53 | 54 | Raises 55 | ------ 56 | ValueError 57 | If inputs did not match the expected types. 58 | """ 59 | 60 | _token_address: ChecksumAddress 61 | if isinstance(token, Erc20Token): 62 | _token_address = token.address 63 | elif isinstance(token, str): 64 | _token_address = Web3.toChecksumAddress(token) 65 | elif isinstance(token, ChecksumAddress): 66 | _token_address = token 67 | else: 68 | raise ValueError( 69 | f"Token may be type Erc20Token, str, or ChecksumAddress. Was {type(token)}" 70 | ) 71 | 72 | _address = Web3.toChecksumAddress(address) 73 | 74 | address_balance: Dict[ChecksumAddress, int] 75 | try: 76 | address_balance = self._balances[_address] 77 | except KeyError: 78 | address_balance = {} 79 | self._balances[_address] = address_balance 80 | 81 | logger.debug( 82 | f"BALANCE: {_address} {'+' if amount > 0 else ''}{amount} {_token_address}" 83 | ) 84 | 85 | try: 86 | address_balance[_token_address] 87 | except KeyError: 88 | address_balance[_token_address] = 0 89 | finally: 90 | address_balance[_token_address] += amount 91 | if address_balance[_token_address] == 0: 92 | del address_balance[_token_address] 93 | if not address_balance: 94 | del self._balances[_address] 95 | 96 | def token_balance( 97 | self, 98 | address: Union[str, ChecksumAddress], 99 | token: Union[Erc20Token, str, ChecksumAddress], 100 | ) -> int: 101 | """ 102 | Get the balance for a given address and token. 103 | 104 | The method checksums all addresses prior to use. 105 | 106 | Parameters 107 | ---------- 108 | address: str | ChecksumAddress 109 | The address holding the token balance. 110 | token: Erc20Token | str | ChecksumAddress 111 | The token being held. May be passed as an address or an ``Erc20Token`` 112 | 113 | Returns 114 | ------- 115 | int 116 | The balance of ``token`` at ``address`` 117 | 118 | Raises 119 | ------ 120 | ValueError 121 | If inputs did not match the expected types. 122 | """ 123 | 124 | _address = Web3.toChecksumAddress(address) 125 | 126 | if isinstance(token, Erc20Token): 127 | _token_address = token.address 128 | elif isinstance(token, str): 129 | _token_address = Web3.toChecksumAddress(token) 130 | elif isinstance(token, ChecksumAddress): 131 | _token_address = token 132 | else: 133 | raise ValueError( 134 | f"Expected token type Erc20Token, str, or ChecksumAddress. Was {type(token)}" 135 | ) 136 | 137 | address_balances: Dict[ChecksumAddress, int] 138 | try: 139 | address_balances = self._balances[_address] 140 | except KeyError: 141 | address_balances = {} 142 | 143 | return address_balances.get(_token_address, 0) 144 | 145 | def transfer( 146 | self, 147 | token: Union[Erc20Token, str, ChecksumAddress], 148 | amount: int, 149 | from_addr: Union[ChecksumAddress, str], 150 | to_addr: Union[ChecksumAddress, str], 151 | ) -> None: 152 | """ 153 | Transfer a balance between addresses. 154 | 155 | The method checksums all addresses prior to use. 156 | 157 | Parameters 158 | ---------- 159 | token: Erc20Token | str | ChecksumAddress 160 | The token being held. May be passed as an address or an ``Erc20Token`` 161 | amount: int 162 | The balance to transfer. 163 | from_addr: str | ChecksumAddress 164 | The address holding the token balance. 165 | to_addr: str | ChecksumAddress 166 | The address holding the token balance. 167 | 168 | Returns 169 | ------- 170 | None 171 | 172 | Raises 173 | ------ 174 | ValueError 175 | If inputs did not match the expected types. 176 | """ 177 | 178 | if isinstance(token, Erc20Token): 179 | _token_address = token.address 180 | elif isinstance(token, str): 181 | _token_address = Web3.toChecksumAddress(token) 182 | elif isinstance(token, ChecksumAddress): 183 | _token_address = token 184 | else: 185 | raise ValueError( 186 | f"Expected token type Erc20Token, str, or ChecksumAddress. Was {type(token)}" 187 | ) 188 | 189 | self.adjust( 190 | address=from_addr, 191 | token=_token_address, 192 | amount=-amount, 193 | ) 194 | self.adjust( 195 | address=to_addr, 196 | token=_token_address, 197 | amount=amount, 198 | ) 199 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/SqrtPriceMath.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from degenbot.constants import MAX_UINT128, MIN_UINT128, MIN_UINT160 4 | from degenbot.exceptions import EVMRevertError 5 | from degenbot.uniswap.v3.libraries import FixedPoint96, FullMath, UnsafeMath 6 | from degenbot.uniswap.v3.libraries.functions import ( 7 | to_int256, 8 | to_uint160, 9 | uint128, 10 | uint256, 11 | ) 12 | 13 | # type hinting aliases 14 | Int128 = int 15 | Int256 = int 16 | Uint128 = int 17 | Uint160 = int 18 | Uint256 = int 19 | 20 | 21 | def getAmount0Delta( 22 | sqrtRatioAX96: Uint160, 23 | sqrtRatioBX96: Uint160, 24 | liquidity: Union[Int128, Uint128], 25 | roundUp: Optional[bool] = None, 26 | ) -> Uint256: 27 | if roundUp is not None or MIN_UINT128 <= liquidity <= MAX_UINT128: 28 | if sqrtRatioAX96 > sqrtRatioBX96: 29 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96) 30 | 31 | numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION 32 | numerator2 = sqrtRatioBX96 - sqrtRatioAX96 33 | 34 | if not (sqrtRatioAX96 > 0): 35 | raise EVMRevertError("require sqrtRatioAX96 > 0") 36 | 37 | return ( 38 | UnsafeMath.divRoundingUp( 39 | FullMath.mulDivRoundingUp( 40 | numerator1, numerator2, sqrtRatioBX96 41 | ), 42 | sqrtRatioAX96, 43 | ) 44 | if roundUp 45 | else (FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96)) 46 | // sqrtRatioAX96 47 | ) 48 | else: 49 | return to_int256( 50 | -getAmount0Delta( 51 | sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), False 52 | ) 53 | if liquidity < 0 54 | else to_int256( 55 | getAmount0Delta( 56 | sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), True 57 | ) 58 | ) 59 | ) 60 | 61 | 62 | def getAmount1Delta( 63 | sqrtRatioAX96: Uint160, 64 | sqrtRatioBX96: Uint160, 65 | liquidity: Union[Int128, Uint128], 66 | roundUp: Optional[bool] = None, 67 | ) -> Uint256: 68 | if roundUp is not None or MIN_UINT128 <= liquidity <= MAX_UINT128: 69 | if sqrtRatioAX96 > sqrtRatioBX96: 70 | sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 71 | 72 | return ( 73 | FullMath.mulDivRoundingUp( 74 | liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96 75 | ) 76 | if roundUp 77 | else FullMath.mulDiv( 78 | liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96 79 | ) 80 | ) 81 | else: 82 | return to_int256( 83 | -getAmount1Delta( 84 | sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), False 85 | ) 86 | if liquidity < 0 87 | else to_int256( 88 | getAmount1Delta( 89 | sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), True 90 | ) 91 | ) 92 | ) 93 | 94 | 95 | def getNextSqrtPriceFromAmount0RoundingUp( 96 | sqrtPX96: Uint160, 97 | liquidity: Uint128, 98 | amount: Uint256, 99 | add: bool, 100 | ) -> Uint160: 101 | # we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price 102 | if amount == 0: 103 | return sqrtPX96 104 | 105 | numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION 106 | 107 | if add: 108 | product = amount * sqrtPX96 109 | if product // amount == sqrtPX96: 110 | denominator = numerator1 + product 111 | if denominator >= numerator1: 112 | # always fits in 160 bits 113 | return FullMath.mulDivRoundingUp( 114 | numerator1, sqrtPX96, denominator 115 | ) 116 | 117 | return UnsafeMath.divRoundingUp( 118 | numerator1, numerator1 // sqrtPX96 + amount 119 | ) 120 | else: 121 | product = amount * sqrtPX96 122 | # if the product overflows, we know the denominator underflows 123 | # in addition, we must check that the denominator does not underflow 124 | 125 | if not (product // amount == sqrtPX96 and numerator1 > product): 126 | raise EVMRevertError( 127 | "product / amount == sqrtPX96 && numerator1 > product" 128 | ) 129 | 130 | denominator = numerator1 - product 131 | return to_uint160( 132 | FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator) 133 | ) 134 | 135 | 136 | def getNextSqrtPriceFromAmount1RoundingDown( 137 | sqrtPX96: Uint160, 138 | liquidity: Uint128, 139 | amount: Uint256, 140 | add: bool, 141 | ) -> Uint160: 142 | if add: 143 | quotient = ( 144 | (amount << FixedPoint96.RESOLUTION) // liquidity 145 | if amount <= 2**160 - 1 146 | else FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity) 147 | ) 148 | return to_uint160(uint256(sqrtPX96) + quotient) 149 | else: 150 | quotient = ( 151 | UnsafeMath.divRoundingUp( 152 | amount << FixedPoint96.RESOLUTION, liquidity 153 | ) 154 | if amount <= (2**160) - 1 155 | else FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity) 156 | ) 157 | 158 | if not (sqrtPX96 > quotient): 159 | raise EVMRevertError("require sqrtPX96 > quotient") 160 | 161 | # always fits 160 bits 162 | return sqrtPX96 - quotient 163 | 164 | 165 | def getNextSqrtPriceFromInput( 166 | sqrtPX96: Uint160, 167 | liquidity: Uint128, 168 | amountIn: Uint256, 169 | zeroForOne: bool, 170 | ) -> Uint160: 171 | if not (sqrtPX96 > MIN_UINT160): 172 | raise EVMRevertError("sqrtPX96 must be greater than 0") 173 | 174 | if not (liquidity > MIN_UINT160): 175 | raise EVMRevertError("liquidity must be greater than 0") 176 | 177 | # round to make sure that we don't pass the target price 178 | return ( 179 | getNextSqrtPriceFromAmount0RoundingUp( 180 | sqrtPX96, liquidity, amountIn, True 181 | ) 182 | if zeroForOne 183 | else getNextSqrtPriceFromAmount1RoundingDown( 184 | sqrtPX96, liquidity, amountIn, True 185 | ) 186 | ) 187 | 188 | 189 | def getNextSqrtPriceFromOutput( 190 | sqrtPX96: int, 191 | liquidity: int, 192 | amountOut: int, 193 | zeroForOne: bool, 194 | ): 195 | if not (sqrtPX96 > 0): 196 | raise EVMRevertError 197 | 198 | if not (liquidity > 0): 199 | raise EVMRevertError 200 | 201 | # round to make sure that we pass the target price 202 | return ( 203 | getNextSqrtPriceFromAmount1RoundingDown( 204 | sqrtPX96, liquidity, amountOut, False 205 | ) 206 | if zeroForOne 207 | else getNextSqrtPriceFromAmount0RoundingUp( 208 | sqrtPX96, liquidity, amountOut, False 209 | ) 210 | ) 211 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/swapmath_test.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, localcontext 2 | 3 | from degenbot.uniswap.v3.libraries import SqrtPriceMath, SwapMath 4 | 5 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 6 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/SwapMath.spec.ts 7 | 8 | 9 | def expandTo18Decimals(x: int): 10 | return x * 10**18 11 | 12 | 13 | def encodePriceSqrt(reserve1: int, reserve0: int): 14 | """ 15 | Returns the sqrt price as a Q64.96 value 16 | """ 17 | with localcontext() as ctx: 18 | # Change the rounding method to match the BigNumber unit test at https://github.com/Uniswap/v3-core/blob/main/test/shared/utilities.ts 19 | # which specifies .integerValue(3), the 'ROUND_FLOOR' rounding method per https://mikemcl.github.io/bignumber.js/#bignumber 20 | ctx.rounding = "ROUND_FLOOR" 21 | return round( 22 | (Decimal(reserve1) / Decimal(reserve0)).sqrt() * Decimal(2**96) 23 | ) 24 | 25 | 26 | def test_computeSwapStep(): 27 | # exact amount in that gets capped at price target in one for zero 28 | price = encodePriceSqrt(1, 1) 29 | priceTarget = encodePriceSqrt(101, 100) 30 | liquidity = expandTo18Decimals(2) 31 | amount = expandTo18Decimals(1) 32 | fee = 600 33 | zeroForOne = False 34 | 35 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 36 | price, priceTarget, liquidity, amount, fee 37 | ) 38 | 39 | assert amountIn == 9975124224178055 40 | assert feeAmount == 5988667735148 41 | assert amountOut == 9925619580021728 42 | assert amountIn + feeAmount < amount 43 | 44 | priceAfterWholeInputAmount = SqrtPriceMath.getNextSqrtPriceFromInput( 45 | price, liquidity, amount, zeroForOne 46 | ) 47 | 48 | assert sqrtQ == priceTarget 49 | assert sqrtQ < priceAfterWholeInputAmount 50 | 51 | # exact amount out that gets capped at price target in one for zero 52 | price = encodePriceSqrt(1, 1) 53 | priceTarget = encodePriceSqrt(101, 100) 54 | liquidity = expandTo18Decimals(2) 55 | amount = -expandTo18Decimals(1) 56 | fee = 600 57 | zeroForOne = False 58 | 59 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 60 | price, priceTarget, liquidity, amount, fee 61 | ) 62 | 63 | assert amountIn == 9975124224178055 64 | assert feeAmount == 5988667735148 65 | assert amountOut == 9925619580021728 66 | assert amountOut < -amount 67 | 68 | priceAfterWholeOutputAmount = SqrtPriceMath.getNextSqrtPriceFromOutput( 69 | price, liquidity, -amount, zeroForOne 70 | ) 71 | 72 | assert sqrtQ == priceTarget 73 | assert sqrtQ < priceAfterWholeOutputAmount 74 | 75 | # exact amount in that is fully spent in one for zero 76 | price = encodePriceSqrt(1, 1) 77 | priceTarget = encodePriceSqrt(1000, 100) 78 | liquidity = expandTo18Decimals(2) 79 | amount = expandTo18Decimals(1) 80 | fee = 600 81 | zeroForOne = False 82 | 83 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 84 | price, priceTarget, liquidity, amount, fee 85 | ) 86 | 87 | assert amountIn == 999400000000000000 88 | assert feeAmount == 600000000000000 89 | assert amountOut == 666399946655997866 90 | assert amountIn + feeAmount == amount 91 | 92 | priceAfterWholeInputAmountLessFee = ( 93 | SqrtPriceMath.getNextSqrtPriceFromInput( 94 | price, liquidity, amount - feeAmount, zeroForOne 95 | ) 96 | ) 97 | 98 | assert sqrtQ < priceTarget 99 | assert sqrtQ == priceAfterWholeInputAmountLessFee 100 | 101 | # exact amount out that is fully received in one for zero 102 | price = encodePriceSqrt(1, 1) 103 | priceTarget = encodePriceSqrt(10000, 100) 104 | liquidity = expandTo18Decimals(2) 105 | amount = -expandTo18Decimals(1) 106 | fee = 600 107 | zeroForOne = False 108 | 109 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 110 | price, priceTarget, liquidity, amount, fee 111 | ) 112 | 113 | assert amountIn == 2000000000000000000 114 | assert feeAmount == 1200720432259356 115 | assert amountOut == -amount 116 | 117 | priceAfterWholeOutputAmount = SqrtPriceMath.getNextSqrtPriceFromOutput( 118 | price, liquidity, -amount, zeroForOne 119 | ) 120 | 121 | assert sqrtQ < priceTarget 122 | assert sqrtQ == priceAfterWholeOutputAmount 123 | 124 | # amount out is capped at the desired amount out 125 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 126 | 417332158212080721273783715441582, 127 | 1452870262520218020823638996, 128 | 159344665391607089467575320103, 129 | -1, 130 | 1, 131 | ) 132 | 133 | assert amountIn == 1 134 | assert feeAmount == 1 135 | assert amountOut == 1 # would be 2 if not capped 136 | assert sqrtQ == 417332158212080721273783715441581 137 | 138 | # target price of 1 uses partial input amount 139 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 140 | 2, 141 | 1, 142 | 1, 143 | 3915081100057732413702495386755767, 144 | 1, 145 | ) 146 | assert amountIn == 39614081257132168796771975168 147 | assert feeAmount == 39614120871253040049813 148 | assert amountIn + feeAmount <= 3915081100057732413702495386755767 149 | assert amountOut == 0 150 | assert sqrtQ == 1 151 | 152 | # entire input amount taken as fee 153 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 154 | 2413, 155 | 79887613182836312, 156 | 1985041575832132834610021537970, 157 | 10, 158 | 1872, 159 | ) 160 | assert amountIn == 0 161 | assert feeAmount == 10 162 | assert amountOut == 0 163 | assert sqrtQ == 2413 164 | 165 | # handles intermediate insufficient liquidity in zero for one exact output case 166 | sqrtP = 20282409603651670423947251286016 167 | sqrtPTarget = sqrtP * 11 // 10 168 | liquidity = 1024 169 | # virtual reserves of one are only 4 170 | # https://www.wolframalpha.com/input/?i=1024+%2F+%2820282409603651670423947251286016+%2F+2**96%29 171 | amountRemaining = -4 172 | feePips = 3000 173 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 174 | sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips 175 | ) 176 | assert amountOut == 0 177 | assert sqrtQ == sqrtPTarget 178 | assert amountIn == 26215 179 | assert feeAmount == 79 180 | 181 | # handles intermediate insufficient liquidity in one for zero exact output case 182 | sqrtP = 20282409603651670423947251286016 183 | sqrtPTarget = sqrtP * 9 // 10 184 | liquidity = 1024 185 | # virtual reserves of zero are only 262144 186 | # https://www.wolframalpha.com/input/?i=1024+*+%2820282409603651670423947251286016+%2F+2**96%29 187 | amountRemaining = -263000 188 | feePips = 3000 189 | sqrtQ, amountIn, amountOut, feeAmount = SwapMath.computeSwapStep( 190 | sqrtP, sqrtPTarget, liquidity, amountRemaining, feePips 191 | ) 192 | assert amountOut == 26214 193 | assert sqrtQ == sqrtPTarget 194 | assert amountIn == 1 195 | assert feeAmount == 1 196 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/TickMath.py: -------------------------------------------------------------------------------- 1 | import degenbot.uniswap.v3.libraries.YulOperations as yul 2 | from degenbot.constants import MAX_UINT256 3 | from degenbot.exceptions import EVMRevertError 4 | from degenbot.uniswap.v3.libraries.functions import ( 5 | int24, 6 | int256, 7 | uint160, 8 | uint256, 9 | ) 10 | 11 | MIN_TICK = -887272 12 | MAX_TICK = -MIN_TICK 13 | MIN_SQRT_RATIO = 4295128739 14 | MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342 15 | 16 | 17 | def getSqrtRatioAtTick(tick: int) -> int: 18 | abs_tick = uint256(-int256(tick)) if tick < 0 else uint256(int256(tick)) 19 | if not (0 <= abs_tick <= uint256(MAX_TICK)): 20 | raise EVMRevertError("T") 21 | 22 | ratio = ( 23 | 0xFFFCB933BD6FAD37AA2D162D1A594001 24 | if (abs_tick & 0x1 != 0) 25 | else 0x100000000000000000000000000000000 26 | ) 27 | 28 | if abs_tick & 0x2 != 0: 29 | ratio = (ratio * 0xFFF97272373D413259A46990580E213A) >> 128 30 | if abs_tick & 0x4 != 0: 31 | ratio = (ratio * 0xFFF2E50F5F656932EF12357CF3C7FDCC) >> 128 32 | if abs_tick & 0x8 != 0: 33 | ratio = (ratio * 0xFFE5CACA7E10E4E61C3624EAA0941CD0) >> 128 34 | if abs_tick & 0x10 != 0: 35 | ratio = (ratio * 0xFFCB9843D60F6159C9DB58835C926644) >> 128 36 | if abs_tick & 0x20 != 0: 37 | ratio = (ratio * 0xFF973B41FA98C081472E6896DFB254C0) >> 128 38 | if abs_tick & 0x40 != 0: 39 | ratio = (ratio * 0xFF2EA16466C96A3843EC78B326B52861) >> 128 40 | if abs_tick & 0x80 != 0: 41 | ratio = (ratio * 0xFE5DEE046A99A2A811C461F1969C3053) >> 128 42 | if abs_tick & 0x100 != 0: 43 | ratio = (ratio * 0xFCBE86C7900A88AEDCFFC83B479AA3A4) >> 128 44 | if abs_tick & 0x200 != 0: 45 | ratio = (ratio * 0xF987A7253AC413176F2B074CF7815E54) >> 128 46 | if abs_tick & 0x400 != 0: 47 | ratio = (ratio * 0xF3392B0822B70005940C7A398E4B70F3) >> 128 48 | if abs_tick & 0x800 != 0: 49 | ratio = (ratio * 0xE7159475A2C29B7443B29C7FA6E889D9) >> 128 50 | if abs_tick & 0x1000 != 0: 51 | ratio = (ratio * 0xD097F3BDFD2022B8845AD8F792AA5825) >> 128 52 | if abs_tick & 0x2000 != 0: 53 | ratio = (ratio * 0xA9F746462D870FDF8A65DC1F90E061E5) >> 128 54 | if abs_tick & 0x4000 != 0: 55 | ratio = (ratio * 0x70D869A156D2A1B890BB3DF62BAF32F7) >> 128 56 | if abs_tick & 0x8000 != 0: 57 | ratio = (ratio * 0x31BE135F97D08FD981231505542FCFA6) >> 128 58 | if abs_tick & 0x10000 != 0: 59 | ratio = (ratio * 0x9AA508B5B7A84E1C677DE54F3E99BC9) >> 128 60 | if abs_tick & 0x20000 != 0: 61 | ratio = (ratio * 0x5D6AF8DEDB81196699C329225EE604) >> 128 62 | if abs_tick & 0x40000 != 0: 63 | ratio = (ratio * 0x2216E584F5FA1EA926041BEDFE98) >> 128 64 | if abs_tick & 0x80000 != 0: 65 | ratio = (ratio * 0x48A170391F7DC42444E8FA2) >> 128 66 | 67 | if tick > 0: 68 | ratio = (MAX_UINT256) // ratio 69 | 70 | # this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96 71 | # we then downcast because we know the result always fits within 160 bits due to our tick input constraint 72 | # we round up in the division so getTickAtSqrtRatio of the output price is always consistent 73 | return uint160((ratio >> 32) + (0 if (ratio % (1 << 32) == 0) else 1)) 74 | 75 | 76 | def getTickAtSqrtRatio(sqrt_price_x96: int) -> int: 77 | if not (0 <= sqrt_price_x96 <= 2**160 - 1): 78 | raise EVMRevertError("not a valid uint160") 79 | 80 | # second inequality must be < because the price can never reach the price at the max tick 81 | if not ( 82 | sqrt_price_x96 >= MIN_SQRT_RATIO and sqrt_price_x96 < MAX_SQRT_RATIO 83 | ): 84 | raise EVMRevertError("R") 85 | 86 | ratio = uint256(sqrt_price_x96) << 32 87 | 88 | r: int = ratio 89 | msb: int = 0 90 | 91 | f = yul.shl(7, yul.gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) 92 | msb = yul.or_(msb, f) 93 | r = yul.shr(f, r) 94 | 95 | f = yul.shl(7, yul.gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) 96 | msb = yul.or_(msb, f) 97 | r = yul.shr(f, r) 98 | 99 | f = yul.shl(6, yul.gt(r, 0xFFFFFFFFFFFFFFFF)) 100 | msb = yul.or_(msb, f) 101 | r = yul.shr(f, r) 102 | 103 | f = yul.shl(5, yul.gt(r, 0xFFFFFFFF)) 104 | msb = yul.or_(msb, f) 105 | r = yul.shr(f, r) 106 | 107 | f = yul.shl(4, yul.gt(r, 0xFFFF)) 108 | msb = yul.or_(msb, f) 109 | r = yul.shr(f, r) 110 | 111 | f = yul.shl(3, yul.gt(r, 0xFF)) 112 | msb = yul.or_(msb, f) 113 | r = yul.shr(f, r) 114 | 115 | f = yul.shl(2, yul.gt(r, 0xF)) 116 | msb = yul.or_(msb, f) 117 | r = yul.shr(f, r) 118 | 119 | f = yul.shl(1, yul.gt(r, 0x3)) 120 | msb = yul.or_(msb, f) 121 | r = yul.shr(f, r) 122 | 123 | f = yul.gt(r, 0x1) 124 | msb = yul.or_(msb, f) 125 | 126 | if msb >= 128: 127 | r = ratio >> (msb - 127) 128 | else: 129 | r = ratio << (127 - msb) 130 | 131 | log_2 = (int(msb) - 128) << 64 132 | 133 | r = yul.shr(127, yul.mul(r, r)) 134 | f = yul.shr(128, r) 135 | log_2 = yul.or_(log_2, yul.shl(63, f)) 136 | r = yul.shr(f, r) 137 | 138 | r = yul.shr(127, yul.mul(r, r)) 139 | f = yul.shr(128, r) 140 | log_2 = yul.or_(log_2, yul.shl(62, f)) 141 | r = yul.shr(f, r) 142 | 143 | r = yul.shr(127, yul.mul(r, r)) 144 | f = yul.shr(128, r) 145 | log_2 = yul.or_(log_2, yul.shl(61, f)) 146 | r = yul.shr(f, r) 147 | 148 | r = yul.shr(127, yul.mul(r, r)) 149 | f = yul.shr(128, r) 150 | log_2 = yul.or_(log_2, yul.shl(60, f)) 151 | r = yul.shr(f, r) 152 | 153 | r = yul.shr(127, yul.mul(r, r)) 154 | f = yul.shr(128, r) 155 | log_2 = yul.or_(log_2, yul.shl(59, f)) 156 | r = yul.shr(f, r) 157 | 158 | r = yul.shr(127, yul.mul(r, r)) 159 | f = yul.shr(128, r) 160 | log_2 = yul.or_(log_2, yul.shl(58, f)) 161 | r = yul.shr(f, r) 162 | 163 | r = yul.shr(127, yul.mul(r, r)) 164 | f = yul.shr(128, r) 165 | log_2 = yul.or_(log_2, yul.shl(57, f)) 166 | r = yul.shr(f, r) 167 | 168 | r = yul.shr(127, yul.mul(r, r)) 169 | f = yul.shr(128, r) 170 | log_2 = yul.or_(log_2, yul.shl(56, f)) 171 | r = yul.shr(f, r) 172 | 173 | r = yul.shr(127, yul.mul(r, r)) 174 | f = yul.shr(128, r) 175 | log_2 = yul.or_(log_2, yul.shl(55, f)) 176 | r = yul.shr(f, r) 177 | 178 | r = yul.shr(127, yul.mul(r, r)) 179 | f = yul.shr(128, r) 180 | log_2 = yul.or_(log_2, yul.shl(54, f)) 181 | r = yul.shr(f, r) 182 | 183 | r = yul.shr(127, yul.mul(r, r)) 184 | f = yul.shr(128, r) 185 | log_2 = yul.or_(log_2, yul.shl(53, f)) 186 | r = yul.shr(f, r) 187 | 188 | r = yul.shr(127, yul.mul(r, r)) 189 | f = yul.shr(128, r) 190 | log_2 = yul.or_(log_2, yul.shl(52, f)) 191 | r = yul.shr(f, r) 192 | 193 | r = yul.shr(127, yul.mul(r, r)) 194 | f = yul.shr(128, r) 195 | log_2 = yul.or_(log_2, yul.shl(51, f)) 196 | r = yul.shr(f, r) 197 | 198 | r = yul.shr(127, yul.mul(r, r)) 199 | f = yul.shr(128, r) 200 | log_2 = yul.or_(log_2, yul.shl(50, f)) 201 | 202 | log_sqrt10001 = log_2 * 255738958999603826347141 # 128.128 number 203 | 204 | tick_low = int24( 205 | (log_sqrt10001 - 3402992956809132418596140100660247210) >> 128 206 | ) 207 | tick_high = int24( 208 | (log_sqrt10001 + 291339464771989622907027621153398088495) >> 128 209 | ) 210 | 211 | tick = ( 212 | tick_low 213 | if (tick_low == tick_high) 214 | else ( 215 | tick_high 216 | if getSqrtRatioAtTick(tick_high) <= sqrt_price_x96 217 | else tick_low 218 | ) 219 | ) 220 | 221 | return tick 222 | -------------------------------------------------------------------------------- /source/manager/arbitrage_manager.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | from typing import Dict, List, Optional, Union 3 | 4 | from web3 import Web3 5 | 6 | from degenbot import Erc20TokenHelperManager 7 | from degenbot.arbitrage.uniswap_lp_cycle import UniswapLpCycle 8 | from degenbot.constants import WRAPPED_NATIVE_TOKENS 9 | from degenbot.exceptions import ManagerError 10 | from degenbot.token import Erc20Token 11 | from degenbot.types import ArbitrageHelper, HelperManager 12 | from degenbot.uniswap.uniswap_managers import ( 13 | UniswapV2LiquidityPoolManager, 14 | UniswapV3LiquidityPoolManager, 15 | ) 16 | from degenbot.uniswap.v2 import LiquidityPool 17 | from degenbot.uniswap.v3 import V3LiquidityPool 18 | 19 | 20 | class ArbitrageHelperManager(HelperManager): 21 | """ 22 | A class that generates and tracks Arbitrage helpers 23 | 24 | The dictionary of arbitrage helpers is held as a class attribute, so all manager 25 | objects reference the same state data 26 | """ 27 | 28 | _state: Dict = {} 29 | 30 | def __init__(self, chain_id: int): 31 | # the internal state data for this object is held in the 32 | # class-level _state dictionary, keyed by the chain ID 33 | if self._state.get(chain_id): 34 | self.__dict__ = self._state[chain_id] 35 | else: 36 | self._state[chain_id] = {} 37 | self.__dict__ = self._state[chain_id] 38 | 39 | # initialize internal attributes 40 | self._arbs: Dict = {} # all known arbs, keyed by id 41 | self._blacklisted_ids: set = set() 42 | self._chain_id = chain_id 43 | self._erc20tokenmanager = Erc20TokenHelperManager(chain_id) 44 | self._lock = Lock() 45 | self._v2_pool_managers: Dict[ 46 | str, UniswapV2LiquidityPoolManager 47 | ] = {} # all V2 pool managers, keyed by factory address 48 | self._v3_pool_managers: Dict[ 49 | str, UniswapV3LiquidityPoolManager 50 | ] = {} # all V3 pool managers, keyed by factory address 51 | 52 | def add_pool_manager(self, factory_address: str, uniswap_version: int): 53 | """ 54 | Create a Uniswap pool manager from the factory contract address and version, store in the internal dictionary of pool managers 55 | """ 56 | 57 | if ( 58 | factory_address in self._v2_pool_managers 59 | or factory_address in self._v3_pool_managers 60 | ): 61 | raise ValueError( 62 | f"Pool manager for factory={factory_address} already exists" 63 | ) 64 | 65 | if uniswap_version == 2: 66 | self._v2_pool_managers[ 67 | factory_address 68 | ] = UniswapV2LiquidityPoolManager(factory_address) 69 | elif uniswap_version == 3: 70 | self._v3_pool_managers[ 71 | factory_address 72 | ] = UniswapV3LiquidityPoolManager(factory_address) 73 | else: 74 | raise ValueError 75 | 76 | def build( 77 | self, 78 | arb_type: str, 79 | swap_pools: Union[ 80 | List[Union[LiquidityPool, V3LiquidityPool]], 81 | List[str], 82 | ], 83 | update_method: str = "polling", 84 | input_token: Optional[Union[str, Erc20Token]] = None, 85 | ) -> ArbitrageHelper: 86 | """ 87 | Returns the arb helper 88 | """ 89 | 90 | native_wrapped_token_address = WRAPPED_NATIVE_TOKENS[self._chain_id] 91 | 92 | print(native_wrapped_token_address) 93 | 94 | input_token = self._erc20tokenmanager.get_erc20token( 95 | native_wrapped_token_address 96 | ) 97 | 98 | _swap_pools: List[Union[LiquidityPool, V3LiquidityPool]] = [] 99 | 100 | # replace all pool addresses with helper objects 101 | for i, pool in enumerate(swap_pools): 102 | if isinstance(pool, str): 103 | # if an address was provided, get the pool helper object 104 | pool_helper: Union[LiquidityPool, V3LiquidityPool] 105 | 106 | # iterate through the pool managers (may be multiple compatible DEX on one chain) 107 | for v2_pool_manager in self._v2_pool_managers.values(): 108 | try: 109 | pool_helper = v2_pool_manager.get_pool(pool) 110 | except: 111 | pass 112 | for v3_pool_manager in self._v3_pool_managers.values(): 113 | try: 114 | pool_helper = v3_pool_manager.get_pool(pool) 115 | except: 116 | pass 117 | try: 118 | pool_helper 119 | except: 120 | # will throw if the pool helper could not be found 121 | raise ValueError( 122 | f"Could not generate Uniswap LP helper for pool {pool}" 123 | ) 124 | else: 125 | print(pool_helper) 126 | elif isinstance(pool, (LiquidityPool, V3LiquidityPool)): 127 | # otherwise, use the helper directly 128 | pool_helper = pool 129 | else: 130 | raise TypeError( 131 | f"Pool {pool} is {type(pool)}! Expected LiquidityPool, V3LiquidityPool, or string" 132 | ) 133 | 134 | _swap_pools[i] = pool_helper 135 | 136 | arb_id = Web3.keccak( 137 | hexstr="".join([pool.address[2:] for pool in _swap_pools]) 138 | ).hex() 139 | 140 | # check if the helper is already known, throw exception if so 141 | try: 142 | arb_helper = self._arbs[arb_id] 143 | except KeyError: 144 | pass 145 | else: 146 | raise ValueError(f"Arbitrage helper already exists") 147 | 148 | arb_helper = UniswapLpCycle( 149 | input_token=input_token, 150 | swap_pools=_swap_pools, 151 | max_input=None, 152 | id=arb_id, 153 | ) 154 | return arb_helper 155 | 156 | def get( 157 | self, 158 | arb_id: str, 159 | arb_type: str, 160 | chain_id: int, 161 | input_token: Optional[Union[str, Erc20Token]] = None, 162 | update_method: str = "polling", 163 | ) -> ArbitrageHelper: 164 | """ 165 | Get an arbitrage path object from its ID and the type. An ID is a keccak address of all pool addresses, in order. 166 | 167 | Type can only be "cycle", but additional types will be added later 168 | """ 169 | 170 | # attempt to retrieve the arb (might already exist) 171 | try: 172 | arb_helper = self._arbs[arb_id][arb_type] 173 | except KeyError: 174 | pass 175 | else: 176 | return arb_helper 177 | 178 | # otherwise create a new arb helper 179 | try: 180 | if arb_type == "cycle": 181 | if input_token is None: 182 | native_wrapped_token_address = WRAPPED_NATIVE_TOKENS[ 183 | chain_id 184 | ] 185 | input_token = self._erc20tokenmanager.get_erc20token( 186 | native_wrapped_token_address 187 | ) 188 | elif not isinstance(input_token, Erc20Token): 189 | input_token = self._erc20tokenmanager.get_erc20token( 190 | input_token 191 | ) 192 | # arb_helper = UniswapLpCycle(input_token=input_token) 193 | arb_helper = self.build( 194 | arb_type=arb_type, 195 | # TODO: read pools from a file, 196 | # swap_pools=XXX, 197 | update_method=update_method, 198 | input_token=input_token, 199 | ) 200 | except: 201 | raise ManagerError(f"Could not create Arbitrage helper: {arb_id=}") 202 | else: 203 | return arb_helper 204 | -------------------------------------------------------------------------------- /source/uniswap/v2/multi_liquidity_pool.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from degenbot.token import Erc20Token 3 | from degenbot.uniswap.v2.liquidity_pool import LiquidityPool 4 | 5 | 6 | class MultiLiquidityPool: 7 | def __init__( 8 | self, 9 | token_in: Erc20Token, 10 | token_out: Erc20Token, 11 | pool_addresses: List[str], 12 | pool_tokens: List[List[Erc20Token]], 13 | name: str = "", 14 | update_method: str = "polling", 15 | silent: bool = False, 16 | ): 17 | self.token_in = token_in 18 | self.token_out = token_out 19 | self.token_in_quantity = 0 20 | self.token_out_quantity = 0 21 | self.init = True 22 | 23 | if len(pool_addresses) != len(pool_tokens): 24 | raise ValueError( 25 | "Number of pool addresses and token pairs must match!" 26 | ) 27 | 28 | # assert len(pool_addresses) == len( 29 | # pool_tokens 30 | # ), "Number of pool addresses and token pairs must match!" 31 | 32 | if not (len(pool_addresses) > 1): 33 | raise ValueError( 34 | f"Expected 2 pool addresses, found {len(pool_addresses)}" 35 | ) 36 | 37 | # assert ( 38 | # len(pool_addresses) > 1 39 | # ), "Only one LP submitted, use LiquidityPool() instead" 40 | 41 | number_of_pools = len(pool_addresses) 42 | 43 | # build the list of pool objects for the given addresses 44 | self._pools = [] 45 | for i in range(number_of_pools): 46 | self._pools.append( 47 | LiquidityPool( 48 | address=pool_addresses[i], 49 | update_method=update_method, 50 | tokens=pool_tokens[i], 51 | silent=silent, 52 | ) 53 | ) 54 | self.pool_addresses = pool_addresses 55 | 56 | if not ( 57 | (token_in == self._pools[0].token0) 58 | or (token_in == self._pools[0].token1) 59 | ): 60 | raise ValueError( 61 | f"First LP does not contain the submitted token_in ({token_in})" 62 | ) 63 | 64 | # assert (token_in == self._pools[0].token0) or ( 65 | # token_in == self._pools[0].token1 66 | # ), f"First LP does not contain the submitted token_in ({token_in})" 67 | 68 | if not ( 69 | (token_out == self._pools[-1].token0) 70 | or (token_out == self._pools[-1].token1) 71 | ): 72 | raise ValueError( 73 | f"Last LP does not contain the submitted token_out ({token_out})" 74 | ) 75 | 76 | # assert (token_out == self._pools[-1].token0) or ( 77 | # token_out == self._pools[-1].token1 78 | # ), f"Last LP does not contain the submitted token_out ({token_out})" 79 | 80 | # check that pools have a valid token path 81 | for i in range(number_of_pools - 1): 82 | if not ( 83 | (self._pools[i].token0 == self._pools[i + 1].token0) 84 | or (self._pools[i].token1 == self._pools[i + 1].token1) 85 | ): 86 | raise ValueError( 87 | f"LPs {self._pools[i]} and {self._pools[i+1]} do not share a common token!" 88 | ) 89 | # assert (self._pools[i].token0 == self._pools[i + 1].token0) or ( 90 | # self._pools[i].token1 == self._pools[i + 1].token1 91 | # ), f"LPs {self._pools[i]} and {self._pools[i+1]} do not share a common token!" 92 | 93 | if name: 94 | self.name = name 95 | 96 | def update_reserves( 97 | self, 98 | silent: bool = False, 99 | print_reserves: bool = True, 100 | print_ratios: bool = True, 101 | ) -> bool: 102 | """ 103 | Checks each liquidity pool for updates by passing a call to .update_reserves(), which returns False if there are no updates. 104 | Will calculate arbitrage amounts only after checking all pools and finding an update, or on startup (via the self._init variable) 105 | """ 106 | 107 | recalculate = False 108 | 109 | if self.init == True: 110 | self.init = False 111 | recalculate = True 112 | 113 | for pool in self._pools: 114 | if pool.update_reserves( 115 | silent=silent, 116 | print_reserves=print_reserves, 117 | print_ratios=print_ratios, 118 | ): 119 | recalculate = True 120 | 121 | if recalculate: 122 | self.calculate_multipool_tokens_out_from_tokens_in( 123 | token_in=self.token_in, 124 | token_in_quantity=self.token_in_quantity, 125 | silent=silent, 126 | ) 127 | return True 128 | else: 129 | return False 130 | 131 | def calculate_multipool_tokens_out_from_tokens_in( 132 | self, 133 | token_in: Erc20Token, 134 | token_in_quantity: int, 135 | silent: bool = False, 136 | ) -> None: 137 | """ 138 | Calculates the expected token OUTPUT from the last pool for a given token INPUT to the first pool at current pool reserves. 139 | Uses the self.token0 and self.token1 pointers to determine which token is being swapped in 140 | and uses the appropriate formula 141 | """ 142 | 143 | number_of_pools = len(self._pools) 144 | 145 | for i in range(number_of_pools): 146 | # determine the output token for this pool 147 | if token_in.address == self._pools[i].token0.address: 148 | token_out = self._pools[i].token1 149 | elif token_in.address == self._pools[i].token1.address: 150 | token_out = self._pools[i].token0 151 | else: 152 | print("wtf?") 153 | raise Exception 154 | 155 | # calculate the swap output from this pool 156 | token_out_quantity = self._pools[ 157 | i 158 | ].calculate_tokens_out_from_tokens_in( 159 | token_in=token_in, 160 | token_in_quantity=token_in_quantity, 161 | ) 162 | 163 | if i == number_of_pools - 1: 164 | # if we've reached the last pool, build amounts_out and store the output quantity 165 | self.token_out_quantity = token_out_quantity 166 | else: 167 | # otherwise, use the output as input on the next loop 168 | token_in = token_out 169 | token_in_quantity = token_out_quantity 170 | 171 | self._build_multipool_amounts_out( 172 | token_in=self.token_in, 173 | token_in_quantity=self.token_in_quantity, 174 | silent=silent, 175 | ) 176 | 177 | def update_balance( 178 | self, 179 | token_in_quantity: int, 180 | silent: bool = False, 181 | ): 182 | self.token_in_quantity = token_in_quantity 183 | self.calculate_multipool_tokens_out_from_tokens_in( 184 | token_in=self.token_in, 185 | token_in_quantity=self.token_in_quantity, 186 | silent=silent, 187 | ) 188 | 189 | def __str__(self): 190 | """ 191 | Return the pool name when the object is included in a print statement, or cast as a string 192 | """ 193 | return self.name 194 | 195 | def _build_multipool_amounts_out( 196 | self, 197 | token_in: Erc20Token, 198 | token_in_quantity: int, 199 | silent: bool = True, 200 | ) -> None: 201 | number_of_pools = len(self._pools) 202 | 203 | self.pools_amounts_out = [] 204 | 205 | for i in range(number_of_pools): 206 | # determine the output token for pool0 207 | if token_in.address == self._pools[i].token0.address: 208 | token_out = self._pools[i].token1 209 | elif token_in.address == self._pools[i].token1.address: 210 | token_out = self._pools[i].token0 211 | else: 212 | print("wtf?") 213 | raise Exception 214 | 215 | # calculate the swap output through pool[i] 216 | token_out_quantity = self._pools[ 217 | i 218 | ].calculate_tokens_out_from_tokens_in( 219 | token_in=token_in, 220 | token_in_quantity=token_in_quantity, 221 | ) 222 | 223 | if not silent: 224 | print( 225 | f"Swap {token_in_quantity} {token_in} for {token_out_quantity} {token_out} via {self._pools[i]}" 226 | ) 227 | 228 | if token_in.address == self._pools[i].token0.address: 229 | self.pools_amounts_out.append([0, token_out_quantity]) 230 | elif token_in.address == self._pools[i].token1.address: 231 | self.pools_amounts_out.append([token_out_quantity, 0]) 232 | 233 | # use the swap output as the swap input through the next pool 234 | token_in = token_out 235 | token_in_quantity = token_out_quantity 236 | -------------------------------------------------------------------------------- /source/uniswap/v3/snapshot.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import json 3 | from io import TextIOWrapper 4 | from typing import Dict, List, Optional, TextIO, Tuple, Union 5 | 6 | import brownie # type: ignore 7 | from eth_typing import ChecksumAddress 8 | from web3 import Web3 9 | from web3._utils.events import get_event_data 10 | from web3._utils.filters import construct_event_filter_params 11 | 12 | from degenbot.logging import logger 13 | from degenbot.uniswap.abi import UNISWAP_V3_POOL_ABI 14 | from degenbot.uniswap.v3.v3_liquidity_pool import ( 15 | UniswapV3BitmapAtWord, 16 | UniswapV3LiquidityAtTick, 17 | UniswapV3PoolExternalUpdate, 18 | V3LiquidityPool, 19 | ) 20 | 21 | 22 | @dataclasses.dataclass(slots=True) 23 | class UniswapV3LiquidityEvent: 24 | block_number: int 25 | liquidity: int 26 | tick_lower: int 27 | tick_upper: int 28 | tx_index: int 29 | 30 | 31 | def _process_log( 32 | log, event_abi 33 | ) -> Tuple[ChecksumAddress, UniswapV3LiquidityEvent]: 34 | decoded_event = get_event_data(brownie.web3.codec, event_abi, log) 35 | 36 | pool_address = Web3.toChecksumAddress(decoded_event["address"]) 37 | tx_index = decoded_event["transactionIndex"] 38 | liquidity_block = decoded_event["blockNumber"] 39 | liquidity = decoded_event["args"]["amount"] * ( 40 | -1 if decoded_event["event"] == "Burn" else 1 41 | ) 42 | tick_lower = decoded_event["args"]["tickLower"] 43 | tick_upper = decoded_event["args"]["tickUpper"] 44 | 45 | return pool_address, UniswapV3LiquidityEvent( 46 | block_number=liquidity_block, 47 | liquidity=liquidity, 48 | tick_lower=tick_lower, 49 | tick_upper=tick_upper, 50 | tx_index=tx_index, 51 | ) 52 | 53 | 54 | class UniswapV3LiquiditySnapshot: 55 | """ 56 | Retrieve and maintain liquidity positions for Uniswap V3 pools. 57 | """ 58 | 59 | def __init__( 60 | self, file: Union[TextIO, str], chain_id: Optional[int] = None 61 | ): 62 | _file: TextIOWrapper 63 | 64 | try: 65 | if isinstance(file, TextIOWrapper): 66 | _file = file 67 | json_liquidity_snapshot = json.load(file) 68 | elif isinstance(file, str): 69 | with open(file) as _file: 70 | json_liquidity_snapshot = json.load(_file) 71 | else: 72 | raise ValueError(f"GOT {type(file)}") 73 | except: 74 | raise 75 | finally: 76 | _file.close() 77 | 78 | if chain_id is None: 79 | chain_id = brownie.chain.id 80 | self._chain_id = chain_id 81 | self.newest_block = json_liquidity_snapshot.pop("snapshot_block") 82 | 83 | self._liquidity_snapshot: Dict[ChecksumAddress, Dict] = dict() 84 | for ( 85 | pool_address, 86 | pool_liquidity_snapshot, 87 | ) in json_liquidity_snapshot.items(): 88 | self._liquidity_snapshot[Web3.toChecksumAddress(pool_address)] = { 89 | "tick_bitmap": { 90 | int(k): UniswapV3BitmapAtWord(**v) 91 | for k, v in pool_liquidity_snapshot["tick_bitmap"].items() 92 | }, 93 | "tick_data": { 94 | int(k): UniswapV3LiquidityAtTick(**v) 95 | for k, v in pool_liquidity_snapshot["tick_data"].items() 96 | }, 97 | } 98 | 99 | logger.info( 100 | f"Loaded LP snapshot: {len(json_liquidity_snapshot)} pools @ block {self.newest_block}" 101 | ) 102 | 103 | self._liquidity_events: Dict[ 104 | ChecksumAddress, List[UniswapV3LiquidityEvent] 105 | ] = dict() 106 | 107 | def _add_pool_if_missing(self, pool_address: ChecksumAddress) -> None: 108 | try: 109 | self._liquidity_events[pool_address] 110 | except KeyError: 111 | self._liquidity_events[pool_address] = [] 112 | 113 | try: 114 | self._liquidity_snapshot[pool_address] 115 | except KeyError: 116 | self._liquidity_snapshot[pool_address] = {} 117 | 118 | def fetch_new_liquidity_events( 119 | self, 120 | to_block: int, 121 | span: int = 1000, 122 | ) -> None: 123 | logger.info( 124 | f"Updating snapshot from block {self.newest_block} to {to_block}" 125 | ) 126 | 127 | v3pool = Web3().eth.contract(abi=UNISWAP_V3_POOL_ABI) 128 | 129 | for event in [v3pool.events.Mint, v3pool.events.Burn]: 130 | logger.info(f"Processing {event.event_name} events") 131 | event_abi = event._get_event_abi() 132 | start_block = self.newest_block + 1 133 | 134 | while True: 135 | end_block = min(to_block, start_block + span - 1) 136 | 137 | _, event_filter_params = construct_event_filter_params( 138 | event_abi=event_abi, 139 | abi_codec=brownie.web3.codec, 140 | fromBlock=start_block, 141 | toBlock=end_block, 142 | ) 143 | 144 | event_logs = brownie.web3.eth.get_logs(event_filter_params) 145 | 146 | for log in event_logs: 147 | pool_address, liquidity_event = _process_log( 148 | log, event_abi 149 | ) 150 | 151 | # skip zero liquidity events 152 | if liquidity_event.liquidity == 0: 153 | continue 154 | 155 | self._add_pool_if_missing(pool_address) 156 | self._liquidity_events[pool_address].append( 157 | liquidity_event 158 | ) 159 | 160 | if end_block == to_block: 161 | break 162 | else: 163 | start_block = end_block + 1 164 | 165 | logger.info(f"Updated snapshot to block {to_block}") 166 | self.newest_block = to_block 167 | 168 | def get_pool_updates( 169 | self, pool_address 170 | ) -> List[UniswapV3PoolExternalUpdate]: 171 | try: 172 | self._liquidity_events[pool_address] 173 | except KeyError: 174 | return [] 175 | else: 176 | # Sort the liquidity events by block, then transaction index 177 | # before returning them. 178 | # @dev the V3LiquidityPool helper will reject liquidity events 179 | # associated with a past block, so they must be processed in 180 | # chronological order 181 | sorted_events = sorted( 182 | self._liquidity_events[pool_address], 183 | key=lambda event: (event.block_number, event.tx_index), 184 | ) 185 | self._liquidity_events[pool_address].clear() 186 | 187 | return [ 188 | UniswapV3PoolExternalUpdate( 189 | block_number=event.block_number, 190 | liquidity_change=( 191 | event.liquidity, 192 | event.tick_lower, 193 | event.tick_upper, 194 | ), 195 | ) 196 | for event in sorted_events 197 | ] 198 | 199 | def get_tick_bitmap( 200 | self, pool: Union[ChecksumAddress, V3LiquidityPool] 201 | ) -> Dict[int, UniswapV3BitmapAtWord]: 202 | if isinstance(pool, V3LiquidityPool): 203 | pool_address = pool.address 204 | elif isinstance(pool, str): 205 | pool_address = Web3.toChecksumAddress(pool) 206 | else: 207 | raise ValueError(f"Unexpected input for pool: {type(pool)}") 208 | 209 | try: 210 | return self._liquidity_snapshot[pool_address]["tick_bitmap"] 211 | except KeyError: 212 | return {} 213 | 214 | def get_tick_data( 215 | self, pool: Union[ChecksumAddress, V3LiquidityPool] 216 | ) -> Dict[int, UniswapV3LiquidityAtTick]: 217 | if isinstance(pool, V3LiquidityPool): 218 | pool_address = pool.address 219 | elif isinstance(pool, str): 220 | pool_address = Web3.toChecksumAddress(pool) 221 | else: 222 | raise ValueError(f"Unexpected input for pool: {type(pool)}") 223 | 224 | try: 225 | return self._liquidity_snapshot[pool_address]["tick_data"] 226 | except KeyError: 227 | return {} 228 | 229 | def update_snapshot( 230 | self, 231 | pool: Union[V3LiquidityPool, ChecksumAddress], 232 | tick_data: Dict[int, UniswapV3LiquidityAtTick], 233 | tick_bitmap: Dict[int, UniswapV3BitmapAtWord], 234 | ) -> None: 235 | if isinstance(pool, V3LiquidityPool): 236 | pool_address = pool.address 237 | elif isinstance(pool, str): 238 | pool_address = Web3.toChecksumAddress(pool) 239 | else: 240 | raise ValueError(f"Unexpected input for pool: {type(pool)}") 241 | 242 | self._add_pool_if_missing(pool_address) 243 | self._liquidity_snapshot[pool_address].update( 244 | { 245 | "tick_bitmap": tick_bitmap, 246 | } 247 | ) 248 | self._liquidity_snapshot[pool_address].update( 249 | { 250 | "tick_data": tick_data, 251 | } 252 | ) 253 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/tickbitmap_test.py: -------------------------------------------------------------------------------- 1 | from degenbot.uniswap.v3.libraries import TickBitmap, TickMath 2 | from degenbot.uniswap.v3.v3_liquidity_pool import UniswapV3BitmapAtWord 3 | from typing import Dict 4 | 5 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 6 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/TickBitmap.spec.ts 7 | 8 | 9 | def is_initialized(tick_bitmap: Dict[int, UniswapV3BitmapAtWord], tick: int): 10 | # Adapted from Uniswap test contract 11 | # ref: https://github.com/Uniswap/v3-core/blob/main/contracts/test/TickBitmapTest.sol 12 | 13 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 14 | tick_bitmap, tick, 1, True 15 | ) 16 | return next == tick if initialized else False 17 | 18 | 19 | def empty_bitmap(): 20 | """ 21 | Generates an empty tick bitmap of maximum size 22 | """ 23 | 24 | tick_bitmap = {} 25 | for tick in range(TickMath.MIN_TICK, TickMath.MAX_TICK): 26 | wordPos, _ = TickBitmap.position(tick=tick) 27 | tick_bitmap[wordPos] = UniswapV3BitmapAtWord() 28 | return tick_bitmap 29 | 30 | 31 | def test_isInitialized(): 32 | tick_bitmap = empty_bitmap() 33 | 34 | assert is_initialized(tick_bitmap, 1) == False 35 | 36 | TickBitmap.flipTick(tick_bitmap, 1, tick_spacing=1) 37 | assert is_initialized(tick_bitmap, 1) == True 38 | 39 | # TODO: The repo flips this tick twice, which may be a mistake 40 | # TickBitmap.flipTick(tick_bitmap, 1, tick_spacing=1) 41 | TickBitmap.flipTick(tick_bitmap, tick=1, tick_spacing=1) 42 | assert is_initialized(tick_bitmap, 1) == False 43 | 44 | TickBitmap.flipTick(tick_bitmap, tick=2, tick_spacing=1) 45 | assert is_initialized(tick_bitmap, 1) == False 46 | 47 | TickBitmap.flipTick(tick_bitmap, tick=1 + 256, tick_spacing=1) 48 | assert is_initialized(tick_bitmap, 257) == True 49 | assert is_initialized(tick_bitmap, 1) == False 50 | 51 | 52 | def test_flipTick(): 53 | tick_bitmap = empty_bitmap() 54 | 55 | TickBitmap.flipTick(tick_bitmap, tick=-230, tick_spacing=1) 56 | assert is_initialized(tick_bitmap, -230) == True 57 | assert is_initialized(tick_bitmap, -231) == False 58 | assert is_initialized(tick_bitmap, -229) == False 59 | assert is_initialized(tick_bitmap, -230 + 256) == False 60 | assert is_initialized(tick_bitmap, -230 - 256) == False 61 | 62 | TickBitmap.flipTick(tick_bitmap, tick=-230, tick_spacing=1) 63 | assert is_initialized(tick_bitmap, -230) == False 64 | assert is_initialized(tick_bitmap, -231) == False 65 | assert is_initialized(tick_bitmap, -229) == False 66 | assert is_initialized(tick_bitmap, -230 + 256) == False 67 | assert is_initialized(tick_bitmap, -230 - 256) == False 68 | 69 | TickBitmap.flipTick(tick_bitmap, tick=-230, tick_spacing=1) 70 | TickBitmap.flipTick(tick_bitmap, tick=-259, tick_spacing=1) 71 | TickBitmap.flipTick(tick_bitmap, tick=-229, tick_spacing=1) 72 | TickBitmap.flipTick(tick_bitmap, tick=500, tick_spacing=1) 73 | TickBitmap.flipTick(tick_bitmap, tick=-259, tick_spacing=1) 74 | TickBitmap.flipTick(tick_bitmap, tick=-229, tick_spacing=1) 75 | TickBitmap.flipTick(tick_bitmap, tick=-259, tick_spacing=1) 76 | 77 | assert is_initialized(tick_bitmap, -259) == True 78 | assert is_initialized(tick_bitmap, -229) == False 79 | 80 | 81 | def test_nextInitializedTickWithinOneWord(): 82 | tick_bitmap: Dict[int, UniswapV3BitmapAtWord] = {} 83 | 84 | # set up a full-sized empty tick bitmap 85 | for tick in range(TickMath.MIN_TICK, TickMath.MAX_TICK): 86 | wordPos, _ = TickBitmap.position(tick=tick) 87 | if not tick_bitmap.get(wordPos): 88 | tick_bitmap[wordPos] = UniswapV3BitmapAtWord() 89 | 90 | # set the specified ticks to initialized 91 | for tick in [-200, -55, -4, 70, 78, 84, 139, 240, 535]: 92 | TickBitmap.flipTick(tick_bitmap=tick_bitmap, tick=tick, tick_spacing=1) 93 | 94 | # lte = false tests 95 | 96 | # returns tick to right if at initialized tick 97 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 98 | tick_bitmap=tick_bitmap, 99 | tick=78, 100 | tick_spacing=1, 101 | less_than_or_equal=False, 102 | ) 103 | assert next == 84 104 | assert initialized == True 105 | 106 | # returns tick to right if at initialized tick 107 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 108 | tick_bitmap=tick_bitmap, 109 | tick=-55, 110 | tick_spacing=1, 111 | less_than_or_equal=False, 112 | ) 113 | assert next == -4 114 | assert initialized == True 115 | 116 | # returns the tick directly to the right 117 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 118 | tick_bitmap=tick_bitmap, 119 | tick=77, 120 | tick_spacing=1, 121 | less_than_or_equal=False, 122 | ) 123 | assert next == 78 124 | assert initialized == True 125 | 126 | # returns the tick directly to the right 127 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 128 | tick_bitmap=tick_bitmap, 129 | tick=-56, 130 | tick_spacing=1, 131 | less_than_or_equal=False, 132 | ) 133 | assert next == -55 134 | assert initialized == True 135 | 136 | # returns the next words initialized tick if on the right boundary 137 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 138 | tick_bitmap=tick_bitmap, 139 | tick=255, 140 | tick_spacing=1, 141 | less_than_or_equal=False, 142 | ) 143 | assert next == 511 144 | assert initialized == False 145 | 146 | # returns the next words initialized tick if on the right boundary 147 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 148 | tick_bitmap=tick_bitmap, 149 | tick=-257, 150 | tick_spacing=1, 151 | less_than_or_equal=False, 152 | ) 153 | assert next == -200 154 | assert initialized == True 155 | 156 | # does not exceed boundary 157 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 158 | tick_bitmap=tick_bitmap, 159 | tick=508, 160 | tick_spacing=1, 161 | less_than_or_equal=False, 162 | ) 163 | assert next == 511 164 | assert initialized == False 165 | 166 | # skips entire word 167 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 168 | tick_bitmap=tick_bitmap, 169 | tick=255, 170 | tick_spacing=1, 171 | less_than_or_equal=False, 172 | ) 173 | assert next == 511 174 | assert initialized == False 175 | 176 | # skips half word 177 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 178 | tick_bitmap=tick_bitmap, 179 | tick=383, 180 | tick_spacing=1, 181 | less_than_or_equal=False, 182 | ) 183 | assert next == 511 184 | assert initialized == False 185 | 186 | # lte = true tests 187 | 188 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 189 | tick_bitmap=tick_bitmap, 190 | tick=78, 191 | tick_spacing=1, 192 | less_than_or_equal=True, 193 | ) 194 | assert next == 78 195 | assert initialized == True 196 | 197 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 198 | tick_bitmap=tick_bitmap, 199 | tick=79, 200 | tick_spacing=1, 201 | less_than_or_equal=True, 202 | ) 203 | assert next == 78 204 | assert initialized == True 205 | 206 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 207 | tick_bitmap=tick_bitmap, 208 | tick=258, 209 | tick_spacing=1, 210 | less_than_or_equal=True, 211 | ) 212 | assert next == 256 213 | assert initialized == False 214 | 215 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 216 | tick_bitmap=tick_bitmap, 217 | tick=256, 218 | tick_spacing=1, 219 | less_than_or_equal=True, 220 | ) 221 | assert next == 256 222 | assert initialized == False 223 | 224 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 225 | tick_bitmap=tick_bitmap, 226 | tick=72, 227 | tick_spacing=1, 228 | less_than_or_equal=True, 229 | ) 230 | assert next == 70 231 | assert initialized == True 232 | 233 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 234 | tick_bitmap=tick_bitmap, 235 | tick=-257, 236 | tick_spacing=1, 237 | less_than_or_equal=True, 238 | ) 239 | assert next == -512 240 | assert initialized == False 241 | 242 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 243 | tick_bitmap=tick_bitmap, 244 | tick=1023, 245 | tick_spacing=1, 246 | less_than_or_equal=True, 247 | ) 248 | assert next == 768 249 | assert initialized == False 250 | 251 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 252 | tick_bitmap=tick_bitmap, 253 | tick=900, 254 | tick_spacing=1, 255 | less_than_or_equal=True, 256 | ) 257 | assert next == 768 258 | assert initialized == False 259 | 260 | next, initialized = TickBitmap.nextInitializedTickWithinOneWord( 261 | tick_bitmap=tick_bitmap, 262 | tick=900, 263 | tick_spacing=1, 264 | less_than_or_equal=True, 265 | ) 266 | -------------------------------------------------------------------------------- /source/uniswap/v2/liquidity_pool_test.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | 3 | import pytest 4 | import web3 5 | 6 | from degenbot import Erc20Token, LiquidityPool 7 | from degenbot.exceptions import ZeroSwapError 8 | from degenbot.uniswap.v2.liquidity_pool import ( 9 | UniswapV2PoolSimulationResult, 10 | UniswapV2PoolState, 11 | ) 12 | 13 | 14 | class MockErc20Token(Erc20Token): 15 | def __init__(self): 16 | pass 17 | 18 | 19 | class MockLiquidityPool(LiquidityPool): 20 | def __init__(self): 21 | pass 22 | 23 | 24 | # Test is based on the WBTC-WETH Uniswap V2 pool on Ethereum mainnet, 25 | # evaluated against the results from the Uniswap V2 Router 2 contract 26 | # functions `getAmountsOut` and `getAmountsIn` 27 | # 28 | # Pool address: 0xBb2b8038a1640196FbE3e38816F3e67Cba72D940 29 | # Router address: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D 30 | 31 | 32 | token0 = MockErc20Token() 33 | token0.address = web3.Web3.toChecksumAddress( 34 | "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 35 | ) 36 | token0.decimals = 8 37 | token0.name = "Wrapped BTC" 38 | token0.symbol = "WBTC" 39 | 40 | token1 = MockErc20Token() 41 | token1.address = web3.Web3.toChecksumAddress( 42 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 43 | ) 44 | token1.decimals = 18 45 | token1.name = "Wrapped Ether" 46 | token1.symbol = "WETH" 47 | 48 | lp = MockLiquidityPool() 49 | lp.name = "WBTC-WETH (V2, 0.30%)" 50 | lp.address = web3.Web3.toChecksumAddress( 51 | "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940" 52 | ) 53 | lp.factory = web3.Web3.toChecksumAddress( 54 | "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" 55 | ) 56 | lp.fee = None 57 | lp.fee_token0 = Fraction(3, 1000) 58 | lp.fee_token1 = Fraction(3, 1000) 59 | lp.reserves_token0 = 16231137593 60 | lp.reserves_token1 = 2571336301536722443178 61 | lp.token0 = token0 62 | lp.token1 = token1 63 | lp._update_pool_state() 64 | 65 | 66 | def test_calculate_tokens_out_from_tokens_in(): 67 | # Reserve values for this test are taken at block height 17,600,000 68 | 69 | assert ( 70 | lp.calculate_tokens_out_from_tokens_in( 71 | lp.token0, 72 | 8000000000, 73 | ) 74 | == 847228560678214929944 75 | ) 76 | assert ( 77 | lp.calculate_tokens_out_from_tokens_in( 78 | lp.token1, 79 | 1200000000000000000000, 80 | ) 81 | == 5154005339 82 | ) 83 | 84 | 85 | def test_calculate_tokens_out_from_tokens_in_with_override(): 86 | # Overridden reserve values for this test are taken at block height 17,650,000 87 | # token0 reserves: 16027096956 88 | # token1 reserves: 2602647332090181827846 89 | 90 | pool_state_override = UniswapV2PoolState( 91 | pool=lp, 92 | reserves_token0=16027096956, 93 | reserves_token1=2602647332090181827846, 94 | ) 95 | 96 | assert ( 97 | lp.calculate_tokens_out_from_tokens_in( 98 | token_in=lp.token0, 99 | token_in_quantity=8000000000, 100 | override_state=pool_state_override, 101 | ) 102 | == 864834865217768537471 103 | ) 104 | 105 | with pytest.raises( 106 | ValueError, 107 | match="Must provide reserve override values for both tokens", 108 | ): 109 | lp.calculate_tokens_out_from_tokens_in( 110 | token_in=lp.token0, 111 | token_in_quantity=8000000000, 112 | override_reserves_token0=0, 113 | override_reserves_token1=10, 114 | ) 115 | 116 | 117 | def test_calculate_tokens_in_from_tokens_out(): 118 | # Reserve values for this test are taken at block height 17,600,000 119 | 120 | assert ( 121 | lp.calculate_tokens_in_from_tokens_out( 122 | 8000000000, 123 | lp.token1, 124 | ) 125 | == 2506650866141614297072 126 | ) 127 | 128 | assert ( 129 | lp.calculate_tokens_in_from_tokens_out( 130 | 1200000000000000000000, 131 | lp.token0, 132 | ) 133 | == 14245938804 134 | ) 135 | 136 | 137 | def test_calculate_tokens_in_from_tokens_out_with_override(): 138 | # Overridden reserve values for this test are taken at block height 17,650,000 139 | # token0 reserves: 16027096956 140 | # token1 reserves: 2602647332090181827846 141 | 142 | pool_state_override = UniswapV2PoolState( 143 | pool=lp, 144 | reserves_token0=16027096956, 145 | reserves_token1=2602647332090181827846, 146 | ) 147 | 148 | assert ( 149 | lp.calculate_tokens_in_from_tokens_out( 150 | token_in=lp.token0, 151 | token_out_quantity=1200000000000000000000, 152 | override_state=pool_state_override, 153 | ) 154 | == 13752842264 155 | ) 156 | 157 | with pytest.raises( 158 | ValueError, 159 | match="Must provide reserve override values for both tokens", 160 | ): 161 | lp.calculate_tokens_in_from_tokens_out( 162 | token_in=lp.token0, 163 | token_out_quantity=1200000000000000000000, 164 | override_reserves_token0=0, 165 | override_reserves_token1=10, 166 | ) 167 | 168 | 169 | def test_comparisons(): 170 | assert lp == "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940" 171 | assert lp == "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940".lower() 172 | 173 | other_lp = MockLiquidityPool() 174 | other_lp.name = "WBTC-WETH (V2, 0.30%)" 175 | other_lp.address = web3.Web3.toChecksumAddress( 176 | "0xBb2b8038a1640196FbE3e38816F3e67Cba72D940" 177 | ) 178 | 179 | assert lp == other_lp 180 | 181 | with pytest.raises(NotImplementedError): 182 | assert lp == 420 183 | 184 | # sets depend on __hash__ dunder method 185 | set([lp, other_lp]) 186 | 187 | 188 | def test_simulations(): 189 | sim_result = UniswapV2PoolSimulationResult( 190 | amount0_delta=8000000000, 191 | amount1_delta=-847228560678214929944, 192 | current_state=lp.state, 193 | future_state=UniswapV2PoolState( 194 | pool=lp, 195 | reserves_token0=lp.reserves_token0 + 8000000000, 196 | reserves_token1=lp.reserves_token1 - 847228560678214929944, 197 | ), 198 | ) 199 | 200 | # token_in = lp.token0 should have same result as token_out = lp.token1 201 | assert ( 202 | lp.simulate_swap( 203 | token_in=lp.token0, 204 | token_in_quantity=8000000000, 205 | ) 206 | == sim_result 207 | ) 208 | assert ( 209 | lp.simulate_swap( 210 | token_out=lp.token1, 211 | token_in_quantity=8000000000, 212 | ) 213 | == sim_result 214 | ) 215 | 216 | sim_result = UniswapV2PoolSimulationResult( 217 | amount0_delta=-5154005339, 218 | amount1_delta=1200000000000000000000, 219 | current_state=lp.state, 220 | future_state=UniswapV2PoolState( 221 | pool=lp, 222 | reserves_token0=lp.reserves_token0 - 5154005339, 223 | reserves_token1=lp.reserves_token1 + 1200000000000000000000, 224 | ), 225 | ) 226 | 227 | assert ( 228 | lp.simulate_swap( 229 | token_in=lp.token1, 230 | token_in_quantity=1200000000000000000000, 231 | ) 232 | == sim_result 233 | ) 234 | 235 | assert ( 236 | lp.simulate_swap( 237 | token_out=lp.token0, 238 | token_in_quantity=1200000000000000000000, 239 | ) 240 | == sim_result 241 | ) 242 | 243 | 244 | def test_simulations_with_override(): 245 | sim_result = UniswapV2PoolSimulationResult( 246 | amount0_delta=8000000000, 247 | amount1_delta=-864834865217768537471, 248 | current_state=lp.state, 249 | future_state=UniswapV2PoolState( 250 | pool=lp, 251 | reserves_token0=lp.reserves_token0 + 8000000000, 252 | reserves_token1=lp.reserves_token1 - 864834865217768537471, 253 | ), 254 | ) 255 | 256 | pool_state_override = UniswapV2PoolState( 257 | pool=lp, 258 | reserves_token0=16027096956, 259 | reserves_token1=2602647332090181827846, 260 | ) 261 | 262 | assert ( 263 | lp.simulate_swap( 264 | token_in=lp.token0, 265 | token_in_quantity=8000000000, 266 | override_state=pool_state_override, 267 | ) 268 | == sim_result 269 | ) 270 | 271 | sim_result = UniswapV2PoolSimulationResult( 272 | amount0_delta=13752842264, 273 | amount1_delta=-1200000000000000000000, 274 | current_state=lp.state, 275 | future_state=UniswapV2PoolState( 276 | pool=lp, 277 | reserves_token0=lp.reserves_token0 + 13752842264, 278 | reserves_token1=lp.reserves_token1 - 1200000000000000000000, 279 | ), 280 | ) 281 | 282 | assert ( 283 | lp.simulate_swap( 284 | token_out=lp.token1, 285 | token_out_quantity=1200000000000000000000, 286 | override_state=pool_state_override, 287 | ) 288 | == sim_result 289 | ) 290 | 291 | 292 | def test_zero_swaps(): 293 | with pytest.raises(ZeroSwapError): 294 | assert ( 295 | lp.calculate_tokens_out_from_tokens_in( 296 | lp.token0, 297 | 0, 298 | ) 299 | == 0 300 | ) 301 | 302 | with pytest.raises(ZeroSwapError): 303 | assert ( 304 | lp.calculate_tokens_out_from_tokens_in( 305 | lp.token1, 306 | 0, 307 | ) 308 | == 0 309 | ) 310 | 311 | 312 | def test_swap_for_all(): 313 | # The last token in a pool can never be swapped for 314 | assert ( 315 | lp.calculate_tokens_out_from_tokens_in( 316 | lp.token1, 317 | 2**256 - 1, 318 | ) 319 | == lp.reserves_token0 - 1 320 | ) 321 | assert ( 322 | lp.calculate_tokens_out_from_tokens_in( 323 | lp.token0, 324 | 2**256 - 1, 325 | ) 326 | == lp.reserves_token1 - 1 327 | ) 328 | -------------------------------------------------------------------------------- /source/arbitrage/flash_borrow_to_router_swap.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import List 3 | 4 | from brownie import Contract # type: ignore 5 | from scipy import optimize # type: ignore 6 | 7 | from degenbot.uniswap.v2.liquidity_pool import LiquidityPool 8 | from degenbot.types import ArbitrageHelper 9 | from degenbot.token import Erc20Token 10 | 11 | 12 | class FlashBorrowToRouterSwap(ArbitrageHelper): 13 | def __init__( 14 | self, 15 | borrow_pool: LiquidityPool, 16 | borrow_token: Erc20Token, 17 | swap_factory_address: str, 18 | swap_router_address: str, 19 | swap_token_addresses: List[str], 20 | swap_router_fee=Fraction(3, 1000), 21 | name: str = "", 22 | update_method="polling", 23 | ): 24 | if borrow_token.address != swap_token_addresses[0]: 25 | raise ValueError( 26 | "Token addresses must begin with the borrowed token" 27 | ) 28 | # assert ( 29 | # borrow_token.address == swap_token_addresses[0] 30 | # ), "Token addresses must begin with the borrowed token" 31 | 32 | if borrow_pool.token0 == borrow_token: 33 | if borrow_pool.token1.address != swap_token_addresses[-1]: 34 | raise ValueError( 35 | "Token addresses must end with the repaid token" 36 | ) 37 | # assert ( 38 | # borrow_pool.token1.address == swap_token_addresses[-1] 39 | # ), "Token addresses must end with the repaid token" 40 | else: 41 | if borrow_pool.token0.address != swap_token_addresses[-1]: 42 | raise ValueError( 43 | "Token addresses must end with the repaid token" 44 | ) 45 | # assert ( 46 | # borrow_pool.token0.address == swap_token_addresses[-1] 47 | # ), "Token addresses must end with the repaid token" 48 | 49 | self.swap_router_address = swap_router_address 50 | 51 | # build a list of all tokens involved in this swapping path 52 | self.tokens = [] 53 | for address in swap_token_addresses: 54 | self.tokens.append(Erc20Token(address=address)) 55 | 56 | if name: 57 | self.name = name 58 | else: 59 | self.name = "-".join([token.symbol for token in self.tokens]) 60 | 61 | self.token_path = [token.address for token in self.tokens] 62 | 63 | # build the list of intermediate pool pairs for the given multi-token path. 64 | # Pool list length will be 1 less than the token path length, e.g. a token1->token2->token3 65 | # path will result in a pool list consisting of token1/token2 and token2/token3 66 | self.swap_pools = [] 67 | self.swap_pool_addresses = [] 68 | try: 69 | _factory = Contract(swap_factory_address) 70 | except Exception as e: 71 | print(e) 72 | _factory = Contract.from_explorer(swap_factory_address) 73 | 74 | for i in range(len(self.token_path) - 1): 75 | self.swap_pools.append( 76 | LiquidityPool( 77 | address=_factory.getPair( 78 | self.token_path[i], self.token_path[i + 1] 79 | ), 80 | name=" - ".join( 81 | [self.tokens[i].symbol, self.tokens[i + 1].symbol] 82 | ), 83 | tokens=[self.tokens[i], self.tokens[i + 1]], 84 | update_method=update_method, 85 | fee=swap_router_fee, 86 | ) 87 | ) 88 | print( 89 | f"Loaded LP: {self.tokens[i].symbol} - {self.tokens[i+1].symbol}" 90 | ) 91 | # build a list of pool addresses in the swap path 92 | self.swap_pool_addresses.append(self.swap_pools[i].address) 93 | 94 | self.borrow_pool = borrow_pool 95 | self.borrow_token = borrow_token 96 | 97 | if self.borrow_token == self.borrow_pool.token0: 98 | self.repay_token = self.borrow_pool.token1 99 | elif self.borrow_token == self.borrow_pool.token1: 100 | self.repay_token = self.borrow_pool.token0 101 | 102 | self.best = { 103 | "init": True, 104 | "borrow": 0, 105 | "borrow_token": self.borrow_token, 106 | "profit": 0, 107 | "profit_token": self.repay_token, 108 | } 109 | 110 | def __str__(self): 111 | return self.name 112 | 113 | def update_reserves( 114 | self, 115 | silent: bool = False, 116 | print_reserves: bool = True, 117 | print_ratios: bool = True, 118 | ) -> bool: 119 | """ 120 | Checks each liquidity pool for updates by passing a call to .update_reserves(), which returns False if there are no updates. 121 | Will calculate arbitrage amounts only after checking all pools and finding an update, or on startup (via the 'init' dictionary key) 122 | """ 123 | recalculate = False 124 | 125 | # calculate initial arbitrage after the object is instantiated, otherwise proceed with normal checks 126 | if self.best["init"] == True: 127 | self.best["init"] = False 128 | recalculate = True 129 | 130 | # flag for recalculation if the borrowing pool has been updated 131 | if self.borrow_pool.update_reserves( 132 | silent=silent, 133 | print_reserves=print_reserves, 134 | print_ratios=print_ratios, 135 | ): 136 | recalculate = True 137 | 138 | # flag for recalculation if any of the pools along the swap path have been updated 139 | for pool in self.swap_pools: 140 | if pool.update_reserves( 141 | silent=silent, 142 | print_reserves=print_reserves, 143 | print_ratios=print_ratios, 144 | ): 145 | recalculate = True 146 | 147 | if recalculate: 148 | self._calculate_arbitrage() 149 | return True 150 | else: 151 | return False 152 | 153 | def _calculate_arbitrage(self): 154 | # set up the boundaries for the Brent optimizer based on which token is being borrowed 155 | if self.borrow_token.address == self.borrow_pool.token0.address: 156 | bounds = ( 157 | 1, 158 | float(self.borrow_pool.reserves_token0), 159 | ) 160 | bracket = ( 161 | 0.01 * self.borrow_pool.reserves_token0, 162 | 0.05 * self.borrow_pool.reserves_token0, 163 | ) 164 | else: 165 | bounds = ( 166 | 1, 167 | float(self.borrow_pool.reserves_token1), 168 | ) 169 | bracket = ( 170 | 0.01 * self.borrow_pool.reserves_token1, 171 | 0.05 * self.borrow_pool.reserves_token1, 172 | ) 173 | 174 | opt = optimize.minimize_scalar( 175 | lambda x: -float( 176 | self.calculate_multipool_tokens_out_from_tokens_in( 177 | token_in=self.borrow_token, 178 | token_in_quantity=x, 179 | ) 180 | - self.borrow_pool.calculate_tokens_in_from_tokens_out( 181 | token_in=self.repay_token, 182 | token_out_quantity=x, 183 | ) 184 | ), 185 | method="bounded", 186 | bounds=bounds, 187 | bracket=bracket, 188 | ) 189 | 190 | best_borrow = opt.x 191 | best_profit = -opt.fun 192 | swap_out = self.calculate_multipool_tokens_out_from_tokens_in( 193 | token_in=self.borrow_token, 194 | token_in_quantity=best_borrow, 195 | ) 196 | 197 | # only save opportunities with rational, positive values 198 | if best_borrow > 0 and best_profit > 0: 199 | self.best.update( 200 | { 201 | "borrow": best_borrow, 202 | "profit": best_profit, 203 | "swap_out": swap_out, 204 | } 205 | ) 206 | else: 207 | self.best.update( 208 | { 209 | "borrow": 0, 210 | "profit": 0, 211 | "swap_out": 0, 212 | } 213 | ) 214 | 215 | def calculate_multipool_tokens_out_from_tokens_in( 216 | self, 217 | token_in: Erc20Token, 218 | token_in_quantity: int, 219 | ) -> int: 220 | """ 221 | Calculates the expected token OUTPUT from the last pool for a given token INPUT to the first pool at current pool reserves. 222 | Uses the self.token0 and self.token1 pointers to determine which token is being swapped in 223 | and uses the appropriate formula 224 | """ 225 | 226 | number_of_pools = len(self.swap_pools) 227 | 228 | for i in range(number_of_pools): 229 | # determine the output token for pool0 230 | if token_in.address == self.swap_pools[i].token0.address: 231 | token_out = self.swap_pools[i].token1 232 | elif token_in.address == self.swap_pools[i].token1.address: 233 | token_out = self.swap_pools[i].token0 234 | else: 235 | print("wtf?") 236 | raise Exception 237 | 238 | # calculate the swap output through pool[i] 239 | token_out_quantity = self.swap_pools[ 240 | i 241 | ].calculate_tokens_out_from_tokens_in( 242 | token_in=token_in, 243 | token_in_quantity=token_in_quantity, 244 | ) 245 | 246 | if i == number_of_pools - 1: 247 | break 248 | else: 249 | # otherwise, use the output as input on the next loop 250 | token_in = token_out 251 | token_in_quantity = token_out_quantity 252 | 253 | return token_out_quantity 254 | -------------------------------------------------------------------------------- /source/token.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional, Union 3 | from warnings import catch_warnings, simplefilter, warn 4 | 5 | import brownie # type: ignore 6 | from brownie.convert.datatypes import HexString # type: ignore 7 | from brownie.network.account import LocalAccount # type: ignore 8 | from eth_typing import ChecksumAddress 9 | from hexbytes import HexBytes 10 | from web3 import Web3 11 | 12 | from degenbot.chainlink import ChainlinkPriceContract 13 | from degenbot.constants import MAX_UINT256, MIN_UINT256 14 | from degenbot.logging import logger 15 | 16 | MIN_ERC20_ABI = json.loads( 17 | """ 18 | [{"constant": true, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "balance", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" } ], "name": "allowance", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event"}] 19 | """ 20 | ) 21 | 22 | 23 | class Erc20Token: 24 | """ 25 | Represents an ERC-20 token. Must be initialized with an address. 26 | Brownie will load the Contract object from storage, then attempt to load the verified ABI from the block explorer. 27 | If both methods fail, it will attempt to use a supplied ERC-20 ABI 28 | 29 | If built with `min_abi=True`, a minimal ERC-20 ABI will be used instead of 30 | fetching the verified contract ABI from Etherscan (or similar). 31 | """ 32 | 33 | def __init__( 34 | self, 35 | address: str, 36 | user: Optional[LocalAccount] = None, 37 | abi: Optional[list] = None, 38 | oracle_address: Optional[str] = None, 39 | silent: bool = False, 40 | unload_brownie_contract_after_init: bool = False, 41 | min_abi: bool = False, 42 | ) -> None: 43 | self.address: ChecksumAddress = Web3.toChecksumAddress(address) 44 | 45 | if user: 46 | self._user = user 47 | 48 | if min_abi: 49 | self._brownie_contract = brownie.Contract.from_abi( 50 | name=f"ERC-20 @ {address}", 51 | address=self.address, 52 | abi=MIN_ERC20_ABI, 53 | persist=False, 54 | ) 55 | else: 56 | with catch_warnings(): 57 | simplefilter("ignore") 58 | try: 59 | # attempt to load stored contract 60 | self._brownie_contract = brownie.Contract(self.address) 61 | except: 62 | # use the provided ABI if given 63 | if abi: 64 | try: 65 | self._brownie_contract = brownie.Contract.from_abi( 66 | name=f"ERC-20 @ {address}", 67 | address=self.address, 68 | abi=abi, 69 | ) 70 | except: 71 | raise 72 | # otherwise attempt to fetch from the block explorer 73 | else: 74 | try: 75 | self._brownie_contract = ( 76 | brownie.Contract.from_explorer(address) 77 | ) 78 | except: 79 | raise 80 | 81 | try: 82 | self.name = self._brownie_contract.name() 83 | except (OverflowError, ValueError): 84 | self.name = brownie.web3.eth.call( 85 | { 86 | "to": self.address, 87 | "data": Web3.keccak(text="name()"), 88 | } 89 | ) 90 | except: 91 | warn( 92 | f"Token contract at {address} does not implement a 'name' function." 93 | ) 94 | self.name = f"UNKNOWN TOKEN @ {self.address}" 95 | if type(self.name) in [HexString, HexBytes]: 96 | self.name = self.name.decode() 97 | 98 | try: 99 | self.symbol = self._brownie_contract.symbol() 100 | except (OverflowError, ValueError): 101 | self.symbol = brownie.web3.eth.call( 102 | { 103 | "to": self.address, 104 | "data": Web3.keccak(text="symbol()"), 105 | } 106 | ) 107 | if type(self.symbol) in [HexString, HexBytes]: 108 | self.symbol = self.symbol.decode() 109 | 110 | self.decimals: int 111 | 112 | try: 113 | self.decimals = self._brownie_contract.decimals() 114 | except: 115 | warn( 116 | f"Token contract at {address} does not implement a 'decimals' function. Setting to 0." 117 | ) 118 | self.decimals = 0 119 | 120 | if user: 121 | self.balance = self._brownie_contract.balanceOf(self._user) 122 | self.normalized_balance = self.balance / (10**self.decimals) 123 | 124 | self.price: Optional[float] 125 | 126 | if oracle_address: 127 | self._price_oracle = ChainlinkPriceContract(address=oracle_address) 128 | self.price = self._price_oracle.price 129 | else: 130 | self.price = None 131 | 132 | if not silent: 133 | logger.info(f"• {self.symbol} ({self.name})") 134 | 135 | # Memory savings if token contract object is not used after initialization 136 | if unload_brownie_contract_after_init: 137 | self._brownie_contract = None 138 | 139 | # The Brownie contract object cannot be pickled, so remove it and return the state 140 | def __getstate__(self): 141 | keys_to_remove = ["_brownie_contract"] 142 | state = self.__dict__.copy() 143 | for key in keys_to_remove: 144 | if key in state: 145 | del state[key] 146 | return state 147 | 148 | def __setstate__(self, state): 149 | self.__dict__ = state 150 | 151 | def __eq__(self, other) -> bool: 152 | if isinstance(other, Erc20Token): 153 | return self.address == other.address 154 | elif isinstance(other, str): 155 | return self.address.lower() == other.lower() 156 | else: 157 | return NotImplemented 158 | 159 | def __lt__(self, other) -> bool: 160 | if isinstance(other, Erc20Token): 161 | return self.address < other.address 162 | elif isinstance(other, str): 163 | return self.address.lower() < other.lower() 164 | else: 165 | return NotImplemented 166 | 167 | def __gt__(self, other) -> bool: 168 | if isinstance(other, Erc20Token): 169 | return self.address > other.address 170 | elif isinstance(other, str): 171 | return self.address.lower() > other.lower() 172 | else: 173 | return NotImplemented 174 | 175 | def __str__(self): 176 | return self.symbol 177 | 178 | def get_approval(self, external_address: str): 179 | return self._brownie_contract.allowance( 180 | self._user.address, external_address 181 | ) 182 | 183 | def set_approval(self, external_address: str, value: Union[int, str]): 184 | """ 185 | Sets the approval value for an external contract to transfer tokens 186 | quantites up to the specified amount from this address. For unlimited 187 | approval, set value to the string "UNLIMITED". 188 | """ 189 | 190 | if isinstance(value, int): 191 | if not (MIN_UINT256 <= value <= MAX_UINT256): 192 | raise ValueError( 193 | f"Provide an integer value between 0 and 2**256-1" 194 | ) 195 | elif isinstance(value, str): 196 | if value != "UNLIMITED": 197 | raise ValueError("Value must be 'UNLIMITED' or an integer") 198 | else: 199 | print("Setting unlimited approval!") 200 | value = MAX_UINT256 201 | else: 202 | raise TypeError( 203 | f"Value must be an integer or string! Was {type(value)}" 204 | ) 205 | 206 | try: 207 | self._brownie_contract.approve( 208 | external_address, 209 | value, 210 | {"from": self._user.address}, 211 | ) 212 | except Exception as e: 213 | print(f"Exception in token_approve: {e}") 214 | raise 215 | 216 | def update_balance(self): 217 | self.balance = self._brownie_contract.balanceOf(self._user) 218 | self.normalized_balance = self.balance / (10**self.decimals) 219 | 220 | def update_price(self): 221 | self._price_oracle.update_price() 222 | self.price = self._price_oracle.price 223 | -------------------------------------------------------------------------------- /source/uniswap/v3/libraries/sqrtpricemath_test.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, localcontext 2 | 3 | import pytest 4 | 5 | from degenbot.constants import MAX_UINT128, MAX_UINT256 6 | from degenbot.exceptions import EVMRevertError 7 | from degenbot.uniswap.v3.libraries import SqrtPriceMath 8 | 9 | # Tests adapted from Typescript tests on Uniswap V3 Github repo 10 | # ref: https://github.com/Uniswap/v3-core/blob/main/test/SqrtPriceMath.spec.ts 11 | 12 | 13 | def expandTo18Decimals(x: int): 14 | return x * 10**18 15 | 16 | 17 | def encodePriceSqrt(reserve1: int, reserve0: int): 18 | """ 19 | Returns the sqrt price as a Q64.96 value 20 | """ 21 | with localcontext() as ctx: 22 | # Change the rounding method to match the BigNumber unit test at https://github.com/Uniswap/v3-core/blob/main/test/shared/utilities.ts 23 | # which specifies .integerValue(3), the 'ROUND_FLOOR' rounding method per https://mikemcl.github.io/bignumber.js/#bignumber 24 | ctx.rounding = "ROUND_FLOOR" 25 | return round( 26 | (Decimal(reserve1) / Decimal(reserve0)).sqrt() * Decimal(2**96) 27 | ) 28 | 29 | 30 | def test_getNextSqrtPriceFromInput(): 31 | # fails if price is zero 32 | with pytest.raises(EVMRevertError): 33 | # this test should fail 34 | SqrtPriceMath.getNextSqrtPriceFromInput( 35 | 0, 0, expandTo18Decimals(1) // 10, False 36 | ) 37 | 38 | # fails if liquidity is zero 39 | with pytest.raises(EVMRevertError): 40 | # this test should fail 41 | SqrtPriceMath.getNextSqrtPriceFromInput( 42 | 1, 0, expandTo18Decimals(1) // 10, True 43 | ) 44 | 45 | # fails if input amount overflows the price 46 | price = 2**160 - 1 47 | liquidity = 1024 48 | amountIn = 1024 49 | with pytest.raises(EVMRevertError): 50 | # this test should fail 51 | SqrtPriceMath.getNextSqrtPriceFromInput( 52 | price, liquidity, amountIn, False 53 | ) 54 | 55 | # any input amount cannot underflow the price 56 | price = 1 57 | liquidity = 1 58 | amountIn = 2**255 59 | assert ( 60 | SqrtPriceMath.getNextSqrtPriceFromInput( 61 | price, liquidity, amountIn, True 62 | ) 63 | == 1 64 | ) 65 | 66 | # returns input price if amount in is zero and zeroForOne = true 67 | price = encodePriceSqrt(1, 1) 68 | assert ( 69 | SqrtPriceMath.getNextSqrtPriceFromInput( 70 | price, expandTo18Decimals(1) // 10, 0, True 71 | ) 72 | == price 73 | ) 74 | 75 | # returns input price if amount in is zero and zeroForOne = false 76 | price = encodePriceSqrt(1, 1) 77 | assert ( 78 | SqrtPriceMath.getNextSqrtPriceFromInput( 79 | price, expandTo18Decimals(1) // 10, 0, False 80 | ) 81 | == price 82 | ) 83 | 84 | # returns the minimum price for max inputs 85 | sqrtP = 2**160 - 1 86 | liquidity = MAX_UINT128 87 | maxAmountNoOverflow = MAX_UINT256 - ((liquidity << 96) // sqrtP) 88 | assert ( 89 | SqrtPriceMath.getNextSqrtPriceFromInput( 90 | sqrtP, liquidity, maxAmountNoOverflow, True 91 | ) 92 | == 1 93 | ) 94 | 95 | # input amount of 0.1 token1 96 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromInput( 97 | encodePriceSqrt(1, 1), 98 | expandTo18Decimals(1), 99 | expandTo18Decimals(1) // 10, 100 | False, 101 | ) 102 | assert sqrtQ == 87150978765690771352898345369 103 | 104 | # input amount of 0.1 token0 105 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromInput( 106 | encodePriceSqrt(1, 1), 107 | expandTo18Decimals(1), 108 | expandTo18Decimals(1) // 10, 109 | True, 110 | ) 111 | assert sqrtQ == 72025602285694852357767227579 112 | 113 | # amountIn > type(uint96).max and zeroForOne = true 114 | assert ( 115 | SqrtPriceMath.getNextSqrtPriceFromInput( 116 | encodePriceSqrt(1, 1), expandTo18Decimals(10), 2**100, True 117 | ) 118 | == 624999999995069620 119 | ) 120 | # perfect answer: https://www.wolframalpha.com/input/?i=624999999995069620+-+%28%281e19+*+1+%2F+%281e19+%2B+2%5E100+*+1%29%29+*+2%5E96%29 121 | 122 | # can return 1 with enough amountIn and zeroForOne = true 123 | assert ( 124 | SqrtPriceMath.getNextSqrtPriceFromInput( 125 | encodePriceSqrt(1, 1), 1, MAX_UINT256 // 2, True 126 | ) 127 | == 1 128 | ) 129 | 130 | 131 | def test_getNextSqrtPriceFromOutput(): 132 | with pytest.raises(EVMRevertError): 133 | # this test should fail 134 | SqrtPriceMath.getNextSqrtPriceFromOutput( 135 | 0, 0, expandTo18Decimals(1) // 10, False 136 | ) 137 | 138 | with pytest.raises(EVMRevertError): 139 | # this test should fail 140 | SqrtPriceMath.getNextSqrtPriceFromOutput( 141 | 1, 0, expandTo18Decimals(1) // 10, True 142 | ) 143 | 144 | price = 20282409603651670423947251286016 145 | liquidity = 1024 146 | amountOut = 4 147 | with pytest.raises(EVMRevertError): 148 | # this test should fail 149 | SqrtPriceMath.getNextSqrtPriceFromOutput( 150 | price, liquidity, amountOut, False 151 | ) 152 | 153 | price = 20282409603651670423947251286016 154 | liquidity = 1024 155 | amountOut = 5 156 | with pytest.raises(EVMRevertError): 157 | # this test should fail 158 | assert SqrtPriceMath.getNextSqrtPriceFromOutput( 159 | price, liquidity, amountOut, False 160 | ) 161 | 162 | price = 20282409603651670423947251286016 163 | liquidity = 1024 164 | amountOut = 262145 165 | with pytest.raises(EVMRevertError): 166 | # this test should fail 167 | SqrtPriceMath.getNextSqrtPriceFromOutput( 168 | price, liquidity, amountOut, True 169 | ) 170 | 171 | price = 20282409603651670423947251286016 172 | liquidity = 1024 173 | amountOut = 262144 174 | with pytest.raises(EVMRevertError): 175 | # this test should fail 176 | SqrtPriceMath.getNextSqrtPriceFromOutput( 177 | price, liquidity, amountOut, True 178 | ) 179 | 180 | price = 20282409603651670423947251286016 181 | liquidity = 1024 182 | amountOut = 262143 183 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromOutput( 184 | price, liquidity, amountOut, True 185 | ) 186 | assert sqrtQ == 77371252455336267181195264 187 | 188 | price = 20282409603651670423947251286016 189 | liquidity = 1024 190 | amountOut = 4 191 | 192 | with pytest.raises(EVMRevertError): 193 | # this test should fail 194 | SqrtPriceMath.getNextSqrtPriceFromOutput( 195 | price, liquidity, amountOut, False 196 | ) 197 | 198 | price = encodePriceSqrt(1, 1) 199 | assert ( 200 | SqrtPriceMath.getNextSqrtPriceFromOutput( 201 | price, expandTo18Decimals(1) // 10, 0, True 202 | ) 203 | == price 204 | ) 205 | 206 | price = encodePriceSqrt(1, 1) 207 | assert ( 208 | SqrtPriceMath.getNextSqrtPriceFromOutput( 209 | price, expandTo18Decimals(1) // 10, 0, False 210 | ) 211 | == price 212 | ) 213 | 214 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromOutput( 215 | encodePriceSqrt(1, 1), 216 | expandTo18Decimals(1), 217 | expandTo18Decimals(1) // 10, 218 | False, 219 | ) 220 | assert sqrtQ == 88031291682515930659493278152 221 | 222 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromOutput( 223 | encodePriceSqrt(1, 1), 224 | expandTo18Decimals(1), 225 | expandTo18Decimals(1) // 10, 226 | True, 227 | ) 228 | assert sqrtQ == 71305346262837903834189555302 229 | 230 | with pytest.raises(EVMRevertError): 231 | # this test should fail 232 | SqrtPriceMath.getNextSqrtPriceFromOutput( 233 | encodePriceSqrt(1, 1), 1, MAX_UINT256, True 234 | ) 235 | 236 | with pytest.raises(EVMRevertError): 237 | # this test should fail 238 | SqrtPriceMath.getNextSqrtPriceFromOutput( 239 | encodePriceSqrt(1, 1), 1, MAX_UINT256, False 240 | ) 241 | 242 | 243 | def test_getAmount0Delta(): 244 | amount0 = SqrtPriceMath.getAmount0Delta( 245 | encodePriceSqrt(1, 1), encodePriceSqrt(2, 1), 0, True 246 | ) 247 | assert amount0 == 0 248 | 249 | amount0 = SqrtPriceMath.getAmount0Delta( 250 | encodePriceSqrt(1, 1), encodePriceSqrt(1, 1), 0, True 251 | ) 252 | assert amount0 == 0 253 | 254 | amount0 = SqrtPriceMath.getAmount0Delta( 255 | encodePriceSqrt(1, 1), 256 | encodePriceSqrt(121, 100), 257 | expandTo18Decimals(1), 258 | True, 259 | ) 260 | assert amount0 == 90909090909090910 261 | 262 | amount0RoundedDown = SqrtPriceMath.getAmount0Delta( 263 | encodePriceSqrt(1, 1), 264 | encodePriceSqrt(121, 100), 265 | expandTo18Decimals(1), 266 | False, 267 | ) 268 | assert amount0RoundedDown == amount0 - 1 269 | 270 | amount0Up = SqrtPriceMath.getAmount0Delta( 271 | encodePriceSqrt(2**90, 1), 272 | encodePriceSqrt(2**96, 1), 273 | expandTo18Decimals(1), 274 | True, 275 | ) 276 | amount0Down = SqrtPriceMath.getAmount0Delta( 277 | encodePriceSqrt(2**90, 1), 278 | encodePriceSqrt(2**96, 1), 279 | expandTo18Decimals(1), 280 | False, 281 | ) 282 | assert amount0Up == amount0Down + 1 283 | 284 | 285 | def test_getAmount1Delta(): 286 | amount1 = SqrtPriceMath.getAmount1Delta( 287 | encodePriceSqrt(1, 1), encodePriceSqrt(2, 1), 0, True 288 | ) 289 | assert amount1 == 0 290 | 291 | amount1 = SqrtPriceMath.getAmount0Delta( 292 | encodePriceSqrt(1, 1), encodePriceSqrt(1, 1), 0, True 293 | ) 294 | assert amount1 == 0 295 | 296 | # returns 0.1 amount1 for price of 1 to 1.21 297 | amount1 = SqrtPriceMath.getAmount1Delta( 298 | encodePriceSqrt(1, 1), 299 | encodePriceSqrt(121, 100), 300 | expandTo18Decimals(1), 301 | True, 302 | ) 303 | # TODO: investigate github test - asserts value == 100000000000000000, 304 | # but test fails (off-by-one) 305 | assert amount1 == 100000000000000001 306 | 307 | amount1RoundedDown = SqrtPriceMath.getAmount1Delta( 308 | encodePriceSqrt(1, 1), 309 | encodePriceSqrt(121, 100), 310 | expandTo18Decimals(1), 311 | False, 312 | ) 313 | assert amount1RoundedDown == amount1 - 1 314 | 315 | 316 | def test_swap_computation(): 317 | sqrtP = 1025574284609383690408304870162715216695788925244 318 | liquidity = 50015962439936049619261659728067971248 319 | zeroForOne = True 320 | amountIn = 406 321 | 322 | sqrtQ = SqrtPriceMath.getNextSqrtPriceFromInput( 323 | sqrtP, liquidity, amountIn, zeroForOne 324 | ) 325 | assert sqrtQ == 1025574284609383582644711336373707553698163132913 326 | 327 | amount0Delta = SqrtPriceMath.getAmount0Delta(sqrtQ, sqrtP, liquidity, True) 328 | assert amount0Delta == 406 329 | -------------------------------------------------------------------------------- /source/arbitrage/flash_borrow_to_lp_swap.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from typing import List, Optional 3 | 4 | from brownie import Contract # type: ignore 5 | from scipy import optimize # type: ignore 6 | 7 | from degenbot.token import Erc20Token 8 | from degenbot.types import ArbitrageHelper 9 | from degenbot.uniswap.v2.liquidity_pool import LiquidityPool 10 | 11 | 12 | class FlashBorrowToLpSwap(ArbitrageHelper): 13 | def __init__( 14 | self, 15 | borrow_pool: LiquidityPool, 16 | borrow_token: Erc20Token, 17 | swap_factory_address: str, 18 | swap_token_addresses: List[str], 19 | swap_router_fee=Fraction(3, 1000), 20 | name: str = "", 21 | update_method="polling", 22 | ): 23 | if borrow_token.address != swap_token_addresses[0]: 24 | raise ValueError( 25 | "Token addresses must begin with the borrowed token" 26 | ) 27 | # assert ( 28 | # borrow_token.address == swap_token_addresses[0] 29 | # ), "Token addresses must begin with the borrowed token" 30 | 31 | if borrow_pool.token0 == borrow_token: 32 | if borrow_pool.token1.address != swap_token_addresses[-1]: 33 | raise ValueError( 34 | "Token addresses must end with the repaid token" 35 | ) 36 | # assert ( 37 | # borrow_pool.token1.address == swap_token_addresses[-1] 38 | # ), "Token addresses must end with the repaid token" 39 | else: 40 | if borrow_pool.token0.address != swap_token_addresses[-1]: 41 | raise ValueError( 42 | "Token addresses must end with the repaid token" 43 | ) 44 | # assert ( 45 | # borrow_pool.token0.address == swap_token_addresses[-1] 46 | # ), "Token addresses must end with the repaid token" 47 | 48 | # build a list of all tokens involved in this swapping path 49 | self.tokens = [] 50 | for address in swap_token_addresses: 51 | self.tokens.append(Erc20Token(address=address)) 52 | 53 | if name: 54 | self.name = name 55 | else: 56 | self.name = "-".join([token.symbol for token in self.tokens]) 57 | 58 | self.token_path = [token.address for token in self.tokens] 59 | 60 | # build the list of intermediate pool pairs for the given multi-token path. 61 | # Pool list length will be 1 less than the token path length, e.g. a token1->token2->token3 62 | # path will result in a pool list consisting of token1/token2 and token2/token3 63 | self.swap_pools = [] 64 | try: 65 | _factory = Contract(swap_factory_address) 66 | except Exception as e: 67 | print(e) 68 | _factory = Contract.from_explorer(swap_factory_address) 69 | 70 | for i in range(len(self.token_path) - 1): 71 | self.swap_pools.append( 72 | LiquidityPool( 73 | address=_factory.getPair( 74 | self.token_path[i], self.token_path[i + 1] 75 | ), 76 | name=" - ".join( 77 | [self.tokens[i].symbol, self.tokens[i + 1].symbol] 78 | ), 79 | tokens=[self.tokens[i], self.tokens[i + 1]], 80 | update_method=update_method, 81 | fee=swap_router_fee, 82 | ) 83 | ) 84 | print( 85 | f"Loaded LP: {self.tokens[i].symbol} - {self.tokens[i+1].symbol}" 86 | ) 87 | 88 | self.swap_pool_addresses = [pool.address for pool in self.swap_pools] 89 | 90 | self.borrow_pool = borrow_pool 91 | self.borrow_token = borrow_token 92 | 93 | if self.borrow_token == self.borrow_pool.token0: 94 | self.repay_token = self.borrow_pool.token1 95 | elif self.borrow_token == self.borrow_pool.token1: 96 | self.repay_token = self.borrow_pool.token0 97 | 98 | self.best = { 99 | "init": True, 100 | "strategy": "flash borrow swap", 101 | "borrow_amount": 0, 102 | "borrow_token": self.borrow_token, 103 | "borrow_pool_amounts": [], 104 | "repay_amount": 0, 105 | "profit_amount": 0, 106 | "profit_token": self.repay_token, 107 | "swap_pools": self.swap_pools, 108 | "swap_pool_amounts": [], 109 | } 110 | 111 | def __str__(self): 112 | return self.name 113 | 114 | def update_reserves( 115 | self, 116 | silent: bool = False, 117 | print_reserves: bool = True, 118 | print_ratios: bool = True, 119 | ) -> bool: 120 | """ 121 | Checks each liquidity pool for updates by passing a call to .update_reserves(), which returns False if there are no updates. 122 | Will calculate arbitrage amounts only after checking all pools and finding an update, or on startup (via the 'init' dictionary key) 123 | """ 124 | recalculate = False 125 | 126 | # calculate initial arbitrage after the object is instantiated, otherwise proceed with normal checks 127 | if self.best["init"] == True: 128 | self.best["init"] = False 129 | recalculate = True 130 | 131 | # flag for recalculation if the borrowing pool has been updated 132 | if self.borrow_pool.update_reserves( 133 | silent=silent, 134 | print_reserves=print_reserves, 135 | print_ratios=print_ratios, 136 | ): 137 | recalculate = True 138 | 139 | # flag for recalculation if any of the pools along the swap path have been updated 140 | for pool in self.swap_pools: 141 | if pool.update_reserves( 142 | silent=silent, 143 | print_reserves=print_reserves, 144 | print_ratios=print_ratios, 145 | ): 146 | recalculate = True 147 | 148 | if recalculate: 149 | self._calculate_arbitrage() 150 | return True 151 | else: 152 | return False 153 | 154 | def _calculate_arbitrage(self): 155 | # set up the boundaries for the Brent optimizer based on which token is being borrowed 156 | if self.borrow_token.address == self.borrow_pool.token0.address: 157 | bounds = ( 158 | 1, 159 | float(self.borrow_pool.reserves_token0), 160 | ) 161 | bracket = ( 162 | 0.01 * self.borrow_pool.reserves_token0, 163 | 0.05 * self.borrow_pool.reserves_token0, 164 | ) 165 | else: 166 | bounds = ( 167 | 1, 168 | float(self.borrow_pool.reserves_token1), 169 | ) 170 | bracket = ( 171 | 0.01 * self.borrow_pool.reserves_token1, 172 | 0.05 * self.borrow_pool.reserves_token1, 173 | ) 174 | 175 | opt = optimize.minimize_scalar( 176 | lambda x: -float( 177 | self.calculate_multipool_tokens_out_from_tokens_in( 178 | token_in=self.borrow_token, 179 | token_in_quantity=x, 180 | ) 181 | - self.borrow_pool.calculate_tokens_in_from_tokens_out( 182 | token_in=self.repay_token, 183 | token_out_quantity=x, 184 | ) 185 | ), 186 | method="bounded", 187 | bounds=bounds, 188 | bracket=bracket, 189 | ) 190 | 191 | best_borrow = int(opt.x) 192 | 193 | if self.borrow_token.address == self.borrow_pool.token0.address: 194 | borrow_amounts = [best_borrow, 0] 195 | elif self.borrow_token.address == self.borrow_pool.token1.address: 196 | borrow_amounts = [0, best_borrow] 197 | else: 198 | print("wtf?") 199 | raise Exception 200 | 201 | best_repay = self.borrow_pool.calculate_tokens_in_from_tokens_out( 202 | token_in=self.repay_token, 203 | token_out_quantity=best_borrow, 204 | ) 205 | best_profit = -int(opt.fun) 206 | 207 | # only save opportunities with rational, positive values 208 | if best_borrow > 0 and best_profit > 0: 209 | self.best.update( 210 | { 211 | "borrow_amount": best_borrow, 212 | "borrow_pool_amounts": borrow_amounts, 213 | "repay_amount": best_repay, 214 | "profit_amount": best_profit, 215 | "swap_pool_amounts": self._build_multipool_amounts_out( 216 | token_in=self.borrow_token, 217 | token_in_quantity=best_borrow, 218 | ), 219 | } 220 | ) 221 | else: 222 | self.best.update( 223 | { 224 | "borrow_amount": 0, 225 | "borrow_pool_amounts": [], 226 | "repay_amount": 0, 227 | "profit_amount": 0, 228 | "swap_pool_amounts": [], 229 | } 230 | ) 231 | 232 | def calculate_multipool_tokens_out_from_tokens_in( 233 | self, 234 | token_in: Erc20Token, 235 | token_in_quantity: int, 236 | ) -> int: 237 | """ 238 | Calculates the expected token OUTPUT from the last pool for a given token INPUT to the first pool at current pool reserves. 239 | Uses the self.token0 and self.token1 pointers to determine which token is being swapped in 240 | and uses the appropriate formula 241 | """ 242 | 243 | number_of_pools = len(self.swap_pools) 244 | 245 | for i in range(number_of_pools): 246 | # determine the output token for pool0 247 | if token_in.address == self.swap_pools[i].token0.address: 248 | token_out = self.swap_pools[i].token1 249 | elif token_in.address == self.swap_pools[i].token1.address: 250 | token_out = self.swap_pools[i].token0 251 | else: 252 | print("wtf?") 253 | raise Exception 254 | 255 | # calculate the swap output through pool[i] 256 | token_out_quantity = self.swap_pools[ 257 | i 258 | ].calculate_tokens_out_from_tokens_in( 259 | token_in=token_in, 260 | token_in_quantity=token_in_quantity, 261 | ) 262 | 263 | if i == number_of_pools - 1: 264 | break 265 | else: 266 | # otherwise, use the output as input on the next loop 267 | token_in = token_out 268 | token_in_quantity = token_out_quantity 269 | 270 | return token_out_quantity 271 | 272 | def _build_multipool_amounts_out( 273 | self, 274 | token_in: Erc20Token, 275 | token_in_quantity: int, 276 | silent: bool = False, 277 | ) -> List[list]: 278 | number_of_pools = len(self.swap_pools) 279 | 280 | pools_amounts_out = [] 281 | 282 | for i in range(number_of_pools): 283 | # determine the output token for pool0 284 | if token_in.address == self.swap_pools[i].token0.address: 285 | token_out = self.swap_pools[i].token1 286 | elif token_in.address == self.swap_pools[i].token1.address: 287 | token_out = self.swap_pools[i].token0 288 | else: 289 | print("wtf?") 290 | raise Exception 291 | 292 | # calculate the swap output through pool[i] 293 | token_out_quantity = self.swap_pools[ 294 | i 295 | ].calculate_tokens_out_from_tokens_in( 296 | token_in=token_in, 297 | token_in_quantity=token_in_quantity, 298 | ) 299 | 300 | if token_in.address == self.swap_pools[i].token0.address: 301 | pools_amounts_out.append([0, token_out_quantity]) 302 | elif token_in.address == self.swap_pools[i].token1.address: 303 | pools_amounts_out.append([token_out_quantity, 0]) 304 | 305 | if i == number_of_pools - 1: 306 | # if we've reached the last pool, return the amounts_out list 307 | break 308 | else: 309 | # otherwise, use the output as input on the next loop 310 | token_in = token_out 311 | token_in_quantity = token_out_quantity 312 | 313 | return pools_amounts_out 314 | -------------------------------------------------------------------------------- /source/arbitrage/flash_borrow_to_lp_swap_new.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from scipy.optimize import minimize_scalar # type: ignore 4 | 5 | from degenbot.token import Erc20Token 6 | from degenbot.types import ArbitrageHelper 7 | from degenbot.uniswap.v2.liquidity_pool import LiquidityPool 8 | 9 | # TODO: improve arbitrage calculation for repaying with same token, instead of borrow A -> repay B 10 | 11 | 12 | class FlashBorrowToLpSwapNew(ArbitrageHelper): 13 | def __init__( 14 | self, 15 | borrow_pool: LiquidityPool, 16 | borrow_token: Erc20Token, 17 | repay_token: Erc20Token, 18 | swap_pool_addresses: Optional[List[str]] = None, 19 | swap_pools: Optional[List[LiquidityPool]] = None, 20 | name: str = "", 21 | update_method="polling", 22 | ): 23 | if swap_pools is None and swap_pool_addresses is None: 24 | raise TypeError( 25 | "Provide a list of LiquidityPool objects or a list of pool addresses." 26 | ) 27 | 28 | if not (swap_pools is not None) ^ (swap_pool_addresses is not None): 29 | # NOTE: ^ is the exclusive-or operator, which allows us to check that only one condition is True, but not both and not neither 30 | raise TypeError( 31 | "Provide a list of LiquidityPool objects or a list of pool addresses, but not both." 32 | ) 33 | 34 | if update_method not in [ 35 | "polling", 36 | "external", 37 | ]: 38 | raise ValueError("update_method must be 'polling' or 'external'") 39 | 40 | if update_method == "external" and swap_pool_addresses: 41 | raise ValueError( 42 | "swap pools passed by address must be updated with the 'polling' method" 43 | ) 44 | 45 | if swap_pool_addresses is None: 46 | swap_pool_addresses = [] 47 | 48 | if swap_pools is None: 49 | swap_pools = [] 50 | 51 | self.borrow_pool = borrow_pool 52 | self.borrow_token = borrow_token 53 | self.repay_token = repay_token 54 | self._update_method = update_method 55 | 56 | # if the object was initialized with pool objects directly, use these directly 57 | if swap_pools: 58 | self.swap_pools = swap_pools 59 | 60 | # otherwise, create internal objects 61 | else: 62 | self.swap_pools = [] 63 | for address in swap_pool_addresses: 64 | self.swap_pools.append( 65 | LiquidityPool( 66 | address=address, 67 | update_method=update_method, 68 | ) 69 | ) 70 | 71 | self.swap_pool_addresses = [pool.address for pool in self.swap_pools] 72 | self.swap_pool_tokens = [ 73 | [pool.token0, pool.token1] for pool in self.swap_pools 74 | ] 75 | 76 | self.all_pool_addresses = [ 77 | pool.address for pool in self.swap_pools + [self.borrow_pool] 78 | ] 79 | 80 | if name: 81 | self.name = name 82 | else: 83 | self.name = ( 84 | self.borrow_pool.name 85 | + " -> " 86 | + " -> ".join([pool.name for pool in self.swap_pools]) 87 | ) 88 | 89 | if not self.borrow_token.address in [ 90 | self.swap_pools[0].token0.address, 91 | self.swap_pools[0].token1.address, 92 | ]: 93 | raise ValueError("Borrowed token not found in the first swap pool") 94 | 95 | if not self.repay_token.address in [ 96 | self.swap_pools[-1].token0.address, 97 | self.swap_pools[-1].token1.address, 98 | ]: 99 | raise ValueError("Repay token not found in the last swap pool") 100 | 101 | if self.swap_pools[0].token0.address == borrow_token.address: 102 | forward_token_address = self.swap_pools[0].token1.address 103 | else: 104 | forward_token_address = self.swap_pools[0].token0.address 105 | 106 | token_in_address = forward_token_address 107 | for pool in self.swap_pools: 108 | if pool.token0.address == token_in_address: 109 | forward_token_address = pool.token1.address 110 | elif pool.token1.address == token_in_address: 111 | forward_token_address = pool.token0.address 112 | else: 113 | raise Exception( 114 | "Swap pools are invalid, no swap route possible!" 115 | ) 116 | 117 | self.best = { 118 | "init": True, 119 | "strategy": "flash borrow swap", 120 | "borrow_amount": 0, 121 | "borrow_token": self.borrow_token, 122 | "borrow_pool": self.borrow_pool, 123 | "borrow_pool_amounts": [], 124 | "repay_amount": 0, 125 | "profit_amount": 0, 126 | "profit_token": self.repay_token, 127 | "swap_pools": self.swap_pools, 128 | "swap_pool_addresses": self.swap_pool_addresses, 129 | "swap_pool_amounts": [], 130 | "swap_pool_tokens": self.swap_pool_tokens, 131 | } 132 | 133 | self.best_future = { 134 | "strategy": "flash borrow swap", 135 | "borrow_amount": 0, 136 | "borrow_token": self.borrow_token, 137 | "borrow_pool": self.borrow_pool, 138 | "borrow_pool_amounts": [], 139 | "repay_amount": 0, 140 | "profit_amount": 0, 141 | "profit_token": self.repay_token, 142 | "swap_pools": self.swap_pools, 143 | "swap_pool_addresses": self.swap_pool_addresses, 144 | "swap_pool_amounts": [], 145 | "swap_pool_tokens": self.swap_pool_tokens, 146 | } 147 | 148 | self.update_reserves() 149 | 150 | def _build_multipool_amounts_out( 151 | self, 152 | token_in: Erc20Token, 153 | token_in_quantity: int, 154 | ) -> List[List[int]]: 155 | number_of_pools = len(self.swap_pools) 156 | 157 | pools_amounts_out = [] 158 | 159 | for i in range(number_of_pools): 160 | # determine the output token for pool0 161 | if token_in.address == self.swap_pools[i].token0.address: 162 | token_out = self.swap_pools[i].token1 163 | elif token_in.address == self.swap_pools[i].token1.address: 164 | token_out = self.swap_pools[i].token0 165 | else: 166 | raise ValueError(f"Could not identify token_in {token_in}") 167 | 168 | # calculate the swap output through pool[i] 169 | token_out_quantity = self.swap_pools[ 170 | i 171 | ].calculate_tokens_out_from_tokens_in( 172 | token_in=token_in, 173 | token_in_quantity=token_in_quantity, 174 | ) 175 | 176 | if token_in.address == self.swap_pools[i].token0.address: 177 | pools_amounts_out.append([0, token_out_quantity]) 178 | elif token_in.address == self.swap_pools[i].token1.address: 179 | pools_amounts_out.append([token_out_quantity, 0]) 180 | 181 | if i == number_of_pools - 1: 182 | break 183 | else: 184 | # otherwise, feed the results back into the loop 185 | token_in = token_out 186 | token_in_quantity = token_out_quantity 187 | 188 | return pools_amounts_out 189 | 190 | def _calculate_arbitrage( 191 | self, 192 | override_future: bool = False, 193 | override_future_borrow_pool_reserves_token0: Optional[int] = None, 194 | override_future_borrow_pool_reserves_token1: Optional[int] = None, 195 | ): 196 | if override_future: 197 | if ( 198 | override_future_borrow_pool_reserves_token0 is None 199 | or override_future_borrow_pool_reserves_token1 is None 200 | ): 201 | raise ValueError( 202 | "Must override reserves for token0 and token1" 203 | ) 204 | reserves_token0 = override_future_borrow_pool_reserves_token0 205 | reserves_token1 = override_future_borrow_pool_reserves_token1 206 | else: 207 | if ( 208 | override_future_borrow_pool_reserves_token0 is not None 209 | or override_future_borrow_pool_reserves_token1 is not None 210 | ): 211 | raise ValueError( 212 | "Do not provide override reserves without setting override_future = True" 213 | ) 214 | reserves_token0 = self.borrow_pool.reserves_token0 215 | reserves_token1 = self.borrow_pool.reserves_token1 216 | 217 | # set up the boundaries for the Brent optimizer based on which token is being borrowed 218 | if self.borrow_token == self.borrow_pool.token0: 219 | bounds = ( 220 | 1, 221 | float(reserves_token0), 222 | ) 223 | bracket = ( 224 | 0.001 * reserves_token0, 225 | 0.01 * reserves_token0, 226 | ) 227 | elif self.borrow_token == self.borrow_pool.token1: 228 | bounds = ( 229 | 1, 230 | float(reserves_token1), 231 | ) 232 | bracket = ( 233 | 0.001 * reserves_token1, 234 | 0.01 * reserves_token1, 235 | ) 236 | else: 237 | raise ValueError( 238 | f"Could not identify borrow token {self.borrow_token}" 239 | ) 240 | 241 | # TODO: extend calculate_multipool_tokens_out_from_tokens_in() to support overriding token reserves for an arbitrary pool, 242 | # currently only supports overriding the borrow pool reserves 243 | opt = minimize_scalar( 244 | lambda x: -float( 245 | self.calculate_multipool_tokens_out_from_tokens_in( 246 | token_in=self.borrow_token, 247 | token_in_quantity=x, 248 | ) 249 | - self.borrow_pool.calculate_tokens_in_from_tokens_out( 250 | token_in=self.repay_token, 251 | token_out_quantity=x, 252 | override_reserves_token0=override_future_borrow_pool_reserves_token0, 253 | override_reserves_token1=override_future_borrow_pool_reserves_token1, 254 | ) 255 | ), 256 | method="bounded", 257 | bounds=bounds, 258 | bracket=bracket, 259 | options={"xatol": 1.0}, 260 | ) 261 | 262 | best_borrow = int(opt.x) 263 | 264 | if self.borrow_token == self.borrow_pool.token0: 265 | borrow_amounts = [best_borrow, 0] 266 | elif self.borrow_token == self.borrow_pool.token1: 267 | borrow_amounts = [0, best_borrow] 268 | else: 269 | raise ValueError( 270 | f"Could not identify borrow token {self.borrow_token}" 271 | ) 272 | 273 | best_repay = self.borrow_pool.calculate_tokens_in_from_tokens_out( 274 | token_in=self.repay_token, 275 | token_out_quantity=best_borrow, 276 | override_reserves_token0=override_future_borrow_pool_reserves_token0, 277 | override_reserves_token1=override_future_borrow_pool_reserves_token1, 278 | ) 279 | best_profit = -int(opt.fun) 280 | 281 | if override_future: 282 | if best_borrow > 0 and best_profit > 0: 283 | self.best_future.update( 284 | { 285 | "borrow_amount": best_borrow, 286 | "borrow_pool_amounts": borrow_amounts, 287 | "repay_amount": best_repay, 288 | "profit_amount": best_profit, 289 | "swap_pool_amounts": self._build_multipool_amounts_out( 290 | token_in=self.borrow_token, 291 | token_in_quantity=best_borrow, 292 | ), 293 | } 294 | ) 295 | else: 296 | self.best_future.update( 297 | { 298 | "borrow_amount": 0, 299 | "borrow_pool_amounts": [], 300 | "repay_amount": 0, 301 | "profit_amount": 0, 302 | "swap_pool_amounts": [], 303 | } 304 | ) 305 | else: 306 | # only save opportunities with rational, positive values 307 | if best_borrow > 0 and best_profit > 0: 308 | self.best.update( 309 | { 310 | "borrow_amount": best_borrow, 311 | "borrow_pool_amounts": borrow_amounts, 312 | "repay_amount": best_repay, 313 | "profit_amount": best_profit, 314 | "swap_pool_amounts": self._build_multipool_amounts_out( 315 | token_in=self.borrow_token, 316 | token_in_quantity=best_borrow, 317 | ), 318 | } 319 | ) 320 | else: 321 | self.best.update( 322 | { 323 | "borrow_amount": 0, 324 | "borrow_pool_amounts": [], 325 | "repay_amount": 0, 326 | "profit_amount": 0, 327 | "swap_pool_amounts": [], 328 | } 329 | ) 330 | 331 | def __str__(self) -> str: 332 | return self.name 333 | 334 | def calculate_multipool_tokens_out_from_tokens_in( 335 | self, token_in: Erc20Token, token_in_quantity: int 336 | ) -> int: 337 | """ 338 | Calculates the expected token OUTPUT from the last pool for a given token INPUT to the first pool at current pool reserves. 339 | Uses the self.token0 and self.token1 pointers to determine which token is being swapped in 340 | and uses the appropriate formula 341 | """ 342 | 343 | number_of_pools = len(self.swap_pools) 344 | 345 | for i in range(number_of_pools): 346 | # determine the output token for pool0 347 | if token_in.address == self.swap_pools[i].token0.address: 348 | token_out = self.swap_pools[i].token1 349 | elif token_in.address == self.swap_pools[i].token1.address: 350 | token_out = self.swap_pools[i].token0 351 | else: 352 | raise ValueError(f"Could not identify token_in {token_in}") 353 | 354 | # calculate the swap output through pool[i] 355 | token_out_quantity = self.swap_pools[ 356 | i 357 | ].calculate_tokens_out_from_tokens_in( 358 | token_in=token_in, 359 | token_in_quantity=token_in_quantity, 360 | ) 361 | 362 | if i == number_of_pools - 1: 363 | break 364 | else: 365 | # otherwise, use the output as input on the next loop 366 | token_in = token_out 367 | token_in_quantity = token_out_quantity 368 | 369 | return token_out_quantity 370 | 371 | def update_reserves( 372 | self, 373 | silent: bool = False, 374 | print_reserves: bool = True, 375 | print_ratios: bool = True, 376 | override_future: bool = False, 377 | override_future_borrow_pool_reserves_token0: Optional[int] = None, 378 | override_future_borrow_pool_reserves_token1: Optional[int] = None, 379 | ) -> bool: 380 | """ 381 | Checks each liquidity pool for updates by passing a call to .update_reserves(), which returns False if there are no updates. 382 | Will calculate arbitrage amounts only after checking all pools and finding a reason to update, or on startup (via the 'init' dictionary key) 383 | """ 384 | recalculate = False 385 | 386 | if self._update_method != "external": 387 | # calculate initial arbitrage after the object is instantiated, otherwise proceed with normal checks 388 | if self.best["init"] == True: 389 | self.best["init"] = False 390 | recalculate = True 391 | 392 | # flag for recalculation if the borrowing pool has been updated 393 | if self.borrow_pool.update_reserves( 394 | silent=silent, 395 | print_reserves=print_reserves, 396 | print_ratios=print_ratios, 397 | ): 398 | recalculate = True 399 | 400 | # flag for recalculation if any of the pools along the swap path have been updated 401 | for pool in self.swap_pools: 402 | if pool.update_reserves( 403 | silent=silent, 404 | print_reserves=print_reserves, 405 | print_ratios=print_ratios, 406 | ): 407 | recalculate = True 408 | 409 | if override_future: 410 | recalculate = True 411 | if ( 412 | override_future_borrow_pool_reserves_token0 is None 413 | or override_future_borrow_pool_reserves_token1 is None 414 | ): 415 | raise ValueError( 416 | "Must override reserves for token0 and token1" 417 | ) 418 | else: 419 | if ( 420 | override_future_borrow_pool_reserves_token0 is not None 421 | or override_future_borrow_pool_reserves_token1 is not None 422 | ): 423 | raise ValueError( 424 | "Do not provide override reserves without setting override_future = True" 425 | ) 426 | 427 | if self.borrow_pool.new_reserves: 428 | recalculate = True 429 | for pool in self.swap_pools: 430 | if pool.new_reserves: 431 | recalculate = True 432 | break 433 | 434 | if recalculate: 435 | self._calculate_arbitrage( 436 | override_future=override_future, 437 | override_future_borrow_pool_reserves_token0=override_future_borrow_pool_reserves_token0, 438 | override_future_borrow_pool_reserves_token1=override_future_borrow_pool_reserves_token1, 439 | ) 440 | return True 441 | else: 442 | return False 443 | --------------------------------------------------------------------------------