merge commit

This commit is contained in:
carlomazzaferro 2021-10-28 11:15:51 +01:00
commit f6719cdfc8
No known key found for this signature in database
GPG Key ID: 0CED3103EF7B2187
45 changed files with 487 additions and 209 deletions

36
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,36 @@
# Contributing guide
Welcome to the Flashbots collective! We just ask you to be nice when you play with us.
## Pre-commit
We use pre-commit to maintain a consistent style, prevent errors, and ensure test coverage.
To set up, install dependencies through `poetry`:
```
poetry install
```
Then install pre-commit hooks with:
```
poetry run pre-commit install
```
## Tests
Run tests with:
```
kubectl exec deploy/mev-inspect-deployment -- poetry run pytest --cov=mev_inspect tests
```
## Send a pull request
- Your proposed changes should be first described and discussed in an issue.
- Open the branch in a personal fork, not in the team repository.
- Every pull request should be small and represent a single change. If the problem is complicated, split it in multiple issues and pull requests.
- Every pull request should be covered by unit tests.
We appreciate you, friend <3.

190
README.md
View File

@ -1,7 +1,9 @@
# mev-inspect-py # mev-inspect-py
> illuminating the dark forest 🌲💡
**mev-inspect-py** is an MEV inspector for Ethereum [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![Discord](https://img.shields.io/discord/755466764501909692)](https://discord.gg/7hvTycdNcK)
[Maximal extractable value](https://ethereum.org/en/developers/docs/mev/) inspector for Ethereum, to illuminate the [dark forest](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest/) 🌲💡
Given a block, mev-inspect finds: Given a block, mev-inspect finds:
- miner payments (gas + coinbase) - miner payments (gas + coinbase)
@ -9,106 +11,141 @@ Given a block, mev-inspect finds:
- swaps and [arbitrages](https://twitter.com/bertcmiller/status/1427632028263059462) - swaps and [arbitrages](https://twitter.com/bertcmiller/status/1427632028263059462)
- ...and more - ...and more
Data is stored in Postgres for analysis Data is stored in Postgres for analysis.
## Running locally ## Install
mev-inspect-py is built to run on kubernetes locally and in production
### Install dependencies mev-inspect-py is built to run on kubernetes locally and in production.
First, setup a local kubernetes deployment - we use [Docker](https://www.docker.com/products/docker-desktop) and [kind](https://kind.sigs.k8s.io/docs/user/quick-start) ### Dependencies
- [docker](https://www.docker.com/products/docker-desktop)
- [kind](https://kind.sigs.k8s.io/docs/user/quick-start), or a similar tool for running local Kubernetes clusters
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [helm](https://helm.sh/docs/intro/install/)
- [tilt](https://docs.tilt.dev/install.html)
### Set up
Create a new cluster with:
If using kind, create a new cluster with:
``` ```
kind create cluster kind create cluster
``` ```
Next, install the kubernetes CLI [`kubectl`](https://kubernetes.io/docs/tasks/tools/) Set an environment variable `RPC_URL` to an RPC for fetching blocks.
Then, install [helm](https://helm.sh/docs/intro/install/) - helm is a package manager for kubernetes
Lastly, setup [Tilt](https://docs.tilt.dev/install.html) which manages running and updating kubernetes resources locally
### Start up
Set an environment variable `RPC_URL` to an RPC for fetching blocks
Example: Example:
``` ```
export RPC_URL="http://111.111.111.111:8546" export RPC_URL="http://111.111.111.111:8546"
``` ```
**Note: mev-inspect-py currently requires an RPC with support for Erigon traces and receipts (not geth 😔)** **Note**: mev-inspect-py currently requires an RPC of a full archive node with support for Erigon traces and receipts (not geth 😔).
Next, start all services with: Next, start all services with:
``` ```
tilt up tilt up
``` ```
Press "space" to see a browser of the services starting up Press "space" to see a browser of the services starting up.
On first startup, you'll need to apply database migrations with:
On first startup, you'll need to apply database migrations. Apply with:
``` ```
kubectl exec deploy/mev-inspect -- alembic upgrade head kubectl exec deploy/mev-inspect -- alembic upgrade head
``` ```
## Inspecting ## Usage
### Inspect a single block ### Inspect a single block
Inspecting block [12914944](https://twitter.com/mevalphaleak/status/1420416437575901185) Inspecting block [12914944](https://twitter.com/mevalphaleak/status/1420416437575901185):
``` ```
kubectl exec deploy/mev-inspect -- poetry run inspect-block 12914944 ./mev inspect 12914944
``` ```
### Inspect many blocks ### Inspect many blocks
Inspecting blocks 12914944 to 12914954 Inspecting blocks 12914944 to 12914954:
``` ```
kubectl exec deploy/mev-inspect -- poetry run inspect-many-blocks 12914944 12914954 ./mev inspect-many 12914944 12914954
``` ```
### Inspect all incoming blocks ### Inspect all incoming blocks
Start a block listener with Start a block listener with:
``` ```
kubectl exec deploy/mev-inspect -- /app/listener start ./mev listener start
``` ```
By default, it will pick up wherever you left off. By default, it will pick up wherever you left off.
If running for the first time, listener starts at the latest block If running for the first time, listener starts at the latest block.
Tail logs for the listener with:
See logs for the listener with
``` ```
kubectl exec deploy/mev-inspect -- tail -f listener.log ./mev listener tail
``` ```
And stop the listener with And stop the listener with:
``` ```
kubectl exec deploy/mev-inspect -- /app/listener stop ./mev listener stop
``` ```
## Exploring ### Backfilling
For larger backfills, you can inspect many blocks in parallel using kubernetes
To inspect blocks 12914944 to 12915044 divided across 10 worker pods:
```
./mev backfill 12914944 12915044 10
```
You can see worker pods spin up then complete by watching the status of all pods
```
watch kubectl get pods
```
To watch the logs for a given pod, take its pod name using the above, then run:
```
kubectl logs -f pod/mev-inspect-backfill-abcdefg
```
(where `mev-inspect-backfill-abcdefg` is your actual pod name)
### Exploring
All inspect output data is stored in Postgres. All inspect output data is stored in Postgres.
To connect to the local Postgres database for querying, launch a client container with: To connect to the local Postgres database for querying, launch a client container with:
``` ```
kubectl run -i --rm --tty postgres-client --env="PGPASSWORD=password" --image=jbergknoff/postgresql-client -- mev_inspect --host=postgresql --user=postgres ./mev db
``` ```
When you see the prompt When you see the prompt:
``` ```
mev_inspect=# mev_inspect=#
``` ```
You're ready to query! You're ready to query!
Try finding the total number of swaps decoded with UniswapV3Pool Try finding the total number of swaps decoded with UniswapV3Pool:
``` ```
SELECT COUNT(*) FROM swaps WHERE abi_name='UniswapV3Pool'; SELECT COUNT(*) FROM swaps WHERE abi_name='UniswapV3Pool';
``` ```
or top 10 arbs by gross profit that took profit in WETH or top 10 arbs by gross profit that took profit in WETH:
``` ```
SELECT * SELECT *
FROM arbitrages FROM arbitrages
@ -117,78 +154,83 @@ ORDER BY profit_amount DESC
LIMIT 10; LIMIT 10;
``` ```
Postgres tip: Enter `\x` to enter "Explanded display" mode which looks nicer for results with many columns Postgres tip: Enter `\x` to enter "Explanded display" mode which looks nicer for results with many columns.
## Contributing
### Guide
✨ Coming soon
### Pre-commit
We use pre-commit to maintain a consistent style, prevent errors, and ensure test coverage.
To set up, install dependencies through poetry
```
poetry install
```
Then install pre-commit hooks with
```
poetry run pre-commit install
```
### Tests
Run tests with
```
kubectl exec deploy/mev-inspect -- poetry run pytest --cov=mev_inspect tests
```
## FAQ ## FAQ
### How do I delete / reset my local postgres data? ### How do I delete / reset my local postgres data?
Stop the system if running Stop the system if running:
``` ```
tilt down tilt down
``` ```
Delete it with Delete it with:
``` ```
kubectl delete pvc data-postgresql-postgresql-0 kubectl delete pvc data-postgresql-postgresql-0
``` ```
Start back up again Start back up again:
``` ```
tilt up tilt up
``` ```
And rerun migrations to create the tables again And rerun migrations to create the tables again:
``` ```
kubectl exec deploy/mev-inspect -- alembic upgrade head kubectl exec deploy/mev-inspect -- alembic upgrade head
``` ```
### I was using the docker-compose setup and want to switch to kube, now what? ### I was using the docker-compose setup and want to switch to kube, now what?
Re-add the old `docker-compose.yml` file to your mev-inspect-py directory Re-add the old `docker-compose.yml` file to your mev-inspect-py directory.
A copy can be found [here](https://github.com/flashbots/mev-inspect-py/blob/ef60c097719629a7d2dc56c6e6c9a100fb706f76/docker-compose.yml) A copy can be found [here](https://github.com/flashbots/mev-inspect-py/blob/ef60c097719629a7d2dc56c6e6c9a100fb706f76/docker-compose.yml)
Tear down docker-compose resources Tear down docker-compose resources:
``` ```
docker compose down docker compose down
``` ```
Then go through the steps in the current README for kube setup Then go through the steps in the current README for kube setup.
### Error from server (AlreadyExists): pods "postgres-client" already exists ### Error from server (AlreadyExists): pods "postgres-client" already exists
This means the postgres client container didn't shut down correctly
Delete this one with This means the postgres client container didn't shut down correctly.
Delete this one with:
``` ```
kubectl delete pod/postgres-client kubectl delete pod/postgres-client
``` ```
Then start it back up again Then start it back up again.
## Maintainers
- [@lukevs](https://github.com/lukevs)
- [@gheise](https://github.com/gheise)
- [@bertmiller](https://github.com/bertmiller)
## Contributing
[Flashbots](https://flashbots.net) is a research and development collective working on mitigating the negative externalities of decentralized economies. We contribute with the larger free software community to illuminate the dark forest.
You are welcome here <3.
- If you want to join us, come and say hi in our [Discord chat](https://discord.gg/7hvTycdNcK).
- If you have a question, feedback or a bug report for this project, please [open a new Issue](https://github.com/flashbots/mev-inspect-py/issues).
- If you would like to contribute with code, check the [CONTRIBUTING file](CONTRIBUTING.md).
- We just ask you to be nice.
## Security
If you find a security vulnerability on this project or any other initiative related to Flashbots, please let us know sending an email to security@flashbots.net.
---
Made with ☀️ by the ⚡🤖 collective.

23
cli.py
View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import logging import logging
import os
import sys import sys
import os
from functools import wraps from functools import wraps
import click import click
@ -12,6 +12,8 @@ from mev_inspect.classifiers.trace import TraceClassifier
from mev_inspect.db import get_inspect_session, get_trace_session from mev_inspect.db import get_inspect_session, get_trace_session
from mev_inspect.inspect_block import inspect_block from mev_inspect.inspect_block import inspect_block
from mev_inspect.provider import get_base_provider from mev_inspect.provider import get_base_provider
from mev_inspect.block import create_from_block_number
from mev_inspect.retry import http_retry_with_backoff_request_middleware from mev_inspect.retry import http_retry_with_backoff_request_middleware
RPC_URL_ENV = "RPC_URL" RPC_URL_ENV = "RPC_URL"
@ -66,6 +68,25 @@ async def inspect_block_command(block_number: int, rpc: str, cache: bool):
) )
@cli.command()
@click.argument("block_number", type=int)
@click.option("--rpc", default=lambda: os.environ.get(RPC_URL_ENV, ""))
@coro
async def fetch_block_command(block_number: int, rpc: str):
base_provider = get_base_provider(rpc)
w3 = Web3(base_provider)
trace_db_session = get_trace_session()
block = await create_from_block_number(
base_provider,
w3,
block_number,
trace_db_session=trace_db_session,
)
print(block.json())
@cli.command() @cli.command()
@click.argument("after_block", type=int) @click.argument("after_block", type=int)
@click.argument("before_block", type=int) @click.argument("before_block", type=int)

View File

@ -43,6 +43,24 @@ spec:
secretKeyRef: secretKeyRef:
name: mev-inspect-db-credentials name: mev-inspect-db-credentials
key: password 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 - name: RPC_URL
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View File

@ -25,6 +25,9 @@ case "$1" in
start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
echo "." echo "."
;; ;;
tail)
tail -f listener.log
;;
restart) restart)
echo -n "Restarting daemon: "$NAME echo -n "Restarting daemon: "$NAME
start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE
@ -40,7 +43,7 @@ case "$1" in
;; ;;
*) *)
echo "Usage: "$1" {start|stop|restart}" echo "Usage: "$1" {start|stop|restart|tail}"
exit 1 exit 1
esac esac

17
mev
View File

@ -24,6 +24,9 @@ case "$1" in
echo "Connecting to $DB_NAME" echo "Connecting to $DB_NAME"
db db
;; ;;
listener)
./listener $2
;;
backfill) backfill)
start_block_number=$2 start_block_number=$2
end_block_number=$3 end_block_number=$3
@ -37,12 +40,24 @@ case "$1" in
echo "Inspecting block $block_number" echo "Inspecting block $block_number"
kubectl exec -ti deploy/mev-inspect -- poetry run inspect-block $block_number kubectl exec -ti deploy/mev-inspect -- poetry run inspect-block $block_number
;; ;;
inspect-many)
start_block_number=$2
end_block_number=$3
echo "Inspecting from block $start_block_number to $end_block_number"
kubectl exec -ti deploy/mev-inspect -- \
poetry run inspect-many-blocks $start_block_number $end_block_number
;;
test) test)
echo "Running tests" echo "Running tests"
kubectl exec -ti deploy/mev-inspect -- poetry run pytest tests kubectl exec -ti deploy/mev-inspect -- poetry run pytest tests
;; ;;
fetch)
block_number=$2
echo "Fetching block $block_number"
kubectl exec -ti deploy/mev-inspect -- poetry run fetch-block $block_number
;;
*) *)
echo "Usage: "$1" {inspect|test}" echo "Usage: "$1" {db|backfill|inspect|test}"
exit 1 exit 1
esac esac

View File

@ -4,7 +4,7 @@ from mev_inspect.traces import (
get_child_traces, get_child_traces,
is_child_of_any_address, is_child_of_any_address,
) )
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
ClassifiedTrace, ClassifiedTrace,
DecodedCallTrace, DecodedCallTrace,
Classification, Classification,

View File

@ -4,8 +4,8 @@ from typing import Optional
from pydantic import parse_obj_as from pydantic import parse_obj_as
from mev_inspect.schemas import ABI from mev_inspect.schemas.abi import ABI
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
THIS_FILE_DIRECTORY = Path(__file__).parents[0] THIS_FILE_DIRECTORY = Path(__file__).parents[0]

View File

@ -1,5 +1,5 @@
from itertools import groupby from itertools import groupby
from typing import List, Optional from typing import List, Tuple
from mev_inspect.schemas.arbitrages import Arbitrage from mev_inspect.schemas.arbitrages import Arbitrage
from mev_inspect.schemas.swaps import Swap from mev_inspect.schemas.swaps import Swap
@ -23,70 +23,111 @@ def get_arbitrages(swaps: List[Swap]) -> List[Arbitrage]:
def _get_arbitrages_from_swaps(swaps: List[Swap]) -> List[Arbitrage]: def _get_arbitrages_from_swaps(swaps: List[Swap]) -> List[Arbitrage]:
pool_addresses = {swap.pool_address for swap in swaps} """
An arbitrage is defined as multiple swaps in a series that result in the initial token being returned
to the initial sender address.
There are 2 types of swaps that are most common (99%+).
Case I (fully routed):
BOT -> A/B -> B/C -> C/A -> BOT
Case II (always return to bot):
BOT -> A/B -> BOT -> B/C -> BOT -> A/C -> BOT
There is only 1 correct way to route Case I, but for Case II the following valid routes could be found:
A->B->C->A / B->C->A->B / C->A->B->C. Thus when multiple valid routes are found we filter to the set that
happen in valid order.
"""
all_arbitrages = [] all_arbitrages = []
for index, first_swap in enumerate(swaps): start_ends = _get_all_start_end_swaps(swaps)
other_swaps = swaps[:index] + swaps[index + 1 :] if len(start_ends) == 0:
return []
if first_swap.from_address not in pool_addresses: # for (start, end) in filtered_start_ends:
arbitrage = _get_arbitrage_starting_with_swap(first_swap, other_swaps) for (start, end) in start_ends:
potential_intermediate_swaps = [
swap for swap in swaps if swap is not start and swap is not end
]
routes = _get_all_routes(start, end, potential_intermediate_swaps)
if arbitrage is not None: for route in routes:
all_arbitrages.append(arbitrage) start_amount = route[0].token_in_amount
end_amount = route[-1].token_out_amount
return all_arbitrages
def _get_arbitrage_starting_with_swap(
start_swap: Swap,
other_swaps: List[Swap],
) -> Optional[Arbitrage]:
swap_path = [start_swap]
current_swap: Swap = start_swap
while True:
next_swap = _get_swap_from_address(
current_swap.to_address,
current_swap.token_out_address,
other_swaps,
)
if next_swap is None:
return None
swap_path.append(next_swap)
current_swap = next_swap
if (
current_swap.to_address == start_swap.from_address
and current_swap.token_out_address == start_swap.token_in_address
):
start_amount = start_swap.token_in_amount
end_amount = current_swap.token_out_amount
profit_amount = end_amount - start_amount profit_amount = end_amount - start_amount
return Arbitrage( arb = Arbitrage(
swaps=swap_path, swaps=route,
block_number=start_swap.block_number, block_number=route[0].block_number,
transaction_hash=start_swap.transaction_hash, transaction_hash=route[0].transaction_hash,
account_address=start_swap.from_address, account_address=route[0].from_address,
profit_token_address=start_swap.token_in_address, profit_token_address=route[0].token_in_address,
start_amount=start_amount, start_amount=start_amount,
end_amount=end_amount, end_amount=end_amount,
profit_amount=profit_amount, profit_amount=profit_amount,
) )
all_arbitrages.append(arb)
return None if len(all_arbitrages) == 1:
return all_arbitrages
else:
return [
arb
for arb in all_arbitrages
if (arb.swaps[0].trace_address < arb.swaps[-1].trace_address)
]
def _get_swap_from_address( def _get_all_start_end_swaps(swaps: List[Swap]) -> List[Tuple[Swap, Swap]]:
address: str, token_address: str, swaps: List[Swap] """
) -> Optional[Swap]: Gets the set of all possible opening and closing swap pairs in an arbitrage via
for swap in swaps: - swap[start].token_in == swap[end].token_out
if swap.pool_address == address and swap.token_in_address == token_address: - swap[start].from_address == swap[end].to_address
return swap - not swap[start].from_address in all_pool_addresses
- not swap[end].to_address in all_pool_addresses
"""
pool_addrs = [swap.pool_address for swap in swaps]
valid_start_ends: List[Tuple[Swap, Swap]] = []
for potential_start_swap in swaps:
for potential_end_swap in swaps:
if (
potential_start_swap.token_in_address
== potential_end_swap.token_out_address
and potential_start_swap.from_address == potential_end_swap.to_address
and not potential_start_swap.from_address in pool_addrs
):
valid_start_ends.append((potential_start_swap, potential_end_swap))
return valid_start_ends
return None
def _get_all_routes(
start_swap: Swap, end_swap: Swap, other_swaps: List[Swap]
) -> List[List[Swap]]:
"""
Returns all routes (List[Swap]) from start to finish between a start_swap and an end_swap only accounting for token_address_in and token_address_out.
"""
# If the path is complete, return
if start_swap.token_out_address == end_swap.token_in_address:
return [[start_swap, end_swap]]
elif len(other_swaps) == 0:
return []
# Collect all potential next steps, check if valid, recursively find routes from next_step to end_swap
routes: List[List[Swap]] = []
for potential_next_swap in other_swaps:
if start_swap.token_out_address == potential_next_swap.token_in_address and (
start_swap.pool_address == potential_next_swap.from_address
or start_swap.to_address == potential_next_swap.pool_address
or start_swap.to_address == potential_next_swap.from_address
):
remaining_swaps = [
swap for swap in other_swaps if swap != potential_next_swap
]
next_swap_routes = _get_all_routes(
potential_next_swap, end_swap, remaining_swaps
)
if len(next_swap_routes) > 0:
for next_swap_route in next_swap_routes:
next_swap_route.insert(0, start_swap)
routes.append(next_swap_route)
return routes

View File

@ -5,8 +5,9 @@ from sqlalchemy import orm
from web3 import Web3 from web3 import Web3
from mev_inspect.fees import fetch_base_fee_per_gas from mev_inspect.fees import fetch_base_fee_per_gas
from mev_inspect.schemas import Block, Trace, TraceType from mev_inspect.schemas.blocks import Block
from mev_inspect.schemas.receipts import Receipt from mev_inspect.schemas.receipts import Receipt
from mev_inspect.schemas.traces import Trace, TraceType
cache_directory = "./cache" cache_directory = "./cache"

View File

@ -1,6 +1,6 @@
from typing import Dict, Optional, Tuple, Type from typing import Dict, Optional, Tuple, Type
from mev_inspect.schemas.classified_traces import DecodedCallTrace, Protocol from mev_inspect.schemas.traces import DecodedCallTrace, Protocol
from mev_inspect.schemas.classifiers import ClassifierSpec, Classifier from mev_inspect.schemas.classifiers import ClassifierSpec, Classifier
from .aave import AAVE_CLASSIFIER_SPECS from .aave import AAVE_CLASSIFIER_SPECS

View File

@ -1,13 +1,10 @@
from mev_inspect.schemas.classified_traces import (
Protocol,
)
from mev_inspect.schemas.classifiers import ( from mev_inspect.schemas.classifiers import (
ClassifierSpec, ClassifierSpec,
DecodedCallTrace, DecodedCallTrace,
TransferClassifier, TransferClassifier,
LiquidationClassifier, LiquidationClassifier,
) )
from mev_inspect.schemas.traces import Protocol
from mev_inspect.schemas.transfers import Transfer from mev_inspect.schemas.transfers import Transfer

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
DecodedCallTrace, DecodedCallTrace,
Protocol, Protocol,
) )

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
Protocol, Protocol,
) )
from mev_inspect.schemas.classifiers import ( from mev_inspect.schemas.classifiers import (

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
Protocol, Protocol,
) )

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import DecodedCallTrace from mev_inspect.schemas.traces import DecodedCallTrace
from mev_inspect.schemas.classifiers import ( from mev_inspect.schemas.classifiers import (
ClassifierSpec, ClassifierSpec,
TransferClassifier, TransferClassifier,

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
DecodedCallTrace, DecodedCallTrace,
Protocol, Protocol,
) )

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
Protocol, Protocol,
) )
from mev_inspect.schemas.classifiers import ( from mev_inspect.schemas.classifiers import (

View File

@ -1,4 +1,4 @@
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
Protocol, Protocol,
) )
from mev_inspect.schemas.classifiers import ( from mev_inspect.schemas.classifiers import (

View File

@ -2,13 +2,14 @@ from typing import Dict, List, Optional
from mev_inspect.abi import get_abi from mev_inspect.abi import get_abi
from mev_inspect.decode import ABIDecoder from mev_inspect.decode import ABIDecoder
from mev_inspect.schemas.blocks import CallAction, CallResult, Trace, TraceType from mev_inspect.schemas.blocks import CallAction, CallResult
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
Classification, Classification,
ClassifiedTrace, ClassifiedTrace,
CallTrace, CallTrace,
DecodedCallTrace, DecodedCallTrace,
) )
from mev_inspect.schemas.traces import Trace, TraceType
from .specs import ALL_CLASSIFIER_SPECS from .specs import ALL_CLASSIFIER_SPECS

View File

@ -2,7 +2,7 @@ from typing import Dict, List, Optional
from web3 import Web3 from web3 import Web3
from mev_inspect.traces import get_child_traces from mev_inspect.traces import get_child_traces
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
ClassifiedTrace, ClassifiedTrace,
Classification, Classification,
Protocol, Protocol,

View File

@ -1,8 +1,8 @@
import json import json
from typing import List from typing import List
from mev_inspect.models.classified_traces import ClassifiedTraceModel from mev_inspect.models.traces import ClassifiedTraceModel
from mev_inspect.schemas.classified_traces import ClassifiedTrace from mev_inspect.schemas.traces import ClassifiedTrace
def delete_classified_traces_for_block( def delete_classified_traces_for_block(

View File

@ -11,7 +11,7 @@ from mev_inspect.crud.arbitrages import (
delete_arbitrages_for_block, delete_arbitrages_for_block,
write_arbitrages, write_arbitrages,
) )
from mev_inspect.crud.classified_traces import ( from mev_inspect.crud.traces import (
delete_classified_traces_for_block, delete_classified_traces_for_block,
write_classified_traces, write_classified_traces,
) )

View File

@ -1,7 +1,7 @@
from typing import List from typing import List
from mev_inspect.aave_liquidations import get_aave_liquidations from mev_inspect.aave_liquidations import get_aave_liquidations
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
ClassifiedTrace, ClassifiedTrace,
Classification, Classification,
) )

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from mev_inspect.schemas.classified_traces import ClassifiedTrace from mev_inspect.schemas.traces import ClassifiedTrace
from mev_inspect.schemas.miner_payments import MinerPayment from mev_inspect.schemas.miner_payments import MinerPayment
from mev_inspect.schemas.receipts import Receipt from mev_inspect.schemas.receipts import Receipt
from mev_inspect.traces import get_traces_by_transaction_hash from mev_inspect.traces import get_traces_by_transaction_hash

View File

@ -1,2 +0,0 @@
from .abi import ABI
from .blocks import Block, Trace, TraceType

View File

@ -1,11 +1,11 @@
from enum import Enum from typing import List
from typing import List, Optional
from pydantic import validator from pydantic import validator
from mev_inspect.utils import hex_to_int from mev_inspect.utils import hex_to_int
from .receipts import Receipt from .receipts import Receipt
from .traces import Trace
from .utils import CamelModel, Web3Model from .utils import CamelModel, Web3Model
@ -36,27 +36,6 @@ class CallAction(Web3Model):
fields = {"from_": "from"} fields = {"from_": "from"}
class TraceType(Enum):
call = "call"
create = "create"
delegate_call = "delegateCall"
reward = "reward"
suicide = "suicide"
class Trace(CamelModel):
action: dict
block_hash: str
block_number: int
result: Optional[dict]
subtraces: int
trace_address: List[int]
transaction_hash: Optional[str]
transaction_position: Optional[int]
type: TraceType
error: Optional[str]
class Block(Web3Model): class Block(Web3Model):
block_number: int block_number: int
miner: str miner: str

View File

@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Type
from pydantic import BaseModel from pydantic import BaseModel
from .classified_traces import Classification, DecodedCallTrace, Protocol from .traces import Classification, DecodedCallTrace, Protocol
from .transfers import Transfer from .transfers import Transfer

View File

@ -1,6 +1,6 @@
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
class Liquidation(BaseModel): class Liquidation(BaseModel):

View File

@ -2,7 +2,7 @@ from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
class Swap(BaseModel): class Swap(BaseModel):

View File

@ -1,7 +1,28 @@
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from .blocks import Trace from .utils import CamelModel
class TraceType(Enum):
call = "call"
create = "create"
delegate_call = "delegateCall"
reward = "reward"
suicide = "suicide"
class Trace(CamelModel):
action: dict
block_hash: str
block_number: int
result: Optional[dict]
subtraces: int
trace_address: List[int]
transaction_hash: Optional[str]
transaction_position: Optional[int]
type: TraceType
error: Optional[str]
class Classification(Enum): class Classification(Enum):
@ -26,16 +47,13 @@ class Protocol(Enum):
class ClassifiedTrace(Trace): class ClassifiedTrace(Trace):
transaction_hash: str
block_number: int
trace_address: List[int]
classification: Classification classification: Classification
error: Optional[str]
to_address: Optional[str] to_address: Optional[str]
from_address: Optional[str] from_address: Optional[str]
gas: Optional[int] gas: Optional[int]
value: Optional[int] value: Optional[int]
gas_used: Optional[int] gas_used: Optional[int]
transaction_hash: str
protocol: Optional[Protocol] protocol: Optional[Protocol]
function_name: Optional[str] function_name: Optional[str]
function_signature: Optional[str] function_signature: Optional[str]

View File

@ -1,7 +1,7 @@
from typing import List, Optional from typing import List, Optional
from mev_inspect.classifiers.specs import get_classifier from mev_inspect.classifiers.specs import get_classifier
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
ClassifiedTrace, ClassifiedTrace,
Classification, Classification,
DecodedCallTrace, DecodedCallTrace,

View File

@ -1,6 +1,7 @@
from typing import List, Optional from typing import List, Optional
from mev_inspect.schemas import Block, Trace, TraceType from mev_inspect.schemas.blocks import Block
from mev_inspect.schemas.traces import Trace, TraceType
weth_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" weth_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"

View File

@ -1,7 +1,7 @@
from itertools import groupby from itertools import groupby
from typing import Dict, List from typing import Dict, List
from mev_inspect.schemas.classified_traces import ClassifiedTrace from mev_inspect.schemas.traces import ClassifiedTrace
def is_child_trace_address( def is_child_trace_address(

View File

@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Sequence
from mev_inspect.classifiers.specs import get_classifier from mev_inspect.classifiers.specs import get_classifier
from mev_inspect.schemas.classifiers import TransferClassifier from mev_inspect.schemas.classifiers import TransferClassifier
from mev_inspect.schemas.classified_traces import ( from mev_inspect.schemas.traces import (
ClassifiedTrace, ClassifiedTrace,
DecodedCallTrace, DecodedCallTrace,
) )

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
from typing import List, Optional from typing import List, Optional
from mev_inspect.schemas.blocks import TraceType from mev_inspect.schemas.traces import (
from mev_inspect.schemas.classified_traces import (
Classification, Classification,
ClassifiedTrace, ClassifiedTrace,
DecodedCallTrace, DecodedCallTrace,
Protocol, Protocol,
TraceType,
) )

View File

@ -2,7 +2,7 @@ from typing import List
from mev_inspect.aave_liquidations import get_aave_liquidations from mev_inspect.aave_liquidations import get_aave_liquidations
from mev_inspect.schemas.liquidations import Liquidation from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
from mev_inspect.classifiers.trace import TraceClassifier from mev_inspect.classifiers.trace import TraceClassifier
from tests.utils import load_test_block from tests.utils import load_test_block

View File

@ -15,12 +15,47 @@ def test_arbitrage_real_block():
assert len(swaps) == 51 assert len(swaps) == 51
arbitrages = get_arbitrages(list(swaps)) arbitrages = get_arbitrages(list(swaps))
assert len(arbitrages) == 1 assert len(arbitrages) == 2
arbitrage = arbitrages[0] arbitrage_1 = [
arb
for arb in arbitrages
if arb.transaction_hash
== "0x448245bf1a507b73516c4eeee01611927dada6610bf26d403012f2e66800d8f0"
][0]
arbitrage_2 = [
arb
for arb in arbitrages
if arb.transaction_hash
== "0xfcf4558f6432689ea57737fe63124a5ec39fd6ba6aaf198df13a825dd599bffc"
][0]
assert len(arbitrage.swaps) == 3 assert len(arbitrage_1.swaps) == 3
assert ( assert (
arbitrage.profit_token_address == "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" arbitrage_1.profit_token_address == "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
) )
assert arbitrage.profit_amount == 53560707941943273628 assert len(arbitrage_1.swaps) == 3
assert (
arbitrage_1.swaps[1].token_in_address
== "0x25f8087ead173b73d6e8b84329989a8eea16cf73"
)
assert (
arbitrage_1.swaps[1].token_out_address
== "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
)
assert arbitrage_1.profit_amount == 750005273675102326
assert len(arbitrage_2.swaps) == 3
assert (
arbitrage_2.profit_token_address == "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
)
assert len(arbitrage_2.swaps) == 3
assert (
arbitrage_2.swaps[1].token_in_address
== "0x25f8087ead173b73d6e8b84329989a8eea16cf73"
)
assert (
arbitrage_2.swaps[1].token_out_address
== "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
)
assert arbitrage_2.profit_amount == 53560707941943273628

View File

@ -1,9 +1,11 @@
from mev_inspect.arbitrages import get_arbitrages from typing import List
from mev_inspect.arbitrages import get_arbitrages, _get_all_routes
from mev_inspect.schemas.swaps import Swap
from mev_inspect.classifiers.specs.uniswap import ( from mev_inspect.classifiers.specs.uniswap import (
UNISWAP_V2_PAIR_ABI_NAME, UNISWAP_V2_PAIR_ABI_NAME,
UNISWAP_V3_POOL_ABI_NAME, UNISWAP_V3_POOL_ABI_NAME,
) )
from mev_inspect.schemas.swaps import Swap
def test_two_pool_arbitrage(get_transaction_hashes, get_addresses): def test_two_pool_arbitrage(get_transaction_hashes, get_addresses):
@ -17,10 +19,11 @@ def test_two_pool_arbitrage(get_transaction_hashes, get_addresses):
unrelated_pool_address, unrelated_pool_address,
first_token_address, first_token_address,
second_token_address, second_token_address,
] = get_addresses(6) third_token_address,
] = get_addresses(7)
first_token_in_amount = 10 first_token_in_amount = 10
first_token_out_amount = 10 first_token_out_amount = 11
second_token_amount = 15 second_token_amount = 15
arb_swaps = [ arb_swaps = [
@ -62,7 +65,7 @@ def test_two_pool_arbitrage(get_transaction_hashes, get_addresses):
to_address=account_address, to_address=account_address,
token_in_address=second_token_address, token_in_address=second_token_address,
token_in_amount=first_token_in_amount, token_in_amount=first_token_in_amount,
token_out_address=first_token_address, token_out_address=third_token_address,
token_out_amount=first_token_out_amount, token_out_amount=first_token_out_amount,
) )
@ -100,7 +103,7 @@ def test_three_pool_arbitrage(get_transaction_hashes, get_addresses):
] = get_addresses(7) ] = get_addresses(7)
first_token_in_amount = 10 first_token_in_amount = 10
first_token_out_amount = 10 first_token_out_amount = 11
second_token_amount = 15 second_token_amount = 15
third_token_amount = 40 third_token_amount = 40
@ -158,3 +161,70 @@ def test_three_pool_arbitrage(get_transaction_hashes, get_addresses):
assert arbitrage.start_amount == first_token_in_amount assert arbitrage.start_amount == first_token_in_amount
assert arbitrage.end_amount == first_token_out_amount assert arbitrage.end_amount == first_token_out_amount
assert arbitrage.profit_amount == first_token_out_amount - first_token_in_amount assert arbitrage.profit_amount == first_token_out_amount - first_token_in_amount
def test_get_all_routes():
# A -> B, B -> A
start_swap = create_generic_swap("0xa", "0xb")
end_swap = create_generic_swap("0xb", "0xa")
routes = _get_all_routes(start_swap, end_swap, [])
assert len(routes) == 1
# A->B, B->C, C->A
start_swap = create_generic_swap("0xa", "0xb")
other_swaps = [create_generic_swap("0xb", "0xc")]
end_swap = create_generic_swap("0xc", "0xa")
routes = _get_all_routes(start_swap, end_swap, other_swaps)
assert len(routes) == 1
# A->B, B->C, C->A + A->D
other_swaps.append(create_generic_swap("0xa", "0xd"))
routes = _get_all_routes(start_swap, end_swap, other_swaps)
assert len(routes) == 1
# A->B, B->C, C->A + A->D B->E
other_swaps.append(create_generic_swap("0xb", "0xe"))
routes = _get_all_routes(start_swap, end_swap, other_swaps)
assert len(routes) == 1
# A->B, B->A, B->C, C->A
other_swaps = [create_generic_swap("0xb", "0xa"), create_generic_swap("0xb", "0xc")]
routes = _get_all_routes(start_swap, end_swap, other_swaps)
assert len(routes) == 1
expect_simple_route = [["0xa", "0xb"], ["0xb", "0xc"], ["0xc", "0xa"]]
assert len(routes[0]) == len(expect_simple_route)
for i in range(len(expect_simple_route)):
assert expect_simple_route[i][0] == routes[0][i].token_in_address
assert expect_simple_route[i][1] == routes[0][i].token_out_address
# A->B, B->C, C->D, D->A, B->D
end_swap = create_generic_swap("0xd", "0xa")
other_swaps = [
create_generic_swap("0xb", "0xc"),
create_generic_swap("0xc", "0xd"),
create_generic_swap("0xb", "0xd"),
]
routes = _get_all_routes(start_swap, end_swap, other_swaps)
assert len(routes) == 2
def create_generic_swap(
tok_a: str = "0xa",
tok_b: str = "0xb",
amount_a_in: int = 1,
amount_b_out: int = 1,
trace_address: List[int] = [],
):
return Swap(
abi_name=UNISWAP_V3_POOL_ABI_NAME,
transaction_hash="0xfake",
block_number=0,
trace_address=trace_address,
pool_address="0xfake",
from_address="0xfake",
to_address="0xfake",
token_in_address=tok_a,
token_in_amount=amount_a_in,
token_out_address=tok_b,
token_out_amount=amount_b_out,
)

View File

@ -1,6 +1,6 @@
from mev_inspect.compound_liquidations import get_compound_liquidations from mev_inspect.compound_liquidations import get_compound_liquidations
from mev_inspect.schemas.liquidations import Liquidation from mev_inspect.schemas.liquidations import Liquidation
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
from mev_inspect.classifiers.trace import TraceClassifier from mev_inspect.classifiers.trace import TraceClassifier
from tests.utils import load_test_block, load_comp_markets, load_cream_markets from tests.utils import load_test_block, load_comp_markets, load_cream_markets

View File

@ -4,7 +4,7 @@ from mev_inspect.classifiers.specs.uniswap import (
UNISWAP_V2_PAIR_ABI_NAME, UNISWAP_V2_PAIR_ABI_NAME,
UNISWAP_V3_POOL_ABI_NAME, UNISWAP_V3_POOL_ABI_NAME,
) )
from mev_inspect.schemas.classified_traces import Protocol from mev_inspect.schemas.traces import Protocol
from .helpers import ( from .helpers import (
make_unknown_trace, make_unknown_trace,

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from mev_inspect.schemas.classified_traces import ClassifiedTrace from mev_inspect.schemas.traces import ClassifiedTrace
from mev_inspect.traces import is_child_trace_address, get_child_traces from mev_inspect.traces import is_child_trace_address, get_child_traces
from .helpers import make_many_unknown_traces from .helpers import make_many_unknown_traces