Compare commits
1 Commits
liquidatio
...
add-1inch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91cd16533d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,6 +22,3 @@ cache
|
||||
|
||||
# env
|
||||
.envrc
|
||||
|
||||
# pycharm
|
||||
.idea
|
||||
|
||||
@@ -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
57
backfill.py
Normal 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
6
backfill.sh
Normal 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
7
cli.py
@@ -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)
|
||||
|
||||
23
k8s/mev-inspect-backfill/.helmignore
Normal file
23
k8s/mev-inspect-backfill/.helmignore
Normal 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/
|
||||
24
k8s/mev-inspect-backfill/Chart.yaml
Normal file
24
k8s/mev-inspect-backfill/Chart.yaml
Normal 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"
|
||||
62
k8s/mev-inspect-backfill/templates/_helpers.tpl
Normal file
62
k8s/mev-inspect-backfill/templates/_helpers.tpl
Normal 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 }}
|
||||
68
k8s/mev-inspect-backfill/templates/job.yaml
Normal file
68
k8s/mev-inspect-backfill/templates/job.yaml
Normal 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
|
||||
42
k8s/mev-inspect-backfill/values.yaml
Normal file
42
k8s/mev-inspect-backfill/values.yaml
Normal 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: {}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
99
mev_inspect/aave_liquidations.py
Normal file
99
mev_inspect/aave_liquidations.py
Normal 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!")
|
||||
1
mev_inspect/abis/1inch/AggregationRouterV3.json
Normal file
1
mev_inspect/abis/1inch/AggregationRouterV3.json
Normal 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"}]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
60
mev_inspect/classifiers/specs/one_inch.py
Normal file
60
mev_inspect/classifiers/specs/one_inch.py
Normal 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]
|
||||
@@ -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
46
mev_inspect/coinbase.py
Normal 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
|
||||
80
mev_inspect/compound_liquidations.py
Normal file
80
mev_inspect/compound_liquidations.py
Normal 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
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
20
mev_inspect/schemas/coinbase.py
Normal file
20
mev_inspect/schemas/coinbase.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -11,5 +11,3 @@ class Sandwich(BaseModel):
|
||||
frontrun_swap: Swap
|
||||
backrun_swap: Swap
|
||||
sandwiched_swaps: List[Swap]
|
||||
profit_token_address: str
|
||||
profit_amount: int
|
||||
|
||||
@@ -50,6 +50,7 @@ class Protocol(Enum):
|
||||
cryptopunks = "cryptopunks"
|
||||
bancor = "bancor"
|
||||
opensea = "opensea"
|
||||
one_inch = "1inch"
|
||||
|
||||
|
||||
class ClassifiedTrace(Trace):
|
||||
|
||||
@@ -2,6 +2,8 @@ from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
ETH_TOKEN_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||
|
||||
|
||||
class Transfer(BaseModel):
|
||||
block_number: int
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
17
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user