Resolve merge conflicts
This commit is contained in:
commit
e11f5b6741
2
.github/workflows/github-actions.yml
vendored
2
.github/workflows/github-actions.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Run precommit
|
||||
run: |
|
||||
poetry run pre-commit
|
||||
poetry run pre-commit run --all-files
|
||||
|
||||
- name: Test with pytest
|
||||
shell: bash
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,3 +19,6 @@ cache
|
||||
|
||||
# k8s
|
||||
.helm
|
||||
|
||||
# env
|
||||
.envrc
|
||||
|
@ -37,9 +37,9 @@ Example:
|
||||
export RPC_URL="http://111.111.111.111:8546"
|
||||
```
|
||||
|
||||
**Note: mev-inspect-py currently requires and RPC with support for OpenEthereum / Erigon traces (not geth 😔)**
|
||||
**Note: mev-inspect-py currently requires an RPC with support for OpenEthereum / Erigon traces (not geth 😔)**
|
||||
|
||||
Next, start all servcies with:
|
||||
Next, start all services with:
|
||||
```
|
||||
tilt up
|
||||
```
|
||||
@ -127,7 +127,7 @@ Postgres tip: Enter `\x` to enter "Explanded display" mode which looks nicer for
|
||||
|
||||
### Pre-commit
|
||||
|
||||
We use pre-commit to maintain a consistent style, prevent errors, and ensure test coverage.
|
||||
We use pre-commit to maintain a consistent style, prevent errors, and ensure test coverage.
|
||||
|
||||
To set up, install dependencies through poetry
|
||||
```
|
||||
|
@ -19,6 +19,10 @@ logging.basicConfig(filename="listener.log", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# lag to make sure the blocks we see are settled
|
||||
BLOCK_NUMBER_LAG = 5
|
||||
|
||||
|
||||
def run():
|
||||
rpc = os.getenv("RPC_URL")
|
||||
if rpc is None:
|
||||
@ -39,7 +43,9 @@ def run():
|
||||
logger.info(f"Latest block: {latest_block_number}")
|
||||
logger.info(f"Last written block: {last_written_block}")
|
||||
|
||||
if last_written_block is None or last_written_block < latest_block_number:
|
||||
if (last_written_block is None) or (
|
||||
last_written_block < (latest_block_number - BLOCK_NUMBER_LAG)
|
||||
):
|
||||
block_number = (
|
||||
latest_block_number
|
||||
if last_written_block is None
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from mev_inspect.traces import (
|
||||
get_child_traces,
|
||||
@ -11,7 +11,9 @@ from mev_inspect.schemas.classified_traces import (
|
||||
Protocol,
|
||||
)
|
||||
|
||||
from mev_inspect.schemas.transfers import ERC20Transfer, Transfer
|
||||
|
||||
from mev_inspect.transfers import get_transfer
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.schemas.liquidations import Liquidation
|
||||
|
||||
AAVE_CONTRACT_ADDRESSES: List[str] = [
|
||||
@ -45,6 +47,7 @@ def get_aave_liquidations(
|
||||
trace.classification == Classification.liquidate
|
||||
and isinstance(trace, DecodedCallTrace)
|
||||
and not is_child_of_any_address(trace, parent_liquidations)
|
||||
and trace.protocol == Protocol.aave
|
||||
):
|
||||
|
||||
parent_liquidations.append(trace.trace_address)
|
||||
@ -84,7 +87,7 @@ def _get_payback_token_and_amount(
|
||||
|
||||
"""Look for and return liquidator payback from liquidation"""
|
||||
child: ClassifiedTrace
|
||||
child_transfer: Transfer
|
||||
child_transfer: Optional[Transfer]
|
||||
|
||||
for child in child_traces:
|
||||
|
||||
@ -92,10 +95,11 @@ def _get_payback_token_and_amount(
|
||||
child, DecodedCallTrace
|
||||
):
|
||||
|
||||
child_transfer = ERC20Transfer.from_trace(child)
|
||||
child_transfer = get_transfer(child)
|
||||
|
||||
if (
|
||||
child_transfer.to_address == liquidator
|
||||
child_transfer is not None
|
||||
and child_transfer.to_address == liquidator
|
||||
and child.from_address in AAVE_CONTRACT_ADDRESSES
|
||||
):
|
||||
return child_transfer.token_address, child_transfer.amount
|
||||
|
@ -12,15 +12,32 @@ THIS_FILE_DIRECTORY = Path(__file__).parents[0]
|
||||
ABI_DIRECTORY_PATH = THIS_FILE_DIRECTORY / "abis"
|
||||
|
||||
|
||||
def get_abi(abi_name: str, protocol: Optional[Protocol]) -> Optional[ABI]:
|
||||
def get_abi_path(abi_name: str, protocol: Optional[Protocol]) -> Optional[Path]:
|
||||
abi_filename = f"{abi_name}.json"
|
||||
abi_path = (
|
||||
ABI_DIRECTORY_PATH / abi_filename
|
||||
if protocol is None
|
||||
else ABI_DIRECTORY_PATH / protocol.value / abi_filename
|
||||
)
|
||||
|
||||
if abi_path.is_file():
|
||||
return abi_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# raw abi, for instantiating contract for queries (as opposed to classification, see below)
|
||||
def get_raw_abi(abi_name: str, protocol: Optional[Protocol]) -> Optional[str]:
|
||||
abi_path = get_abi_path(abi_name, protocol)
|
||||
if abi_path is not None:
|
||||
with abi_path.open() as abi_file:
|
||||
return abi_file.read()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_abi(abi_name: str, protocol: Optional[Protocol]) -> Optional[ABI]:
|
||||
abi_path = get_abi_path(abi_name, protocol)
|
||||
if abi_path is not None:
|
||||
with abi_path.open() as abi_file:
|
||||
abi_json = json.load(abi_file)
|
||||
return parse_obj_as(ABI, abi_json)
|
||||
|
1
mev_inspect/abis/compound_v2/CEther.json
Normal file
1
mev_inspect/abis/compound_v2/CEther.json
Normal file
File diff suppressed because one or more lines are too long
1
mev_inspect/abis/compound_v2/CToken.json
Normal file
1
mev_inspect/abis/compound_v2/CToken.json
Normal file
File diff suppressed because one or more lines are too long
1
mev_inspect/abis/compound_v2/Comptroller.json
Normal file
1
mev_inspect/abis/compound_v2/Comptroller.json
Normal file
File diff suppressed because one or more lines are too long
@ -82,4 +82,4 @@ def cache_block(cache_path: Path, block: Block):
|
||||
|
||||
def _get_cache_path(block_number: int) -> Path:
|
||||
cache_directory_path = Path(cache_directory)
|
||||
return cache_directory_path / f"{block_number}-new.json"
|
||||
return cache_directory_path / f"{block_number}.json"
|
||||
|
@ -1,11 +1,16 @@
|
||||
from typing import Dict, Optional, Tuple, Type
|
||||
|
||||
from mev_inspect.schemas.classified_traces import DecodedCallTrace, Protocol
|
||||
from mev_inspect.schemas.classifiers import ClassifierSpec, Classifier
|
||||
|
||||
from .aave import AAVE_CLASSIFIER_SPECS
|
||||
from .curve import CURVE_CLASSIFIER_SPECS
|
||||
from .erc20 import ERC20_CLASSIFIER_SPECS
|
||||
from .uniswap import UNISWAP_CLASSIFIER_SPECS
|
||||
from .weth import WETH_CLASSIFIER_SPECS
|
||||
from .weth import WETH_CLASSIFIER_SPECS, WETH_ADDRESS
|
||||
from .zero_ex import ZEROX_CLASSIFIER_SPECS
|
||||
from .balancer import BALANCER_CLASSIFIER_SPECS
|
||||
|
||||
from .compound import COMPOUND_CLASSIFIER_SPECS
|
||||
|
||||
ALL_CLASSIFIER_SPECS = (
|
||||
ERC20_CLASSIFIER_SPECS
|
||||
@ -15,4 +20,21 @@ ALL_CLASSIFIER_SPECS = (
|
||||
+ AAVE_CLASSIFIER_SPECS
|
||||
+ ZEROX_CLASSIFIER_SPECS
|
||||
+ BALANCER_CLASSIFIER_SPECS
|
||||
+ COMPOUND_CLASSIFIER_SPECS
|
||||
)
|
||||
|
||||
_SPECS_BY_ABI_NAME_AND_PROTOCOL: Dict[
|
||||
Tuple[str, Optional[Protocol]], ClassifierSpec
|
||||
] = {(spec.abi_name, spec.protocol): spec for spec in ALL_CLASSIFIER_SPECS}
|
||||
|
||||
|
||||
def get_classifier(
|
||||
trace: DecodedCallTrace,
|
||||
) -> Optional[Type[Classifier]]:
|
||||
abi_name_and_protocol = (trace.abi_name, trace.protocol)
|
||||
spec = _SPECS_BY_ABI_NAME_AND_PROTOCOL.get(abi_name_and_protocol)
|
||||
|
||||
if spec is not None:
|
||||
return spec.classifiers.get(trace.function_signature)
|
||||
|
||||
return None
|
||||
|
@ -1,14 +1,35 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
ClassifierSpec,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
DecodedCallTrace,
|
||||
TransferClassifier,
|
||||
LiquidationClassifier,
|
||||
)
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
|
||||
|
||||
class AaveTransferClassifier(TransferClassifier):
|
||||
@staticmethod
|
||||
def get_transfer(trace: DecodedCallTrace) -> Transfer:
|
||||
return Transfer(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["value"],
|
||||
to_address=trace.inputs["to"],
|
||||
from_address=trace.inputs["from"],
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
||||
|
||||
AAVE_SPEC = ClassifierSpec(
|
||||
abi_name="AaveLendingPool",
|
||||
protocol=Protocol.aave,
|
||||
classifications={
|
||||
"liquidationCall(address,address,address,uint256,bool)": Classification.liquidate,
|
||||
classifiers={
|
||||
"liquidationCall(address,address,address,uint256,bool)": LiquidationClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
@ -16,8 +37,8 @@ ATOKENS_SPEC = ClassifierSpec(
|
||||
abi_name="aTokens",
|
||||
protocol=Protocol.aave,
|
||||
classifications={
|
||||
"transferOnLiquidation(address,address,uint256)": Classification.transfer,
|
||||
"transferFrom(address,address,uint256)": Classification.transfer,
|
||||
"transferOnLiquidation(address,address,uint256)": AaveTransferClassifier,
|
||||
"transferFrom(address,address,uint256)": AaveTransferClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,16 +1,29 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
ClassifierSpec,
|
||||
DecodedCallTrace,
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
SwapClassifier,
|
||||
)
|
||||
|
||||
|
||||
BALANCER_V1_POOL_ABI_NAME = "BPool"
|
||||
|
||||
|
||||
class BalancerSwapClassifier(SwapClassifier):
|
||||
@staticmethod
|
||||
def get_swap_recipient(trace: DecodedCallTrace) -> str:
|
||||
return trace.from_address
|
||||
|
||||
|
||||
BALANCER_V1_SPECS = [
|
||||
ClassifierSpec(
|
||||
abi_name="BPool",
|
||||
abi_name=BALANCER_V1_POOL_ABI_NAME,
|
||||
protocol=Protocol.balancer_v1,
|
||||
classifications={
|
||||
"swapExactAmountIn(address,uint256,address,uint256,uint256)": Classification.swap,
|
||||
"swapExactAmountOut(address,uint256,address,uint256,uint256)": Classification.swap,
|
||||
classifiers={
|
||||
"swapExactAmountIn(address,uint256,address,uint256,uint256)": BalancerSwapClassifier,
|
||||
"swapExactAmountOut(address,uint256,address,uint256,uint256)": BalancerSwapClassifier,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
28
mev_inspect/classifiers/specs/compound.py
Normal file
28
mev_inspect/classifiers/specs/compound.py
Normal file
@ -0,0 +1,28 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
LiquidationClassifier,
|
||||
SeizeClassifier,
|
||||
)
|
||||
|
||||
COMPOUND_V2_CETH_SPEC = ClassifierSpec(
|
||||
abi_name="CEther",
|
||||
protocol=Protocol.compound_v2,
|
||||
classifiers={
|
||||
"liquidateBorrow(address,address)": LiquidationClassifier,
|
||||
"seize(address,address,uint256)": SeizeClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
COMPOUND_V2_CTOKEN_SPEC = ClassifierSpec(
|
||||
abi_name="CToken",
|
||||
protocol=Protocol.compound_v2,
|
||||
classifiers={
|
||||
"liquidateBorrow(address,uint256,address)": LiquidationClassifier,
|
||||
"seize(address,address,uint256)": SeizeClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
COMPOUND_CLASSIFIER_SPECS = [COMPOUND_V2_CETH_SPEC, COMPOUND_V2_CTOKEN_SPEC]
|
@ -1,29 +1,20 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
ClassifierSpec,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
"""
|
||||
Deployment addresses found here
|
||||
https://curve.readthedocs.io/ref-addresses.html
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
DecodedCallTrace,
|
||||
SwapClassifier,
|
||||
)
|
||||
|
||||
|
||||
class CurveSwapClassifier(SwapClassifier):
|
||||
@staticmethod
|
||||
def get_swap_recipient(trace: DecodedCallTrace) -> str:
|
||||
return trace.from_address
|
||||
|
||||
|
||||
organized into 3 groups
|
||||
1. Base Pools: 2 or more tokens implementing stable swap
|
||||
- StableSwap<pool>
|
||||
- Deposit<pool>
|
||||
- CurveContract<version>
|
||||
- CurveTokenV1/V2
|
||||
2. Meta Pools: 1 token trading with an LP from above
|
||||
- StableSwap<pool>
|
||||
- Deposit<pool>
|
||||
- CurveTokenV1/V2
|
||||
3. Liquidity Gauges: stake LP get curve governance token?
|
||||
- LiquidityGauge
|
||||
- LiquidityGaugeV1/V2
|
||||
- LiquidityGaugeReward
|
||||
4. DAO stuff
|
||||
5..? Other stuff, haven't decided if important
|
||||
"""
|
||||
CURVE_BASE_POOLS = [
|
||||
ClassifierSpec(
|
||||
abi_name="CurveTokenV1",
|
||||
@ -72,101 +63,171 @@ CURVE_BASE_POOLS = [
|
||||
abi_name="StableSwap3Pool",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapAAVE",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xDeBF20617708857ebe4F679508E7b7863a8A8EeE"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapAETH",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xA96A65c051bF88B4095Ee1f2451C2A9d43F53Ae2"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapBUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x79a8C46DeA5aDa233ABaFFD40F3A0A2B1e5A4F27"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapCompound",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapEURS",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x0Ce6a5fF5217e38315f87032CF90686C96627CAA"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwaphBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x4CA9b3063Ec5866A4B82E437059D2C43d1be596F"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapIronBank",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x2dded6Da1BF5DBdF597C45fcFaa3194e53EcfeAF"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapLink",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xf178c0b5bb7e7abf4e12a4838c7b7c5ba2c623c0"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapPAX",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x06364f10B501e868329afBc005b3492902d6C763"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwaprenBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x93054188d876f558f4a66B2EF1d97d16eDf0895B"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwaprETH",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xF9440930043eb3997fc70e1339dBb11F341de7A8"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapsAAVE",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xEB16Ae0052ed37f479f7fe63849198Df1765a733"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapsBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapsETH",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xc5424B857f758E906013F3555Dad202e4bdB4567"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapstETH",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapsUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xA5407eAE9Ba41422680e2e00537571bcC53efBfD"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapUSDT",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x52EA46506B9CC5Ef470C5bf89f17Dc28bB35D85C"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapY",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapYv2",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x8925D9d9B4569D737a48499DeF3f67BaA5a144b9"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="DepositBUSD",
|
||||
@ -300,51 +361,91 @@ CURVE_META_POOLS = [
|
||||
abi_name="StableSwapbBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x071c661B4DeefB59E2a3DdB20Db036821eeE8F4b"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapDUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x8038C01A0390a8c547446a0b2c18fc9aEFEcc10c"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapGUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x4f062658EaAF2C1ccf8C8e36D6824CDf41167956"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapHUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x3eF6A01A0f81D6046290f3e2A8c5b843e738E604"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapLinkUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xE7a24EF0C5e95Ffb0f6684b813A78F2a3AD7D171"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapMUSD",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x8474DdbE98F5aA3179B3B3F5942D724aFcdec9f6"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapoBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xd81dA8D904b52208541Bade1bD6595D8a251F8dd"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwappBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x7F55DDe206dbAD629C080068923b36fe9D6bDBeF"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapRSV",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xC18cC39da8b11dA8c3541C598eE022258F9744da"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwaptBTC",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0xC25099792E9349C7DD09759744ea681C7de2cb66"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapUSD",
|
||||
@ -353,82 +454,29 @@ CURVE_META_POOLS = [
|
||||
"0x3E01dD8a5E1fb3481F0F589056b428Fc308AF0Fb",
|
||||
"0x0f9cb53Ebe405d49A0bbdBD291A65Ff571bC83e1",
|
||||
],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapUSDP",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x42d7025938bEc20B69cBae5A77421082407f053A"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="StableSwapUST",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=["0x890f4e345B1dAED0367A877a1612f86A1f86985f"],
|
||||
classifiers={
|
||||
"exchange(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
"exchange_underlying(int128,int128,uint256,uint256)": CurveSwapClassifier,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
"""
|
||||
CURVE_LIQUIDITY_GAUGES = [
|
||||
ClassifierSpec(
|
||||
abi_name="LiquidityGauge",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=[
|
||||
"0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A", # 3Pool
|
||||
"0x69Fb7c45726cfE2baDeE8317005d3F94bE838840", # BUSD
|
||||
"0x7ca5b0a2910B33e9759DC7dDB0413949071D7575", # Compound
|
||||
"0xC5cfaDA84E902aD92DD40194f0883ad49639b023", # GUSD
|
||||
"0x4c18E409Dc8619bFb6a1cB56D114C3f592E0aE79", # hBTC
|
||||
"0x2db0E83599a91b508Ac268a6197b8B14F5e72840", # HUSD
|
||||
"0x64E3C23bfc40722d3B649844055F1D51c1ac041d", # PAX
|
||||
"0xB1F2cdeC61db658F091671F5f199635aEF202CAC", # renBTC
|
||||
"0xC2b1DF84112619D190193E48148000e3990Bf627", # USDK
|
||||
"0xF98450B5602fa59CC66e1379DFfB6FDDc724CfC4", # USDN
|
||||
"0xBC89cd85491d81C6AD2954E6d0362Ee29fCa8F53", # USDT
|
||||
"0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1", # Y
|
||||
],
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="LiquidityGaugeV2",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=[
|
||||
"0xd662908ADA2Ea1916B3318327A97eB18aD588b5d", # AAVE
|
||||
"0x6d10ed2cF043E6fcf51A0e7b4C2Af3Fa06695707", # ankrETH
|
||||
"0xdFc7AdFa664b08767b735dE28f9E84cd30492aeE", # bBTC
|
||||
"0x90Bb609649E0451E5aD952683D64BD2d1f245840", # EURS
|
||||
"0x72e158d38dbd50a483501c24f792bdaaa3e7d55c", # FRAX
|
||||
"0x11137B10C210b579405c21A07489e28F3c040AB1", # oBTC
|
||||
"0xF5194c3325202F456c95c1Cf0cA36f8475C1949F", # IronBank
|
||||
"0xFD4D8a17df4C27c1dD245d153ccf4499e806C87D", # Link
|
||||
"0xd7d147c6Bb90A718c3De8C0568F9B560C79fa416", # pBTC
|
||||
"0x462253b8F74B72304c145DB0e4Eebd326B22ca39", # sAAVE
|
||||
"0x3C0FFFF15EA30C35d7A85B85c0782D6c94e1d238", # sETH
|
||||
"0x182B723a58739a9c974cFDB385ceaDb237453c28", # stETH
|
||||
"0x055be5DDB7A925BfEF3417FC157f53CA77cA7222", # USDP
|
||||
"0x3B7020743Bc2A4ca9EaF9D0722d42E20d6935855", # UST
|
||||
"0x8101E6760130be2C8Ace79643AB73500571b7162", # Yv2
|
||||
],
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="LiquidityGaugeV3",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=[
|
||||
"0x9582C4ADACB3BCE56Fea3e590F05c3ca2fb9C477", # alUSD
|
||||
"0x824F13f1a2F29cFEEa81154b46C0fc820677A637", # rETH
|
||||
"0x6955a55416a06839309018A8B0cB72c4DDC11f15", # TriCrypto
|
||||
],
|
||||
),
|
||||
ClassifierSpec(
|
||||
abi_name="LiquidityGaugeReward",
|
||||
protocol=Protocol.curve,
|
||||
valid_contract_addresses=[
|
||||
"0xAEA6c312f4b3E04D752946d329693F7293bC2e6D", # DUSD
|
||||
"0x5f626c30EC1215f4EdCc9982265E8b1F411D1352", # MUSD
|
||||
"0x4dC4A289a8E33600D8bD4cf5F6313E43a37adec7", # RSV
|
||||
"0x705350c4BcD35c9441419DdD5d2f097d7a55410F", # sBTC
|
||||
"0xA90996896660DEcC6E997655E065b23788857849", # sUSDv2
|
||||
"0x6828bcF74279eE32f2723eC536c22c51Eed383C6", # tBTC
|
||||
],
|
||||
),
|
||||
]
|
||||
"""
|
||||
|
||||
CURVE_CLASSIFIER_SPECS = [*CURVE_BASE_POOLS, *CURVE_META_POOLS]
|
||||
|
@ -1,15 +1,30 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
from mev_inspect.schemas.classified_traces import DecodedCallTrace
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
TransferClassifier,
|
||||
)
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
|
||||
|
||||
class ERC20TransferClassifier(TransferClassifier):
|
||||
@staticmethod
|
||||
def get_transfer(trace: DecodedCallTrace) -> Transfer:
|
||||
return Transfer(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["amount"],
|
||||
to_address=trace.inputs["recipient"],
|
||||
from_address=trace.inputs.get("sender", trace.from_address),
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
||||
|
||||
ERC20_SPEC = ClassifierSpec(
|
||||
abi_name="ERC20",
|
||||
classifications={
|
||||
"transferFrom(address,address,uint256)": Classification.transfer,
|
||||
"transfer(address,uint256)": Classification.transfer,
|
||||
"burn(address)": Classification.burn,
|
||||
classifiers={
|
||||
"transferFrom(address,address,uint256)": ERC20TransferClassifier,
|
||||
"transfer(address,uint256)": ERC20TransferClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,8 +1,33 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
ClassifierSpec,
|
||||
DecodedCallTrace,
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
SwapClassifier,
|
||||
)
|
||||
|
||||
|
||||
UNISWAP_V2_PAIR_ABI_NAME = "UniswapV2Pair"
|
||||
UNISWAP_V3_POOL_ABI_NAME = "UniswapV3Pool"
|
||||
|
||||
|
||||
class UniswapV3SwapClassifier(SwapClassifier):
|
||||
@staticmethod
|
||||
def get_swap_recipient(trace: DecodedCallTrace) -> str:
|
||||
if trace.inputs is not None and "recipient" in trace.inputs:
|
||||
return trace.inputs["recipient"]
|
||||
else:
|
||||
return trace.from_address
|
||||
|
||||
|
||||
class UniswapV2SwapClassifier(SwapClassifier):
|
||||
@staticmethod
|
||||
def get_swap_recipient(trace: DecodedCallTrace) -> str:
|
||||
if trace.inputs is not None and "to" in trace.inputs:
|
||||
return trace.inputs["to"]
|
||||
else:
|
||||
return trace.from_address
|
||||
|
||||
|
||||
UNISWAP_V3_CONTRACT_SPECS = [
|
||||
@ -65,9 +90,9 @@ UNISWAP_V3_CONTRACT_SPECS = [
|
||||
|
||||
UNISWAP_V3_GENERAL_SPECS = [
|
||||
ClassifierSpec(
|
||||
abi_name="UniswapV3Pool",
|
||||
classifications={
|
||||
"swap(address,bool,int256,uint160,bytes)": Classification.swap,
|
||||
abi_name=UNISWAP_V3_POOL_ABI_NAME,
|
||||
classifiers={
|
||||
"swap(address,bool,int256,uint160,bytes)": UniswapV3SwapClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
@ -96,9 +121,9 @@ UNISWAPPY_V2_CONTRACT_SPECS = [
|
||||
]
|
||||
|
||||
UNISWAPPY_V2_PAIR_SPEC = ClassifierSpec(
|
||||
abi_name="UniswapV2Pair",
|
||||
classifications={
|
||||
"swap(uint256,uint256,address,bytes)": Classification.swap,
|
||||
abi_name=UNISWAP_V2_PAIR_ABI_NAME,
|
||||
classifiers={
|
||||
"swap(uint256,uint256,address,bytes)": UniswapV2SwapClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,16 +1,37 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
ClassifierSpec,
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
DecodedCallTrace,
|
||||
TransferClassifier,
|
||||
)
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
|
||||
|
||||
class WethTransferClassifier(TransferClassifier):
|
||||
@staticmethod
|
||||
def get_transfer(trace: DecodedCallTrace) -> Transfer:
|
||||
return Transfer(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["wad"],
|
||||
to_address=trace.inputs["dst"],
|
||||
from_address=trace.from_address,
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
||||
|
||||
WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||
|
||||
WETH_SPEC = ClassifierSpec(
|
||||
abi_name="WETH9",
|
||||
protocol=Protocol.weth,
|
||||
valid_contract_addresses=["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],
|
||||
classifications={
|
||||
"transferFrom(address,address,uint256)": Classification.transfer,
|
||||
"transfer(address,uint256)": Classification.transfer,
|
||||
valid_contract_addresses=[WETH_ADDRESS],
|
||||
classifiers={
|
||||
"transferFrom(address,address,uint256)": WethTransferClassifier,
|
||||
"transfer(address,uint256)": WethTransferClassifier,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
ClassifierSpec,
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import (
|
||||
ClassifierSpec,
|
||||
)
|
||||
|
||||
|
||||
ZEROX_CONTRACT_SPECS = [
|
||||
|
@ -67,8 +67,11 @@ class TraceClassifier:
|
||||
|
||||
if call_data is not None:
|
||||
signature = call_data.function_signature
|
||||
classification = spec.classifications.get(
|
||||
signature, Classification.unknown
|
||||
classifier = spec.classifiers.get(signature)
|
||||
classification = (
|
||||
Classification.unknown
|
||||
if classifier is None
|
||||
else classifier.get_classification()
|
||||
)
|
||||
|
||||
return DecodedCallTrace(
|
||||
|
102
mev_inspect/compound_liquidations.py
Normal file
102
mev_inspect/compound_liquidations.py
Normal file
@ -0,0 +1,102 @@
|
||||
from typing import Dict, List, Optional
|
||||
from web3 import Web3
|
||||
|
||||
from mev_inspect.traces import get_child_traces
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
ClassifiedTrace,
|
||||
Classification,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
from mev_inspect.schemas.liquidations import Liquidation
|
||||
from mev_inspect.classifiers.specs import WETH_ADDRESS
|
||||
from mev_inspect.abi import get_raw_abi
|
||||
|
||||
V2_COMPTROLLER_ADDRESS = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"
|
||||
V2_C_ETHER = "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5"
|
||||
|
||||
# helper, only queried once in the beginning (inspect_block)
|
||||
def fetch_all_comp_markets(w3: Web3) -> Dict[str, str]:
|
||||
c_token_mapping = {}
|
||||
comp_v2_comptroller_abi = get_raw_abi("Comptroller", Protocol.compound_v2)
|
||||
comptroller_instance = w3.eth.contract(
|
||||
address=V2_COMPTROLLER_ADDRESS, abi=comp_v2_comptroller_abi
|
||||
)
|
||||
markets = comptroller_instance.functions.getAllMarkets().call()
|
||||
comp_v2_ctoken_abi = get_raw_abi("CToken", Protocol.compound_v2)
|
||||
for c_token in markets:
|
||||
# make an exception for cETH (as it has no .underlying())
|
||||
if c_token != V2_C_ETHER:
|
||||
ctoken_instance = w3.eth.contract(address=c_token, abi=comp_v2_ctoken_abi)
|
||||
underlying_token = ctoken_instance.functions.underlying().call()
|
||||
c_token_mapping[
|
||||
c_token.lower()
|
||||
] = underlying_token.lower() # make k:v lowercase for consistancy
|
||||
return c_token_mapping
|
||||
|
||||
|
||||
def get_compound_liquidations(
|
||||
traces: List[ClassifiedTrace], collateral_by_c_token_address: Dict[str, str]
|
||||
) -> List[Liquidation]:
|
||||
|
||||
"""Inspect list of classified traces and identify liquidation"""
|
||||
liquidations: List[Liquidation] = []
|
||||
|
||||
for trace in traces:
|
||||
if (
|
||||
trace.classification == Classification.liquidate
|
||||
and trace.protocol == Protocol.compound_v2
|
||||
and trace.inputs is not None
|
||||
and trace.to_address is not None
|
||||
):
|
||||
# First, we look for cEther liquidations (position paid back via tx.value)
|
||||
child_traces = get_child_traces(
|
||||
trace.transaction_hash, trace.trace_address, traces
|
||||
)
|
||||
seize_trace = _get_seize_call(child_traces)
|
||||
if seize_trace is not None and seize_trace.inputs is not None:
|
||||
c_token_collateral = trace.inputs["cTokenCollateral"]
|
||||
if trace.abi_name == "CEther":
|
||||
liquidations.append(
|
||||
Liquidation(
|
||||
liquidated_user=trace.inputs["borrower"],
|
||||
collateral_token_address=WETH_ADDRESS, # WETH since all cEther liquidations provide Ether
|
||||
debt_token_address=c_token_collateral,
|
||||
liquidator_user=seize_trace.inputs["liquidator"],
|
||||
debt_purchase_amount=trace.value,
|
||||
protocol=Protocol.compound_v2,
|
||||
received_amount=seize_trace.inputs["seizeTokens"],
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
block_number=trace.block_number,
|
||||
)
|
||||
)
|
||||
elif (
|
||||
trace.abi_name == "CToken"
|
||||
): # cToken liquidations where liquidator pays back via token transfer
|
||||
c_token_address = trace.to_address
|
||||
liquidations.append(
|
||||
Liquidation(
|
||||
liquidated_user=trace.inputs["borrower"],
|
||||
collateral_token_address=collateral_by_c_token_address[
|
||||
c_token_address
|
||||
],
|
||||
debt_token_address=c_token_collateral,
|
||||
liquidator_user=seize_trace.inputs["liquidator"],
|
||||
debt_purchase_amount=trace.inputs["repayAmount"],
|
||||
protocol=Protocol.compound_v2,
|
||||
received_amount=seize_trace.inputs["seizeTokens"],
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
block_number=trace.block_number,
|
||||
)
|
||||
)
|
||||
return liquidations
|
||||
|
||||
|
||||
def _get_seize_call(traces: List[ClassifiedTrace]) -> Optional[ClassifiedTrace]:
|
||||
"""Find the call to `seize` in the child traces (successful liquidation)"""
|
||||
for trace in traces:
|
||||
if trace.classification == Classification.seize:
|
||||
return trace
|
||||
return None
|
@ -2,7 +2,7 @@ import json
|
||||
from typing import List
|
||||
|
||||
from mev_inspect.models.transfers import TransferModel
|
||||
from mev_inspect.schemas.transfers import ERC20Transfer
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
|
||||
|
||||
def delete_transfers_for_block(
|
||||
@ -20,7 +20,7 @@ def delete_transfers_for_block(
|
||||
|
||||
def write_transfers(
|
||||
db_session,
|
||||
transfers: List[ERC20Transfer],
|
||||
transfers: List[Transfer],
|
||||
) -> None:
|
||||
models = [TransferModel(**json.loads(transfer.json())) for transfer in transfers]
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
import eth_utils.abi
|
||||
|
||||
from hexbytes import HexBytes
|
||||
from eth_abi import decode_abi
|
||||
from eth_abi.exceptions import InsufficientDataBytes, NonEmptyPaddingBytes
|
||||
@ -26,7 +28,12 @@ class ABIDecoder:
|
||||
return None
|
||||
|
||||
names = [input.name for input in func.inputs]
|
||||
types = [input.type for input in func.inputs]
|
||||
types = [
|
||||
input.type
|
||||
if input.type != "tuple"
|
||||
else eth_utils.abi.collapse_if_tuple(input.dict())
|
||||
for input in func.inputs
|
||||
]
|
||||
|
||||
try:
|
||||
decoded = decode_abi(types, params)
|
||||
|
@ -27,8 +27,7 @@ from mev_inspect.crud.liquidations import (
|
||||
from mev_inspect.miner_payments import get_miner_payments
|
||||
from mev_inspect.swaps import get_swaps
|
||||
from mev_inspect.transfers import get_transfers
|
||||
from mev_inspect.aave_liquidations import get_aave_liquidations
|
||||
|
||||
from mev_inspect.liquidations import get_liquidations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -82,7 +81,7 @@ def inspect_block(
|
||||
delete_arbitrages_for_block(db_session, block_number)
|
||||
write_arbitrages(db_session, arbitrages)
|
||||
|
||||
liquidations = get_aave_liquidations(classified_traces)
|
||||
liquidations = get_liquidations(classified_traces, w3)
|
||||
logger.info(f"Found {len(liquidations)} liquidations")
|
||||
|
||||
if should_write_liquidations:
|
||||
|
19
mev_inspect/liquidations.py
Normal file
19
mev_inspect/liquidations.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
from web3 import Web3
|
||||
from mev_inspect.aave_liquidations import get_aave_liquidations
|
||||
from mev_inspect.compound_liquidations import (
|
||||
get_compound_liquidations,
|
||||
fetch_all_comp_markets,
|
||||
)
|
||||
from mev_inspect.schemas.classified_traces import ClassifiedTrace
|
||||
from mev_inspect.schemas.liquidations import Liquidation
|
||||
|
||||
|
||||
def get_liquidations(
|
||||
classified_traces: List[ClassifiedTrace], w3: Web3
|
||||
) -> List[Liquidation]:
|
||||
aave_liquidations = get_aave_liquidations(classified_traces)
|
||||
comp_markets = fetch_all_comp_markets(w3)
|
||||
compound_liquidations = get_compound_liquidations(classified_traces, comp_markets)
|
||||
return aave_liquidations + compound_liquidations
|
@ -1,7 +1,8 @@
|
||||
from enum import Enum
|
||||
from typing import List, Union
|
||||
from typing import List, Optional, Union
|
||||
from typing_extensions import Literal
|
||||
|
||||
import eth_utils.abi
|
||||
from hexbytes import HexBytes
|
||||
from pydantic import BaseModel
|
||||
from web3 import Web3
|
||||
@ -26,6 +27,10 @@ NON_FUNCTION_DESCRIPTION_TYPES = Union[
|
||||
class ABIDescriptionInput(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
components: Optional[List["ABIDescriptionInput"]]
|
||||
|
||||
|
||||
ABIDescriptionInput.update_forward_refs()
|
||||
|
||||
|
||||
class ABIGenericDescription(BaseModel):
|
||||
@ -42,7 +47,12 @@ class ABIFunctionDescription(BaseModel):
|
||||
return Web3.sha3(text=signature)[0:4]
|
||||
|
||||
def get_signature(self) -> str:
|
||||
joined_input_types = ",".join(input.type for input in self.inputs)
|
||||
joined_input_types = ",".join(
|
||||
input.type
|
||||
if input.type != "tuple"
|
||||
else eth_utils.abi.collapse_if_tuple(input.dict())
|
||||
for input in self.inputs
|
||||
)
|
||||
return f"{self.name}({joined_input_types})"
|
||||
|
||||
|
||||
|
@ -1,17 +1,15 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .blocks import Trace
|
||||
|
||||
|
||||
class Classification(Enum):
|
||||
unknown = "unknown"
|
||||
swap = "swap"
|
||||
burn = "burn"
|
||||
transfer = "transfer"
|
||||
liquidate = "liquidate"
|
||||
seize = "seize"
|
||||
|
||||
|
||||
class Protocol(Enum):
|
||||
@ -23,6 +21,7 @@ class Protocol(Enum):
|
||||
curve = "curve"
|
||||
zero_ex = "0x"
|
||||
balancer_v1 = "balancer_v1"
|
||||
compound_v2 = "compound_v2"
|
||||
|
||||
|
||||
class ClassifiedTrace(Trace):
|
||||
@ -62,12 +61,5 @@ class DecodedCallTrace(CallTrace):
|
||||
protocol: Optional[Protocol]
|
||||
gas: Optional[int]
|
||||
gas_used: Optional[int]
|
||||
function_name: Optional[str]
|
||||
function_signature: Optional[str]
|
||||
|
||||
|
||||
class ClassifierSpec(BaseModel):
|
||||
abi_name: str
|
||||
protocol: Optional[Protocol] = None
|
||||
valid_contract_addresses: Optional[List[str]] = None
|
||||
classifications: Dict[str, Classification] = {}
|
||||
function_name: str
|
||||
function_signature: str
|
||||
|
55
mev_inspect/schemas/classifiers.py
Normal file
55
mev_inspect/schemas/classifiers.py
Normal file
@ -0,0 +1,55 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .classified_traces import Classification, DecodedCallTrace, Protocol
|
||||
from .transfers import Transfer
|
||||
|
||||
|
||||
class Classifier(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_classification() -> Classification:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TransferClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.transfer
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_transfer(trace: DecodedCallTrace) -> Transfer:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SwapClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.swap
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_swap_recipient(trace: DecodedCallTrace) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class LiquidationClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.liquidate
|
||||
|
||||
|
||||
class SeizeClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.seize
|
||||
|
||||
|
||||
class ClassifierSpec(BaseModel):
|
||||
abi_name: str
|
||||
protocol: Optional[Protocol] = None
|
||||
valid_contract_addresses: Optional[List[str]] = None
|
||||
classifiers: Dict[str, Type[Classifier]] = {}
|
@ -1,12 +1,9 @@
|
||||
from typing import List, TypeVar
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .classified_traces import (
|
||||
Classification,
|
||||
ClassifiedTrace,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
ETH_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
||||
|
||||
|
||||
class Transfer(BaseModel):
|
||||
@ -16,62 +13,4 @@ class Transfer(BaseModel):
|
||||
from_address: str
|
||||
to_address: str
|
||||
amount: int
|
||||
|
||||
|
||||
# To preserve the specific Transfer type
|
||||
TransferGeneric = TypeVar("TransferGeneric", bound="Transfer")
|
||||
|
||||
|
||||
class EthTransfer(Transfer):
|
||||
@classmethod
|
||||
def from_trace(cls, trace: ClassifiedTrace) -> "EthTransfer":
|
||||
return cls(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.value,
|
||||
to_address=trace.to_address,
|
||||
from_address=trace.from_address,
|
||||
)
|
||||
|
||||
|
||||
class ERC20Transfer(Transfer):
|
||||
token_address: str
|
||||
|
||||
@classmethod
|
||||
def from_trace(cls, trace: ClassifiedTrace) -> "ERC20Transfer":
|
||||
if trace.classification != Classification.transfer or trace.inputs is None:
|
||||
raise ValueError("Invalid transfer")
|
||||
|
||||
if trace.protocol == Protocol.aave:
|
||||
return cls(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["value"],
|
||||
to_address=trace.inputs["to"],
|
||||
from_address=trace.inputs["from"],
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
||||
if trace.protocol == Protocol.weth:
|
||||
return cls(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["wad"],
|
||||
to_address=trace.inputs["dst"],
|
||||
from_address=trace.from_address,
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
||||
else:
|
||||
return cls(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.inputs["amount"],
|
||||
to_address=trace.inputs["recipient"],
|
||||
from_address=trace.inputs.get("sender", trace.from_address),
|
||||
token_address=trace.to_address,
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
import json
|
||||
|
||||
from hexbytes import HexBytes
|
||||
from pydantic import BaseModel
|
||||
from web3.datastructures import AttributeDict
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def to_camel(string: str) -> str:
|
||||
|
@ -1,24 +1,24 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from mev_inspect.classifiers.specs import get_classifier
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
ClassifiedTrace,
|
||||
Classification,
|
||||
DecodedCallTrace,
|
||||
)
|
||||
from mev_inspect.schemas.classifiers import SwapClassifier
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
from mev_inspect.schemas.transfers import ERC20Transfer
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.traces import get_traces_by_transaction_hash
|
||||
from mev_inspect.transfers import (
|
||||
build_eth_transfer,
|
||||
get_child_transfers,
|
||||
get_transfer,
|
||||
filter_transfers,
|
||||
remove_child_transfers_of_transfers,
|
||||
)
|
||||
|
||||
|
||||
UNISWAP_V2_PAIR_ABI_NAME = "UniswapV2Pair"
|
||||
UNISWAP_V3_POOL_ABI_NAME = "UniswapV3Pool"
|
||||
BALANCER_V1_POOL_ABI_NAME = "BPool"
|
||||
|
||||
|
||||
def get_swaps(traces: List[ClassifiedTrace]) -> List[Swap]:
|
||||
swaps = []
|
||||
|
||||
@ -32,11 +32,16 @@ def _get_swaps_for_transaction(traces: List[ClassifiedTrace]) -> List[Swap]:
|
||||
ordered_traces = list(sorted(traces, key=lambda t: t.trace_address))
|
||||
|
||||
swaps: List[Swap] = []
|
||||
prior_transfers: List[ERC20Transfer] = []
|
||||
prior_transfers: List[Transfer] = []
|
||||
|
||||
for trace in ordered_traces:
|
||||
if trace.classification == Classification.transfer:
|
||||
prior_transfers.append(ERC20Transfer.from_trace(trace))
|
||||
if not isinstance(trace, DecodedCallTrace):
|
||||
continue
|
||||
|
||||
elif trace.classification == Classification.transfer:
|
||||
transfer = get_transfer(trace)
|
||||
if transfer is not None:
|
||||
prior_transfers.append(transfer)
|
||||
|
||||
elif trace.classification == Classification.swap:
|
||||
child_transfers = get_child_transfers(
|
||||
@ -58,9 +63,9 @@ def _get_swaps_for_transaction(traces: List[ClassifiedTrace]) -> List[Swap]:
|
||||
|
||||
|
||||
def _parse_swap(
|
||||
trace: ClassifiedTrace,
|
||||
prior_transfers: List[ERC20Transfer],
|
||||
child_transfers: List[ERC20Transfer],
|
||||
trace: DecodedCallTrace,
|
||||
prior_transfers: List[Transfer],
|
||||
child_transfers: List[Transfer],
|
||||
) -> Optional[Swap]:
|
||||
pool_address = trace.to_address
|
||||
recipient_address = _get_recipient_address(trace)
|
||||
@ -68,7 +73,13 @@ def _parse_swap(
|
||||
if recipient_address is None:
|
||||
return None
|
||||
|
||||
transfers_to_pool = filter_transfers(prior_transfers, to_address=pool_address)
|
||||
transfers_to_pool = []
|
||||
|
||||
if trace.value is not None and trace.value > 0:
|
||||
transfers_to_pool = [build_eth_transfer(trace)]
|
||||
|
||||
if len(transfers_to_pool) == 0:
|
||||
transfers_to_pool = filter_transfers(prior_transfers, to_address=pool_address)
|
||||
|
||||
if len(transfers_to_pool) == 0:
|
||||
transfers_to_pool = filter_transfers(child_transfers, to_address=pool_address)
|
||||
@ -92,6 +103,7 @@ def _parse_swap(
|
||||
block_number=trace.block_number,
|
||||
trace_address=trace.trace_address,
|
||||
pool_address=pool_address,
|
||||
protocol=trace.protocol,
|
||||
from_address=transfer_in.from_address,
|
||||
to_address=transfer_out.to_address,
|
||||
token_in_address=transfer_in.token_address,
|
||||
@ -102,20 +114,9 @@ def _parse_swap(
|
||||
)
|
||||
|
||||
|
||||
def _get_recipient_address(trace: ClassifiedTrace) -> Optional[str]:
|
||||
if trace.abi_name == UNISWAP_V3_POOL_ABI_NAME:
|
||||
return (
|
||||
trace.inputs["recipient"]
|
||||
if trace.inputs is not None and "recipient" in trace.inputs
|
||||
else trace.from_address
|
||||
)
|
||||
elif trace.abi_name == UNISWAP_V2_PAIR_ABI_NAME:
|
||||
return (
|
||||
trace.inputs["to"]
|
||||
if trace.inputs is not None and "to" in trace.inputs
|
||||
else trace.from_address
|
||||
)
|
||||
elif trace.abi_name == BALANCER_V1_POOL_ABI_NAME:
|
||||
return trace.from_address
|
||||
else:
|
||||
return None
|
||||
def _get_recipient_address(trace: DecodedCallTrace) -> Optional[str]:
|
||||
classifier = get_classifier(trace)
|
||||
if classifier is not None and issubclass(classifier, SwapClassifier):
|
||||
return classifier.get_swap_recipient(trace)
|
||||
|
||||
return None
|
||||
|
@ -1,49 +1,95 @@
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
|
||||
from mev_inspect.schemas.classified_traces import Classification, ClassifiedTrace
|
||||
from mev_inspect.schemas.transfers import ERC20Transfer, EthTransfer, TransferGeneric
|
||||
from mev_inspect.classifiers.specs import get_classifier
|
||||
from mev_inspect.schemas.classifiers import TransferClassifier
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
ClassifiedTrace,
|
||||
DecodedCallTrace,
|
||||
)
|
||||
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer
|
||||
from mev_inspect.traces import is_child_trace_address, get_child_traces
|
||||
|
||||
|
||||
def get_eth_transfers(traces: List[ClassifiedTrace]) -> List[EthTransfer]:
|
||||
def get_transfers(traces: List[ClassifiedTrace]) -> List[Transfer]:
|
||||
transfers = []
|
||||
|
||||
for trace in traces:
|
||||
if trace.value is not None and trace.value > 0:
|
||||
transfers.append(EthTransfer.from_trace(trace))
|
||||
transfer = get_transfer(trace)
|
||||
if transfer is not None:
|
||||
transfers.append(transfer)
|
||||
|
||||
return transfers
|
||||
|
||||
|
||||
def get_transfers(traces: List[ClassifiedTrace]) -> List[ERC20Transfer]:
|
||||
transfers = []
|
||||
def get_eth_transfers(traces: List[ClassifiedTrace]) -> List[Transfer]:
|
||||
transfers = get_transfers(traces)
|
||||
|
||||
for trace in traces:
|
||||
if trace.classification == Classification.transfer:
|
||||
transfers.append(ERC20Transfer.from_trace(trace))
|
||||
return [
|
||||
transfer
|
||||
for transfer in transfers
|
||||
if transfer.token_address == ETH_TOKEN_ADDRESS
|
||||
]
|
||||
|
||||
return transfers
|
||||
|
||||
def get_transfer(trace: ClassifiedTrace) -> Optional[Transfer]:
|
||||
if _is_simple_eth_transfer(trace):
|
||||
return build_eth_transfer(trace)
|
||||
|
||||
if isinstance(trace, DecodedCallTrace):
|
||||
return _build_erc20_transfer(trace)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_simple_eth_transfer(trace: ClassifiedTrace) -> bool:
|
||||
return (
|
||||
trace.value is not None
|
||||
and trace.value > 0
|
||||
and "input" in trace.action
|
||||
and trace.action["input"] == "0x"
|
||||
)
|
||||
|
||||
|
||||
def build_eth_transfer(trace: ClassifiedTrace) -> Transfer:
|
||||
return Transfer(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
amount=trace.value,
|
||||
to_address=trace.to_address,
|
||||
from_address=trace.from_address,
|
||||
token_address=ETH_TOKEN_ADDRESS,
|
||||
)
|
||||
|
||||
|
||||
def _build_erc20_transfer(trace: DecodedCallTrace) -> Optional[Transfer]:
|
||||
classifier = get_classifier(trace)
|
||||
if classifier is not None and issubclass(classifier, TransferClassifier):
|
||||
return classifier.get_transfer(trace)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_child_transfers(
|
||||
transaction_hash: str,
|
||||
parent_trace_address: List[int],
|
||||
traces: List[ClassifiedTrace],
|
||||
) -> List[ERC20Transfer]:
|
||||
) -> List[Transfer]:
|
||||
child_transfers = []
|
||||
|
||||
for child_trace in get_child_traces(transaction_hash, parent_trace_address, traces):
|
||||
if child_trace.classification == Classification.transfer:
|
||||
child_transfers.append(ERC20Transfer.from_trace(child_trace))
|
||||
transfer = get_transfer(child_trace)
|
||||
if transfer is not None:
|
||||
child_transfers.append(transfer)
|
||||
|
||||
return child_transfers
|
||||
|
||||
|
||||
def filter_transfers(
|
||||
transfers: Sequence[TransferGeneric],
|
||||
transfers: Sequence[Transfer],
|
||||
to_address: Optional[str] = None,
|
||||
from_address: Optional[str] = None,
|
||||
) -> List[TransferGeneric]:
|
||||
) -> List[Transfer]:
|
||||
filtered_transfers = []
|
||||
|
||||
for transfer in transfers:
|
||||
@ -59,8 +105,8 @@ def filter_transfers(
|
||||
|
||||
|
||||
def remove_child_transfers_of_transfers(
|
||||
transfers: List[ERC20Transfer],
|
||||
) -> List[ERC20Transfer]:
|
||||
transfers: List[Transfer],
|
||||
) -> List[Transfer]:
|
||||
updated_transfers = []
|
||||
transfer_addresses_by_transaction: Dict[str, List[List[int]]] = {}
|
||||
|
||||
|
1
tests/blocks/13207907.json
Normal file
1
tests/blocks/13207907.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/13234998.json
Normal file
1
tests/blocks/13234998.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/13298725.json
Normal file
1
tests/blocks/13298725.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/13323642.json
Normal file
1
tests/blocks/13323642.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/13326607.json
Normal file
1
tests/blocks/13326607.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/comp_markets.json
Normal file
1
tests/comp_markets.json
Normal file
@ -0,0 +1 @@
|
||||
{"0x6c8c6b02e7b2be14d4fa6022dfd6d75921d90e4e": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", "0x5d3a536e4d6dbd6114cc1ead35777bab948e3643": "0x6b175474e89094c44da98b954eedeac495271d0f", "0x158079ee67fce2f58472a96584a73c7ab9ac95c1": "0x1985365e9f78359a9b6ad760e32412f4a445e862", "0x39aa39c021dfbae8fac545936693ac917d5e7563": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9": "0xdac17f958d2ee523a2206206994597c13d831ec7", "0xc11b1268c1a384e55c48c2391d8d480264a3a7f4": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "0xb3319f5d18bc0d84dd1b4825dcde5d5f7266d407": "0xe41d2489571d322189246dafa5ebde1f4699f498", "0xf5dce57282a584d2746faf1593d3121fcac444dc": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", "0x35a18000230da775cac24873d00ff85bccded550": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x70e36f6bf80a52b3b46b3af8e106cc0ed743e8e4": "0xc00e94cb662c3520282e6f5717214004a7f26888", "0xccf4429db6322d5c611ee964527d42e5d685dd6a": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "0x12392f67bdf24fae0af363c24ac620a2f67dad86": "0x0000000000085d4780b73119b644ae5ecd22b376", "0xface851a4921ce59e912d19329929ce6da6eb0c7": "0x514910771af9ca656af840dff83e8264ecf986ca", "0x95b4ef2869ebd94beb4eee400a99824bf5dc325b": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", "0x4b0181102a0112a2ef11abee5563bb4a3176c9d7": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "0xe65cdb6479bac1e22340e4e755fae7e509ecd06c": "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", "0x80a2ae356fc9ef4305676f7a3e2ed04e12c33946": "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e"}
|
@ -1,11 +1,11 @@
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from mev_inspect.schemas.blocks import TraceType
|
||||
from mev_inspect.schemas.classified_traces import (
|
||||
Classification,
|
||||
ClassifiedTrace,
|
||||
CallTrace,
|
||||
DecodedCallTrace,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ def make_transfer_trace(
|
||||
token_address: str,
|
||||
amount: int,
|
||||
):
|
||||
return CallTrace(
|
||||
return DecodedCallTrace(
|
||||
transaction_hash=transaction_hash,
|
||||
block_number=block_number,
|
||||
type=TraceType.call,
|
||||
@ -26,6 +26,9 @@ def make_transfer_trace(
|
||||
classification=Classification.transfer,
|
||||
from_address=from_address,
|
||||
to_address=token_address,
|
||||
abi_name="ERC20",
|
||||
function_name="transfer",
|
||||
function_signature="transfer(address,uint256)",
|
||||
inputs={
|
||||
"recipient": to_address,
|
||||
"amount": amount,
|
||||
@ -43,6 +46,8 @@ def make_swap_trace(
|
||||
from_address: str,
|
||||
pool_address: str,
|
||||
abi_name: str,
|
||||
function_signature: str,
|
||||
protocol: Optional[Protocol],
|
||||
recipient_address: str,
|
||||
recipient_input_key: str,
|
||||
):
|
||||
@ -56,8 +61,11 @@ def make_swap_trace(
|
||||
classification=Classification.swap,
|
||||
from_address=from_address,
|
||||
to_address=pool_address,
|
||||
function_name="swap",
|
||||
function_signature=function_signature,
|
||||
inputs={recipient_input_key: recipient_address},
|
||||
abi_name=abi_name,
|
||||
protocol=protocol,
|
||||
block_hash=str(block_number),
|
||||
)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
from mev_inspect.arbitrages import get_arbitrages
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
from mev_inspect.swaps import (
|
||||
from mev_inspect.classifiers.specs.uniswap import (
|
||||
UNISWAP_V2_PAIR_ABI_NAME,
|
||||
UNISWAP_V3_POOL_ABI_NAME,
|
||||
)
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
|
||||
|
||||
def test_two_pool_arbitrage(get_transaction_hashes, get_addresses):
|
||||
|
112
tests/test_compound.py
Normal file
112
tests/test_compound.py
Normal file
@ -0,0 +1,112 @@
|
||||
from mev_inspect.compound_liquidations import get_compound_liquidations
|
||||
from mev_inspect.schemas.liquidations import Liquidation
|
||||
from mev_inspect.schemas.classified_traces import Protocol
|
||||
from mev_inspect.classifiers.trace import TraceClassifier
|
||||
from tests.utils import load_test_block, load_comp_markets
|
||||
|
||||
comp_markets = load_comp_markets()
|
||||
|
||||
|
||||
def test_c_ether_liquidations():
|
||||
block_number = 13234998
|
||||
transaction_hash = (
|
||||
"0x78f7e67391c2bacde45e5057241f8b9e21a59330bce4332eecfff8fac279d090"
|
||||
)
|
||||
|
||||
liquidations = [
|
||||
Liquidation(
|
||||
liquidated_user="0xb5535a3681cf8d5431b8acfd779e2f79677ecce9",
|
||||
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
|
||||
collateral_token_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
debt_token_address="0x39aa39c021dfbae8fac545936693ac917d5e7563",
|
||||
debt_purchase_amount=268066492249420078,
|
||||
received_amount=4747650169097,
|
||||
protocol=Protocol.compound_v2,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[1],
|
||||
block_number=block_number,
|
||||
)
|
||||
]
|
||||
block = load_test_block(block_number)
|
||||
trace_classifier = TraceClassifier()
|
||||
classified_traces = trace_classifier.classify(block.traces)
|
||||
result = get_compound_liquidations(classified_traces, comp_markets)
|
||||
assert result == liquidations
|
||||
|
||||
block_number = 13207907
|
||||
transaction_hash = (
|
||||
"0x42a575e3f41d24f3bb00ae96f220a8bd1e24e6a6282c2e0059bb7820c61e91b1"
|
||||
)
|
||||
|
||||
liquidations = [
|
||||
Liquidation(
|
||||
liquidated_user="0x45df6f00166c3fb77dc16b9e47ff57bc6694e898",
|
||||
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
|
||||
collateral_token_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
|
||||
debt_purchase_amount=414547860568297082,
|
||||
received_amount=321973320649,
|
||||
protocol=Protocol.compound_v2,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[1],
|
||||
block_number=block_number,
|
||||
)
|
||||
]
|
||||
|
||||
block = load_test_block(block_number)
|
||||
trace_classifier = TraceClassifier()
|
||||
classified_traces = trace_classifier.classify(block.traces)
|
||||
result = get_compound_liquidations(classified_traces, comp_markets)
|
||||
assert result == liquidations
|
||||
|
||||
block_number = 13298725
|
||||
transaction_hash = (
|
||||
"0x22a98b27a1d2c4f3cba9d65257d18ee961d6c98f21c7eade37da0543847eb654"
|
||||
)
|
||||
|
||||
liquidations = [
|
||||
Liquidation(
|
||||
liquidated_user="0xacbcf5d2970eef25f02a27e9d9cd31027b058b9b",
|
||||
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
|
||||
collateral_token_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
|
||||
debt_purchase_amount=1106497772527562662,
|
||||
received_amount=910895850496,
|
||||
protocol=Protocol.compound_v2,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[1],
|
||||
block_number=block_number,
|
||||
)
|
||||
]
|
||||
block = load_test_block(block_number)
|
||||
trace_classifier = TraceClassifier()
|
||||
classified_traces = trace_classifier.classify(block.traces)
|
||||
result = get_compound_liquidations(classified_traces, comp_markets)
|
||||
assert result == liquidations
|
||||
|
||||
|
||||
def test_c_token_liquidation():
|
||||
block_number = 13326607
|
||||
transaction_hash = (
|
||||
"0x012215bedd00147c58e1f59807664914b2abbfc13c260190dc9cfc490be3e343"
|
||||
)
|
||||
|
||||
liquidations = [
|
||||
Liquidation(
|
||||
liquidated_user="0xacdd5528c1c92b57045041b5278efa06cdade4d8",
|
||||
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
|
||||
collateral_token_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
debt_token_address="0x70e36f6bf80a52b3b46b3af8e106cc0ed743e8e4",
|
||||
debt_purchase_amount=1207055531,
|
||||
received_amount=21459623305,
|
||||
protocol=Protocol.compound_v2,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[1],
|
||||
block_number=block_number,
|
||||
)
|
||||
]
|
||||
block = load_test_block(block_number)
|
||||
trace_classifier = TraceClassifier()
|
||||
classified_traces = trace_classifier.classify(block.traces)
|
||||
result = get_compound_liquidations(classified_traces, comp_markets)
|
||||
assert result == liquidations
|
69
tests/test_decode.py
Normal file
69
tests/test_decode.py
Normal file
@ -0,0 +1,69 @@
|
||||
import pydantic
|
||||
|
||||
from mev_inspect import decode
|
||||
from mev_inspect.schemas import abi
|
||||
|
||||
|
||||
def test_decode_function_with_simple_argument():
|
||||
test_function_name = "testFunction"
|
||||
test_parameter_name = "testParameter"
|
||||
test_abi = pydantic.parse_obj_as(
|
||||
abi.ABI,
|
||||
[
|
||||
{
|
||||
"name": test_function_name,
|
||||
"type": "function",
|
||||
"inputs": [{"name": test_parameter_name, "type": "uint256"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
# 4byte signature of the test function.
|
||||
# https://www.4byte.directory/signatures/?bytes4_signature=0x350c530b
|
||||
test_function_selector = "350c530b"
|
||||
test_function_argument = (
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
)
|
||||
abi_decoder = decode.ABIDecoder(test_abi)
|
||||
call_data = abi_decoder.decode(
|
||||
"0x" + test_function_selector + test_function_argument
|
||||
)
|
||||
assert call_data.function_name == test_function_name
|
||||
assert call_data.function_signature == "testFunction(uint256)"
|
||||
assert call_data.inputs == {test_parameter_name: 1}
|
||||
|
||||
|
||||
def test_decode_function_with_tuple_argument():
|
||||
test_function_name = "testFunction"
|
||||
test_tuple_name = "testTuple"
|
||||
test_parameter_name = "testParameter"
|
||||
test_abi = pydantic.parse_obj_as(
|
||||
abi.ABI,
|
||||
[
|
||||
{
|
||||
"name": test_function_name,
|
||||
"type": "function",
|
||||
"inputs": [
|
||||
{
|
||||
"name": test_tuple_name,
|
||||
"type": "tuple",
|
||||
"components": [
|
||||
{"name": test_parameter_name, "type": "uint256"}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
# 4byte signature of the test function.
|
||||
# https://www.4byte.directory/signatures/?bytes4_signature=0x98568079
|
||||
test_function_selector = "98568079"
|
||||
test_function_argument = (
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
)
|
||||
abi_decoder = decode.ABIDecoder(test_abi)
|
||||
call_data = abi_decoder.decode(
|
||||
"0x" + test_function_selector + test_function_argument
|
||||
)
|
||||
assert call_data.function_name == test_function_name
|
||||
assert call_data.function_signature == "testFunction((uint256))"
|
||||
assert call_data.inputs == {test_tuple_name: (1,)}
|
@ -1,9 +1,10 @@
|
||||
from mev_inspect.swaps import (
|
||||
get_swaps,
|
||||
from mev_inspect.swaps import get_swaps
|
||||
from mev_inspect.classifiers.specs.balancer import BALANCER_V1_POOL_ABI_NAME
|
||||
from mev_inspect.classifiers.specs.uniswap import (
|
||||
UNISWAP_V2_PAIR_ABI_NAME,
|
||||
UNISWAP_V3_POOL_ABI_NAME,
|
||||
BALANCER_V1_POOL_ABI_NAME,
|
||||
)
|
||||
from mev_inspect.schemas.classified_traces import Protocol
|
||||
|
||||
from .helpers import (
|
||||
make_unknown_trace,
|
||||
@ -64,6 +65,8 @@ def test_swaps(
|
||||
from_address=alice_address,
|
||||
pool_address=first_pool_address,
|
||||
abi_name=UNISWAP_V2_PAIR_ABI_NAME,
|
||||
protocol=None,
|
||||
function_signature="swap(uint256,uint256,address,bytes)",
|
||||
recipient_address=bob_address,
|
||||
recipient_input_key="to",
|
||||
),
|
||||
@ -83,6 +86,8 @@ def test_swaps(
|
||||
from_address=bob_address,
|
||||
pool_address=second_pool_address,
|
||||
abi_name=UNISWAP_V3_POOL_ABI_NAME,
|
||||
protocol=None,
|
||||
function_signature="swap(address,bool,int256,uint160,bytes)",
|
||||
recipient_address=carl_address,
|
||||
recipient_input_key="recipient",
|
||||
),
|
||||
@ -129,6 +134,8 @@ def test_swaps(
|
||||
from_address=bob_address,
|
||||
pool_address=third_pool_address,
|
||||
abi_name=BALANCER_V1_POOL_ABI_NAME,
|
||||
protocol=Protocol.balancer_v1,
|
||||
function_signature="swapExactAmountIn(address,uint256,address,uint256,uint256)",
|
||||
recipient_address=bob_address,
|
||||
recipient_input_key="recipient",
|
||||
),
|
||||
@ -178,7 +185,7 @@ def test_swaps(
|
||||
assert bal_v1_swap.transaction_hash == third_transaction_hash
|
||||
assert bal_v1_swap.block_number == block_number
|
||||
assert bal_v1_swap.trace_address == [6]
|
||||
assert bal_v1_swap.protocol is None
|
||||
assert bal_v1_swap.protocol == Protocol.balancer_v1
|
||||
assert bal_v1_swap.pool_address == third_pool_address
|
||||
assert bal_v1_swap.from_address == bob_address
|
||||
assert bal_v1_swap.to_address == bob_address
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mev_inspect.schemas.transfers import ERC20Transfer
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.transfers import remove_child_transfers_of_transfers
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_address
|
||||
third_token_address,
|
||||
] = get_addresses(5)
|
||||
|
||||
outer_transfer = ERC20Transfer(
|
||||
outer_transfer = Transfer(
|
||||
block_number=123,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[0],
|
||||
@ -23,7 +23,7 @@ def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_address
|
||||
token_address=first_token_address,
|
||||
)
|
||||
|
||||
inner_transfer = ERC20Transfer(
|
||||
inner_transfer = Transfer(
|
||||
**{
|
||||
**outer_transfer.dict(),
|
||||
**dict(
|
||||
@ -33,7 +33,7 @@ def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_address
|
||||
}
|
||||
)
|
||||
|
||||
other_transfer = ERC20Transfer(
|
||||
other_transfer = Transfer(
|
||||
block_number=123,
|
||||
transaction_hash=transaction_hash,
|
||||
trace_address=[1],
|
||||
@ -43,7 +43,7 @@ def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_address
|
||||
token_address=third_token_address,
|
||||
)
|
||||
|
||||
separate_transaction_transfer = ERC20Transfer(
|
||||
separate_transaction_transfer = Transfer(
|
||||
**{
|
||||
**inner_transfer.dict(),
|
||||
**dict(transaction_hash=other_transaction_hash),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
from mev_inspect.schemas.blocks import Block
|
||||
|
||||
@ -14,3 +15,10 @@ def load_test_block(block_number: int) -> Block:
|
||||
with open(block_path, "r") as block_file:
|
||||
block_json = json.load(block_file)
|
||||
return Block(**block_json)
|
||||
|
||||
|
||||
def load_comp_markets() -> Dict[str, str]:
|
||||
comp_markets_path = f"{THIS_FILE_DIRECTORY}/comp_markets.json"
|
||||
with open(comp_markets_path, "r") as markets_file:
|
||||
markets = json.load(markets_file)
|
||||
return markets
|
||||
|
Loading…
x
Reference in New Issue
Block a user