Compare commits

..

1 Commits

Author SHA1 Message Date
Luke Van Seters
91cd16533d Add support for 1inch router 2022-01-08 13:36:30 -05:00
41 changed files with 825 additions and 568 deletions

3
.gitignore vendored
View File

@@ -22,6 +22,3 @@ cache
# env
.envrc
# pycharm
.idea

View File

@@ -1,27 +0,0 @@
"""add profit_amount column to sandwiches table
Revision ID: b26ab0051a88
Revises: 3c54832385e3
Create Date: 2022-01-16 13:45:10.190969
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b26ab0051a88"
down_revision = "3c54832385e3"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"sandwiches", sa.Column("profit_token_address", sa.String(256), nullable=True)
)
op.add_column("sandwiches", sa.Column("profit_amount", sa.Numeric, nullable=True))
def downgrade():
op.drop_column("sandwiches", "profit_token_address")
op.drop_column("sandwiches", "profit_amount")

57
backfill.py Normal file
View File

@@ -0,0 +1,57 @@
import subprocess
import sys
from typing import Iterator, Tuple
def get_block_after_before_chunks(
after_block: int,
before_block: int,
n_workers: int,
) -> Iterator[Tuple[int, int]]:
n_blocks = before_block - after_block
remainder = n_blocks % n_workers
floor_chunk_size = n_blocks // n_workers
last_before_block = None
for worker_index in range(n_workers):
chunk_size = floor_chunk_size
if worker_index < remainder:
chunk_size += 1
batch_after_block = (
last_before_block if last_before_block is not None else after_block
)
batch_before_block = batch_after_block + chunk_size
yield batch_after_block, batch_before_block
last_before_block = batch_before_block
def backfill(after_block: int, before_block: int, n_workers: int):
if n_workers <= 0:
raise ValueError("Need at least one worker")
for batch_after_block, batch_before_block in get_block_after_before_chunks(
after_block,
before_block,
n_workers,
):
print(f"Backfilling {batch_after_block} to {batch_before_block}")
backfill_command = f"sh backfill.sh {batch_after_block} {batch_before_block}"
process = subprocess.Popen(backfill_command.split(), stdout=subprocess.PIPE)
output, _ = process.communicate()
print(output)
def main():
after_block = int(sys.argv[1])
before_block = int(sys.argv[2])
n_workers = int(sys.argv[3])
backfill(after_block, before_block, n_workers)
if __name__ == "__main__":
main()

6
backfill.sh Normal file
View File

@@ -0,0 +1,6 @@
current_image=$(kubectl get deployment mev-inspect -o=jsonpath='{$.spec.template.spec.containers[:1].image}')
helm template mev-inspect-backfill ./k8s/mev-inspect-backfill \
--set image.repository=$current_image \
--set command.startBlockNumber=$1 \
--set command.endBlockNumber=$2 | kubectl apply -f -

7
cli.py
View File

@@ -8,7 +8,7 @@ from mev_inspect.concurrency import coro
from mev_inspect.crud.prices import write_prices
from mev_inspect.db import get_inspect_session, get_trace_session
from mev_inspect.inspector import MEVInspector
from mev_inspect.prices import fetch_prices
from mev_inspect.prices import fetch_all_supported_prices
RPC_URL_ENV = "RPC_URL"
@@ -107,11 +107,12 @@ def enqueue_many_blocks_command(after_block: int, before_block: int, batch_size:
@cli.command()
def fetch_all_prices():
@coro
async def fetch_all_prices():
inspect_db_session = get_inspect_session()
logger.info("Fetching prices")
prices = fetch_prices()
prices = await fetch_all_supported_prices()
logger.info("Writing prices")
write_prices(inspect_db_session, prices)

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: mev-inspect-backfill
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "mev-inspect-backfill.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "mev-inspect-backfill.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mev-inspect-backfill.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "mev-inspect-backfill.labels" -}}
helm.sh/chart: {{ include "mev-inspect-backfill.chart" . }}
{{ include "mev-inspect-backfill.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "mev-inspect-backfill.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mev-inspect-backfill.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "mev-inspect-backfill.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mev-inspect-backfill.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,68 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "mev-inspect-backfill.fullname" . }}-{{ randAlphaNum 5 | lower }}
labels:
{{- include "mev-inspect-backfill.labels" . | nindent 4 }}
spec:
completions: 1
parallelism: 1
ttlSecondsAfterFinished: 5
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- run
- inspect-many-blocks
- {{ .Values.command.startBlockNumber | quote }}
- {{ .Values.command.endBlockNumber | quote }}
env:
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: mev-inspect-db-credentials
key: host
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: mev-inspect-db-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: mev-inspect-db-credentials
key: password
- name: TRACE_DB_HOST
valueFrom:
secretKeyRef:
name: trace-db-credentials
key: host
optional: true
- name: TRACE_DB_USER
valueFrom:
secretKeyRef:
name: trace-db-credentials
key: username
optional: true
- name: TRACE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: trace-db-credentials
key: password
optional: true
- name: RPC_URL
valueFrom:
configMapKeyRef:
name: mev-inspect-rpc
key: url
restartPolicy: OnFailure

View File

@@ -0,0 +1,42 @@
# Default values for mev-inspect.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
repository: mev-inspect-py
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -17,14 +17,13 @@ podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
# runAsNonRoot: true
# runAsUser: 1000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious

View File

@@ -17,15 +17,13 @@ podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- all
#readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious

View File

@@ -0,0 +1,99 @@
from typing import List, Optional, Tuple
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import (
Classification,
ClassifiedTrace,
DecodedCallTrace,
Protocol,
)
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.traces import get_child_traces, is_child_of_any_address
from mev_inspect.transfers import get_transfer
def get_aave_liquidations(
traces: List[ClassifiedTrace],
) -> List[Liquidation]:
"""Inspect list of classified traces and identify liquidation"""
liquidations: List[Liquidation] = []
parent_liquidations: List[List[int]] = []
for trace in traces:
if (
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)
liquidator = trace.from_address
child_traces = get_child_traces(
trace.transaction_hash, trace.trace_address, traces
)
(debt_token_address, debt_purchase_amount) = _get_debt_data(
child_traces, liquidator
)
(received_token_address, received_amount) = _get_received_data(
child_traces, liquidator
)
liquidations.append(
Liquidation(
liquidated_user=trace.inputs["_user"],
debt_token_address=debt_token_address,
liquidator_user=liquidator,
debt_purchase_amount=debt_purchase_amount,
protocol=Protocol.aave,
received_amount=received_amount,
received_token_address=received_token_address,
transaction_hash=trace.transaction_hash,
trace_address=trace.trace_address,
block_number=trace.block_number,
error=trace.error,
)
)
return liquidations
def _get_received_data(
child_traces: List[ClassifiedTrace], liquidator: str
) -> Tuple[str, int]:
"""Look for and return liquidator payback from liquidation"""
for child in child_traces:
child_transfer: Optional[Transfer] = get_transfer(child)
if child_transfer is not None:
if child_transfer.to_address == liquidator:
return child_transfer.token_address, child_transfer.amount
raise RuntimeError("Transfer from AAVE to liquidator not found!")
def _get_debt_data(
child_traces: List[ClassifiedTrace], liquidator: str
) -> Tuple[str, int]:
"""Get transfer from liquidator to AAVE"""
for child in child_traces:
child_transfer: Optional[Transfer] = get_transfer(child)
if child_transfer is not None:
if child_transfer.from_address == liquidator:
return child_transfer.token_address, child_transfer.amount
raise RuntimeError("Transfer from liquidator to AAVE not found!")

View File

@@ -0,0 +1 @@
[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"reason","type":"string"}],"name":"Error","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"contract IERC20","name":"srcToken","type":"address"},{"indexed":false,"internalType":"contract IERC20","name":"dstToken","type":"address"},{"indexed":false,"internalType":"address","name":"dstReceiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"spentAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"returnAmount","type":"uint256"}],"name":"Swapped","type":"event"},{"inputs":[],"name":"destroy","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IAggregationExecutor","name":"caller","type":"address"},{"components":[{"internalType":"contract IERC20","name":"srcToken","type":"address"},{"internalType":"contract IERC20","name":"dstToken","type":"address"},{"internalType":"address","name":"srcReceiver","type":"address"},{"internalType":"address","name":"dstReceiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"minReturnAmount","type":"uint256"},{"internalType":"uint256","name":"flags","type":"uint256"},{"internalType":"bytes","name":"permit","type":"bytes"}],"internalType":"struct AggregationRouterV3.SwapDescription","name":"desc","type":"tuple"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"discountedSwap","outputs":[{"internalType":"uint256","name":"returnAmount","type":"uint256"},{"internalType":"uint256","name":"gasLeft","type":"uint256"},{"internalType":"uint256","name":"chiSpent","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"rescueFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IAggregationExecutor","name":"caller","type":"address"},{"components":[{"internalType":"contract IERC20","name":"srcToken","type":"address"},{"internalType":"contract IERC20","name":"dstToken","type":"address"},{"internalType":"address","name":"srcReceiver","type":"address"},{"internalType":"address","name":"dstReceiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"minReturnAmount","type":"uint256"},{"internalType":"uint256","name":"flags","type":"uint256"},{"internalType":"bytes","name":"permit","type":"bytes"}],"internalType":"struct AggregationRouterV3.SwapDescription","name":"desc","type":"tuple"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"swap","outputs":[{"internalType":"uint256","name":"returnAmount","type":"uint256"},{"internalType":"uint256","name":"gasLeft","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"srcToken","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"minReturn","type":"uint256"},{"internalType":"bytes32[]","name":"","type":"bytes32[]"}],"name":"unoswap","outputs":[{"internalType":"uint256","name":"returnAmount","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"srcToken","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"minReturn","type":"uint256"},{"internalType":"bytes32[]","name":"pools","type":"bytes32[]"},{"internalType":"bytes","name":"permit","type":"bytes"}],"name":"unoswapWithPermit","outputs":[{"internalType":"uint256","name":"returnAmount","type":"uint256"}],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]

View File

@@ -3,7 +3,6 @@ from typing import List, Optional, Tuple
from mev_inspect.schemas.arbitrages import Arbitrage
from mev_inspect.schemas.swaps import Swap
from mev_inspect.utils import equal_within_percent
MAX_TOKEN_AMOUNT_PERCENT_DIFFERENCE = 0.01
@@ -176,16 +175,16 @@ def _get_all_start_end_swaps(swaps: List[Swap]) -> List[Tuple[Swap, List[Swap]]]
def _swap_outs_match_swap_ins(swap_out, swap_in) -> bool:
return (
swap_out.token_out_address == swap_in.token_in_address
and (
swap_out.contract_address == swap_in.from_address
or swap_out.to_address == swap_in.contract_address
or swap_out.to_address == swap_in.from_address
if swap_out.token_out_address == swap_in.token_in_address and (
swap_out.contract_address == swap_in.from_address
or swap_out.to_address == swap_in.contract_address
or swap_out.to_address == swap_in.from_address
):
amount_percent_difference = abs(
(float(swap_out.token_out_amount) / swap_in.token_in_amount) - 1.0
)
and equal_within_percent(
swap_out.token_out_amount,
swap_in.token_in_amount,
MAX_TOKEN_AMOUNT_PERCENT_DIFFERENCE,
)
)
if amount_percent_difference < MAX_TOKEN_AMOUNT_PERCENT_DIFFERENCE:
return True
return False

View File

@@ -1,10 +1,9 @@
from typing import List, Optional, Sequence
from mev_inspect.schemas.nft_trades import NftTrade
from mev_inspect.schemas.prices import ETH_TOKEN_ADDRESS
from mev_inspect.schemas.swaps import Swap
from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer
def create_nft_trade_from_transfers(
@@ -179,27 +178,3 @@ def _filter_transfers(
filtered_transfers.append(transfer)
return filtered_transfers
def get_received_transfer(
liquidator: str, child_transfers: List[Transfer]
) -> Optional[Transfer]:
"""Get transfer from AAVE to liquidator"""
for transfer in child_transfers:
if transfer.to_address == liquidator:
return transfer
return None
def get_debt_transfer(
liquidator: str, child_transfers: List[Transfer]
) -> Optional[Transfer]:
"""Get transfer from liquidator to AAVE"""
for transfer in child_transfers:
if transfer.from_address == liquidator:
return transfer
return None

View File

@@ -10,9 +10,10 @@ from .compound import COMPOUND_CLASSIFIER_SPECS
from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS
from .curve import CURVE_CLASSIFIER_SPECS
from .erc20 import ERC20_CLASSIFIER_SPECS
from .one_inch import ONE_INCH_CLASSIFIER_SPECS
from .opensea import OPENSEA_CLASSIFIER_SPECS
from .uniswap import UNISWAP_CLASSIFIER_SPECS
from .weth import WETH_CLASSIFIER_SPECS
from .weth import WETH_ADDRESS, WETH_CLASSIFIER_SPECS
from .zero_ex import ZEROX_CLASSIFIER_SPECS
ALL_CLASSIFIER_SPECS = (
@@ -27,6 +28,7 @@ ALL_CLASSIFIER_SPECS = (
+ CRYPTOPUNKS_CLASSIFIER_SPECS
+ OPENSEA_CLASSIFIER_SPECS
+ BANCOR_CLASSIFIER_SPECS
+ ONE_INCH_CLASSIFIER_SPECS
)
_SPECS_BY_ABI_NAME_AND_PROTOCOL: Dict[

View File

@@ -1,65 +1,13 @@
from typing import List, Optional
from mev_inspect.classifiers.helpers import get_debt_transfer, get_received_transfer
from mev_inspect.schemas.classifiers import (
ClassifiedTrace,
ClassifierSpec,
DecodedCallTrace,
LiquidationClassifier,
TransferClassifier,
)
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import Protocol
from mev_inspect.schemas.transfers import Transfer
class AaveLiquidationClassifier(LiquidationClassifier):
@staticmethod
def parse_liquidation(
liquidation_trace: DecodedCallTrace,
child_transfers: List[Transfer],
child_traces: List[ClassifiedTrace],
) -> Optional[Liquidation]:
liquidator = liquidation_trace.from_address
liquidated = liquidation_trace.inputs["_user"]
debt_token_address = liquidation_trace.inputs["_reserve"]
received_token_address = liquidation_trace.inputs["_collateral"]
debt_purchase_amount = None
received_amount = None
debt_transfer = get_debt_transfer(liquidator, child_transfers)
received_transfer = get_received_transfer(liquidator, child_transfers)
if debt_transfer is not None and received_transfer is not None:
debt_token_address = debt_transfer.token_address
debt_purchase_amount = debt_transfer.amount
received_token_address = received_transfer.token_address
received_amount = received_transfer.amount
return Liquidation(
liquidated_user=liquidated,
debt_token_address=debt_token_address,
liquidator_user=liquidator,
debt_purchase_amount=debt_purchase_amount,
protocol=Protocol.aave,
received_amount=received_amount,
received_token_address=received_token_address,
transaction_hash=liquidation_trace.transaction_hash,
trace_address=liquidation_trace.trace_address,
block_number=liquidation_trace.block_number,
error=liquidation_trace.error,
)
else:
return None
class AaveTransferClassifier(TransferClassifier):
@staticmethod
def get_transfer(trace: DecodedCallTrace) -> Transfer:
@@ -78,7 +26,7 @@ AAVE_SPEC = ClassifierSpec(
abi_name="AaveLendingPool",
protocol=Protocol.aave,
classifiers={
"liquidationCall(address,address,address,uint256,bool)": AaveLiquidationClassifier,
"liquidationCall(address,address,address,uint256,bool)": LiquidationClassifier,
},
)
@@ -87,7 +35,8 @@ ATOKENS_SPEC = ClassifierSpec(
protocol=Protocol.aave,
classifiers={
"transferOnLiquidation(address,address,uint256)": AaveTransferClassifier,
"transferFrom(address,address,uint256)": AaveTransferClassifier,
},
)
AAVE_CLASSIFIER_SPECS: List[ClassifierSpec] = [AAVE_SPEC, ATOKENS_SPEC]
AAVE_CLASSIFIER_SPECS = [AAVE_SPEC, ATOKENS_SPEC]

View File

@@ -1,86 +1,16 @@
from typing import List, Optional
from mev_inspect.classifiers.helpers import get_debt_transfer, get_received_transfer
from mev_inspect.schemas.classifiers import (
Classification,
ClassifiedTrace,
ClassifierSpec,
DecodedCallTrace,
LiquidationClassifier,
SeizeClassifier,
)
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.prices import CETH_TOKEN_ADDRESS
from mev_inspect.schemas.traces import Protocol
from mev_inspect.schemas.transfers import Transfer
class CompoundLiquidationClassifier(LiquidationClassifier):
@staticmethod
def parse_liquidation(
liquidation_trace: DecodedCallTrace,
child_transfers: List[Transfer],
child_traces: List[ClassifiedTrace],
) -> Optional[Liquidation]:
liquidator = liquidation_trace.from_address
liquidated = liquidation_trace.inputs["borrower"]
debt_token_address = liquidation_trace.to_address
received_token_address = liquidation_trace.inputs["cTokenCollateral"]
debt_purchase_amount = None
received_amount = None
debt_purchase_amount = (
liquidation_trace.value
if debt_token_address == CETH_TOKEN_ADDRESS and liquidation_trace.value != 0
else liquidation_trace.inputs["repayAmount"]
)
debt_transfer = get_debt_transfer(liquidator, child_transfers)
received_transfer = get_received_transfer(liquidator, child_transfers)
seize_trace = _get_seize_call(child_traces)
if debt_transfer is not None:
debt_token_address = debt_transfer.token_address
debt_purchase_amount = debt_transfer.amount
if received_transfer is not None:
received_token_address = received_transfer.token_address
received_amount = received_transfer.amount
elif seize_trace is not None and seize_trace.inputs is not None:
received_amount = seize_trace.inputs["seizeTokens"]
if received_amount is None:
return None
return Liquidation(
liquidated_user=liquidated,
debt_token_address=debt_token_address,
liquidator_user=liquidator,
debt_purchase_amount=debt_purchase_amount,
protocol=liquidation_trace.protocol,
received_amount=received_amount,
received_token_address=received_token_address,
transaction_hash=liquidation_trace.transaction_hash,
trace_address=liquidation_trace.trace_address,
block_number=liquidation_trace.block_number,
error=liquidation_trace.error,
)
return None
COMPOUND_V2_CETH_SPEC = ClassifierSpec(
abi_name="CEther",
protocol=Protocol.compound_v2,
valid_contract_addresses=["0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5"],
classifiers={
"liquidateBorrow(address,address)": CompoundLiquidationClassifier,
"liquidateBorrow(address,address)": LiquidationClassifier,
"seize(address,address,uint256)": SeizeClassifier,
},
)
@@ -90,7 +20,7 @@ CREAM_CETH_SPEC = ClassifierSpec(
protocol=Protocol.cream,
valid_contract_addresses=["0xD06527D5e56A3495252A528C4987003b712860eE"],
classifiers={
"liquidateBorrow(address,address)": CompoundLiquidationClassifier,
"liquidateBorrow(address,address)": LiquidationClassifier,
"seize(address,address,uint256)": SeizeClassifier,
},
)
@@ -118,7 +48,7 @@ COMPOUND_V2_CTOKEN_SPEC = ClassifierSpec(
"0x80a2ae356fc9ef4305676f7a3e2ed04e12c33946",
],
classifiers={
"liquidateBorrow(address,uint256,address)": CompoundLiquidationClassifier,
"liquidateBorrow(address,uint256,address)": LiquidationClassifier,
"seize(address,address,uint256)": SeizeClassifier,
},
)
@@ -220,22 +150,14 @@ CREAM_CTOKEN_SPEC = ClassifierSpec(
"0x58da9c9fc3eb30abbcbbab5ddabb1e6e2ef3d2ef",
],
classifiers={
"liquidateBorrow(address,uint256,address)": CompoundLiquidationClassifier,
"liquidateBorrow(address,uint256,address)": LiquidationClassifier,
"seize(address,address,uint256)": SeizeClassifier,
},
)
COMPOUND_CLASSIFIER_SPECS: List[ClassifierSpec] = [
COMPOUND_CLASSIFIER_SPECS = [
COMPOUND_V2_CETH_SPEC,
COMPOUND_V2_CTOKEN_SPEC,
CREAM_CETH_SPEC,
CREAM_CTOKEN_SPEC,
]
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

View File

@@ -0,0 +1,60 @@
from typing import List, Optional
from mev_inspect.schemas.classifiers import ClassifierSpec, SwapClassifier
from mev_inspect.schemas.swaps import Swap
from mev_inspect.schemas.traces import DecodedCallTrace, Protocol
from mev_inspect.schemas.transfers import Transfer
class OneInchSwapClassifier(SwapClassifier):
@staticmethod
def parse_swap(
trace: DecodedCallTrace,
prior_transfers: List[Transfer],
child_transfers: List[Transfer],
) -> Optional[Swap]:
if trace.error is not None:
return None
desc = trace.inputs["desc"]
[srcToken, dstToken, srcReceiver, dstReceiver, amountIn, *_] = desc
transfer_out_candidates = [
transfer
for transfer in child_transfers
if (
transfer.token_address == dstToken
and transfer.to_address == dstReceiver
)
]
if len(transfer_out_candidates) == 0:
raise RuntimeError("1inch expected at least one transfer out")
return Swap(
abi_name=trace.abi_name,
transaction_hash=trace.transaction_hash,
transaction_position=trace.transaction_position,
block_number=trace.block_number,
trace_address=trace.trace_address,
contract_address=trace.to_address,
protocol=trace.protocol,
from_address=srcReceiver,
to_address=dstReceiver,
token_in_address=srcToken,
token_in_amount=amountIn,
token_out_address=dstToken,
token_out_amount=transfer_out_candidates[0].amount,
error=trace.error,
)
ONE_INCH_ROUTER_SPEC = ClassifierSpec(
abi_name="AggregationRouterV3",
protocol=Protocol.one_inch,
classifiers={
"swap(address,(address,address,address,address,uint256,uint256,uint256,bytes),bytes)": OneInchSwapClassifier,
},
)
ONE_INCH_CLASSIFIER_SPECS = [ONE_INCH_ROUTER_SPEC]

View File

@@ -3,7 +3,6 @@ from mev_inspect.schemas.classifiers import (
DecodedCallTrace,
TransferClassifier,
)
from mev_inspect.schemas.prices import WETH_TOKEN_ADDRESS
from mev_inspect.schemas.traces import Protocol
from mev_inspect.schemas.transfers import Transfer
@@ -22,10 +21,12 @@ class WethTransferClassifier(TransferClassifier):
)
WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
WETH_SPEC = ClassifierSpec(
abi_name="WETH9",
protocol=Protocol.weth,
valid_contract_addresses=[WETH_TOKEN_ADDRESS],
valid_contract_addresses=[WETH_ADDRESS],
classifiers={
"transferFrom(address,address,uint256)": WethTransferClassifier,
"transfer(address,uint256)": WethTransferClassifier,

46
mev_inspect/coinbase.py Normal file
View File

@@ -0,0 +1,46 @@
import aiohttp
from mev_inspect.classifiers.specs.weth import WETH_ADDRESS
from mev_inspect.schemas.coinbase import CoinbasePrices, CoinbasePricesResponse
from mev_inspect.schemas.prices import (
AAVE_TOKEN_ADDRESS,
CDAI_TOKEN_ADDRESS,
CUSDC_TOKEN_ADDRESS,
DAI_TOKEN_ADDRESS,
LINK_TOKEN_ADDRESS,
REN_TOKEN_ADDRESS,
UNI_TOKEN_ADDRESS,
USDC_TOKEN_ADDRESS,
WBTC_TOKEN_ADDRESS,
YEARN_TOKEN_ADDRESS,
)
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS
COINBASE_API_BASE = "https://www.coinbase.com/api/v2"
COINBASE_TOKEN_NAME_BY_ADDRESS = {
WETH_ADDRESS: "weth",
ETH_TOKEN_ADDRESS: "ethereum",
WBTC_TOKEN_ADDRESS: "wrapped-bitcoin",
LINK_TOKEN_ADDRESS: "link",
YEARN_TOKEN_ADDRESS: "yearn-finance",
AAVE_TOKEN_ADDRESS: "aave",
UNI_TOKEN_ADDRESS: "uniswap",
USDC_TOKEN_ADDRESS: "usdc",
DAI_TOKEN_ADDRESS: "dai",
REN_TOKEN_ADDRESS: "ren",
CUSDC_TOKEN_ADDRESS: "compound-usd-coin",
CDAI_TOKEN_ADDRESS: "compound-dai",
}
async def fetch_coinbase_prices(token_address: str) -> CoinbasePrices:
if token_address not in COINBASE_TOKEN_NAME_BY_ADDRESS:
raise ValueError(f"Unsupported token_address {token_address}")
coinbase_token_name = COINBASE_TOKEN_NAME_BY_ADDRESS[token_address]
url = f"{COINBASE_API_BASE}/assets/prices/{coinbase_token_name}"
async with aiohttp.ClientSession() as session:
async with session.get(url, params={"base": "USD"}) as response:
json_data = await response.json()
return CoinbasePricesResponse(**json_data).data.prices

View File

@@ -0,0 +1,80 @@
from typing import List, Optional
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import Classification, ClassifiedTrace, Protocol
from mev_inspect.traces import get_child_traces
V2_COMPTROLLER_ADDRESS = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"
V2_C_ETHER = "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5"
CREAM_COMPTROLLER_ADDRESS = "0x3d5BC3c8d13dcB8bF317092d84783c2697AE9258"
CREAM_CR_ETHER = "0xD06527D5e56A3495252A528C4987003b712860eE"
def get_compound_liquidations(
traces: List[ClassifiedTrace],
) -> 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
or trace.protocol == Protocol.cream
)
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"],
debt_token_address=c_token_collateral,
liquidator_user=seize_trace.inputs["liquidator"],
debt_purchase_amount=trace.value,
protocol=trace.protocol,
received_amount=seize_trace.inputs["seizeTokens"],
received_token_address=trace.to_address,
transaction_hash=trace.transaction_hash,
trace_address=trace.trace_address,
block_number=trace.block_number,
error=trace.error,
)
)
elif (
trace.abi_name == "CToken"
): # cToken liquidations where liquidator pays back via token transfer
liquidations.append(
Liquidation(
liquidated_user=trace.inputs["borrower"],
debt_token_address=c_token_collateral,
liquidator_user=seize_trace.inputs["liquidator"],
debt_purchase_amount=trace.inputs["repayAmount"],
protocol=trace.protocol,
received_amount=seize_trace.inputs["seizeTokens"],
received_token_address=trace.to_address,
transaction_hash=trace.transaction_hash,
trace_address=trace.trace_address,
block_number=trace.block_number,
error=trace.error,
)
)
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

View File

@@ -39,8 +39,6 @@ def write_sandwiches(
frontrun_swap_trace_address=sandwich.frontrun_swap.trace_address,
backrun_swap_transaction_hash=sandwich.backrun_swap.transaction_hash,
backrun_swap_trace_address=sandwich.backrun_swap.trace_address,
profit_token_address=sandwich.profit_token_address,
profit_amount=sandwich.profit_amount,
)
)

View File

@@ -1,12 +1,9 @@
from typing import List, Optional
from typing import List
from mev_inspect.classifiers.specs import get_classifier
from mev_inspect.schemas.classifiers import LiquidationClassifier
from mev_inspect.aave_liquidations import get_aave_liquidations
from mev_inspect.compound_liquidations import get_compound_liquidations
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import Classification, ClassifiedTrace, DecodedCallTrace
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.traces import get_child_traces, is_child_trace_address
from mev_inspect.transfers import get_child_transfers
from mev_inspect.schemas.traces import Classification, ClassifiedTrace
def has_liquidations(classified_traces: List[ClassifiedTrace]) -> bool:
@@ -17,58 +14,9 @@ def has_liquidations(classified_traces: List[ClassifiedTrace]) -> bool:
return liquidations_exist
def get_liquidations(classified_traces: List[ClassifiedTrace]) -> List[Liquidation]:
liquidations: List[Liquidation] = []
parent_liquidations: List[DecodedCallTrace] = []
for trace in classified_traces:
if not isinstance(trace, DecodedCallTrace):
continue
if _is_child_liquidation(trace, parent_liquidations):
continue
if trace.classification == Classification.liquidate:
parent_liquidations.append(trace)
child_traces = get_child_traces(
trace.transaction_hash, trace.trace_address, classified_traces
)
child_transfers = get_child_transfers(
trace.transaction_hash, trace.trace_address, child_traces
)
liquidation = _parse_liquidation(trace, child_traces, child_transfers)
if liquidation is not None:
liquidations.append(liquidation)
return liquidations
def _parse_liquidation(
trace: DecodedCallTrace,
child_traces: List[ClassifiedTrace],
child_transfers: List[Transfer],
) -> Optional[Liquidation]:
classifier = get_classifier(trace)
if classifier is not None and issubclass(classifier, LiquidationClassifier):
return classifier.parse_liquidation(trace, child_transfers, child_traces)
return None
def _is_child_liquidation(
trace: DecodedCallTrace, parent_liquidations: List[DecodedCallTrace]
) -> bool:
for parent in parent_liquidations:
if (
trace.transaction_hash == parent.transaction_hash
and is_child_trace_address(trace.trace_address, parent.trace_address)
):
return True
return False
def get_liquidations(
classified_traces: List[ClassifiedTrace],
) -> List[Liquidation]:
aave_liquidations = get_aave_liquidations(classified_traces)
comp_liquidations = get_compound_liquidations(classified_traces)
return aave_liquidations + comp_liquidations

View File

@@ -14,5 +14,3 @@ class SandwichModel(Base):
frontrun_swap_trace_address = Column(ARRAY(Integer), nullable=False)
backrun_swap_transaction_hash = Column(String(256), nullable=False)
backrun_swap_trace_address = Column(ARRAY(Integer), nullable=False)
profit_token_address = Column(String(256), nullable=False)
profit_amount = Column(Numeric, nullable=False)

View File

@@ -1,33 +1,44 @@
from datetime import datetime as dt
from typing import List
from pycoingecko import CoinGeckoAPI
from mev_inspect.classifiers.specs.weth import WETH_ADDRESS
from mev_inspect.coinbase import fetch_coinbase_prices
from mev_inspect.schemas.prices import (
AAVE_TOKEN_ADDRESS,
LINK_TOKEN_ADDRESS,
REN_TOKEN_ADDRESS,
UNI_TOKEN_ADDRESS,
USDC_TOKEN_ADDRESS,
WBTC_TOKEN_ADDRESS,
YEARN_TOKEN_ADDRESS,
Price,
)
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS
from mev_inspect.schemas.prices import COINGECKO_ID_BY_ADDRESS, TOKEN_ADDRESSES, Price
SUPPORTED_TOKENS = [
WETH_ADDRESS,
ETH_TOKEN_ADDRESS,
LINK_TOKEN_ADDRESS,
AAVE_TOKEN_ADDRESS,
USDC_TOKEN_ADDRESS,
REN_TOKEN_ADDRESS,
WBTC_TOKEN_ADDRESS,
YEARN_TOKEN_ADDRESS,
UNI_TOKEN_ADDRESS,
]
def fetch_prices() -> List[Price]:
cg = CoinGeckoAPI()
async def fetch_all_supported_prices() -> List[Price]:
prices = []
for token_address in TOKEN_ADDRESSES:
price_data = cg.get_coin_market_chart_by_id(
id=COINGECKO_ID_BY_ADDRESS[token_address],
vs_currency="usd",
days="max",
interval="daily",
)
price_time_series = price_data["prices"]
for entry in price_time_series:
timestamp = dt.fromtimestamp(entry[0] / 100)
token_price = entry[1]
prices.append(
Price(
timestamp=timestamp,
usd_price=token_price,
token_address=token_address,
)
for token_address in SUPPORTED_TOKENS:
coinbase_prices = await fetch_coinbase_prices(token_address)
for usd_price, timestamp_seconds in coinbase_prices.all.prices:
price = Price(
token_address=token_address,
usd_price=usd_price,
timestamp=timestamp_seconds,
)
prices.append(price)
return prices

View File

@@ -2,9 +2,6 @@ from typing import List, Optional
from mev_inspect.schemas.sandwiches import Sandwich
from mev_inspect.schemas.swaps import Swap
from mev_inspect.utils import equal_within_percent
SANDWICH_IN_OUT_MAX_PERCENT_DIFFERENCE = 0.01
def get_sandwiches(swaps: List[Swap]) -> List[Sandwich]:
@@ -31,7 +28,8 @@ def _get_sandwich_starting_with_swap(
front_swap: Swap,
rest_swaps: List[Swap],
) -> Optional[Sandwich]:
sandwicher_address = front_swap.to_address
sandwicher_address = front_swap.from_address
sandwiched_swaps = []
for other_swap in rest_swaps:
@@ -48,12 +46,7 @@ def _get_sandwich_starting_with_swap(
elif (
other_swap.token_out_address == front_swap.token_in_address
and other_swap.token_in_address == front_swap.token_out_address
and equal_within_percent(
other_swap.token_in_amount,
front_swap.token_out_amount,
SANDWICH_IN_OUT_MAX_PERCENT_DIFFERENCE,
)
and other_swap.from_address == sandwicher_address
and other_swap.to_address == sandwicher_address
):
if len(sandwiched_swaps) > 0:
return Sandwich(
@@ -62,9 +55,6 @@ def _get_sandwich_starting_with_swap(
frontrun_swap=front_swap,
backrun_swap=other_swap,
sandwiched_swaps=sandwiched_swaps,
profit_token_address=front_swap.token_in_address,
profit_amount=other_swap.token_out_amount
- front_swap.token_in_amount,
)
return None

View File

@@ -3,10 +3,9 @@ from typing import Dict, List, Optional, Type
from pydantic import BaseModel
from .liquidations import Liquidation
from .nft_trades import NftTrade
from .swaps import Swap
from .traces import Classification, ClassifiedTrace, DecodedCallTrace, Protocol
from .traces import Classification, DecodedCallTrace, Protocol
from .transfers import Transfer
@@ -48,15 +47,6 @@ class LiquidationClassifier(Classifier):
def get_classification() -> Classification:
return Classification.liquidate
@staticmethod
@abstractmethod
def parse_liquidation(
liquidation_trace: DecodedCallTrace,
child_transfers: List[Transfer],
child_traces: List[ClassifiedTrace],
) -> Optional[Liquidation]:
raise NotImplementedError()
class SeizeClassifier(Classifier):
@staticmethod

View File

@@ -0,0 +1,20 @@
from typing import List, Tuple
from pydantic import BaseModel
class CoinbasePricesEntry(BaseModel):
# tuple of price and timestamp
prices: List[Tuple[float, int]]
class CoinbasePrices(BaseModel):
all: CoinbasePricesEntry
class CoinbasePricesDataResponse(BaseModel):
prices: CoinbasePrices
class CoinbasePricesResponse(BaseModel):
data: CoinbasePricesDataResponse

View File

@@ -2,8 +2,6 @@ from datetime import datetime
from pydantic import BaseModel, validator
ETH_TOKEN_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
WETH_TOKEN_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
WBTC_TOKEN_ADDRESS = "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599"
LINK_TOKEN_ADDRESS = "0x514910771af9ca656af840dff83e8264ecf986ca"
YEARN_TOKEN_ADDRESS = "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e"
@@ -14,48 +12,12 @@ DAI_TOKEN_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f"
REN_TOKEN_ADDRESS = "0x408e41876cccdc0f92210600ef50372656052a38"
CUSDC_TOKEN_ADDRESS = "0x39aa39c021dfbae8fac545936693ac917d5e7563"
CDAI_TOKEN_ADDRESS = "0x5d3a536e4d6dbd6114cc1ead35777bab948e3643"
CETH_TOKEN_ADDRESS = "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5"
CWBTC_TOKEN_ADDRESS = "0xc11b1268c1a384e55c48c2391d8d480264a3a7f4"
TOKEN_ADDRESSES = [
ETH_TOKEN_ADDRESS,
WETH_TOKEN_ADDRESS,
WBTC_TOKEN_ADDRESS,
LINK_TOKEN_ADDRESS,
YEARN_TOKEN_ADDRESS,
AAVE_TOKEN_ADDRESS,
UNI_TOKEN_ADDRESS,
USDC_TOKEN_ADDRESS,
DAI_TOKEN_ADDRESS,
REN_TOKEN_ADDRESS,
CUSDC_TOKEN_ADDRESS,
CDAI_TOKEN_ADDRESS,
CETH_TOKEN_ADDRESS,
CWBTC_TOKEN_ADDRESS,
]
COINGECKO_ID_BY_ADDRESS = {
WETH_TOKEN_ADDRESS: "weth",
ETH_TOKEN_ADDRESS: "ethereum",
WBTC_TOKEN_ADDRESS: "wrapped-bitcoin",
LINK_TOKEN_ADDRESS: "chainlink",
YEARN_TOKEN_ADDRESS: "yearn-finance",
AAVE_TOKEN_ADDRESS: "aave",
UNI_TOKEN_ADDRESS: "uniswap",
USDC_TOKEN_ADDRESS: "usd-coin",
DAI_TOKEN_ADDRESS: "dai",
REN_TOKEN_ADDRESS: "republic-protocol",
CUSDC_TOKEN_ADDRESS: "compound-usd-coin",
CDAI_TOKEN_ADDRESS: "cdai",
CETH_TOKEN_ADDRESS: "compound-ether",
CWBTC_TOKEN_ADDRESS: "compound-wrapped-btc",
}
class Price(BaseModel):
token_address: str
usd_price: float
timestamp: datetime
usd_price: float
@validator("token_address")
def lower_token_address(cls, v: str) -> str:

View File

@@ -11,5 +11,3 @@ class Sandwich(BaseModel):
frontrun_swap: Swap
backrun_swap: Swap
sandwiched_swaps: List[Swap]
profit_token_address: str
profit_amount: int

View File

@@ -50,6 +50,7 @@ class Protocol(Enum):
cryptopunks = "cryptopunks"
bancor = "bancor"
opensea = "opensea"
one_inch = "1inch"
class ClassifiedTrace(Trace):

View File

@@ -2,6 +2,8 @@ from typing import List
from pydantic import BaseModel
ETH_TOKEN_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
class Transfer(BaseModel):
block_number: int

View File

@@ -2,9 +2,8 @@ from typing import Dict, List, Optional, Sequence
from mev_inspect.classifiers.specs import get_classifier
from mev_inspect.schemas.classifiers import TransferClassifier
from mev_inspect.schemas.prices import ETH_TOKEN_ADDRESS
from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS, Transfer
from mev_inspect.traces import get_child_traces, is_child_trace_address

View File

@@ -3,12 +3,3 @@ from hexbytes._utils import hexstr_to_bytes
def hex_to_int(value: str) -> int:
return int.from_bytes(hexstr_to_bytes(value), byteorder="big")
def equal_within_percent(
first_value: int, second_value: int, threshold_percent: float
) -> bool:
difference = abs(
(first_value - second_value) / (0.5 * (first_value + second_value))
)
return difference < threshold_percent

17
poetry.lock generated
View File

@@ -730,17 +730,6 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycoingecko"
version = "2.2.0"
description = "Python wrapper around the CoinGecko API"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
requests = "*"
[[package]]
name = "pycryptodome"
version = "3.10.1"
@@ -1127,7 +1116,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "955c3df01b275e9b4807190e468a2df4d3d18b6a45a7c1659599ef476b35be51"
content-hash = "2ce3bdeb2d8bd31210026e5054a54c67fc766cdf22dc83485eca425643cdf760"
[metadata.files]
aiohttp = [
@@ -1786,10 +1775,6 @@ py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pycoingecko = [
{file = "pycoingecko-2.2.0-py3-none-any.whl", hash = "sha256:3646968c8c6936ca4e94b5f562328a763c12a0e9644141cb0215089dda59fe01"},
{file = "pycoingecko-2.2.0.tar.gz", hash = "sha256:9add73085729b1f10f93c7948490b09e8cd47c29bebe47dccb319e8b49502d0c"},
]
pycryptodome = [
{file = "pycryptodome-3.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06"},
{file = "pycryptodome-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c"},

View File

@@ -13,7 +13,6 @@ click = "^8.0.1"
psycopg2 = "^2.9.1"
aiohttp = "^3.8.0"
dramatiq = {extras = ["redis"], version = "^1.12.1"}
pycoingecko = "^2.2.0"
[tool.poetry.dev-dependencies]
pre-commit = "^2.13.0"

View File

@@ -1,10 +1,10 @@
from typing import List
from mev_inspect.aave_liquidations import get_aave_liquidations
from mev_inspect.classifiers.trace import TraceClassifier
from mev_inspect.liquidations import get_liquidations
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.prices import ETH_TOKEN_ADDRESS
from mev_inspect.schemas.traces import Protocol
from mev_inspect.transfers import ETH_TOKEN_ADDRESS
from tests.utils import load_test_block
@@ -31,10 +31,9 @@ def test_single_weth_liquidation(trace_classifier: TraceClassifier):
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
result = get_aave_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
_assert_equal_list_of_liquidations(result, liquidations)
def test_single_liquidation(trace_classifier: TraceClassifier):
@@ -60,10 +59,9 @@ def test_single_liquidation(trace_classifier: TraceClassifier):
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
result = get_aave_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
_assert_equal_list_of_liquidations(result, liquidations)
def test_single_liquidation_with_atoken_payback(trace_classifier: TraceClassifier):
@@ -89,10 +87,9 @@ def test_single_liquidation_with_atoken_payback(trace_classifier: TraceClassifie
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
result = get_aave_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
_assert_equal_list_of_liquidations(result, liquidations)
def test_multiple_liquidations_in_block(trace_classifier: TraceClassifier):
@@ -142,11 +139,10 @@ def test_multiple_liquidations_in_block(trace_classifier: TraceClassifier):
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
result = get_aave_liquidations(classified_traces)
liquidations = [liquidation1, liquidation2, liquidation3]
for liquidation in liquidations:
assert liquidation in result
_assert_equal_list_of_liquidations(result, liquidations)
def test_liquidations_with_eth_transfer(trace_classifier: TraceClassifier):
@@ -183,11 +179,10 @@ def test_liquidations_with_eth_transfer(trace_classifier: TraceClassifier):
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
result = get_aave_liquidations(classified_traces)
liquidations = [liquidation1, liquidation2]
for liquidation in liquidations:
assert liquidation in result
_assert_equal_list_of_liquidations(result, liquidations)
def _assert_equal_list_of_liquidations(

View File

@@ -1,120 +1,116 @@
[
{
[{
"block_number": 12775690,
"sandwicher_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"frontrun_swap": {
"abi_name": "UniswapV2Pair",
"transaction_hash": "0x91a3abe5f3b806426542252820ba0ab6d56c098fdef6864ecaf4d352f64217a0",
"transaction_position": 2,
"block_number": 12775690,
"sandwicher_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"frontrun_swap": {
"trace_address": [
0,
2
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"to_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 12108789017249529876,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 1114969767487478978357,
"protocol": null,
"error": null
},
"backrun_swap": {
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xc300d1ff79d3901b58dc56489fc7d083a6c13d422bfc1425a0579379300c95a2",
"transaction_position": 7,
"block_number": 12775690,
"trace_address": [
0,
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"to_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"token_in_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_in_amount": 1114969767487478978357,
"token_out_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_out_amount": 12158780499164852150,
"protocol": null,
"error": null
},
"sandwiched_swaps": [
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0x91a3abe5f3b806426542252820ba0ab6d56c098fdef6864ecaf4d352f64217a0",
"transaction_position": 2,
"transaction_hash": "0x9b40deca1f53593b7631ca25485d0c6faf90279b9872845acfd5c98afb185934",
"transaction_position": 3,
"block_number": 12775690,
"trace_address": [
0,
2
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"to_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 12108789017249529876,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 1114969767487478978357,
"protocol": null,
"error": null
},
"backrun_swap": {
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xc300d1ff79d3901b58dc56489fc7d083a6c13d422bfc1425a0579379300c95a2",
"transaction_position": 7,
"block_number": 12775690,
"trace_address": [
0,
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"to_address": "0x000000000027d2efc283613d0c3e24a8b430c4d8",
"token_in_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_in_amount": 1114969767487478978357,
"token_out_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_out_amount": 12158780499164852150,
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0x37e17e96736aee2ca809abd91e0f8744910ca19a",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 652974555369106606,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 60000000000000000000,
"protocol": null,
"error": null
},
"sandwiched_swaps": [
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0x9b40deca1f53593b7631ca25485d0c6faf90279b9872845acfd5c98afb185934",
"transaction_position": 3,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0x37e17e96736aee2ca809abd91e0f8744910ca19a",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 652974555369106606,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 60000000000000000000,
"protocol": null,
"error": null
},
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xf8e45a291cdab5e456375e4d7df30771670d504835c9332b32114e5bc4e315f9",
"transaction_position": 4,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xd3b7ddf9eb72837f0ee3d1d30dec0e45fbdf79b1",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 300000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 27561865602394087181,
"protocol": null,
"error": null
},
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xdf63b22773b66cc41e00fd42c3b3c7f42912f87476ffe6d821e3f5c00284f00b",
"transaction_position": 5,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xcf99e104fdc46bea618d85ac5250067f19a56e41",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 125000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 11483313070817976324,
"protocol": null,
"error": null
},
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0x1fe35f66e24f12bdb54a0d35934aac809c783710d998621b70116ea9f95f4f4f",
"transaction_position": 6,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xd7c9f3010efdff665ee72580ffa7b4141e56b17e",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 30000000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 2742522049933966038599,
"protocol": null,
"error": null
}
],
"profit_token_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"profit_amount": 49991481915322274
}
]
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xf8e45a291cdab5e456375e4d7df30771670d504835c9332b32114e5bc4e315f9",
"transaction_position": 4,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xd3b7ddf9eb72837f0ee3d1d30dec0e45fbdf79b1",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 300000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 27561865602394087181,
"protocol": null,
"error": null
},
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0xdf63b22773b66cc41e00fd42c3b3c7f42912f87476ffe6d821e3f5c00284f00b",
"transaction_position": 5,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xcf99e104fdc46bea618d85ac5250067f19a56e41",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 125000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 11483313070817976324,
"protocol": null,
"error": null
},
{
"abi_name": "UniswapV2Pair",
"transaction_hash": "0x1fe35f66e24f12bdb54a0d35934aac809c783710d998621b70116ea9f95f4f4f",
"transaction_position": 6,
"block_number": 12775690,
"trace_address": [
3
],
"contract_address": "0xefb47fcfcad4f96c83d4ca676842fb03ef20a477",
"from_address": "0x03f7724180aa6b939894b5ca4314783b0b36b329",
"to_address": "0xd7c9f3010efdff665ee72580ffa7b4141e56b17e",
"token_in_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"token_in_amount": 30000000000000000000,
"token_out_address": "0x9813037ee2218799597d83d4a5b6f3b6778218d9",
"token_out_amount": 2742522049933966038599,
"protocol": null,
"error": null
}
]
}]

View File

@@ -1,5 +1,5 @@
from mev_inspect.classifiers.trace import TraceClassifier
from mev_inspect.liquidations import get_liquidations
from mev_inspect.compound_liquidations import get_compound_liquidations
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import Protocol
from tests.utils import load_comp_markets, load_cream_markets, load_test_block
@@ -18,10 +18,10 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
Liquidation(
liquidated_user="0xb5535a3681cf8d5431b8acfd779e2f79677ecce9",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
debt_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
debt_token_address="0x39aa39c021dfbae8fac545936693ac917d5e7563",
debt_purchase_amount=268066492249420078,
received_amount=4747650169097,
received_token_address="0x39aa39c021dfbae8fac545936693ac917d5e7563",
received_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
protocol=Protocol.compound_v2,
transaction_hash=transaction_hash,
trace_address=[1],
@@ -30,10 +30,8 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
]
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
result = get_compound_liquidations(classified_traces)
assert result == liquidations
block_number = 13207907
transaction_hash = (
@@ -44,10 +42,10 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
Liquidation(
liquidated_user="0x45df6f00166c3fb77dc16b9e47ff57bc6694e898",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
debt_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
debt_purchase_amount=414547860568297082,
received_amount=321973320649,
received_token_address="0x35a18000230da775cac24873d00ff85bccded550",
received_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
protocol=Protocol.compound_v2,
transaction_hash=transaction_hash,
trace_address=[1],
@@ -57,10 +55,8 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
result = get_compound_liquidations(classified_traces)
assert result == liquidations
block_number = 13298725
transaction_hash = (
@@ -71,10 +67,10 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
Liquidation(
liquidated_user="0xacbcf5d2970eef25f02a27e9d9cd31027b058b9b",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
debt_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
debt_purchase_amount=1106497772527562662,
received_amount=910895850496,
received_token_address="0x35a18000230da775cac24873d00ff85bccded550",
received_token_address="0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5",
protocol=Protocol.compound_v2,
transaction_hash=transaction_hash,
trace_address=[1],
@@ -83,10 +79,8 @@ def test_c_ether_liquidations(trace_classifier: TraceClassifier):
]
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
result = get_compound_liquidations(classified_traces)
assert result == liquidations
def test_c_token_liquidation(trace_classifier: TraceClassifier):
@@ -99,10 +93,10 @@ def test_c_token_liquidation(trace_classifier: TraceClassifier):
Liquidation(
liquidated_user="0xacdd5528c1c92b57045041b5278efa06cdade4d8",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
debt_token_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
debt_token_address="0x70e36f6bf80a52b3b46b3af8e106cc0ed743e8e4",
debt_purchase_amount=1207055531,
received_amount=21459623305,
received_token_address="0x70e36f6bf80a52b3b46b3af8e106cc0ed743e8e4",
received_token_address="0x39aa39c021dfbae8fac545936693ac917d5e7563",
protocol=Protocol.compound_v2,
transaction_hash=transaction_hash,
trace_address=[1],
@@ -111,10 +105,8 @@ def test_c_token_liquidation(trace_classifier: TraceClassifier):
]
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
result = get_compound_liquidations(classified_traces)
assert result == liquidations
def test_cream_token_liquidation(trace_classifier: TraceClassifier):
@@ -127,10 +119,10 @@ def test_cream_token_liquidation(trace_classifier: TraceClassifier):
Liquidation(
liquidated_user="0x46bf9479dc569bc796b7050344845f6564d45fba",
liquidator_user="0xa2863cad9c318669660eb4eca8b3154b90fb4357",
debt_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_token_address="0x44fbebd2f576670a6c33f6fc0b00aa8c5753b322",
debt_purchase_amount=14857434973806369550,
received_amount=1547215810826,
received_token_address="0x44fbebd2f576670a6c33f6fc0b00aa8c5753b322",
received_token_address="0x697256caa3ccafd62bb6d3aa1c7c5671786a5fd9",
protocol=Protocol.cream,
transaction_hash=transaction_hash,
trace_address=[],
@@ -139,7 +131,5 @@ def test_cream_token_liquidation(trace_classifier: TraceClassifier):
]
block = load_test_block(block_number)
classified_traces = trace_classifier.classify(block.traces)
result = get_liquidations(classified_traces)
for liquidation in liquidations:
assert liquidation in result
result = get_compound_liquidations(classified_traces)
assert result == liquidations