Merge branch 'main' into geth_additions

This commit is contained in:
Supragya Raj 2021-12-09 17:49:22 +05:30 committed by GitHub
commit 6b6dd452f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1244 additions and 93 deletions

View File

@ -18,4 +18,5 @@ COPY . /app
# easter eggs 😝
RUN echo "PS1='🕵️:\[\033[1;36m\]\h \[\033[1;34m\]\W\[\033[0;35m\]\[\033[1;36m\]$ \[\033[0m\]'" >> ~/.bashrc
ENTRYPOINT [ "/app/entrypoint.sh"]
ENTRYPOINT [ "poetry" ]
CMD [ "run", "python", "loop.py" ]

View File

@ -1,5 +1,4 @@
load("ext://helm_remote", "helm_remote")
load("ext://restart_process", "docker_build_with_restart")
load("ext://secret", "secret_from_dict")
load("ext://configmap", "configmap_from_dict")
@ -30,8 +29,7 @@ k8s_yaml(secret_from_dict("mev-inspect-db-credentials", inputs = {
# "host": "trace-db-postgresql",
# }))
docker_build_with_restart("mev-inspect-py", ".",
entrypoint="/app/entrypoint.sh",
docker_build("mev-inspect-py", ".",
live_update=[
sync(".", "/app"),
run("cd /app && poetry install",
@ -41,6 +39,10 @@ docker_build_with_restart("mev-inspect-py", ".",
k8s_yaml(helm('./k8s/mev-inspect', name='mev-inspect'))
k8s_resource(workload="mev-inspect", resource_deps=["postgresql-postgresql"])
# uncomment to enable price monitor
# k8s_yaml(helm('./k8s/mev-inspect-prices', name='mev-inspect-prices'))
# k8s_resource(workload="mev-inspect-prices", resource_deps=["postgresql-postgresql"])
local_resource(
'pg-port-forward',
serve_cmd='kubectl port-forward --namespace default svc/postgresql 5432:5432',

View File

@ -0,0 +1,36 @@
"""Change blocks.timestamp to timestamp
Revision ID: 04b76ab1d2af
Revises: 2c90b2b8a80b
Create Date: 2021-11-26 15:31:21.111693
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "04b76ab1d2af"
down_revision = "0cef835f7b36"
branch_labels = None
depends_on = None
def upgrade():
op.alter_column(
"blocks",
"block_timestamp",
type_=sa.TIMESTAMP,
nullable=False,
postgresql_using="TO_TIMESTAMP(block_timestamp)",
)
def downgrade():
op.alter_column(
"blocks",
"block_timestamp",
type_=sa.Numeric,
nullable=False,
postgresql_using="extract(epoch FROM block_timestamp)",
)

View File

@ -0,0 +1,35 @@
"""empty message
Revision ID: 070819d86587
Revises: d498bdb0a641
Create Date: 2021-11-26 18:25:13.402822
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "d498bdb0a641"
down_revision = "205ce02374b3"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"punk_snipes",
sa.Column("created_at", sa.TIMESTAMP, server_default=sa.func.now()),
sa.Column("block_number", sa.Numeric, nullable=False),
sa.Column("transaction_hash", sa.String(66), nullable=False),
sa.Column("trace_address", sa.String(256), nullable=False),
sa.Column("from_address", sa.String(256), nullable=False),
sa.Column("punk_index", sa.Numeric, nullable=False),
sa.Column("min_acceptance_price", sa.Numeric, nullable=False),
sa.Column("acceptance_price", sa.Numeric, nullable=False),
sa.PrimaryKeyConstraint("block_number", "transaction_hash", "trace_address"),
)
def downgrade():
op.drop_table("punk_snipes")

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: 52d75a7e0533
Revises: 7cf0eeb41da0
Create Date: 2021-11-26 20:35:58.954138
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "52d75a7e0533"
down_revision = "7cf0eeb41da0"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"punk_bid_acceptances",
sa.Column("created_at", sa.TIMESTAMP, server_default=sa.func.now()),
sa.Column("block_number", sa.Numeric, nullable=False),
sa.Column("transaction_hash", sa.String(66), nullable=False),
sa.Column("trace_address", sa.String(256), nullable=False),
sa.Column("from_address", sa.String(256), nullable=False),
sa.Column("punk_index", sa.Numeric, nullable=False),
sa.Column("min_price", sa.Numeric, nullable=False),
sa.PrimaryKeyConstraint("block_number", "transaction_hash", "trace_address"),
)
def downgrade():
op.drop_table("punk_bid_acceptances")

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: 7cf0eeb41da0
Revises: d498bdb0a641
Create Date: 2021-11-26 20:27:28.936516
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "7cf0eeb41da0"
down_revision = "d498bdb0a641"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"punk_bids",
sa.Column("created_at", sa.TIMESTAMP, server_default=sa.func.now()),
sa.Column("block_number", sa.Numeric, nullable=False),
sa.Column("transaction_hash", sa.String(66), nullable=False),
sa.Column("trace_address", sa.String(256), nullable=False),
sa.Column("from_address", sa.String(256), nullable=False),
sa.Column("punk_index", sa.Numeric, nullable=False),
sa.Column("price", sa.Numeric, nullable=False),
sa.PrimaryKeyConstraint("block_number", "transaction_hash", "trace_address"),
)
def downgrade():
op.drop_table("punk_bids")

View File

@ -0,0 +1,27 @@
"""Remove collateral_token_address column
Revision ID: b9fa1ecc9929
Revises: 04b76ab1d2af
Create Date: 2021-12-01 23:32:40.574108
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b9fa1ecc9929"
down_revision = "04b76ab1d2af"
branch_labels = None
depends_on = None
def upgrade():
op.drop_column("liquidations", "collateral_token_address")
def downgrade():
op.add_column(
"liquidations",
sa.Column("collateral_token_address", sa.String(256), nullable=False),
)

15
cli.py
View File

@ -5,13 +5,16 @@ import sys
import click
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.utils import RPCType
from mev_inspect.prices import fetch_all_supported_prices
RPC_URL_ENV = "RPC_URL"
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logger = logging.getLogger(__name__)
@click.group()
@ -101,6 +104,18 @@ async def inspect_many_blocks_command(
)
@cli.command()
@coro
async def fetch_all_prices():
inspect_db_session = get_inspect_session()
logger.info("Fetching prices")
prices = await fetch_all_supported_prices()
logger.info("Writing prices")
write_prices(inspect_db_session, prices)
def get_rpc_url() -> str:
return os.environ["RPC_URL"]

View File

@ -1,3 +0,0 @@
#!/bin/bash
python loop.py

View File

@ -21,8 +21,7 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- poetry
args:
- run
- inspect-many-blocks
- {{ .Values.command.startBlockNumber | quote }}

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-prices
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-prices.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-prices.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-prices.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "mev-inspect-prices.labels" -}}
helm.sh/chart: {{ include "mev-inspect-prices.chart" . }}
{{ include "mev-inspect-prices.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-prices.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mev-inspect-prices.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "mev-inspect-prices.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mev-inspect-prices.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,35 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "mev-inspect-prices.fullname" . }}
spec:
schedule: "0 */1 * * *"
successfulJobsHistoryLimit: 0
jobTemplate:
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- run
- fetch-all-prices
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
restartPolicy: Never

View File

@ -0,0 +1,7 @@
image:
repository: mev-inspect-py
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

View File

@ -30,6 +30,7 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["run", "python", "loop.py"]
livenessProbe:
exec:
command:

13
mev
View File

@ -56,6 +56,19 @@ case "$1" in
echo "Fetching block $block_number"
kubectl exec -ti deploy/mev-inspect -- poetry run fetch-block $block_number
;;
prices)
shift
case "$1" in
fetch-all)
echo "Running price fetch-all"
kubectl exec -ti deploy/mev-inspect -- \
poetry run fetch-all-prices
;;
*)
echo "prices usage: "$1" {fetch-all}"
exit 1
esac
;;
exec)
shift
kubectl exec -ti deploy/mev-inspect -- $@

View File

@ -66,7 +66,6 @@ def get_aave_liquidations(
liquidations.append(
Liquidation(
liquidated_user=trace.inputs["_user"],
collateral_token_address=trace.inputs["_collateral"],
debt_token_address=trace.inputs["_reserve"],
liquidator_user=liquidator,
debt_purchase_amount=trace.inputs["_purchaseAmount"],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -88,8 +88,9 @@ def _get_all_start_end_swaps(swaps: List[Swap]) -> List[Tuple[Swap, Swap]]:
"""
pool_addrs = [swap.contract_address for swap in swaps]
valid_start_ends: List[Tuple[Swap, Swap]] = []
for potential_start_swap in swaps:
for potential_end_swap in swaps:
for index, potential_start_swap in enumerate(swaps):
remaining_swaps = swaps[:index] + swaps[index + 1 :]
for potential_end_swap in remaining_swaps:
if (
potential_start_swap.token_in_address
== potential_end_swap.token_out_address

View File

@ -6,7 +6,7 @@ from mev_inspect.schemas.transfers import Transfer, ETH_TOKEN_ADDRESS
from mev_inspect.schemas.traces import DecodedCallTrace, ClassifiedTrace
def create_swap_from_transfers(
def create_swap_from_pool_transfers(
trace: DecodedCallTrace,
recipient_address: str,
prior_transfers: List[Transfer],
@ -55,6 +55,43 @@ def create_swap_from_transfers(
)
def create_swap_from_recipient_transfers(
trace: DecodedCallTrace,
pool_address: str,
recipient_address: str,
prior_transfers: List[Transfer],
child_transfers: List[Transfer],
) -> Optional[Swap]:
transfers_from_recipient = _filter_transfers(
[*prior_transfers, *child_transfers], from_address=recipient_address
)
transfers_to_recipient = _filter_transfers(
child_transfers, to_address=recipient_address
)
if len(transfers_from_recipient) != 1 or len(transfers_to_recipient) != 1:
return None
transfer_in = transfers_from_recipient[0]
transfer_out = transfers_to_recipient[0]
return Swap(
abi_name=trace.abi_name,
transaction_hash=trace.transaction_hash,
block_number=trace.block_number,
trace_address=trace.trace_address,
contract_address=pool_address,
protocol=trace.protocol,
from_address=transfer_in.from_address,
to_address=transfer_out.to_address,
token_in_address=transfer_in.token_address,
token_in_amount=transfer_in.amount,
token_out_address=transfer_out.token_address,
token_out_amount=transfer_out.amount,
error=trace.error,
)
def _build_eth_transfer(trace: ClassifiedTrace) -> Transfer:
return Transfer(
block_number=trace.block_number,

View File

@ -11,6 +11,8 @@ from .weth import WETH_CLASSIFIER_SPECS, WETH_ADDRESS
from .zero_ex import ZEROX_CLASSIFIER_SPECS
from .balancer import BALANCER_CLASSIFIER_SPECS
from .compound import COMPOUND_CLASSIFIER_SPECS
from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS
from .bancor import BANCOR_CLASSIFIER_SPECS
ALL_CLASSIFIER_SPECS = (
ERC20_CLASSIFIER_SPECS
@ -21,6 +23,8 @@ ALL_CLASSIFIER_SPECS = (
+ ZEROX_CLASSIFIER_SPECS
+ BALANCER_CLASSIFIER_SPECS
+ COMPOUND_CLASSIFIER_SPECS
+ CRYPTOPUNKS_CLASSIFIER_SPECS
+ BANCOR_CLASSIFIER_SPECS
)
_SPECS_BY_ABI_NAME_AND_PROTOCOL: Dict[

View File

@ -9,7 +9,7 @@ from mev_inspect.schemas.classifiers import (
ClassifierSpec,
SwapClassifier,
)
from mev_inspect.classifiers.helpers import create_swap_from_transfers
from mev_inspect.classifiers.helpers import create_swap_from_pool_transfers
BALANCER_V1_POOL_ABI_NAME = "BPool"
@ -24,7 +24,7 @@ class BalancerSwapClassifier(SwapClassifier):
recipient_address = trace.from_address
swap = create_swap_from_transfers(
swap = create_swap_from_pool_transfers(
trace, recipient_address, prior_transfers, child_transfers
)
return swap

View File

@ -0,0 +1,48 @@
from typing import Optional, List
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.schemas.swaps import Swap
from mev_inspect.schemas.traces import (
DecodedCallTrace,
Protocol,
)
from mev_inspect.schemas.classifiers import (
ClassifierSpec,
SwapClassifier,
)
from mev_inspect.classifiers.helpers import (
create_swap_from_recipient_transfers,
)
BANCOR_NETWORK_ABI_NAME = "BancorNetwork"
BANCOR_NETWORK_CONTRACT_ADDRESS = "0x2F9EC37d6CcFFf1caB21733BdaDEdE11c823cCB0"
class BancorSwapClassifier(SwapClassifier):
@staticmethod
def parse_swap(
trace: DecodedCallTrace,
prior_transfers: List[Transfer],
child_transfers: List[Transfer],
) -> Optional[Swap]:
recipient_address = trace.from_address
swap = create_swap_from_recipient_transfers(
trace,
BANCOR_NETWORK_CONTRACT_ADDRESS,
recipient_address,
prior_transfers,
child_transfers,
)
return swap
BANCOR_NETWORK_SPEC = ClassifierSpec(
abi_name=BANCOR_NETWORK_ABI_NAME,
protocol=Protocol.bancor,
classifiers={
"convertByPath(address[],uint256,uint256,address,address,uint256)": BancorSwapClassifier,
},
valid_contract_addresses=[BANCOR_NETWORK_CONTRACT_ADDRESS],
)
BANCOR_CLASSIFIER_SPECS = [BANCOR_NETWORK_SPEC]

View File

@ -0,0 +1,31 @@
from mev_inspect.schemas.traces import Protocol, Classification
from mev_inspect.schemas.classifiers import (
ClassifierSpec,
Classifier,
)
class PunkBidAcceptanceClassifier(Classifier):
@staticmethod
def get_classification() -> Classification:
return Classification.punk_accept_bid
class PunkBidClassifier(Classifier):
@staticmethod
def get_classification() -> Classification:
return Classification.punk_bid
CRYPTO_PUNKS_SPEC = ClassifierSpec(
abi_name="cryptopunks",
protocol=Protocol.cryptopunks,
valid_contract_addresses=["0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB"],
classifiers={
"enterBidForPunk(uint256)": PunkBidClassifier,
"acceptBidForPunk(uint256,uint256)": PunkBidAcceptanceClassifier,
},
)
CRYPTOPUNKS_CLASSIFIER_SPECS = [CRYPTO_PUNKS_SPEC]

View File

@ -10,7 +10,7 @@ from mev_inspect.schemas.classifiers import (
ClassifierSpec,
SwapClassifier,
)
from mev_inspect.classifiers.helpers import create_swap_from_transfers
from mev_inspect.classifiers.helpers import create_swap_from_pool_transfers
class CurveSwapClassifier(SwapClassifier):
@ -23,7 +23,7 @@ class CurveSwapClassifier(SwapClassifier):
recipient_address = trace.from_address
swap = create_swap_from_transfers(
swap = create_swap_from_pool_transfers(
trace, recipient_address, prior_transfers, child_transfers
)
return swap

View File

@ -9,7 +9,7 @@ from mev_inspect.schemas.classifiers import (
ClassifierSpec,
SwapClassifier,
)
from mev_inspect.classifiers.helpers import create_swap_from_transfers
from mev_inspect.classifiers.helpers import create_swap_from_pool_transfers
UNISWAP_V2_PAIR_ABI_NAME = "UniswapV2Pair"
@ -26,7 +26,7 @@ class UniswapV3SwapClassifier(SwapClassifier):
recipient_address = trace.inputs.get("recipient", trace.from_address)
swap = create_swap_from_transfers(
swap = create_swap_from_pool_transfers(
trace, recipient_address, prior_transfers, child_transfers
)
return swap
@ -42,7 +42,7 @@ class UniswapV2SwapClassifier(SwapClassifier):
recipient_address = trace.inputs.get("to", trace.from_address)
swap = create_swap_from_transfers(
swap = create_swap_from_pool_transfers(
trace, recipient_address, prior_transfers, child_transfers
)
return swap

View File

@ -1,10 +1,59 @@
from typing import Optional, List, Tuple
from mev_inspect.schemas.transfers import Transfer
from mev_inspect.schemas.swaps import Swap
from mev_inspect.schemas.traces import (
DecodedCallTrace,
Protocol,
)
from mev_inspect.schemas.classifiers import (
ClassifierSpec,
SwapClassifier,
)
ANY_TAKER_ADDRESS = "0x0000000000000000000000000000000000000000"
RFQ_SIGNATURES = [
"fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)",
"_fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128,address,bool,address)",
]
LIMIT_SIGNATURES = [
"fillOrKillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)",
"fillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)",
"_fillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128,address,address)",
]
class ZeroExSwapClassifier(SwapClassifier):
@staticmethod
def parse_swap(
trace: DecodedCallTrace,
prior_transfers: List[Transfer],
child_transfers: List[Transfer],
) -> Optional[Swap]:
token_in_address, token_in_amount = _get_0x_token_in_data(
trace, child_transfers
)
token_out_address, token_out_amount = _get_0x_token_out_data(trace)
return Swap(
abi_name=trace.abi_name,
transaction_hash=trace.transaction_hash,
block_number=trace.block_number,
trace_address=trace.trace_address,
contract_address=trace.to_address,
protocol=Protocol.zero_ex,
from_address=trace.from_address,
to_address=trace.to_address,
token_in_address=token_in_address,
token_in_amount=token_in_amount,
token_out_address=token_out_address,
token_out_amount=token_out_amount,
error=trace.error,
)
ZEROX_CONTRACT_SPECS = [
ClassifierSpec(
abi_name="exchangeProxy",
@ -121,6 +170,14 @@ ZEROX_GENERIC_SPECS = [
ClassifierSpec(
abi_name="INativeOrdersFeature",
protocol=Protocol.zero_ex,
valid_contract_addresses=["0xdef1c0ded9bec7f1a1670819833240f027b25eff"],
classifiers={
"fillOrKillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)": ZeroExSwapClassifier,
"fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)": ZeroExSwapClassifier,
"fillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128)": ZeroExSwapClassifier,
"_fillRfqOrder((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128,address,bool,address)": ZeroExSwapClassifier,
"_fillLimitOrder((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256),(uint8,uint8,bytes32,bytes32),uint128,address,address)": ZeroExSwapClassifier,
},
),
ClassifierSpec(
abi_name="IOtcOrdersFeature",
@ -165,3 +222,57 @@ ZEROX_GENERIC_SPECS = [
]
ZEROX_CLASSIFIER_SPECS = ZEROX_CONTRACT_SPECS + ZEROX_GENERIC_SPECS
def _get_taker_token_in_amount(
taker_address: str, token_in_address: str, child_transfers: List[Transfer]
) -> int:
if len(child_transfers) != 2:
raise ValueError(
f"A settled order should consist of 2 child transfers, not {len(child_transfers)}."
)
if taker_address == ANY_TAKER_ADDRESS:
for transfer in child_transfers:
if transfer.token_address == token_in_address:
return transfer.amount
else:
for transfer in child_transfers:
if transfer.to_address == taker_address:
return transfer.amount
return 0
def _get_0x_token_in_data(
trace: DecodedCallTrace, child_transfers: List[Transfer]
) -> Tuple[str, int]:
order: List = trace.inputs["order"]
token_in_address = order[0]
if trace.function_signature in RFQ_SIGNATURES:
taker_address = order[5]
elif trace.function_signature in LIMIT_SIGNATURES:
taker_address = order[6]
else:
raise RuntimeError(
f"0x orderbook function {trace.function_signature} is not supported"
)
token_in_amount = _get_taker_token_in_amount(
taker_address, token_in_address, child_transfers
)
return token_in_address, token_in_amount
def _get_0x_token_out_data(trace: DecodedCallTrace) -> Tuple[str, int]:
order: List = trace.inputs["order"]
token_out_address = order[1]
token_out_amount = trace.inputs["takerTokenFillAmount"]
return token_out_address, token_out_amount

25
mev_inspect/coinbase.py Normal file
View File

@ -0,0 +1,25 @@
import aiohttp
from mev_inspect.classifiers.specs.weth import WETH_ADDRESS
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS
from mev_inspect.schemas.coinbase import CoinbasePrices, CoinbasePricesResponse
COINBASE_API_BASE = "https://www.coinbase.com/api/v2"
COINBASE_TOKEN_NAME_BY_ADDRESS = {
WETH_ADDRESS: "weth",
ETH_TOKEN_ADDRESS: "ethereum",
}
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

@ -1,5 +1,4 @@
from typing import Dict, List, Optional
from web3 import Web3
from typing import List, Optional
from mev_inspect.traces import get_child_traces
from mev_inspect.schemas.traces import (
@ -9,44 +8,15 @@ from mev_inspect.schemas.traces import (
)
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.abi import get_raw_abi
from mev_inspect.transfers import ETH_TOKEN_ADDRESS
V2_COMPTROLLER_ADDRESS = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"
V2_C_ETHER = "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5"
CREAM_COMPTROLLER_ADDRESS = "0x3d5BC3c8d13dcB8bF317092d84783c2697AE9258"
CREAM_CR_ETHER = "0xD06527D5e56A3495252A528C4987003b712860eE"
# helper, only queried once in the beginning (inspect_block)
def fetch_all_underlying_markets(w3: Web3, protocol: Protocol) -> Dict[str, str]:
if protocol == Protocol.compound_v2:
c_ether = V2_C_ETHER
address = V2_COMPTROLLER_ADDRESS
elif protocol == Protocol.cream:
c_ether = CREAM_CR_ETHER
address = CREAM_COMPTROLLER_ADDRESS
else:
raise ValueError(f"No Comptroller found for {protocol}")
token_mapping = {}
comptroller_abi = get_raw_abi("Comptroller", Protocol.compound_v2)
comptroller_instance = w3.eth.contract(address=address, abi=comptroller_abi)
markets = comptroller_instance.functions.getAllMarkets().call()
token_abi = get_raw_abi("CToken", Protocol.compound_v2)
for token in markets:
# make an exception for cETH (as it has no .underlying())
if token != c_ether:
token_instance = w3.eth.contract(address=token, abi=token_abi)
underlying_token = token_instance.functions.underlying().call()
token_mapping[
token.lower()
] = underlying_token.lower() # make k:v lowercase for consistancy
return token_mapping
def get_compound_liquidations(
traces: List[ClassifiedTrace],
collateral_by_c_token_address: Dict[str, str],
collateral_by_cr_token_address: Dict[str, str],
) -> List[Liquidation]:
"""Inspect list of classified traces and identify liquidation"""
@ -67,23 +37,13 @@ def get_compound_liquidations(
trace.transaction_hash, trace.trace_address, traces
)
seize_trace = _get_seize_call(child_traces)
underlying_markets = {}
if trace.protocol == Protocol.compound_v2:
underlying_markets = collateral_by_c_token_address
elif trace.protocol == Protocol.cream:
underlying_markets = collateral_by_cr_token_address
if (
seize_trace is not None
and seize_trace.inputs is not None
and len(underlying_markets) != 0
):
if seize_trace is not None and seize_trace.inputs is not None:
c_token_collateral = trace.inputs["cTokenCollateral"]
if trace.abi_name == "CEther":
liquidations.append(
Liquidation(
liquidated_user=trace.inputs["borrower"],
collateral_token_address=ETH_TOKEN_ADDRESS, # WETH since all cEther liquidations provide Ether
debt_token_address=c_token_collateral,
liquidator_user=seize_trace.inputs["liquidator"],
debt_purchase_amount=trace.value,
@ -97,13 +57,9 @@ def get_compound_liquidations(
elif (
trace.abi_name == "CToken"
): # cToken liquidations where liquidator pays back via token transfer
c_token_address = trace.to_address
liquidations.append(
Liquidation(
liquidated_user=trace.inputs["borrower"],
collateral_token_address=underlying_markets[
c_token_address
],
debt_token_address=c_token_collateral,
liquidator_user=seize_trace.inputs["liquidator"],
debt_purchase_amount=trace.inputs["repayAmount"],

View File

@ -1,3 +1,5 @@
from datetime import datetime
from mev_inspect.schemas.blocks import Block
@ -20,7 +22,7 @@ def write_block(
"INSERT INTO blocks (block_number, block_timestamp) VALUES (:block_number, :block_timestamp)",
params={
"block_number": block.block_number,
"block_timestamp": block.block_timestamp,
"block_timestamp": datetime.fromtimestamp(block.block_timestamp),
},
)
db_session.commit()

View File

@ -0,0 +1,17 @@
from typing import List
from sqlalchemy.dialects.postgresql import insert
from mev_inspect.models.prices import PriceModel
from mev_inspect.schemas.prices import Price
def write_prices(db_session, prices: List[Price]) -> None:
insert_statement = (
insert(PriceModel.__table__)
.values([price.dict() for price in prices])
.on_conflict_do_nothing()
)
db_session.execute(insert_statement)
db_session.commit()

85
mev_inspect/crud/punks.py Normal file
View File

@ -0,0 +1,85 @@
import json
from typing import List
from mev_inspect.models.punks import (
PunkSnipeModel,
PunkBidModel,
PunkBidAcceptanceModel,
)
from mev_inspect.schemas.punk_snipe import PunkSnipe
from mev_inspect.schemas.punk_bid import PunkBid
from mev_inspect.schemas.punk_accept_bid import PunkBidAcceptance
def delete_punk_bid_acceptances_for_block(
db_session,
block_number: int,
) -> None:
(
db_session.query(PunkBidAcceptanceModel)
.filter(PunkBidAcceptanceModel.block_number == block_number)
.delete()
)
db_session.commit()
def write_punk_bid_acceptances(
db_session,
punk_bid_acceptances: List[PunkBidAcceptance],
) -> None:
models = [
PunkBidAcceptanceModel(**json.loads(punk_bid_acceptance.json()))
for punk_bid_acceptance in punk_bid_acceptances
]
db_session.bulk_save_objects(models)
db_session.commit()
def delete_punk_bids_for_block(
db_session,
block_number: int,
) -> None:
(
db_session.query(PunkBidModel)
.filter(PunkBidModel.block_number == block_number)
.delete()
)
db_session.commit()
def write_punk_bids(
db_session,
punk_bids: List[PunkBid],
) -> None:
models = [PunkBidModel(**json.loads(punk_bid.json())) for punk_bid in punk_bids]
db_session.bulk_save_objects(models)
db_session.commit()
def delete_punk_snipes_for_block(
db_session,
block_number: int,
) -> None:
(
db_session.query(PunkSnipeModel)
.filter(PunkSnipeModel.block_number == block_number)
.delete()
)
db_session.commit()
def write_punk_snipes(
db_session,
punk_snipes: List[PunkSnipe],
) -> None:
models = [
PunkSnipeModel(**json.loads(punk_snipe.json())) for punk_snipe in punk_snipes
]
db_session.bulk_save_objects(models)
db_session.commit()

View File

@ -11,6 +11,16 @@ from mev_inspect.crud.arbitrages import (
delete_arbitrages_for_block,
write_arbitrages,
)
from mev_inspect.crud.punks import (
delete_punk_snipes_for_block,
write_punk_snipes,
delete_punk_bids_for_block,
write_punk_bids,
delete_punk_bid_acceptances_for_block,
write_punk_bid_acceptances,
)
from mev_inspect.crud.blocks import (
delete_block,
write_block,
@ -31,6 +41,7 @@ from mev_inspect.crud.liquidations import (
write_liquidations,
)
from mev_inspect.miner_payments import get_miner_payments
from mev_inspect.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_snipes
from mev_inspect.swaps import get_swaps
from mev_inspect.transfers import get_transfers
from mev_inspect.liquidations import get_liquidations
@ -101,6 +112,20 @@ async def inspect_block(
delete_liquidations_for_block(inspect_db_session, block_number)
write_liquidations(inspect_db_session, liquidations)
punk_bids = get_punk_bids(classified_traces)
delete_punk_bids_for_block(inspect_db_session, block_number)
write_punk_bids(inspect_db_session, punk_bids)
punk_bid_acceptances = get_punk_bid_acceptances(classified_traces)
delete_punk_bid_acceptances_for_block(inspect_db_session, block_number)
write_punk_bid_acceptances(inspect_db_session, punk_bid_acceptances)
punk_snipes = get_punk_snipes(punk_bids, punk_bid_acceptances)
logger.info(f"Block: {block_number} -- Found {len(punk_snipes)} punk snipes")
delete_punk_snipes_for_block(inspect_db_session, block_number)
write_punk_snipes(inspect_db_session, punk_snipes)
miner_payments = get_miner_payments(
block.miner, block.base_fee_per_gas, classified_traces, block.receipts
)

View File

@ -1,6 +1,7 @@
from typing import List
from mev_inspect.aave_liquidations import get_aave_liquidations
from mev_inspect.compound_liquidations import get_compound_liquidations
from mev_inspect.schemas.traces import (
ClassifiedTrace,
Classification,
@ -20,4 +21,5 @@ def get_liquidations(
classified_traces: List[ClassifiedTrace],
) -> List[Liquidation]:
aave_liquidations = get_aave_liquidations(classified_traces)
return aave_liquidations
comp_liquidations = get_compound_liquidations(classified_traces)
return aave_liquidations + comp_liquidations

View File

@ -8,7 +8,6 @@ class LiquidationModel(Base):
liquidated_user = Column(String, nullable=False)
liquidator_user = Column(String, nullable=False)
collateral_token_address = Column(String, nullable=False)
debt_token_address = Column(String, nullable=False)
debt_purchase_amount = Column(Numeric, nullable=False)
received_amount = Column(Numeric, nullable=False)

View File

@ -0,0 +1,11 @@
from sqlalchemy import Column, Numeric, String, TIMESTAMP
from .base import Base
class PriceModel(Base):
__tablename__ = "prices"
timestamp = Column(TIMESTAMP, nullable=False, primary_key=True)
usd_price = Column(Numeric, nullable=False)
token_address = Column(String, nullable=False, primary_key=True)

View File

@ -0,0 +1,15 @@
from sqlalchemy import Column, Numeric, String, ARRAY, Integer
from .base import Base
class PunkSnipeModel(Base):
__tablename__ = "punk_snipes"
block_number = Column(Numeric, nullable=False)
transaction_hash = Column(String, primary_key=True)
trace_address = Column(ARRAY(Integer), primary_key=True)
from_address = Column(String, nullable=False)
punk_index = Column(Integer, nullable=False)
min_acceptance_price = Column(Numeric, nullable=False)
acceptance_price = Column(Numeric, nullable=False)

View File

@ -0,0 +1,37 @@
from sqlalchemy import Column, Numeric, String, ARRAY, Integer
from .base import Base
class PunkSnipeModel(Base):
__tablename__ = "punk_snipes"
block_number = Column(Numeric, nullable=False)
transaction_hash = Column(String, primary_key=True)
trace_address = Column(ARRAY(Integer), primary_key=True)
from_address = Column(String, nullable=False)
punk_index = Column(Integer, nullable=False)
min_acceptance_price = Column(Numeric, nullable=False)
acceptance_price = Column(Numeric, nullable=False)
class PunkBidModel(Base):
__tablename__ = "punk_bids"
block_number = Column(Numeric, nullable=False)
transaction_hash = Column(String, primary_key=True)
trace_address = Column(ARRAY(Integer), primary_key=True)
from_address = Column(String, nullable=False)
punk_index = Column(Integer, nullable=False)
price = Column(Numeric, nullable=False)
class PunkBidAcceptanceModel(Base):
__tablename__ = "punk_bid_acceptances"
block_number = Column(Numeric, nullable=False)
transaction_hash = Column(String, primary_key=True)
trace_address = Column(ARRAY(Integer), primary_key=True)
from_address = Column(String, nullable=False)
punk_index = Column(Integer, nullable=False)
min_price = Column(Numeric, nullable=False)

29
mev_inspect/prices.py Normal file
View File

@ -0,0 +1,29 @@
from typing import List
from mev_inspect.classifiers.specs.weth import WETH_ADDRESS
from mev_inspect.coinbase import fetch_coinbase_prices
from mev_inspect.schemas.prices import Price
from mev_inspect.schemas.transfers import ETH_TOKEN_ADDRESS
SUPPORTED_TOKENS = [
WETH_ADDRESS,
ETH_TOKEN_ADDRESS,
]
async def fetch_all_supported_prices() -> List[Price]:
prices = []
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

125
mev_inspect/punks.py Normal file
View File

@ -0,0 +1,125 @@
from typing import List, Optional
from mev_inspect.schemas.traces import (
ClassifiedTrace,
Classification,
DecodedCallTrace,
)
from mev_inspect.schemas.punk_bid import PunkBid
from mev_inspect.schemas.punk_accept_bid import PunkBidAcceptance
from mev_inspect.schemas.punk_snipe import PunkSnipe
from mev_inspect.traces import get_traces_by_transaction_hash
def _get_highest_punk_bid_per_index(
punk_bids: List[PunkBid], punk_index: int
) -> Optional[PunkBid]:
highest_punk_bid = None
for punk_bid in punk_bids:
if punk_bid.punk_index == punk_index:
if highest_punk_bid is None:
highest_punk_bid = punk_bid
elif punk_bid.price > highest_punk_bid.price:
highest_punk_bid = punk_bid
return highest_punk_bid
def get_punk_snipes(
punk_bids: List[PunkBid], punk_bid_acceptances: List[PunkBidAcceptance]
) -> List[PunkSnipe]:
punk_snipe_list = []
for punk_bid_acceptance in punk_bid_acceptances:
highest_punk_bid = _get_highest_punk_bid_per_index(
punk_bids, punk_bid_acceptance.punk_index
)
if highest_punk_bid is None:
continue
if highest_punk_bid.price > punk_bid_acceptance.min_price:
punk_snipe = PunkSnipe(
block_number=highest_punk_bid.block_number,
transaction_hash=highest_punk_bid.transaction_hash,
trace_address=highest_punk_bid.trace_address,
from_address=highest_punk_bid.from_address,
punk_index=highest_punk_bid.punk_index,
min_acceptance_price=punk_bid_acceptance.min_price,
acceptance_price=highest_punk_bid.price,
)
punk_snipe_list.append(punk_snipe)
return punk_snipe_list
def get_punk_bid_acceptances(traces: List[ClassifiedTrace]) -> List[PunkBidAcceptance]:
punk_bid_acceptances = []
for _, transaction_traces in get_traces_by_transaction_hash(traces).items():
punk_bid_acceptances += _get_punk_bid_acceptances_for_transaction(
list(transaction_traces)
)
return punk_bid_acceptances
def _get_punk_bid_acceptances_for_transaction(
traces: List[ClassifiedTrace],
) -> List[PunkBidAcceptance]:
ordered_traces = list(sorted(traces, key=lambda t: t.trace_address))
punk_bid_acceptances = []
for trace in ordered_traces:
if not isinstance(trace, DecodedCallTrace):
continue
elif trace.classification == Classification.punk_accept_bid:
punk_accept_bid = PunkBidAcceptance(
block_number=trace.block_number,
transaction_hash=trace.transaction_hash,
trace_address=trace.trace_address,
from_address=trace.from_address,
punk_index=trace.inputs["punkIndex"],
min_price=trace.inputs["minPrice"],
)
punk_bid_acceptances.append(punk_accept_bid)
return punk_bid_acceptances
def get_punk_bids(traces: List[ClassifiedTrace]) -> List[PunkBid]:
punk_bids = []
for _, transaction_traces in get_traces_by_transaction_hash(traces).items():
punk_bids += _get_punk_bids_for_transaction(list(transaction_traces))
return punk_bids
def _get_punk_bids_for_transaction(traces: List[ClassifiedTrace]) -> List[PunkBid]:
ordered_traces = list(sorted(traces, key=lambda t: t.trace_address))
punk_bids = []
for trace in ordered_traces:
if not isinstance(trace, DecodedCallTrace):
continue
elif trace.classification == Classification.punk_bid:
punk_bid = PunkBid(
transaction_hash=trace.transaction_hash,
block_number=trace.block_number,
trace_address=trace.trace_address,
from_address=trace.from_address,
punk_index=trace.inputs["punkIndex"],
price=trace.value,
)
punk_bids.append(punk_bid)
return punk_bids

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

@ -6,7 +6,6 @@ from mev_inspect.schemas.traces import Protocol
class Liquidation(BaseModel):
liquidated_user: str
liquidator_user: str
collateral_token_address: str
debt_token_address: str
debt_purchase_amount: int
received_amount: int

View File

@ -0,0 +1,9 @@
from datetime import datetime
from pydantic import BaseModel
class Price(BaseModel):
token_address: str
timestamp: datetime
usd_price: float

View File

@ -0,0 +1,12 @@
from typing import List
from pydantic import BaseModel
class PunkBidAcceptance(BaseModel):
block_number: int
transaction_hash: str
trace_address: List[int]
from_address: str
punk_index: int
min_price: int

View File

@ -0,0 +1,12 @@
from typing import List
from pydantic import BaseModel
class PunkBid(BaseModel):
block_number: int
transaction_hash: str
trace_address: List[int]
from_address: str
punk_index: int
price: int

View File

@ -0,0 +1,13 @@
from typing import List
from pydantic import BaseModel
class PunkSnipe(BaseModel):
block_number: int
transaction_hash: str
trace_address: List[int]
from_address: str
punk_index: int
min_acceptance_price: int
acceptance_price: int

View File

@ -31,6 +31,8 @@ class Classification(Enum):
transfer = "transfer"
liquidate = "liquidate"
seize = "seize"
punk_bid = "punk_bid"
punk_accept_bid = "punk_accept_bid"
class Protocol(Enum):
@ -44,6 +46,8 @@ class Protocol(Enum):
balancer_v1 = "balancer_v1"
compound_v2 = "compound_v2"
cream = "cream"
cryptopunks = "cryptopunks"
bancor = "bancor"
class ClassifiedTrace(Trace):

View File

@ -1,8 +1,8 @@
import json
from hexbytes import HexBytes
from web3.datastructures import AttributeDict
from pydantic import BaseModel
from web3.datastructures import AttributeDict
def to_camel(string: str) -> str:

View File

@ -35,6 +35,7 @@ build-backend = "poetry.core.masonry.api"
inspect-block = 'cli:inspect_block_command'
inspect-many-blocks = 'cli:inspect_many_blocks_command'
fetch-block = 'cli:fetch_block_command'
fetch-all-prices = 'cli:fetch_all_prices'
[tool.black]
exclude = '''

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ def test_single_weth_liquidation():
Liquidation(
liquidated_user="0xd16404ca0a74a15e66d8ad7c925592fb02422ffe",
liquidator_user="0x19256c009781bc2d1545db745af6dfd30c7e9cfa",
collateral_token_address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
debt_token_address="0xdac17f958d2ee523a2206206994597c13d831ec7",
debt_purchase_amount=26503300291,
received_amount=8182733924513576561,
@ -50,7 +49,6 @@ def test_single_liquidation():
Liquidation(
liquidated_user="0x8d8d912fe4db5917da92d14fea05225b803c359c",
liquidator_user="0xf2d9e54f0e317b8ac94825b2543908e7552fe9c7",
collateral_token_address="0x80fb784b7ed66730e8b1dbd9820afd29931aab03",
debt_token_address="0xdac17f958d2ee523a2206206994597c13d831ec7",
debt_purchase_amount=1069206535,
received_amount=2657946947610159065393,
@ -81,7 +79,6 @@ def test_single_liquidation_with_atoken_payback():
Liquidation(
liquidated_user="0x3d2b6eacd1bca51af57ed8b3ff9ef0bd8ee8c56d",
liquidator_user="0x887668f2dc9612280243f2a6ef834cecf456654e",
collateral_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_token_address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
debt_purchase_amount=767615458043667978,
received_amount=113993647930952952550,
@ -111,7 +108,6 @@ def test_multiple_liquidations_in_block():
liquidation1 = Liquidation(
liquidated_user="0x6c6541ae8a7c6a6f968124a5ff2feac8f0c7875b",
liquidator_user="0x7185e240d8e9e2d692cbc68d30eecf965e9a7feb",
collateral_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_token_address="0x4fabb145d64652a948d72533023f6e7a623c7c53",
debt_purchase_amount=457700000000000000000,
received_amount=10111753901939162887,
@ -125,7 +121,6 @@ def test_multiple_liquidations_in_block():
liquidation2 = Liquidation(
liquidated_user="0x6c6541ae8a7c6a6f968124a5ff2feac8f0c7875b",
liquidator_user="0x7185e240d8e9e2d692cbc68d30eecf965e9a7feb",
collateral_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_token_address="0x0000000000085d4780b73119b644ae5ecd22b376",
debt_purchase_amount=497030000000000000000,
received_amount=21996356316098208090,
@ -139,7 +134,6 @@ def test_multiple_liquidations_in_block():
liquidation3 = Liquidation(
liquidated_user="0xda874f844389df33c0fad140df4970fe1b366726",
liquidator_user="0x7185e240d8e9e2d692cbc68d30eecf965e9a7feb",
collateral_token_address="0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2",
debt_token_address="0x57ab1ec28d129707052df4df418d58a2d46d5f51",
debt_purchase_amount=447810000000000000000,
received_amount=121531358145247546,
@ -169,7 +163,6 @@ def test_liquidations_with_eth_transfer():
liquidation1 = Liquidation(
liquidated_user="0xad346c7762f74c78da86d2941c6eb546e316fbd0",
liquidator_user="0x27239549dd40e1d60f5b80b0c4196923745b1fd2",
collateral_token_address=ETH_TOKEN_ADDRESS,
debt_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_purchase_amount=1809152000000000000,
received_amount=15636807387264000,
@ -183,7 +176,6 @@ def test_liquidations_with_eth_transfer():
liquidation2 = Liquidation(
liquidated_user="0xad346c7762f74c78da86d2941c6eb546e316fbd0",
liquidator_user="0x27239549dd40e1d60f5b80b0c4196923745b1fd2",
collateral_token_address=ETH_TOKEN_ADDRESS,
debt_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_purchase_amount=1809152000000000000,
received_amount=8995273139160873,

129
tests/test_0x.py Normal file
View File

@ -0,0 +1,129 @@
from mev_inspect.schemas.swaps import Swap
from mev_inspect.swaps import get_swaps
from mev_inspect.schemas.traces import Protocol
from mev_inspect.classifiers.trace import TraceClassifier
from tests.utils import load_test_block
def test_fillLimitOrder_swap():
transaction_hash = (
"0xa043976d736ec8dc930c0556dffd0a86a4bfc80bf98fb7995c791fb4dc488b5d"
)
block_number = 13666312
swap = Swap(
abi_name="INativeOrdersFeature",
transaction_hash=transaction_hash,
block_number=block_number,
trace_address=[0, 2, 0, 1],
contract_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
from_address="0x00000000000e1d0dabf7b7c7b68866fc940d0db8",
to_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
token_in_address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
token_in_amount=35000000000000000000,
token_out_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
token_out_amount=143949683150,
protocol=Protocol.zero_ex,
error=None,
)
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_swaps(classified_traces)
assert result.count(swap) == 1
def test__fillLimitOrder_swap():
transaction_hash = (
"0x9255addffa2dbeb9560c5e20e78a78c949488d2054c70b2155c39f9e28394cbf"
)
block_number = 13666184
swap = Swap(
abi_name="INativeOrdersFeature",
transaction_hash=transaction_hash,
block_number=block_number,
trace_address=[0, 1],
contract_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
from_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
to_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
token_in_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
token_in_amount=30000000,
token_out_address="0x9ff79c75ae2bcbe0ec63c0375a3ec90ff75bbe0f",
token_out_amount=100000001,
protocol=Protocol.zero_ex,
error=None,
)
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_swaps(classified_traces)
assert result.count(swap) == 1
def test_RfqLimitOrder_swap():
transaction_hash = (
"0x1c948eb7c59ddbe6b916cf68f5df86eb44a7c9e728221fcd8ab750f137fd2a0f"
)
block_number = 13666326
swap = Swap(
abi_name="INativeOrdersFeature",
transaction_hash=transaction_hash,
block_number=block_number,
trace_address=[0, 1, 13, 0, 1],
contract_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
from_address="0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
to_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
token_in_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
token_in_amount=288948250430,
token_out_address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
token_out_amount=70500000000000000000,
protocol=Protocol.zero_ex,
error=None,
)
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_swaps(classified_traces)
assert result.count(swap) == 1
def test__RfqLimitOrder_swap():
transaction_hash = (
"0x4f66832e654f8a4d773d9769571155df3722401343247376d6bb56626db29b90"
)
block_number = 13666363
swap = Swap(
abi_name="INativeOrdersFeature",
transaction_hash=transaction_hash,
block_number=block_number,
trace_address=[1, 0, 1, 0, 1],
contract_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
from_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
to_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff",
token_in_address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
token_in_amount=979486121594935552,
token_out_address="0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce",
token_out_amount=92404351093861841165644172,
protocol=Protocol.zero_ex,
error=None,
)
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_swaps(classified_traces)
assert result.count(swap) == 1

View File

@ -2,7 +2,6 @@ from mev_inspect.compound_liquidations import get_compound_liquidations
from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.traces import Protocol
from mev_inspect.classifiers.trace import TraceClassifier
from mev_inspect.transfers import ETH_TOKEN_ADDRESS
from tests.utils import load_test_block, load_comp_markets, load_cream_markets
comp_markets = load_comp_markets()
@ -19,7 +18,6 @@ def test_c_ether_liquidations():
Liquidation(
liquidated_user="0xb5535a3681cf8d5431b8acfd779e2f79677ecce9",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
collateral_token_address=ETH_TOKEN_ADDRESS,
debt_token_address="0x39aa39c021dfbae8fac545936693ac917d5e7563",
debt_purchase_amount=268066492249420078,
received_amount=4747650169097,
@ -32,7 +30,7 @@ def test_c_ether_liquidations():
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_compound_liquidations(classified_traces, comp_markets, cream_markets)
result = get_compound_liquidations(classified_traces)
assert result == liquidations
block_number = 13207907
@ -44,7 +42,6 @@ def test_c_ether_liquidations():
Liquidation(
liquidated_user="0x45df6f00166c3fb77dc16b9e47ff57bc6694e898",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
collateral_token_address=ETH_TOKEN_ADDRESS,
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
debt_purchase_amount=414547860568297082,
received_amount=321973320649,
@ -58,7 +55,7 @@ def test_c_ether_liquidations():
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_compound_liquidations(classified_traces, comp_markets, cream_markets)
result = get_compound_liquidations(classified_traces)
assert result == liquidations
block_number = 13298725
@ -70,7 +67,6 @@ def test_c_ether_liquidations():
Liquidation(
liquidated_user="0xacbcf5d2970eef25f02a27e9d9cd31027b058b9b",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
collateral_token_address=ETH_TOKEN_ADDRESS,
debt_token_address="0x35a18000230da775cac24873d00ff85bccded550",
debt_purchase_amount=1106497772527562662,
received_amount=910895850496,
@ -83,7 +79,7 @@ def test_c_ether_liquidations():
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_compound_liquidations(classified_traces, comp_markets, cream_markets)
result = get_compound_liquidations(classified_traces)
assert result == liquidations
@ -97,7 +93,6 @@ def test_c_token_liquidation():
Liquidation(
liquidated_user="0xacdd5528c1c92b57045041b5278efa06cdade4d8",
liquidator_user="0xe0090ec6895c087a393f0e45f1f85098a6c33bef",
collateral_token_address="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
debt_token_address="0x70e36f6bf80a52b3b46b3af8e106cc0ed743e8e4",
debt_purchase_amount=1207055531,
received_amount=21459623305,
@ -110,7 +105,7 @@ def test_c_token_liquidation():
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_compound_liquidations(classified_traces, comp_markets, cream_markets)
result = get_compound_liquidations(classified_traces)
assert result == liquidations
@ -124,7 +119,6 @@ def test_cream_token_liquidation():
Liquidation(
liquidated_user="0x46bf9479dc569bc796b7050344845f6564d45fba",
liquidator_user="0xa2863cad9c318669660eb4eca8b3154b90fb4357",
collateral_token_address="0x514910771af9ca656af840dff83e8264ecf986ca",
debt_token_address="0x44fbebd2f576670a6c33f6fc0b00aa8c5753b322",
debt_purchase_amount=14857434973806369550,
received_amount=1547215810826,
@ -137,5 +131,5 @@ def test_cream_token_liquidation():
block = load_test_block(block_number)
trace_classifier = TraceClassifier()
classified_traces = trace_classifier.classify(block.traces)
result = get_compound_liquidations(classified_traces, comp_markets, cream_markets)
result = get_compound_liquidations(classified_traces)
assert result == liquidations

View File

@ -4,6 +4,10 @@ from mev_inspect.classifiers.specs.uniswap import (
UNISWAP_V2_PAIR_ABI_NAME,
UNISWAP_V3_POOL_ABI_NAME,
)
from mev_inspect.classifiers.specs.bancor import (
BANCOR_NETWORK_ABI_NAME,
BANCOR_NETWORK_CONTRACT_ADDRESS,
)
from mev_inspect.schemas.traces import Protocol
from .helpers import (
@ -23,12 +27,14 @@ def test_swaps(
first_transaction_hash,
second_transaction_hash,
third_transaction_hash,
] = get_transaction_hashes(3)
fourth_transaction_hash,
] = get_transaction_hashes(4)
[
alice_address,
bob_address,
carl_address,
danielle_address,
first_token_in_address,
first_token_out_address,
first_pool_address,
@ -38,7 +44,10 @@ def test_swaps(
third_token_in_address,
third_token_out_address,
third_pool_address,
] = get_addresses(12)
fourth_token_in_address,
fourth_token_out_address,
first_converter_address,
] = get_addresses(16)
first_token_in_amount = 10
first_token_out_amount = 20
@ -46,6 +55,8 @@ def test_swaps(
second_token_out_amount = 40
third_token_in_amount = 50
third_token_out_amount = 60
fourth_token_in_amount = 70
fourth_token_out_amount = 80
traces = [
make_unknown_trace(block_number, first_transaction_hash, []),
@ -139,11 +150,41 @@ def test_swaps(
recipient_address=bob_address,
recipient_input_key="recipient",
),
make_transfer_trace(
block_number,
fourth_transaction_hash,
trace_address=[2],
from_address=danielle_address,
to_address=first_converter_address,
token_address=fourth_token_in_address,
amount=fourth_token_in_amount,
),
make_transfer_trace(
block_number,
fourth_transaction_hash,
trace_address=[1, 2],
from_address=first_converter_address,
to_address=danielle_address,
token_address=fourth_token_out_address,
amount=fourth_token_out_amount,
),
make_swap_trace(
block_number,
fourth_transaction_hash,
trace_address=[],
from_address=danielle_address,
contract_address=BANCOR_NETWORK_CONTRACT_ADDRESS,
abi_name=BANCOR_NETWORK_ABI_NAME,
protocol=Protocol.bancor,
function_signature="convertByPath(address[],uint256,uint256,address,address,uint256)",
recipient_address=danielle_address,
recipient_input_key="recipient",
),
]
swaps = get_swaps(traces)
assert len(swaps) == 3
assert len(swaps) == 4
for swap in swaps:
if swap.abi_name == UNISWAP_V2_PAIR_ABI_NAME:
@ -152,6 +193,8 @@ def test_swaps(
uni_v3_swap = swap
elif swap.abi_name == BALANCER_V1_POOL_ABI_NAME:
bal_v1_swap = swap
elif swap.abi_name == BANCOR_NETWORK_ABI_NAME:
bancor_swap = swap
else:
assert False
@ -193,3 +236,16 @@ def test_swaps(
assert bal_v1_swap.token_in_amount == third_token_in_amount
assert bal_v1_swap.token_out_address == third_token_out_address
assert bal_v1_swap.token_out_amount == third_token_out_amount
assert bancor_swap.abi_name == BANCOR_NETWORK_ABI_NAME
assert bancor_swap.transaction_hash == fourth_transaction_hash
assert bancor_swap.block_number == block_number
assert bancor_swap.trace_address == []
assert bancor_swap.protocol == Protocol.bancor
assert bancor_swap.contract_address == BANCOR_NETWORK_CONTRACT_ADDRESS
assert bancor_swap.from_address == danielle_address
assert bancor_swap.to_address == danielle_address
assert bancor_swap.token_in_address == fourth_token_in_address
assert bancor_swap.token_in_amount == fourth_token_in_amount
assert bancor_swap.token_out_address == fourth_token_out_address
assert bancor_swap.token_out_amount == fourth_token_out_amount