Merge 4f2d178ddd2ba8158def2be3affe87d579bbcc5d into ce8179f07e4fb8740b43570aa2c5826447c2af26
This commit is contained in:
commit
30bb1ac402
43
alembic/versions/a46974a623b3_add_jit_liquidity_table.py
Normal file
43
alembic/versions/a46974a623b3_add_jit_liquidity_table.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""add_jit_liquidity_table
|
||||
|
||||
Revision ID: a46974a623b3
|
||||
Revises: 5c5375de15fd
|
||||
Create Date: 2022-05-10 12:36:57.139209
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a46974a623b3"
|
||||
down_revision = "5c5375de15fd"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"jit_liquidity",
|
||||
sa.Column("id", sa.String, primary_key=True),
|
||||
sa.Column("block_number", sa.Numeric(), nullable=False),
|
||||
sa.Column("bot_address", sa.String(42), nullable=True),
|
||||
sa.Column("pool_address", sa.String(42), nullable=False),
|
||||
sa.Column("token0_address", sa.String(42), nullable=True),
|
||||
sa.Column("token1_address", sa.String(42), nullable=True),
|
||||
sa.Column("mint_transaction_hash", sa.String(66), nullable=False),
|
||||
sa.Column("mint_transaction_trace", sa.ARRAY(sa.Integer)),
|
||||
sa.Column("burn_transaction_hash", sa.String(66), nullable=False),
|
||||
sa.Column("burn_transaction_trace", sa.ARRAY(sa.Integer)),
|
||||
sa.Column("mint_token0_amount", sa.Numeric),
|
||||
sa.Column("mint_token1_amount", sa.Numeric),
|
||||
sa.Column("burn_token0_amount", sa.Numeric),
|
||||
sa.Column("burn_token1_amount", sa.Numeric),
|
||||
sa.Column("token0_swap_volume", sa.Numeric),
|
||||
sa.Column("token1_swap_volume", sa.Numeric),
|
||||
)
|
||||
op.create_index("ix_jit_liquidity_block_number", "jit_liquidity", ["block_number"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_jit_liquidity_block_number")
|
||||
op.drop_table("jit_liquidity")
|
@ -0,0 +1,32 @@
|
||||
"""add_jit_liquidity_swaps_join_table
|
||||
|
||||
Revision ID: c77f5db6105e
|
||||
Revises: a46974a623b3
|
||||
Create Date: 2022-05-10 12:37:25.275799
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c77f5db6105e"
|
||||
down_revision = "a46974a623b3"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"jit_liquidity_swaps",
|
||||
sa.Column("created_at", sa.TIMESTAMP, server_default=sa.func.now()),
|
||||
sa.Column("jit_liquidity_id", sa.String(1024), primary_key=True),
|
||||
sa.Column("swap_transaction_hash", sa.String(66), primary_key=True),
|
||||
sa.Column("swap_trace_address", sa.ARRAY(sa.Integer), primary_key=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["jit_liquidity_id"], ["jit_liquidity.id"], ondelete="CASCADE"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("jit_liquidity_swaps")
|
@ -1,9 +1,9 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from mev_inspect.classifiers.helpers import create_swap_from_pool_transfers
|
||||
from mev_inspect.schemas.classifiers import ClassifierSpec, SwapClassifier
|
||||
from mev_inspect.schemas.classifiers import Classifier, ClassifierSpec, SwapClassifier
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
from mev_inspect.schemas.traces import DecodedCallTrace, Protocol
|
||||
from mev_inspect.schemas.traces import Classification, DecodedCallTrace, Protocol
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
|
||||
UNISWAP_V2_PAIR_ABI_NAME = "UniswapV2Pair"
|
||||
@ -42,6 +42,18 @@ class UniswapV2SwapClassifier(SwapClassifier):
|
||||
return swap
|
||||
|
||||
|
||||
class LiquidityMintClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.liquidity_mint
|
||||
|
||||
|
||||
class LiquidityBurnClassifier(Classifier):
|
||||
@staticmethod
|
||||
def get_classification() -> Classification:
|
||||
return Classification.liquidity_burn
|
||||
|
||||
|
||||
UNISWAP_V3_CONTRACT_SPECS = [
|
||||
ClassifierSpec(
|
||||
abi_name="UniswapV3Factory",
|
||||
@ -106,6 +118,8 @@ UNISWAP_V3_GENERAL_SPECS = [
|
||||
protocol=Protocol.uniswap_v3,
|
||||
classifiers={
|
||||
"swap(address,bool,int256,uint160,bytes)": UniswapV3SwapClassifier,
|
||||
"mint(address,int24,int24,uint128,bytes)": LiquidityMintClassifier,
|
||||
"burn(int24,int24,uint128)": LiquidityBurnClassifier,
|
||||
},
|
||||
),
|
||||
ClassifierSpec(
|
||||
|
75
mev_inspect/crud/jit_liquidity.py
Normal file
75
mev_inspect/crud/jit_liquidity.py
Normal file
@ -0,0 +1,75 @@
|
||||
from typing import List
|
||||
from uuid import uuid4
|
||||
|
||||
from mev_inspect.models.jit_liquidity import JITLiquidityModel
|
||||
from mev_inspect.schemas.jit_liquidity import JITLiquidity
|
||||
|
||||
from .shared import delete_by_block_range
|
||||
|
||||
|
||||
def delete_jit_liquidity_for_blocks(
|
||||
db_session,
|
||||
after_block_number: int,
|
||||
before_block_number: int,
|
||||
) -> None:
|
||||
delete_by_block_range(
|
||||
db_session,
|
||||
JITLiquidityModel,
|
||||
after_block_number,
|
||||
before_block_number,
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def write_jit_liquidity(
|
||||
db_session,
|
||||
jit_liquidity_instances: List[JITLiquidity],
|
||||
) -> None:
|
||||
jit_liquidity_models = []
|
||||
swap_jit_liquidity_ids = []
|
||||
|
||||
for jit_liquidity in jit_liquidity_instances:
|
||||
jit_liquidity_id = str(uuid4())
|
||||
jit_liquidity_models.append(
|
||||
JITLiquidityModel(
|
||||
id=jit_liquidity_id,
|
||||
block_number=jit_liquidity.block_number,
|
||||
bot_address=jit_liquidity.bot_address,
|
||||
pool_address=jit_liquidity.pool_address,
|
||||
token0_address=jit_liquidity.token0_address,
|
||||
token1_address=jit_liquidity.token1_address,
|
||||
mint_transaction_hash=jit_liquidity.mint_transaction_hash,
|
||||
mint_transaction_trace=jit_liquidity.mint_trace,
|
||||
burn_transaction_hash=jit_liquidity.burn_transaction_hash,
|
||||
burn_transaction_trace=jit_liquidity.burn_trace,
|
||||
mint_token0_amount=jit_liquidity.mint_token0_amount,
|
||||
mint_token1_amount=jit_liquidity.mint_token1_amount,
|
||||
burn_token0_amount=jit_liquidity.burn_token0_amount,
|
||||
burn_token1_amount=jit_liquidity.burn_token1_amount,
|
||||
token0_swap_volume=jit_liquidity.token0_swap_volume,
|
||||
token1_swap_volume=jit_liquidity.token1_swap_volume,
|
||||
)
|
||||
)
|
||||
|
||||
for swap in jit_liquidity.swaps:
|
||||
swap_jit_liquidity_ids.append(
|
||||
{
|
||||
"jit_liquidity_id": jit_liquidity_id,
|
||||
"swap_transaction_hash": swap.transaction_hash,
|
||||
"swap_trace_address": swap.trace_address,
|
||||
}
|
||||
)
|
||||
|
||||
if len(jit_liquidity_models) > 0:
|
||||
db_session.bulk_save_objects(jit_liquidity_models)
|
||||
db_session.execute(
|
||||
"""
|
||||
INSERT INTO jit_liquidity_swaps
|
||||
(jit_liquidity_id, swap_transaction_hash, swap_trace_address)
|
||||
VALUES
|
||||
(:jit_liquidity_id, :swap_transaction_hash, :swap_trace_address)
|
||||
""",
|
||||
params=swap_jit_liquidity_ids,
|
||||
)
|
||||
|
||||
db_session.commit()
|
@ -9,6 +9,10 @@ from mev_inspect.block import create_from_block_number
|
||||
from mev_inspect.classifiers.trace import TraceClassifier
|
||||
from mev_inspect.crud.arbitrages import delete_arbitrages_for_blocks, write_arbitrages
|
||||
from mev_inspect.crud.blocks import delete_blocks, write_blocks
|
||||
from mev_inspect.crud.jit_liquidity import (
|
||||
delete_jit_liquidity_for_blocks,
|
||||
write_jit_liquidity,
|
||||
)
|
||||
from mev_inspect.crud.liquidations import (
|
||||
delete_liquidations_for_blocks,
|
||||
write_liquidations,
|
||||
@ -34,6 +38,7 @@ from mev_inspect.crud.traces import (
|
||||
write_classified_traces,
|
||||
)
|
||||
from mev_inspect.crud.transfers import delete_transfers_for_blocks, write_transfers
|
||||
from mev_inspect.jit_liquidity import get_jit_liquidity
|
||||
from mev_inspect.liquidations import get_liquidations
|
||||
from mev_inspect.miner_payments import get_miner_payments
|
||||
from mev_inspect.nft_trades import get_nft_trades
|
||||
@ -41,6 +46,7 @@ from mev_inspect.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_
|
||||
from mev_inspect.sandwiches import get_sandwiches
|
||||
from mev_inspect.schemas.arbitrages import Arbitrage
|
||||
from mev_inspect.schemas.blocks import Block
|
||||
from mev_inspect.schemas.jit_liquidity import JITLiquidity
|
||||
from mev_inspect.schemas.liquidations import Liquidation
|
||||
from mev_inspect.schemas.miner_payments import MinerPayment
|
||||
from mev_inspect.schemas.nft_trades import NftTrade
|
||||
@ -100,6 +106,7 @@ async def inspect_many_blocks(
|
||||
all_miner_payments: List[MinerPayment] = []
|
||||
|
||||
all_nft_trades: List[NftTrade] = []
|
||||
all_jit_liquidity: List[JITLiquidity] = []
|
||||
|
||||
for block_number in range(after_block_number, before_block_number):
|
||||
block = await create_from_block_number(
|
||||
@ -149,6 +156,11 @@ async def inspect_many_blocks(
|
||||
nft_trades = get_nft_trades(classified_traces)
|
||||
logger.info(f"Block: {block_number} -- Found {len(nft_trades)} nft trades")
|
||||
|
||||
jit_liquidity = get_jit_liquidity(classified_traces, swaps)
|
||||
logger.info(
|
||||
f"Block: {block_number} -- Found {len(jit_liquidity)} jit liquidity instances"
|
||||
)
|
||||
|
||||
miner_payments = get_miner_payments(
|
||||
block.miner, block.base_fee_per_gas, classified_traces, block.receipts
|
||||
)
|
||||
@ -167,6 +179,8 @@ async def inspect_many_blocks(
|
||||
|
||||
all_nft_trades.extend(nft_trades)
|
||||
|
||||
all_jit_liquidity.extend(jit_liquidity)
|
||||
|
||||
all_miner_payments.extend(miner_payments)
|
||||
|
||||
logger.info("Writing data")
|
||||
@ -222,6 +236,11 @@ async def inspect_many_blocks(
|
||||
)
|
||||
write_nft_trades(inspect_db_session, all_nft_trades)
|
||||
|
||||
delete_jit_liquidity_for_blocks(
|
||||
inspect_db_session, after_block_number, before_block_number
|
||||
)
|
||||
write_jit_liquidity(inspect_db_session, all_jit_liquidity)
|
||||
|
||||
delete_miner_payments_for_blocks(
|
||||
inspect_db_session, after_block_number, before_block_number
|
||||
)
|
||||
|
276
mev_inspect/jit_liquidity.py
Normal file
276
mev_inspect/jit_liquidity.py
Normal file
@ -0,0 +1,276 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mev_inspect.schemas.jit_liquidity import JITLiquidity
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
from mev_inspect.schemas.traces import (
|
||||
Classification,
|
||||
ClassifiedTrace,
|
||||
DecodedCallTrace,
|
||||
Protocol,
|
||||
)
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.traces import get_traces_by_transaction_hash, is_child_trace_address
|
||||
from mev_inspect.transfers import get_net_transfers
|
||||
|
||||
LIQUIDITY_MINT_ROUTERS = [
|
||||
"0xC36442b4a4522E871399CD717aBDD847Ab11FE88".lower(), # Uniswap V3 NFT Position Manager
|
||||
]
|
||||
|
||||
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
class JITTransferInfo(BaseModel):
|
||||
token0_address: str
|
||||
token1_address: str
|
||||
mint_token0: int
|
||||
mint_token1: int
|
||||
burn_token0: int
|
||||
burn_token1: int
|
||||
error: bool
|
||||
|
||||
|
||||
def get_jit_liquidity(
|
||||
classified_traces: List[ClassifiedTrace], swaps: List[Swap]
|
||||
) -> List[JITLiquidity]:
|
||||
jit_liquidity_instances: List[JITLiquidity] = []
|
||||
|
||||
for index, trace in enumerate(classified_traces):
|
||||
|
||||
if not isinstance(trace, DecodedCallTrace):
|
||||
continue
|
||||
|
||||
if (
|
||||
trace.classification == Classification.liquidity_mint
|
||||
and trace.protocol == Protocol.uniswap_v3
|
||||
):
|
||||
for search_trace in classified_traces[index:]:
|
||||
if (
|
||||
search_trace.classification == Classification.liquidity_burn
|
||||
and search_trace.to_address == trace.to_address
|
||||
and search_trace.transaction_hash != trace.transaction_hash
|
||||
):
|
||||
if (search_trace.error is not None) or (trace.error is not None):
|
||||
continue
|
||||
|
||||
bot_address = _get_bot_address(trace, classified_traces)
|
||||
transfer_info: JITTransferInfo = _get_transfer_info(
|
||||
classified_traces,
|
||||
trace,
|
||||
search_trace,
|
||||
)
|
||||
jit_swaps, token0_volume, token1_volume = _get_swap_info(
|
||||
swaps, trace, search_trace, transfer_info.token0_address
|
||||
)
|
||||
|
||||
# -- Error Checking Section --
|
||||
if transfer_info.error or len(jit_swaps) == 0:
|
||||
continue
|
||||
|
||||
jit_liquidity_instances.append(
|
||||
JITLiquidity(
|
||||
block_number=trace.block_number,
|
||||
bot_address=bot_address,
|
||||
pool_address=trace.to_address,
|
||||
mint_transaction_hash=trace.transaction_hash,
|
||||
mint_trace=trace.trace_address,
|
||||
burn_transaction_hash=search_trace.transaction_hash,
|
||||
burn_trace=search_trace.trace_address,
|
||||
swaps=jit_swaps,
|
||||
token0_address=transfer_info.token0_address,
|
||||
token1_address=transfer_info.token1_address,
|
||||
mint_token0_amount=transfer_info.mint_token0,
|
||||
mint_token1_amount=transfer_info.mint_token1,
|
||||
burn_token0_amount=transfer_info.burn_token0,
|
||||
burn_token1_amount=transfer_info.burn_token1,
|
||||
token0_swap_volume=token0_volume,
|
||||
token1_swap_volume=token1_volume,
|
||||
)
|
||||
)
|
||||
|
||||
return jit_liquidity_instances
|
||||
|
||||
|
||||
def _get_token_order(token_a: str, token_b: str) -> Tuple[str, str]:
|
||||
token_order = True if int(token_a, 16) < int(token_b, 16) else False
|
||||
return (token_a, token_b) if token_order else (token_b, token_a)
|
||||
|
||||
|
||||
def _get_swap_info(
|
||||
swaps: List[Swap],
|
||||
mint_trace: ClassifiedTrace,
|
||||
burn_trace: ClassifiedTrace,
|
||||
token0_address: str,
|
||||
) -> Tuple[List[Swap], int, int]:
|
||||
jit_swaps: List[Swap] = []
|
||||
token0_swap_volume, token1_swap_volume = 0, 0
|
||||
|
||||
ordered_swaps = sorted(
|
||||
swaps, key=lambda s: (s.transaction_position, s.trace_address)
|
||||
)
|
||||
|
||||
for swap in ordered_swaps:
|
||||
if swap.transaction_position <= mint_trace.transaction_position:
|
||||
continue
|
||||
if swap.transaction_position >= burn_trace.transaction_position:
|
||||
break
|
||||
if swap.contract_address == mint_trace.to_address:
|
||||
jit_swaps.append(swap)
|
||||
token0_swap_volume += (
|
||||
swap.token_in_amount if swap.token_in_address == token0_address else 0
|
||||
)
|
||||
token1_swap_volume += (
|
||||
0 if swap.token_in_address == token0_address else swap.token_in_amount
|
||||
)
|
||||
|
||||
return jit_swaps, token0_swap_volume, token1_swap_volume
|
||||
|
||||
|
||||
def _get_transfer_info(
|
||||
classified_traces: List[ClassifiedTrace],
|
||||
mint_trace: ClassifiedTrace,
|
||||
burn_trace: ClassifiedTrace,
|
||||
) -> JITTransferInfo:
|
||||
|
||||
grouped_traces = get_traces_by_transaction_hash(classified_traces)
|
||||
mint_net_transfers, burn_net_transfers = [], []
|
||||
pool_address = mint_trace.to_address
|
||||
|
||||
for transfer in get_net_transfers(grouped_traces[mint_trace.transaction_hash]):
|
||||
if transfer.to_address == pool_address:
|
||||
mint_net_transfers.append(transfer)
|
||||
|
||||
for transfer in get_net_transfers(grouped_traces[burn_trace.transaction_hash]):
|
||||
if transfer.from_address == pool_address:
|
||||
burn_net_transfers.append(transfer)
|
||||
|
||||
mint_len, burn_len = len(mint_net_transfers), len(burn_net_transfers)
|
||||
|
||||
if mint_len == 2 and burn_len == 2:
|
||||
return _parse_standard_liquidity(mint_net_transfers, burn_net_transfers)
|
||||
|
||||
elif (mint_len == 2 and burn_len == 1) or (mint_len == 1 and burn_len == 2):
|
||||
return _parse_liquidity_limit_order(mint_net_transfers, burn_net_transfers)
|
||||
|
||||
else:
|
||||
return JITTransferInfo(
|
||||
token0_address=ZERO_ADDRESS,
|
||||
token1_address=ZERO_ADDRESS,
|
||||
mint_token0=0,
|
||||
mint_token1=0,
|
||||
burn_token0=0,
|
||||
burn_token1=0,
|
||||
error=True,
|
||||
)
|
||||
|
||||
|
||||
def _get_bot_address(
|
||||
mint_trace: ClassifiedTrace,
|
||||
classified_traces: List[ClassifiedTrace],
|
||||
) -> str:
|
||||
if "from_address" in mint_trace.dict().keys():
|
||||
|
||||
if mint_trace.from_address in LIQUIDITY_MINT_ROUTERS:
|
||||
bot_trace = list(
|
||||
filter(
|
||||
lambda t: t.to_address == mint_trace.from_address
|
||||
and t.transaction_hash == mint_trace.transaction_hash,
|
||||
classified_traces,
|
||||
)
|
||||
)
|
||||
if len(bot_trace) == 1 or is_child_trace_address(
|
||||
bot_trace[1].trace_address, bot_trace[0].trace_address
|
||||
):
|
||||
return _get_bot_address(bot_trace[0], classified_traces)
|
||||
else:
|
||||
return ZERO_ADDRESS
|
||||
|
||||
elif type(mint_trace.from_address) == str:
|
||||
return mint_trace.from_address
|
||||
|
||||
return ZERO_ADDRESS
|
||||
|
||||
|
||||
def _parse_standard_liquidity(
|
||||
mint_net_transfers: List[Transfer],
|
||||
burn_net_transfers: List[Transfer],
|
||||
) -> JITTransferInfo:
|
||||
token0_address, token1_address = _get_token_order(
|
||||
mint_net_transfers[0].token_address, mint_net_transfers[1].token_address
|
||||
)
|
||||
|
||||
mint_token0, mint_token1 = _parse_token_amounts(token0_address, mint_net_transfers)
|
||||
|
||||
burn_token0, burn_token1 = _parse_token_amounts(token0_address, burn_net_transfers)
|
||||
return JITTransferInfo(
|
||||
token0_address=token0_address,
|
||||
token1_address=token1_address,
|
||||
mint_token0=mint_token0,
|
||||
mint_token1=mint_token1,
|
||||
burn_token0=burn_token0,
|
||||
burn_token1=burn_token1,
|
||||
error=False,
|
||||
)
|
||||
|
||||
|
||||
def _parse_liquidity_limit_order(
|
||||
mint_net_transfers: List[Transfer],
|
||||
burn_net_transfers: List[Transfer],
|
||||
) -> JITTransferInfo:
|
||||
try:
|
||||
token0_address, token1_address = _get_token_order(
|
||||
burn_net_transfers[0].token_address, burn_net_transfers[1].token_address
|
||||
)
|
||||
except IndexError:
|
||||
token0_address, token1_address = _get_token_order(
|
||||
mint_net_transfers[0].token_address, mint_net_transfers[1].token_address
|
||||
)
|
||||
|
||||
if len(mint_net_transfers) < 2:
|
||||
if token0_address == mint_net_transfers[0].token_address:
|
||||
mint_token0 = mint_net_transfers[0].amount
|
||||
mint_token1 = 0
|
||||
else:
|
||||
mint_token0 = 0
|
||||
mint_token1 = mint_net_transfers[0].amount
|
||||
|
||||
burn_token0, burn_token1 = _parse_token_amounts(
|
||||
token0_address, burn_net_transfers
|
||||
)
|
||||
|
||||
else:
|
||||
if token0_address == burn_net_transfers[0].token_address:
|
||||
burn_token0 = burn_net_transfers[0].amount
|
||||
burn_token1 = 0
|
||||
else:
|
||||
burn_token0 = 0
|
||||
burn_token1 = burn_net_transfers[0].amount
|
||||
|
||||
mint_token0, mint_token1 = _parse_token_amounts(
|
||||
token0_address, mint_net_transfers
|
||||
)
|
||||
|
||||
return JITTransferInfo(
|
||||
token0_address=token0_address,
|
||||
token1_address=token1_address,
|
||||
mint_token0=mint_token0,
|
||||
mint_token1=mint_token1,
|
||||
burn_token0=burn_token0,
|
||||
burn_token1=burn_token1,
|
||||
error=False,
|
||||
)
|
||||
|
||||
|
||||
def _parse_token_amounts(
|
||||
token0_address: str, net_transfers: List[Transfer]
|
||||
) -> Tuple[int, int]:
|
||||
if token0_address == net_transfers[0].token_address:
|
||||
token0_amount = net_transfers[0].amount
|
||||
token1_amount = net_transfers[1].amount
|
||||
|
||||
else:
|
||||
token0_amount = net_transfers[1].amount
|
||||
token1_amount = net_transfers[0].amount
|
||||
|
||||
return token0_amount, token1_amount
|
24
mev_inspect/models/jit_liquidity.py
Normal file
24
mev_inspect/models/jit_liquidity.py
Normal file
@ -0,0 +1,24 @@
|
||||
from sqlalchemy import ARRAY, Column, Integer, Numeric, String
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class JITLiquidityModel(Base):
|
||||
__tablename__ = "jit_liquidity"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
block_number = Column(Numeric(), nullable=False)
|
||||
bot_address = Column(String(42), nullable=True)
|
||||
pool_address = Column(String(42), nullable=False)
|
||||
token0_address = Column(String(42), nullable=True)
|
||||
token1_address = Column(String(42), nullable=True)
|
||||
mint_transaction_hash = Column(String(66), nullable=False)
|
||||
mint_transaction_trace = Column(ARRAY(Integer), nullable=False)
|
||||
burn_transaction_hash = Column(String(66), nullable=False)
|
||||
burn_transaction_trace = Column(ARRAY(Integer), nullable=False)
|
||||
mint_token0_amount = Column(Numeric, nullable=False)
|
||||
mint_token1_amount = Column(Numeric, nullable=False)
|
||||
burn_token0_amount = Column(Numeric, nullable=False)
|
||||
burn_token1_amount = Column(Numeric, nullable=False)
|
||||
token0_swap_volume = Column(Numeric, nullable=True)
|
||||
token1_swap_volume = Column(Numeric, nullable=True)
|
24
mev_inspect/schemas/jit_liquidity.py
Normal file
24
mev_inspect/schemas/jit_liquidity.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import List, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .swaps import Swap
|
||||
|
||||
|
||||
class JITLiquidity(BaseModel):
|
||||
block_number: int
|
||||
bot_address: Union[str, None]
|
||||
pool_address: str
|
||||
mint_transaction_hash: str
|
||||
mint_trace: List[int]
|
||||
burn_transaction_hash: str
|
||||
burn_trace: List[int]
|
||||
swaps: List[Swap]
|
||||
token0_address: str
|
||||
token1_address: str
|
||||
mint_token0_amount: int
|
||||
mint_token1_amount: int
|
||||
burn_token0_amount: int
|
||||
burn_token1_amount: int
|
||||
token0_swap_volume: int
|
||||
token1_swap_volume: int
|
@ -34,6 +34,8 @@ class Classification(Enum):
|
||||
punk_bid = "punk_bid"
|
||||
punk_accept_bid = "punk_accept_bid"
|
||||
nft_trade = "nft_trade"
|
||||
liquidity_mint = "liquidity_mint"
|
||||
liquidity_burn = "liquidity_burn"
|
||||
|
||||
|
||||
class Protocol(Enum):
|
||||
|
@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Sequence
|
||||
from mev_inspect.classifiers.specs import get_classifier
|
||||
from mev_inspect.schemas.classifiers import TransferClassifier
|
||||
from mev_inspect.schemas.prices import ETH_TOKEN_ADDRESS
|
||||
from mev_inspect.schemas.traces import ClassifiedTrace, DecodedCallTrace
|
||||
from mev_inspect.schemas.traces import Classification, ClassifiedTrace, DecodedCallTrace
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.traces import get_child_traces, is_child_trace_address
|
||||
|
||||
@ -126,3 +126,100 @@ def remove_child_transfers_of_transfers(
|
||||
] = existing_addresses + [transfer.trace_address]
|
||||
|
||||
return updated_transfers
|
||||
|
||||
|
||||
def get_net_transfers(
|
||||
classified_traces: List[ClassifiedTrace],
|
||||
) -> List[Transfer]:
|
||||
"""
|
||||
Returns the net transfers per transaction from a list of Classified Traces.
|
||||
If A transfers 200WETH to B ,and later in the transaction, B transfers 50WETH to A,
|
||||
the following transfer would be returned (from_address=A, to_address=B, amount=150)
|
||||
|
||||
If B transferred 300WETH to A, the following would be returned
|
||||
(from_address=contract, to_address=bot, amount=100)
|
||||
|
||||
If B transferred 200WETH to A, no transfer would be returned
|
||||
@param classified_traces:
|
||||
@return: List of Transfer objects representing the net movement of tokens
|
||||
"""
|
||||
found_transfers: List[list] = []
|
||||
return_transfers: List[Transfer] = []
|
||||
for trace in classified_traces:
|
||||
if not isinstance(trace, DecodedCallTrace):
|
||||
continue
|
||||
|
||||
if trace.classification == Classification.transfer:
|
||||
if trace.from_address in [
|
||||
t.token_address for t in return_transfers
|
||||
]: # Proxy Case
|
||||
continue
|
||||
|
||||
if trace.function_signature == "transfer(address,uint256)":
|
||||
net_search_info = {
|
||||
"to_address": trace.inputs["recipient"],
|
||||
"token_address": trace.to_address,
|
||||
"from_address": trace.from_address,
|
||||
}
|
||||
|
||||
elif trace.function_signature == "transferFrom(address,address,uint256)":
|
||||
net_search_info = {
|
||||
"to_address": trace.inputs["recipient"],
|
||||
"token_address": trace.to_address,
|
||||
"from_address": trace.inputs["sender"],
|
||||
}
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
if sorted(list(net_search_info.values())) in found_transfers:
|
||||
for index, transfer in enumerate(return_transfers):
|
||||
if (
|
||||
transfer.token_address != net_search_info["token_address"]
|
||||
or transfer.transaction_hash != trace.transaction_hash
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
transfer.from_address == net_search_info["from_address"]
|
||||
and transfer.to_address == net_search_info["to_address"]
|
||||
):
|
||||
return_transfers[index].amount += trace.inputs["amount"]
|
||||
return_transfers[index].trace_address = [-1]
|
||||
if (
|
||||
transfer.from_address == net_search_info["to_address"]
|
||||
and transfer.to_address == net_search_info["from_address"]
|
||||
):
|
||||
return_transfers[index].amount -= trace.inputs["amount"]
|
||||
return_transfers[index].trace_address = [-1]
|
||||
|
||||
else:
|
||||
return_transfers.append(
|
||||
Transfer(
|
||||
block_number=trace.block_number,
|
||||
transaction_hash=trace.transaction_hash,
|
||||
trace_address=trace.trace_address,
|
||||
from_address=net_search_info["from_address"],
|
||||
to_address=net_search_info["to_address"],
|
||||
amount=trace.inputs["amount"],
|
||||
token_address=net_search_info["token_address"],
|
||||
)
|
||||
)
|
||||
found_transfers.append(sorted(list(net_search_info.values())))
|
||||
|
||||
process_index = -1
|
||||
while True:
|
||||
process_index += 1
|
||||
try:
|
||||
transfer = return_transfers[process_index]
|
||||
except IndexError:
|
||||
break
|
||||
if transfer.amount < 0:
|
||||
return_transfers[process_index].from_address = transfer.to_address
|
||||
return_transfers[process_index].to_address = transfer.from_address
|
||||
return_transfers[process_index].amount = transfer.amount * -1
|
||||
if transfer.amount == 0:
|
||||
return_transfers.pop(process_index)
|
||||
process_index -= 1
|
||||
|
||||
return return_transfers
|
||||
|
1
tests/blocks/13601096.json
Normal file
1
tests/blocks/13601096.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/14621812.json
Normal file
1
tests/blocks/14621812.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/14643923.json
Normal file
1
tests/blocks/14643923.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/blocks/14685550.json
Normal file
1
tests/blocks/14685550.json
Normal file
File diff suppressed because one or more lines are too long
188
tests/test_jit_liquidity.py
Normal file
188
tests/test_jit_liquidity.py
Normal file
@ -0,0 +1,188 @@
|
||||
from mev_inspect.classifiers.trace import TraceClassifier
|
||||
from mev_inspect.jit_liquidity import get_jit_liquidity
|
||||
from mev_inspect.schemas.jit_liquidity import JITLiquidity
|
||||
from mev_inspect.schemas.swaps import Swap
|
||||
from mev_inspect.schemas.traces import Protocol
|
||||
from mev_inspect.swaps import get_swaps
|
||||
|
||||
from .utils import load_test_block
|
||||
|
||||
|
||||
def test_single_sandwich_jit_liquidity_WETH_USDC(trace_classifier: TraceClassifier):
|
||||
test_block = load_test_block(13601096)
|
||||
classified_traces = trace_classifier.classify(test_block.traces)
|
||||
swaps = get_swaps(classified_traces)
|
||||
jit_liquidity_instances = get_jit_liquidity(classified_traces, swaps)
|
||||
|
||||
jit_swap = Swap( # Double check these values
|
||||
abi_name="UniswapV3Pool",
|
||||
transaction_hash="0x943131400defa5db902b1df4ab5108b58527e525da3d507bd6e6465d88fa079c".lower(),
|
||||
transaction_position=1,
|
||||
block_number=13601096,
|
||||
trace_address=[7, 0, 12, 1, 0],
|
||||
contract_address="0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".lower(),
|
||||
from_address="0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD".lower(),
|
||||
to_address="0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD".lower(),
|
||||
token_in_address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".lower(), # USDC Contract
|
||||
token_in_amount=1896817745609,
|
||||
token_out_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
token_out_amount=408818202022592862626,
|
||||
protocol=Protocol.uniswap_v3,
|
||||
)
|
||||
expected_jit_liquidity = [
|
||||
JITLiquidity(
|
||||
block_number=13601096,
|
||||
bot_address="0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF".lower(),
|
||||
pool_address="0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".lower(),
|
||||
mint_transaction_hash="0x80e4abcb0b701e9d2c0d0fd216ef22eca5fc13904e8c7b3967bcad997480d638".lower(),
|
||||
mint_trace=[0, 9, 1],
|
||||
burn_transaction_hash="0x12b3d1f0e29d9093d8f3c7cce2da95edbef01aaab3794237f263da85c37c7d27".lower(),
|
||||
burn_trace=[0, 1, 0],
|
||||
swaps=[jit_swap],
|
||||
token0_address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".lower(),
|
||||
token1_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
mint_token0_amount=10864608891029,
|
||||
mint_token1_amount=8281712219747858010668,
|
||||
burn_token0_amount=12634177387879,
|
||||
burn_token1_amount=7900319851971188832064,
|
||||
token0_swap_volume=1896817745609,
|
||||
token1_swap_volume=0,
|
||||
)
|
||||
]
|
||||
|
||||
assert expected_jit_liquidity == jit_liquidity_instances
|
||||
|
||||
|
||||
def test_single_sandwich_jit_liquidity_CRV_WETH(trace_classifier: TraceClassifier):
|
||||
test_block = load_test_block(14621812)
|
||||
classified_traces = trace_classifier.classify(test_block.traces)
|
||||
swaps = get_swaps(classified_traces)
|
||||
jit_liquidity_instances = get_jit_liquidity(classified_traces, swaps)
|
||||
|
||||
jit_swap = Swap( # Double check these values
|
||||
abi_name="UniswapV3Pool",
|
||||
transaction_hash="0x37e84f64698fe1a4852370b4d043491d5f96078d7c69e52f973932bc15ce8617".lower(),
|
||||
transaction_position=5,
|
||||
block_number=14621812,
|
||||
trace_address=[0, 1],
|
||||
contract_address="0x4c83A7f819A5c37D64B4c5A2f8238Ea082fA1f4e".lower(),
|
||||
from_address="0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45".lower(),
|
||||
to_address="0x1d9d04bf507b86fea6c13a412f3bff40eeb64e96".lower(),
|
||||
token_in_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(), # USDC Contract
|
||||
token_in_amount=6206673612383009024,
|
||||
token_out_address="0xD533a949740bb3306d119CC777fa900bA034cd52".lower(),
|
||||
token_out_amount=8111771836975942396605,
|
||||
protocol=Protocol.uniswap_v3,
|
||||
)
|
||||
expected_jit_liquidity = [
|
||||
JITLiquidity(
|
||||
block_number=14621812,
|
||||
bot_address="0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF".lower(),
|
||||
pool_address="0x4c83A7f819A5c37D64B4c5A2f8238Ea082fA1f4e".lower(),
|
||||
mint_transaction_hash="0xdcb5eac97a6bcade485ee3dc8be0f7d8722e6ebacb3910fb31dea30ff40e694e".lower(),
|
||||
mint_trace=[0, 9, 1],
|
||||
burn_transaction_hash="0x499021c4f87facfffc08d143539019d8638c924a4786de15be99567ab6026b98".lower(),
|
||||
burn_trace=[0, 1, 0],
|
||||
swaps=[jit_swap],
|
||||
token0_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
token1_address="0xD533a949740bb3306d119CC777fa900bA034cd52".lower(),
|
||||
mint_token0_amount=324305525132652136497,
|
||||
mint_token1_amount=182991368595201974557004,
|
||||
burn_token0_amount=330104892856183548121,
|
||||
burn_token1_amount=175411922548908668697796,
|
||||
token0_swap_volume=6206673612383009024,
|
||||
token1_swap_volume=0,
|
||||
)
|
||||
]
|
||||
|
||||
assert jit_liquidity_instances == expected_jit_liquidity
|
||||
|
||||
|
||||
def test_single_mint_token_WETH_APE(trace_classifier):
|
||||
test_block = load_test_block(14643923)
|
||||
classified_traces = trace_classifier.classify(test_block.traces)
|
||||
swaps = get_swaps(classified_traces)
|
||||
jit_liquidity_instances = get_jit_liquidity(classified_traces, swaps)
|
||||
|
||||
jit_swap = Swap( # Double check these values
|
||||
abi_name="UniswapV3Pool",
|
||||
transaction_hash="0x43f9656e051a8e3b37f66668851922c6e8e4749d5a7aad605f21119cde541e49".lower(),
|
||||
transaction_position=4,
|
||||
block_number=14643923,
|
||||
trace_address=[1, 0, 1, 0, 1, 0, 0],
|
||||
contract_address="0xac4b3dacb91461209ae9d41ec517c2b9cb1b7daf".lower(),
|
||||
from_address="0x74de5d4fcbf63e00296fd95d33236b9794016631".lower(),
|
||||
to_address="0xdef1c0ded9bec7f1a1670819833240f027b25eff".lower(),
|
||||
token_in_address="0x4d224452801aced8b2f0aebe155379bb5d594381".lower(), # USDC Contract
|
||||
token_in_amount=6522531010660457256888,
|
||||
token_out_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
token_out_amount=36485453136086109896,
|
||||
protocol=Protocol.uniswap_v3,
|
||||
)
|
||||
expected_jit_liquidity = [
|
||||
JITLiquidity(
|
||||
block_number=14643923,
|
||||
bot_address="0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF".lower(),
|
||||
pool_address="0xac4b3dacb91461209ae9d41ec517c2b9cb1b7daf".lower(),
|
||||
mint_transaction_hash="0x003e36cb5d78924c5beaeef15db00cad94009856fe483a031d52ae975557ef53".lower(),
|
||||
mint_trace=[0, 7, 1],
|
||||
burn_transaction_hash="0xec9b2988f6c88968250c3904f6d2d6573f7284cb422b8022a14b7f0dac546348".lower(),
|
||||
burn_trace=[0, 1, 0],
|
||||
swaps=[jit_swap],
|
||||
token0_address="0x4d224452801aced8b2f0aebe155379bb5d594381".lower(),
|
||||
token1_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
mint_token0_amount=0,
|
||||
mint_token1_amount=9073930631365320229693,
|
||||
burn_token0_amount=2424427669988518000798,
|
||||
burn_token1_amount=9060377725722224517671,
|
||||
token0_swap_volume=6522531010660457256888,
|
||||
token1_swap_volume=0,
|
||||
)
|
||||
]
|
||||
|
||||
assert jit_liquidity_instances == expected_jit_liquidity
|
||||
|
||||
|
||||
def test_single_mint_token_jit_ENS_WETH(trace_classifier):
|
||||
test_block = load_test_block(14685550)
|
||||
classified_traces = trace_classifier.classify(test_block.traces)
|
||||
swaps = get_swaps(classified_traces)
|
||||
jit_liquidity_instances = get_jit_liquidity(classified_traces, swaps)
|
||||
|
||||
jit_swap = Swap( # Double check these values
|
||||
abi_name="UniswapV3Pool",
|
||||
transaction_hash="0xeb9dad13e389ee87d656e9d2ca127061a430b9ccb2dd903a840737c979459aa3".lower(),
|
||||
transaction_position=2,
|
||||
block_number=14685550,
|
||||
trace_address=[17],
|
||||
contract_address="0x92560c178ce069cc014138ed3c2f5221ba71f58a".lower(),
|
||||
from_address="0x36e9b6e7fadc7b8ee289c8a24ad96573cda3d7d9".lower(),
|
||||
to_address="0x36e9b6e7fadc7b8ee289c8a24ad96573cda3d7d9".lower(),
|
||||
token_in_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(), # USDC Contract
|
||||
token_in_amount=25467887766287027275,
|
||||
token_out_address="0xc18360217d8f7ab5e7c516566761ea12ce7f9d72".lower(),
|
||||
token_out_amount=3577729807778124677068,
|
||||
protocol=Protocol.uniswap_v3,
|
||||
)
|
||||
expected_jit_liquidity = [
|
||||
JITLiquidity(
|
||||
block_number=14685550,
|
||||
bot_address="0xa57Bd00134B2850B2a1c55860c9e9ea100fDd6CF".lower(),
|
||||
pool_address="0x92560c178ce069cc014138ed3c2f5221ba71f58a".lower(),
|
||||
mint_transaction_hash="0x1af86b40349a9fdaab5b1290d8fba532c2eefdd13d0ed22e825a45e437a000a4".lower(),
|
||||
mint_trace=[0, 7, 1],
|
||||
burn_transaction_hash="0x3265ce7a2d2c6ca796a87c4904f67324715a9381d6d2200690bfa30c55f238fb".lower(),
|
||||
burn_trace=[0, 1, 0],
|
||||
swaps=[jit_swap],
|
||||
token0_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".lower(),
|
||||
token1_address="0xc18360217d8f7ab5e7c516566761ea12ce7f9d72".lower(),
|
||||
mint_token0_amount=0,
|
||||
mint_token1_amount=2928204597556117752715,
|
||||
burn_token0_amount=17321179792304275130,
|
||||
burn_token1_amount=496888833716052284320,
|
||||
token0_swap_volume=25467887766287027275,
|
||||
token1_swap_volume=0,
|
||||
)
|
||||
]
|
||||
|
||||
assert jit_liquidity_instances == expected_jit_liquidity
|
@ -1,5 +1,6 @@
|
||||
from mev_inspect.schemas.traces import Classification, DecodedCallTrace, TraceType
|
||||
from mev_inspect.schemas.transfers import Transfer
|
||||
from mev_inspect.transfers import remove_child_transfers_of_transfers
|
||||
from mev_inspect.transfers import get_net_transfers, remove_child_transfers_of_transfers
|
||||
|
||||
|
||||
def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_addresses):
|
||||
@ -67,6 +68,59 @@ def test_remove_child_transfers_of_transfers(get_transaction_hashes, get_address
|
||||
assert _equal_ignoring_order(removed_transfers, expected_transfers)
|
||||
|
||||
|
||||
def test_net_transfers_same_token(get_addresses):
|
||||
|
||||
alice_address, bob_address, token_address = get_addresses(3)
|
||||
transfer_alice_to_bob = DecodedCallTrace(
|
||||
block_number=123,
|
||||
transaction_hash="net_transfer_tx_hash",
|
||||
block_hash="block_hash",
|
||||
transaction_position=123,
|
||||
type=TraceType.call,
|
||||
action={},
|
||||
functionName="transfer",
|
||||
abiName="UniswapV3Pool",
|
||||
subtraces=123,
|
||||
trace_address=[0],
|
||||
classification=Classification.transfer,
|
||||
function_signature="transfer(address,uint256)",
|
||||
from_address=alice_address,
|
||||
to_address=token_address,
|
||||
inputs={"recipient": bob_address, "amount": 700},
|
||||
)
|
||||
transfer_bob_to_alice = DecodedCallTrace(
|
||||
block_number=123,
|
||||
transaction_hash="net_transfer_tx_hash",
|
||||
block_hash="block_hash",
|
||||
transaction_position=123,
|
||||
type=TraceType.call,
|
||||
action={},
|
||||
functionName="transfer",
|
||||
abiName="UniswapV3Pool",
|
||||
subtraces=123,
|
||||
trace_address=[3],
|
||||
classification=Classification.transfer,
|
||||
function_signature="transfer(address,uint256)",
|
||||
from_address=bob_address,
|
||||
to_address=token_address,
|
||||
inputs={"recipient": alice_address, "amount": 200},
|
||||
)
|
||||
|
||||
expected_transfer = Transfer(
|
||||
block_number=123,
|
||||
transaction_hash="net_transfer_tx_hash",
|
||||
trace_address=[-1],
|
||||
from_address=alice_address,
|
||||
to_address=bob_address,
|
||||
amount=500,
|
||||
token_address=token_address,
|
||||
)
|
||||
|
||||
net_transfer = get_net_transfers([transfer_alice_to_bob, transfer_bob_to_alice])
|
||||
|
||||
assert expected_transfer == net_transfer[0]
|
||||
|
||||
|
||||
def _equal_ignoring_order(first_list, second_list) -> bool:
|
||||
return all(first in second_list for first in first_list) and all(
|
||||
second in first_list for second in second_list
|
||||
|
Loading…
x
Reference in New Issue
Block a user