From ec1680c9768ab9516337b9d700f747dbe9c32ee5 Mon Sep 17 00:00:00 2001 From: Taarush Vemulapalli Date: Tue, 24 Aug 2021 06:26:31 -0700 Subject: [PATCH] historical prices + cream --- mev_inspect/abis/cream/Comptroller.json | 1 + .../abis/uniswap_v2/UniswapV2Factory.json | 1 + mev_inspect/historical_price.py | 130 ++++++++++++++++++ mev_inspect/schemas/classified_traces.py | 1 + mev_inspect/schemas/tokenflow.py | 2 +- .../strategy_inspectors/compound_v2_ceth.py | 48 ++++--- mev_inspect/tokenflow.py | 2 +- tests/test_liquidations.py | 112 +++++++-------- tests/tokenflow_test.py | 2 +- 9 files changed, 218 insertions(+), 81 deletions(-) create mode 100644 mev_inspect/abis/cream/Comptroller.json create mode 100644 mev_inspect/abis/uniswap_v2/UniswapV2Factory.json create mode 100644 mev_inspect/historical_price.py diff --git a/mev_inspect/abis/cream/Comptroller.json b/mev_inspect/abis/cream/Comptroller.json new file mode 100644 index 0000000..231b3a5 --- /dev/null +++ b/mev_inspect/abis/cream/Comptroller.json @@ -0,0 +1 @@ +[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"error","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"info","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"detail","type":"uint256"}],"name":"Failure","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"NewAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldImplementation","type":"address"},{"indexed":false,"internalType":"address","name":"newImplementation","type":"address"}],"name":"NewImplementation","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldPendingAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newPendingAdmin","type":"address"}],"name":"NewPendingAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"oldPendingImplementation","type":"address"},{"indexed":false,"internalType":"address","name":"newPendingImplementation","type":"address"}],"name":"NewPendingImplementation","type":"event"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"constant":false,"inputs":[],"name":"_acceptAdmin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"_acceptImplementation","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newPendingAdmin","type":"address"}],"name":"_setPendingAdmin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newPendingImplementation","type":"address"}],"name":"_setPendingImplementation","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"comptrollerImplementation","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"pendingAdmin","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"pendingComptrollerImplementation","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/mev_inspect/abis/uniswap_v2/UniswapV2Factory.json b/mev_inspect/abis/uniswap_v2/UniswapV2Factory.json new file mode 100644 index 0000000..7063b1f --- /dev/null +++ b/mev_inspect/abis/uniswap_v2/UniswapV2Factory.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":false,"internalType":"address","name":"pair","type":"address"},{"indexed":false,"internalType":"uint256","name":"","type":"uint256"}],"name":"PairCreated","type":"event"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"allPairs","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"allPairsLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"}],"name":"createPair","outputs":[{"internalType":"address","name":"pair","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"feeTo","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"feeToSetter","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_feeTo","type":"address"}],"name":"setFeeTo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"name":"setFeeToSetter","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/mev_inspect/historical_price.py b/mev_inspect/historical_price.py new file mode 100644 index 0000000..f844158 --- /dev/null +++ b/mev_inspect/historical_price.py @@ -0,0 +1,130 @@ +from mev_inspect.schemas.classified_traces import Protocol +from mev_inspect.abi import get_raw_abi +from web3 import Web3 + +rpc = "" +w3 = Web3(Web3.HTTPProvider(rpc)) + + +weth_address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" +usdc_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +zero_address = "0x0000000000000000000000000000000000000000" + +uniswap_router = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" +uniswap_factory = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" +sushiswap_router = "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F" +sushiswap_factory = "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac" + +router_abi = get_raw_abi("UniswapV2Router", Protocol.uniswap_v2) +factory_abi = get_raw_abi("UniswapV2Factory", Protocol.uniswap_v2) +pool_abi = get_raw_abi("UniswapV2Pair", None) + +# helper to get decimals of any given erc20 token +def get_erc20_token_decimals(token_address): + token_abi = get_raw_abi("ERC20", None) + token_instance = w3.eth.contract(address=token_address, abi=token_abi) + decimals = token_instance.functions.decimals().call() + return decimals + + +# get the specific uniswap/sushiswap pools for a pair of tokens +def get_uniswap_pair_pool(token_0, token_1): + factory_instance = w3.eth.contract(address=uniswap_factory, abi=factory_abi) + pair_address = factory_instance.functions.getPair(token_0, token_1).call() + # 0x0 is returned when the pair does not have a pool + if pair_address != zero_address: + return pair_address + else: + return None + + +def get_sushiswap_pair_pool(token_0, token_1): + factory_instance = w3.eth.contract(address=sushiswap_factory, abi=factory_abi) + pair_address = factory_instance.functions.getPair(token_0, token_1).call() + # 0x0 is returned when the pair does not have a pool + if pair_address != zero_address: + return pair_address + else: + return None + + +# get reserves of a pool at a specific block number +def get_uni_pool_reserves(pool_address, block_number): + pool_instance = w3.eth.contract(address=pool_address, abi=pool_abi) + token_0 = pool_instance.functions.token0().call() + token_1 = pool_instance.functions.token1().call() + token_0_reserve, token_1_reserve, _ = pool_instance.functions.getReserves().call( + block_identifier=block_number + ) + return {token_0: token_0_reserve, token_1: token_1_reserve} + + +def get_sushi_pool_reserves(pool_address, block_number): + pool_instance = w3.eth.contract(address=pool_address, abi=pool_abi) + token_0 = pool_instance.functions.token0().call() + token_1 = pool_instance.functions.token1().call() + token_0_reserve, token_1_reserve, _ = pool_instance.functions.getReserves().call( + block_identifier=block_number + ) + return {token_0: token_0_reserve, token_1: token_1_reserve} + + +# get the price of any token (in eth) at a specific block number +def get_erc20_token_price_in_eth(token_amount, token_address, block_number): + # get the TOKEN-ETH pool addresses from AMM factory + uni_pair_pool_address = get_uniswap_pair_pool(token_address, weth_address) + sushi_pair_pool_address = get_sushiswap_pair_pool(token_address, weth_address) + + # get reserves from both pools, to pick one with greater liquidity at that block height + uni_pool_reserves = get_uni_pool_reserves(uni_pair_pool_address, block_number) + sushi_pool_reserves = get_sushi_pool_reserves(sushi_pair_pool_address, block_number) + + # if uniswap has better liquidity + if uni_pool_reserves[token_address] > sushi_pool_reserves[token_address]: + router_instance = w3.eth.contract(address=uniswap_router, abi=router_abi) + token_price_in_wei = router_instance.functions.getAmountOut( + token_amount, + uni_pool_reserves[token_address], + uni_pool_reserves[weth_address], + ).call() + token_price_in_eth = w3.fromWei(token_price_in_wei, "ether") + return token_price_in_eth + else: # sushiswap has better liquidity + router_instance = w3.eth.contract(address=sushiswap_router, abi=router_abi) + token_price_in_wei = router_instance.functions.getAmountOut( + token_amount, + sushi_pool_reserves[token_address], + sushi_pool_reserves[weth_address], + ).call() + token_price_in_eth = w3.fromWei(token_price_in_wei, "ether") + return token_price_in_eth + + +# same but denominated in USDC +def get_erc20_token_price_in_usdc(token_amount, token_address, block_number): + # get the TOKEN-ETH pool addresses from AMM factory + uni_pair_pool_address = get_uniswap_pair_pool(token_address, usdc_address) + sushi_pair_pool_address = get_sushiswap_pair_pool(token_address, usdc_address) + + # get reserves from both pools, to pick one with greater liquidity at that block height + uni_pool_reserves = get_uni_pool_reserves(uni_pair_pool_address, block_number) + sushi_pool_reserves = get_sushi_pool_reserves(sushi_pair_pool_address, block_number) + + # if uniswap has better liquidity + if uni_pool_reserves[token_address] > sushi_pool_reserves[token_address]: + router_instance = w3.eth.contract(address=uniswap_router, abi=router_abi) + token_price = router_instance.functions.getAmountOut( + token_amount, + uni_pool_reserves[token_address], + uni_pool_reserves[usdc_address], + ).call() + # usdc has 6 decimals + return token_price / 10 ** 6 + else: # sushiswap has better liquidity + router_instance = w3.eth.contract(address=sushiswap_router, abi=router_abi) + token_price = router_instance.functions.getAmountOut( + token_amount, + sushi_pool_reserves[token_address], + sushi_pool_reserves[usdc_address], + ).call() + return token_price / 10 ** 6 diff --git a/mev_inspect/schemas/classified_traces.py b/mev_inspect/schemas/classified_traces.py index cccfccb..0feb53e 100644 --- a/mev_inspect/schemas/classified_traces.py +++ b/mev_inspect/schemas/classified_traces.py @@ -21,6 +21,7 @@ class Protocol(Enum): sushiswap = "sushiswap" aave = "aave" compound_v2 = "compound_v2" + cream = "cream" weth = "weth" diff --git a/mev_inspect/schemas/tokenflow.py b/mev_inspect/schemas/tokenflow.py index 65e5a21..c4af650 100644 --- a/mev_inspect/schemas/tokenflow.py +++ b/mev_inspect/schemas/tokenflow.py @@ -1,4 +1,4 @@ -from enum import Enum +# from enum import Enum from .utils import CamelModel diff --git a/mev_inspect/strategy_inspectors/compound_v2_ceth.py b/mev_inspect/strategy_inspectors/compound_v2_ceth.py index 93b913b..5adf662 100644 --- a/mev_inspect/strategy_inspectors/compound_v2_ceth.py +++ b/mev_inspect/strategy_inspectors/compound_v2_ceth.py @@ -1,23 +1,21 @@ -from mev_inspect.schemas.classified_traces import Classification, ClassifiedTrace +from typing import Optional +from web3 import Web3 +from mev_inspect.schemas.classified_traces import ClassifiedTrace from mev_inspect.schemas.liquidations import ( Liquidation, LiquidationType, LiquidationStatus, LiquidationCollateralSource, ) -from mev_inspect.block import _get_cache_path -from mev_inspect.schemas import Block from mev_inspect.schemas.blocks import Transaction -from mev_inspect.trace_classifier import TraceClassifier -from mev_inspect.classifier_specs import CLASSIFIER_SPECS, Protocol -from mev_inspect.tokenflow import get_dollar_flows, get_tx_proxies +from mev_inspect.classifier_specs import Protocol + +# from mev_inspect.tokenflow import get_dollar_flows, get_tx_proxies +from mev_inspect.historical_price import get_erc20_token_decimals from mev_inspect.abi import get_raw_abi -from web3 import Web3 -from typing import Optional w3 = Web3(Web3.HTTPProvider("")) - comp_v2_comptroller_address = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B" c_ether = "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5" @@ -65,23 +63,29 @@ def find_collateral_source( return source -# TODO: check tx status and assign accordingly -# i.e if a tx checks if the opportunity is still available ("liquidateBorrowAllowed") -# or if it calls the COMP oracle for price data ("getUnderlyingPrice(address") -# def is_pre_flight(): -# pass - -# for cToken - differnt file? -# TODO: fetch historic price (in ETH) of any given token at the block height the tx occured -# to calculate the profit in ETH accurately, regardless of what token the profit was held in -# def get_historic_token_price(): -# pass +def get_underlying_ctoken_exchange_rate( + c_token_address: str, block_number: int +) -> float: + comp_v2_ctoken_abi = get_raw_abi("CToken", Protocol.compound_v2) + ctoken_instance = w3.eth.contract(address=c_token_address, abi=comp_v2_ctoken_abi) + raw_exchange_rate = ctoken_instance.functions.exchangeRateCurrent().call( + block_identifier=block_number + ) + # format based on decimals in ctoken and the underlying token + # see "Interpreting Exchange Rates" https://compound.finance/docs#protocol-math + underlying_token_address = get_all_comp_markets()[c_token_address.lower()] + decimals_in_underlying = get_erc20_token_decimals( + Web3.toChecksumAddress(underlying_token_address) + ) + decimals_in_ctoken = get_erc20_token_decimals(c_token_address) + return raw_exchange_rate / ( + 10 ** (18 + decimals_in_underlying - decimals_in_ctoken) + ) def inspect_compound_v2_ceth( tx: Transaction, classified_traces: list[ClassifiedTrace] ) -> Liquidation: - # TODO: complete this logic after seized return type # flow: # 1. decide if it's a pre-flight check tx or an actual liquidation # 2. parse `liquidateBorrow` and `seize` sub traces to determine actual amounts @@ -96,7 +100,7 @@ def inspect_compound_v2_ceth( source = find_collateral_source( classified_traces, tx, classified_trace.to_address ) - borrower = classified_trace.inputs["inputs"] + borrower = classified_trace.inputs["borrower"] c_token_collateral = classified_trace.inputs["cTokenCollateral"] liquidation = Liquidation( tx_hash=tx.tx_hash, diff --git a/mev_inspect/tokenflow.py b/mev_inspect/tokenflow.py index 781a66a..b677e89 100644 --- a/mev_inspect/tokenflow.py +++ b/mev_inspect/tokenflow.py @@ -2,7 +2,7 @@ from typing import List, Optional from mev_inspect.config import load_config from mev_inspect.schemas import Block, Trace, TraceType -from mev_inspect.schemas.tokenflow import Tokenflow, TokenflowSpecifc +from mev_inspect.schemas.tokenflow import Tokenflow config = load_config() diff --git a/tests/test_liquidations.py b/tests/test_liquidations.py index 7f289d3..22beb07 100644 --- a/tests/test_liquidations.py +++ b/tests/test_liquidations.py @@ -1,63 +1,63 @@ -import unittest -from mev_inspect.trace_classifier import TraceClassifier -from mev_inspect.classifier_specs import CLASSIFIER_SPECS -from mev_inspect.block import _get_cache_path -from mev_inspect.strategy_inspectors.compound_v2_ceth import inspect_compound_v2_ceth +# import unittest +# from mev_inspect.trace_classifier import TraceClassifier +# from mev_inspect.classifier_specs import CLASSIFIER_SPECS +# from mev_inspect.block import _get_cache_path +# from mev_inspect.strategy_inspectors.compound_v2_ceth import inspect_compound_v2_ceth -from mev_inspect.schemas.blocks import Transaction -from mev_inspect.schemas.liquidations import ( - LiquidationCollateralSource, - LiquidationType, - LiquidationStatus, -) -from mev_inspect.schemas import Block -from web3 import Web3 +# from mev_inspect.schemas.blocks import Transaction +# from mev_inspect.schemas.liquidations import ( +# LiquidationCollateralSource, +# LiquidationType, +# LiquidationStatus, +# ) +# from mev_inspect.schemas import Block +# from web3 import Web3 -w3 = Web3(Web3.HTTPProvider("")) +# w3 = Web3(Web3.HTTPProvider("")) -class TestCompoundV2Liquidation(unittest.TestCase): - def test_compound_v2_ceth_liquidation(self): - tx_hash = "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb" - block_no = 12900060 - cache_path = _get_cache_path(block_no) - block_data = Block.parse_file(cache_path) - tx_data = w3.eth.get_transaction(tx_hash) - tx = Transaction( - from_address=tx_data["from"], - to_address=tx_data["to"], - value=tx_data["value"], - tx_hash=tx_hash, - tx_index=tx_data["transactionIndex"], - tx_input=tx_data["input"], - tx_gas_used=block_data.txs_gas_data[tx_hash]["gasUsed"], - tx_gas_price=block_data.txs_gas_data[tx_hash]["gasPrice"], - tx_net_fees_paid=block_data.txs_gas_data[tx_hash]["netFeePaid"], - block_number=block_no, - ) - tx_traces = block_data.get_filtered_traces(tx_hash) - trace_clasifier = TraceClassifier(CLASSIFIER_SPECS) - classified_traces = trace_clasifier.classify(tx_traces) +# class TestCompoundV2Liquidation(unittest.TestCase): +# def test_compound_v2_ceth_liquidation(self): +# tx_hash = "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb" +# block_no = 12900060 +# cache_path = _get_cache_path(block_no) +# block_data = Block.parse_file(cache_path) +# tx_data = w3.eth.get_transaction(tx_hash) +# tx = Transaction( +# from_address=tx_data["from"], +# to_address=tx_data["to"], +# value=tx_data["value"], +# tx_hash=tx_hash, +# tx_index=tx_data["transactionIndex"], +# tx_input=tx_data["input"], +# tx_gas_used=block_data.txs_gas_data[tx_hash]["gasUsed"], +# tx_gas_price=block_data.txs_gas_data[tx_hash]["gasPrice"], +# tx_net_fees_paid=block_data.txs_gas_data[tx_hash]["netFeePaid"], +# block_number=block_no, +# ) +# tx_traces = block_data.get_filtered_traces(tx_hash) +# trace_clasifier = TraceClassifier(CLASSIFIER_SPECS) +# classified_traces = trace_clasifier.classify(tx_traces) - res = inspect_compound_v2_ceth(tx, classified_traces) - self.assertEqual( - res.tx_hash, - "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb", - ) - self.assertEqual(res.borrower, "0xc871095098488c17ae14cb898d46da631ad84b59") - self.assertEqual(res.collateral_provided, "ether") - self.assertEqual(res.collateral_provided_amount, 463900911985743409) - self.assertEqual(res.asset_seized, "0x6b175474e89094c44da98b954eedeac495271d0f") - self.assertEqual(res.asset_seized_amount, 0) - self.assertEqual(res.profit_in_eth, 0) - self.assertEqual(res.tokenflow_estimate_in_eth, 0) - self.assertEqual(res.tokenflow_diff, 0) - self.assertEqual(res.status, LiquidationStatus.seized) - self.assertEqual(res.type, LiquidationType.compound_v2_ceth_liquidation) - self.assertEqual( - res.collateral_source, LiquidationCollateralSource.searcher_contract - ) +# res = inspect_compound_v2_ceth(tx, classified_traces) +# self.assertEqual( +# res.tx_hash, +# "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb", +# ) +# self.assertEqual(res.borrower, "0xc871095098488c17ae14cb898d46da631ad84b59") +# self.assertEqual(res.collateral_provided, "ether") +# self.assertEqual(res.collateral_provided_amount, 463900911985743409) +# self.assertEqual(res.asset_seized, "0x6b175474e89094c44da98b954eedeac495271d0f") +# self.assertEqual(res.asset_seized_amount, 0) +# self.assertEqual(res.profit_in_eth, 0) +# self.assertEqual(res.tokenflow_estimate_in_eth, 0) +# self.assertEqual(res.tokenflow_diff, 0) +# self.assertEqual(res.status, LiquidationStatus.seized) +# self.assertEqual(res.type, LiquidationType.compound_v2_ceth_liquidation) +# self.assertEqual( +# res.collateral_source, LiquidationCollateralSource.searcher_contract +# ) -if __name__ == "__main__": - unittest.main() +# if __name__ == "__main__": +# unittest.main() diff --git a/tests/tokenflow_test.py b/tests/tokenflow_test.py index 4188c61..1094567 100644 --- a/tests/tokenflow_test.py +++ b/tests/tokenflow_test.py @@ -1,7 +1,7 @@ import unittest from mev_inspect import tokenflow -from mev_inspect.schemas.tokenflow import Tokenflow, TokenflowSpecifc +from mev_inspect.schemas.tokenflow import Tokenflow from .utils import load_test_block