diff --git a/mev_inspect/abis/cryptopunks/cryptopunks.json b/mev_inspect/abis/cryptopunks/cryptopunks.json new file mode 100644 index 0000000..200e561 --- /dev/null +++ b/mev_inspect/abis/cryptopunks/cryptopunks.json @@ -0,0 +1 @@ +[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"punksOfferedForSale","outputs":[{"name":"isForSale","type":"bool"},{"name":"punkIndex","type":"uint256"},{"name":"seller","type":"address"},{"name":"minValue","type":"uint256"},{"name":"onlySellTo","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"}],"name":"enterBidForPunk","outputs":[],"payable":true,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"},{"name":"minPrice","type":"uint256"}],"name":"acceptBidForPunk","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"addresses","type":"address[]"},{"name":"indices","type":"uint256[]"}],"name":"setInitialOwners","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"withdraw","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"imageHash","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"nextPunkIndexToAssign","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"punkIndexToAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"standard","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"punkBids","outputs":[{"name":"hasBid","type":"bool"},{"name":"punkIndex","type":"uint256"},{"name":"bidder","type":"address"},{"name":"value","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"allInitialOwnersAssigned","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"allPunksAssigned","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"}],"name":"buyPunk","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"punkIndex","type":"uint256"}],"name":"transferPunk","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"}],"name":"withdrawBidForPunk","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"punkIndex","type":"uint256"}],"name":"setInitialOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"},{"name":"minSalePriceInWei","type":"uint256"},{"name":"toAddress","type":"address"}],"name":"offerPunkForSaleToAddress","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"punksRemainingToAssign","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"},{"name":"minSalePriceInWei","type":"uint256"}],"name":"offerPunkForSale","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"}],"name":"getPunk","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"pendingWithdrawals","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"punkIndex","type":"uint256"}],"name":"punkNoLongerForSale","outputs":[],"payable":false,"type":"function"},{"inputs":[],"payable":true,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"punkIndex","type":"uint256"}],"name":"Assign","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"punkIndex","type":"uint256"}],"name":"PunkTransfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"punkIndex","type":"uint256"},{"indexed":false,"name":"minValue","type":"uint256"},{"indexed":true,"name":"toAddress","type":"address"}],"name":"PunkOffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"punkIndex","type":"uint256"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":true,"name":"fromAddress","type":"address"}],"name":"PunkBidEntered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"punkIndex","type":"uint256"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":true,"name":"fromAddress","type":"address"}],"name":"PunkBidWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"punkIndex","type":"uint256"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":true,"name":"fromAddress","type":"address"},{"indexed":true,"name":"toAddress","type":"address"}],"name":"PunkBought","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"punkIndex","type":"uint256"}],"name":"PunkNoLongerForSale","type":"event"}] \ No newline at end of file diff --git a/mev_inspect/classifiers/specs/__init__.py b/mev_inspect/classifiers/specs/__init__.py index 6d2b4e3..6874e01 100644 --- a/mev_inspect/classifiers/specs/__init__.py +++ b/mev_inspect/classifiers/specs/__init__.py @@ -11,6 +11,7 @@ from .weth import WETH_CLASSIFIER_SPECS, WETH_ADDRESS from .zero_ex import ZEROX_CLASSIFIER_SPECS from .balancer import BALANCER_CLASSIFIER_SPECS from .compound import COMPOUND_CLASSIFIER_SPECS +from .cryptopunks import CRYPTOPUNKS_CLASSIFIER_SPECS ALL_CLASSIFIER_SPECS = ( ERC20_CLASSIFIER_SPECS @@ -21,6 +22,7 @@ ALL_CLASSIFIER_SPECS = ( + ZEROX_CLASSIFIER_SPECS + BALANCER_CLASSIFIER_SPECS + COMPOUND_CLASSIFIER_SPECS + + CRYPTOPUNKS_CLASSIFIER_SPECS ) _SPECS_BY_ABI_NAME_AND_PROTOCOL: Dict[ diff --git a/mev_inspect/classifiers/specs/cryptopunks.py b/mev_inspect/classifiers/specs/cryptopunks.py new file mode 100644 index 0000000..a482b1f --- /dev/null +++ b/mev_inspect/classifiers/specs/cryptopunks.py @@ -0,0 +1,31 @@ +from mev_inspect.schemas.traces import Protocol, Classification + +from mev_inspect.schemas.classifiers import ( + ClassifierSpec, + Classifier, +) + + +class PunkBidAcceptanceClassifier(Classifier): + @staticmethod + def get_classification() -> Classification: + return Classification.punk_accept_bid + + +class PunkBidClassifier(Classifier): + @staticmethod + def get_classification() -> Classification: + return Classification.punk_bid + + +CRYPTO_PUNKS_SPEC = ClassifierSpec( + abi_name="cryptopunks", + protocol=Protocol.cryptopunks, + valid_contract_addresses=["0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB"], + classifiers={ + "enterBidForPunk(uint256)": PunkBidClassifier, + "acceptBidForPunk(uint256,uint256)": PunkBidAcceptanceClassifier, + }, +) + +CRYPTOPUNKS_CLASSIFIER_SPECS = [CRYPTO_PUNKS_SPEC] diff --git a/mev_inspect/inspect_block.py b/mev_inspect/inspect_block.py index c5f849d..c6e58c6 100644 --- a/mev_inspect/inspect_block.py +++ b/mev_inspect/inspect_block.py @@ -31,6 +31,7 @@ from mev_inspect.crud.liquidations import ( write_liquidations, ) from mev_inspect.miner_payments import get_miner_payments +from mev_inspect.punks import get_punk_bid_acceptances, get_punk_bids, get_punk_snipes from mev_inspect.swaps import get_swaps from mev_inspect.transfers import get_transfers from mev_inspect.liquidations import get_liquidations @@ -98,6 +99,12 @@ async def inspect_block( delete_liquidations_for_block(inspect_db_session, block_number) write_liquidations(inspect_db_session, liquidations) + punk_bids = get_punk_bids(classified_traces) + punk_bid_acceptances = get_punk_bid_acceptances(classified_traces) + + punk_snipes = get_punk_snipes(punk_bids, punk_bid_acceptances) + logger.info(f"Block: {block_number} -- Found {len(punk_snipes)} punk snipes") + miner_payments = get_miner_payments( block.miner, block.base_fee_per_gas, classified_traces, block.receipts ) diff --git a/mev_inspect/punks.py b/mev_inspect/punks.py new file mode 100644 index 0000000..ecef500 --- /dev/null +++ b/mev_inspect/punks.py @@ -0,0 +1,125 @@ +from typing import List, Optional +from mev_inspect.schemas.traces import ( + ClassifiedTrace, + Classification, + DecodedCallTrace, +) +from mev_inspect.schemas.punk_bid import PunkBid +from mev_inspect.schemas.punk_accept_bid import PunkBidAcceptance +from mev_inspect.schemas.punk_snipe import PunkSnipe +from mev_inspect.traces import get_traces_by_transaction_hash + + +def _get_highest_punk_bid_per_index( + punk_bids: List[PunkBid], punk_index: int +) -> Optional[PunkBid]: + highest_punk_bid = None + + for punk_bid in punk_bids: + if punk_bid.punk_index == punk_index: + if highest_punk_bid is None: + highest_punk_bid = punk_bid + + elif punk_bid.price > highest_punk_bid.price: + highest_punk_bid = punk_bid + + return highest_punk_bid + + +def get_punk_snipes( + punk_bids: List[PunkBid], punk_bid_acceptances: List[PunkBidAcceptance] +) -> List[PunkSnipe]: + punk_snipe_list = [] + + for punk_bid_acceptance in punk_bid_acceptances: + highest_punk_bid = _get_highest_punk_bid_per_index( + punk_bids, punk_bid_acceptance.punk_index + ) + + if highest_punk_bid is None: + continue + + if highest_punk_bid.price > punk_bid_acceptance.min_price: + punk_snipe = PunkSnipe( + block_number=highest_punk_bid.block_number, + transaction_hash=highest_punk_bid.transaction_hash, + trace_address=highest_punk_bid.trace_address, + from_address=highest_punk_bid.from_address, + punk_index=highest_punk_bid.punk_index, + min_acceptance_price=punk_bid_acceptance.min_price, + acceptance_price=highest_punk_bid.price, + ) + + punk_snipe_list.append(punk_snipe) + + return punk_snipe_list + + +def get_punk_bid_acceptances(traces: List[ClassifiedTrace]) -> List[PunkBidAcceptance]: + punk_bid_acceptances = [] + + for _, transaction_traces in get_traces_by_transaction_hash(traces).items(): + punk_bid_acceptances += _get_punk_bid_acceptances_for_transaction( + list(transaction_traces) + ) + + return punk_bid_acceptances + + +def _get_punk_bid_acceptances_for_transaction( + traces: List[ClassifiedTrace], +) -> List[PunkBidAcceptance]: + ordered_traces = list(sorted(traces, key=lambda t: t.trace_address)) + + punk_bid_acceptances = [] + + for trace in ordered_traces: + if not isinstance(trace, DecodedCallTrace): + continue + + elif trace.classification == Classification.punk_accept_bid: + punk_accept_bid = PunkBidAcceptance( + block_number=trace.block_number, + transaction_hash=trace.transaction_hash, + trace_address=trace.trace_address, + from_address=trace.from_address, + punk_index=trace.inputs["punkIndex"], + min_price=trace.inputs["minPrice"], + ) + + punk_bid_acceptances.append(punk_accept_bid) + + return punk_bid_acceptances + + +def get_punk_bids(traces: List[ClassifiedTrace]) -> List[PunkBid]: + punk_bids = [] + + for _, transaction_traces in get_traces_by_transaction_hash(traces).items(): + punk_bids += _get_punk_bids_for_transaction(list(transaction_traces)) + + return punk_bids + + +def _get_punk_bids_for_transaction(traces: List[ClassifiedTrace]) -> List[PunkBid]: + ordered_traces = list(sorted(traces, key=lambda t: t.trace_address)) + + punk_bids = [] + + for trace in ordered_traces: + if not isinstance(trace, DecodedCallTrace): + continue + + elif trace.classification == Classification.punk_bid: + punk_bid = PunkBid( + transaction_hash=trace.transaction_hash, + block_number=trace.block_number, + trace_address=trace.trace_address, + from_address=trace.from_address, + punk_index=trace.inputs["punkIndex"], + price=trace.value, + ) + + punk_bids.append(punk_bid) + + return punk_bids diff --git a/mev_inspect/schemas/punk_accept_bid.py b/mev_inspect/schemas/punk_accept_bid.py new file mode 100644 index 0000000..29def93 --- /dev/null +++ b/mev_inspect/schemas/punk_accept_bid.py @@ -0,0 +1,12 @@ +from typing import List + +from pydantic import BaseModel + + +class PunkBidAcceptance(BaseModel): + block_number: int + transaction_hash: str + trace_address: List[int] + from_address: str + punk_index: int + min_price: int diff --git a/mev_inspect/schemas/punk_bid.py b/mev_inspect/schemas/punk_bid.py new file mode 100644 index 0000000..d1ed0ae --- /dev/null +++ b/mev_inspect/schemas/punk_bid.py @@ -0,0 +1,12 @@ +from typing import List + +from pydantic import BaseModel + + +class PunkBid(BaseModel): + block_number: int + transaction_hash: str + trace_address: List[int] + from_address: str + punk_index: int + price: int diff --git a/mev_inspect/schemas/punk_snipe.py b/mev_inspect/schemas/punk_snipe.py new file mode 100644 index 0000000..6890802 --- /dev/null +++ b/mev_inspect/schemas/punk_snipe.py @@ -0,0 +1,13 @@ +from typing import List + +from pydantic import BaseModel + + +class PunkSnipe(BaseModel): + block_number: int + transaction_hash: str + trace_address: List[int] + from_address: str + punk_index: int + min_acceptance_price: int + acceptance_price: int diff --git a/mev_inspect/schemas/traces.py b/mev_inspect/schemas/traces.py index faee340..6b4480b 100644 --- a/mev_inspect/schemas/traces.py +++ b/mev_inspect/schemas/traces.py @@ -31,6 +31,8 @@ class Classification(Enum): transfer = "transfer" liquidate = "liquidate" seize = "seize" + punk_bid = "punk_bid" + punk_accept_bid = "punk_accept_bid" class Protocol(Enum): @@ -44,6 +46,7 @@ class Protocol(Enum): balancer_v1 = "balancer_v1" compound_v2 = "compound_v2" cream = "cream" + cryptopunks = "cryptopunks" class ClassifiedTrace(Trace):