Merge 4f2d178ddd2ba8158def2be3affe87d579bbcc5d into ce8179f07e4fb8740b43570aa2c5826447c2af26

This commit is contained in:
Eli Barbieri 2025-01-27 07:40:14 +00:00 committed by GitHub
commit 30bb1ac402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 856 additions and 4 deletions

View 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")

View File

@ -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")

View File

@ -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(

View 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()

View File

@ -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
)

View 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

View 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)

View 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

View File

@ -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):

View File

@ -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

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

188
tests/test_jit_liquidity.py Normal file
View 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

View File

@ -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