types, tests, comp
This commit is contained in:
parent
d677e0b6ed
commit
274cefd1ec
267
GUIDE.md
Normal file
267
GUIDE.md
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
# Contributor guide
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* [Install](https://docs.docker.com/compose/install/) docker compose
|
||||||
|
* To run `mev-inspect`, `postgres`, and `pgadmin` within a local container.
|
||||||
|
* Python
|
||||||
|
* Our pre-commit hook requires v3.9, use pyenv to manage versions and venv, instructions [here](https://www.andreagrandi.it/2020/10/10/install-python-with-pyenv-create-virtual-environment-with-specific-python-version/).
|
||||||
|
* Verify with `pre-commit install && pre-commit run --all-files`
|
||||||
|
* Archive node with `trace_*` rpc module (Erigon/OpenEthereum)
|
||||||
|
|
||||||
|
* If you do not have access to an archive node, reach out to us on our [discord](https://discord.gg/5NB53YEGVM) for raw traces (of the blocks with MEV you're writing inspectors for) or an rpc endpoint.
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
We use poetry for python package management, start with installing the required libraries:
|
||||||
|
* `poetry install`
|
||||||
|
|
||||||
|
To build containers:
|
||||||
|
* `poetry run build`
|
||||||
|
|
||||||
|
Run as daemon:
|
||||||
|
* `poetry run start -d`
|
||||||
|
|
||||||
|
Run inspect on a block:
|
||||||
|
* `poetry run inspect --block-number 11931270 --rpc 'http://111.11.11.111:8545/'
|
||||||
|
`
|
||||||
|
|
||||||
|
Conversely, to stop:
|
||||||
|
* `poetry run stop`
|
||||||
|
|
||||||
|
You will be able to run all the inspectors against a specific transaction, block, and range of blocks once we finalize our data model/architecture but for now, write a protocol specifc inspector script and verify against a test block (with the MEV you're trying to quantify).
|
||||||
|
|
||||||
|
Full list of poetry commands for this repo can be found [here](https://github.com/flashbots/mev-inspect-py#poetry-scripts).
|
||||||
|
|
||||||
|
|
||||||
|
### Tracing
|
||||||
|
|
||||||
|
While simple ETH and token transfers are trivial to parse/filter (by processing their transaction input data, events and/or receipts), contract interactions can be complex to identify. EVM tracing allows us to dig deeper into the transaction execution cycle to look through the internal calls and any other additional proxy contracts the tx interacts with, this is useful for the comprehensive analysis we're interested in.
|
||||||
|
|
||||||
|
Trace types (by `action_type`):
|
||||||
|
|
||||||
|
* `Call`, which is returned when a method on a contract (same as the tx `to` field or a different one within) is executed. We can identify the input parameters in each instance by looking at this sub trace.
|
||||||
|
* `Self-destruct`, when a contract destroys the code at its address and transfers the ETH held in the contract to an EOA. Common pattern among arbitrage bots given the gas refund savings.
|
||||||
|
* `Create`, when a contract deploys another contract and transfers assets to it.
|
||||||
|
* `Reward`, pertaining to the block reward and uncle reward, not relevant here.
|
||||||
|
|
||||||
|
Note that this is for Erigon/OpenEthereum `trace` module and Geth has a different tracing mechanism that is more low-level/irrelevant for inspect.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
|
||||||
|
#### Classified Traces
|
||||||
|
|
||||||
|
For each block we intend to inspect, we first fetch all of its traces, transaction receipts, and other additional information. The raw traces are then processed into classified traces (with protocol name, relevant function signature, relevant call inputs, strategy classification and other information) before being passed onto individual inspectors.
|
||||||
|
|
||||||
|
If we notice these classified traces to only contain liquidation related functions, we can only pass them off to aave/comp inspectors. Similarly, arbitrage profits are reduced by running them through protocol inspectors tagged in this stage (swap/liquidate/buy tx followed by addLiquidity etc).
|
||||||
|
|
||||||
|
#### Strategy Inspectors
|
||||||
|
|
||||||
|
Each strategy has its own inspector and we define the types based on what output we expect it to return. This could include net profits, but also other information such as whether it was a pre-flight check (querying the reserves to see if the arb is still available) or a successful mev opportunity.
|
||||||
|
|
||||||
|
TODO: generic types we've narrowed them down to
|
||||||
|
TODO: table of inspectors pending/wip/ready
|
||||||
|
|
||||||
|
#### Tokenflow
|
||||||
|
|
||||||
|
This module is built to help us identify misclassifications and eventually be used as a protocol agnostic profit estimator (that can be imported by other inspectors after they identify target function signature in the traces) but for now, we'll be using it in addition to our inspectors and store the `diff` for reference purposes.
|
||||||
|
|
||||||
|
The method revolves around iterating over all the traces and makes a note of all the ETH inflows/outflows as well as stablecoins (USDT/USDC/DAI) for the main `eoa`, `contract` (to field, if it's not a known router/aggregator), `proxy` (helpers used by searcher, if any). Once it is done, it finds out net profit by subtracting the gas spent from the MEV revenue. All profits will be converted to ETH, based on the exchange rate at that block height.
|
||||||
|
|
||||||
|
Example: https://etherscan.io/tx/0x4121ce805d33e952b2e6103a5024f70c118432fd0370128d6d7845f9b2987922
|
||||||
|
|
||||||
|
ETH=>ENG=>ETH across DEXs
|
||||||
|
|
||||||
|
Script output:
|
||||||
|
EOA: 0x00000098163d8908dfbd126c873c9c4732a2c2e6
|
||||||
|
Contract: 0x000000000000006f6502b7f2bbac8c30a3f67e9a
|
||||||
|
Tx proxy: 0x0000000000000000000000000000000000000000
|
||||||
|
Stablecoins inflow/outflow: [0, 0]
|
||||||
|
Net ETH profit, Wei 22357881284770142
|
||||||
|
|
||||||
|
More examples can be found under `./tests/tokenflow_test.py`
|
||||||
|
|
||||||
|
#### Database
|
||||||
|
|
||||||
|
Final `mev_inspections` table schema:
|
||||||
|
|
||||||
|
* As of `mev-inspect-rs`:
|
||||||
|
* hash
|
||||||
|
* status
|
||||||
|
* `Success` or `Reverted`
|
||||||
|
* block_number
|
||||||
|
* gas_price
|
||||||
|
* revenue
|
||||||
|
* Revenue searcher makes after accounting for gas used.
|
||||||
|
* protocols
|
||||||
|
* Different protocols that we identify the transaction to touch
|
||||||
|
* actions
|
||||||
|
* Different relevant actions parsed from the transaction traces
|
||||||
|
* eoa
|
||||||
|
* EOA address that initiates the transaction
|
||||||
|
* contract
|
||||||
|
* `to` field, either a custom contract utilized for a searcher to capture MEV or a simple router
|
||||||
|
* proxy_impl
|
||||||
|
* Proxy implementations used by searchers, if any
|
||||||
|
* inserted_at
|
||||||
|
|
||||||
|
Additional fields we're potentially interested in (aside from inspector specific information):
|
||||||
|
* miner
|
||||||
|
* Coinbase address of the block miner
|
||||||
|
* eth_usd_price
|
||||||
|
* Price of ETH that block height
|
||||||
|
* Similarly, for any tokens (say in an arbitrage inspection) we query against the relevant uniswap pools.
|
||||||
|
* tail_gas_price
|
||||||
|
* Gas price of the transaction displaced in the block (last tx that would've otherwise)
|
||||||
|
* tokenflow_estimate_in_eth
|
||||||
|
* Profit outputted by the token flow function
|
||||||
|
* tokenflow_diff
|
||||||
|
* Difference between profit estimated by our inspectors and pure token flow analysis
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Creating an inspector from scratch
|
||||||
|
|
||||||
|
If you intend to create your own inspector and submit it as a PR, this should serve as a useful walkthrough to understand the code structure and types.
|
||||||
|
|
||||||
|
Compound V2 has [two](https://compound.finance/docs/ctokens#liquidate-borrow) primary kinds of protocol liquidations on-chain. `liquidateBorrow()` on the cEther contract and on individual cToken contracts. The former is when a liquidation bot repays ETH debt (via `msg.value`) to seize an account's collateral. The latter is when a liquidation bot repays cTokens (by pre-approving the contract) to liquidate an account.
|
||||||
|
|
||||||
|
**Inspector to capture MEV from the first cEther scenario**
|
||||||
|
|
||||||
|
Target function breakdown, from the compound docs:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<i>function liquidateBorrow(address borrower, address cTokenCollateral) payable</i>
|
||||||
|
|
||||||
|
msg.value payable: The amount of ether to be repaid and converted into collateral, in wei.
|
||||||
|
msg.sender: The account which shall liquidate the borrower by repaying their debt and seizing their collateral.
|
||||||
|
borrower: The account with negative account liquidity that shall be liquidated.
|
||||||
|
cTokenCollateral: The address of the cToken currently held as collateral by a borrower, that the liquidator shall seize.
|
||||||
|
|
||||||
|
RETURN: No return, reverts on error.</pre>
|
||||||
|
|
||||||
|
|
||||||
|
Example [transaction](https://etherscan.io/tx/0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb), found using function signature on [bloxy](https://bloxy.info/functions/aae40a2a), which also has [full execution trace](https://bloxy.info/tx/0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb) that can be helpful for debugging.
|
||||||
|
|
||||||
|
Flow: Classify traces => Parse traces with strategy inspector => Summarize before database insert
|
||||||
|
|
||||||
|
#### Classify traces
|
||||||
|
1. Add the contract ABI of the target function to `abis/protocol_version/`
|
||||||
|
a. In this instance, we create `CEther.json` under `abis/compound_v2/`
|
||||||
|
b. This is to ensure the `TraceClassifier` can utilize the ABI decoder (via `get_abi()`) when initialized in `scripts/inspect_block.py`
|
||||||
|
2. Add matching specs in `mev_inspect/schemas/classified_traces.py` (to identify above function/abi when turning raw traces into classified traces)
|
||||||
|
a. Add the following lines for each strategy/protocol
|
||||||
|
```
|
||||||
|
class Classification(Enum):
|
||||||
|
unknown = "unknown"
|
||||||
|
swap = "swap"
|
||||||
|
burn = "burn"
|
||||||
|
transfer = "transfer"
|
||||||
|
+ liquidate_borrow_ceth = "liquidate_borrow_ceth" #strategy classification/identification name
|
||||||
|
|
||||||
|
|
||||||
|
class Protocol(Enum):
|
||||||
|
uniswap_v2 = "uniswap_v2"
|
||||||
|
uniswap_v3 = "uniswap_v3"
|
||||||
|
sushiswap = "sushiswap"
|
||||||
|
+ compound_v2 = "compound_v2" #should match folder name of `abis`
|
||||||
|
```
|
||||||
|
b. Under `mev_inspect/schemas/classified_specs.py`, mention the actual function signature you're tragetting and export it in `CLASSIFIER_SPECS`
|
||||||
|
```
|
||||||
|
COMPOUND_V2_CETH_SPEC = ClassifierSpec(
|
||||||
|
abi_name="CEther", #should match abi json file name
|
||||||
|
protocol = Protocol.compound_v2,
|
||||||
|
classifications = {
|
||||||
|
"liquidateBorrow(address,address)": Classification.liquidate_borrow_ceth,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
3. Setup a unit test (`test/liquidation_test.py` in this case) to verify the above example tx is being classified properly
|
||||||
|
a. `get_filtered_traces(tx_hash)` on `Block` class allows you to filter traces of a specific transaction (for the purpose of this inspector/test)
|
||||||
|
```
|
||||||
|
|
||||||
|
class TestCompoundV2Liquidation(unittest.TestCase):
|
||||||
|
def test_compound_v2_ceth_liquidation(self):
|
||||||
|
tx_hash = "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb"
|
||||||
|
block_no = 12900060
|
||||||
|
cache_path = _get_cache_path(block_no)
|
||||||
|
block_data = Block.parse_file(cache_path)
|
||||||
|
|
||||||
|
tx_traces = block_data.get_filtered_traces(tx_hash)
|
||||||
|
trace_clasifier = TraceClassifier(CLASSIFIER_SPECS)
|
||||||
|
classified_traces = trace_clasifier.classify(tx_traces)
|
||||||
|
res = inspect_compound_v2_ceth(classified_traces)
|
||||||
|
## res type => Liquidation class with the types defined later below
|
||||||
|
self.assertEqual(res.tx_hash, "0x0")
|
||||||
|
self.assertEqual(res.borrower, "0x0")
|
||||||
|
self.assertEqual(res.collateral_provided, "0x0")
|
||||||
|
self.assertEqual(res.collateral_provided_amount, 0)
|
||||||
|
self.assertEqual(res.asset_seized, "0x0")
|
||||||
|
self.assertEqual(res.asset_seized_amount, 0)
|
||||||
|
self.assertEqual(res.profit_in_eth, 0)
|
||||||
|
self.assertEqual(res.tokenflow_estimate_in_eth, 0)
|
||||||
|
self.assertEqual(res.tokenflow_diff, 0)
|
||||||
|
self.assertEqual(res.status, LiquidationStatus.seized)
|
||||||
|
self.assertEqual(res.type, LiquidationType.compound_v2_ceth_liquidation)
|
||||||
|
self.assertEqual(res.collateral_source, LiquidationCollateralSource.other)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parse traces with strategy inspector
|
||||||
|
|
||||||
|
The custom logic for this scenario is handled here: `./mev_inspect/strategy_inspectors/compound_v2_ceth.py`, where we process the classified traces for profit data and additional information using `inspect_compound_v2_ceth(classified_traces: list[ClassifiedTrace]) -> Liquidation`.
|
||||||
|
|
||||||
|
Before writing the inspector we define the output type to be returned by this function in `./mev_inspect/schema/liquidations.py`, this is unique to each class of strategies (aribitrage/liquidation/sandwich/token sniping etc) to contain all the relevant MEV fields.
|
||||||
|
|
||||||
|
```
|
||||||
|
from .utils import CamelModel
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class LiquidationType(Enum):
|
||||||
|
compound_v2_ceth_liquidation = "compound_v2_ceth_liquidation"
|
||||||
|
compound_v2_ctoken_liquidation = "compound_v2_ctoken_liquidation" # TODO: add logic to handle ctoken liquidations
|
||||||
|
|
||||||
|
class LiquidationStatus(Enum):
|
||||||
|
seized = "seized" # succesfully completed
|
||||||
|
check = "check" # just a liquidation check. i.e searcher only checks if opportunity is still available and reverts accordingly
|
||||||
|
out_of_gas = "out_of_gas" # tx ran out of gas
|
||||||
|
|
||||||
|
class LiquidationCollateralSource(Enum):
|
||||||
|
aave_flashloan = "aave_flashloan"
|
||||||
|
dydx_flashloan = "dydx_flashloan"
|
||||||
|
uniswap_flashloan = "uniswap_flashloan"
|
||||||
|
searcher_eoa = "searcher_eoa" # searchers own funds
|
||||||
|
other = "other"
|
||||||
|
|
||||||
|
class Liquidation(CamelModel):
|
||||||
|
tx_hash: str
|
||||||
|
borrower: str # account that got liquidated
|
||||||
|
collateral_provided: str # collateral provided by searcher, 'ether' or token contract address
|
||||||
|
collateral_provided_amount: int # amount of collateral provided
|
||||||
|
asset_seized: str # asset that was given to searcher at a discount upon liquidation
|
||||||
|
asset_seized_amount: int # amount of asset that was given to searcher upon liquidation
|
||||||
|
profit_in_eth: int # profit estimated by strategy inspector
|
||||||
|
tokenflow_estimate_in_eth: int # profit estimated by tokenflow
|
||||||
|
tokenflow_diff: int # diff between tokenflow and strategy inspector
|
||||||
|
status: LiquidationStatus
|
||||||
|
type: LiquidationType
|
||||||
|
collateral_source: LiquidationCollateralSource
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we get into the core logic ( `inspect_compound_v2_ceth()` in `./mev_inspect/strategy_inspectors/compound_v2_ceth.py`).
|
||||||
|
|
||||||
|
```
|
||||||
|
flow:
|
||||||
|
1. decide if it's a pre-flight check tx or an actual liquidation
|
||||||
|
2. parse `liquidateBorrow` and `seize` sub traces to determine actual amounts sent to the protocol and send back to the searcher
|
||||||
|
3. calculate net profit by finding out the worth of seized tokens
|
||||||
|
4. use tokenflow module to find out profit independent of the inspector, calculate diff
|
||||||
|
5. determine source of funds
|
||||||
|
6. prepare return object to get it ready for db processing
|
||||||
|
```
|
||||||
|
|
||||||
|
For every inspector, try to verify the profit amount by adding unit tests of sample txs that cover a wide variety of edge cases. Comparing the inspector outputs to that of tokenflow (`tokenflow_diff` in `Liquidation` class in this case) should also help catch misclassifications.
|
||||||
|
|
||||||
|
#### Summarize before database insert
|
||||||
|
|
||||||
|
TODO: section about what ends up in the database from all the extracted information, after we finalize tables/schema
|
@ -102,3 +102,5 @@ poetry run pre-commit install
|
|||||||
```
|
```
|
||||||
|
|
||||||
Update README if needed
|
Update README if needed
|
||||||
|
|
||||||
|
[Full contributor guide with sample inspector](./GUIDE.MD)
|
@ -10,15 +10,15 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'c5da44eb072c'
|
revision = "c5da44eb072c"
|
||||||
down_revision = '0660432b9840'
|
down_revision = "0660432b9840"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
op.create_index('i_block_number', 'classified_traces', ['block_number'])
|
op.create_index("i_block_number", "classified_traces", ["block_number"])
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
op.drop_index('i_block_number', 'classified_traces')
|
op.drop_index("i_block_number", "classified_traces")
|
||||||
|
@ -26,3 +26,19 @@ def get_abi(abi_name: str, protocol: Optional[Protocol]) -> Optional[ABI]:
|
|||||||
return parse_obj_as(ABI, abi_json)
|
return parse_obj_as(ABI, abi_json)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# raw abi, for instantiating contract for queries (as opposed to classification)
|
||||||
|
def get_raw_abi(abi_name: str, protocol: Optional[Protocol]) -> str:
|
||||||
|
abi_filename = f"{abi_name}.json"
|
||||||
|
abi_path = (
|
||||||
|
ABI_DIRECTORY_PATH / abi_filename
|
||||||
|
if protocol is None
|
||||||
|
else ABI_DIRECTORY_PATH / protocol.value / abi_filename
|
||||||
|
)
|
||||||
|
|
||||||
|
if abi_path.is_file():
|
||||||
|
with abi_path.open() as abi_file:
|
||||||
|
return abi_file.read()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
1
mev_inspect/abis/compound_v2/CEther.json
Normal file
1
mev_inspect/abis/compound_v2/CEther.json
Normal file
File diff suppressed because one or more lines are too long
1
mev_inspect/abis/compound_v2/CToken.json
Normal file
1
mev_inspect/abis/compound_v2/CToken.json
Normal file
File diff suppressed because one or more lines are too long
1
mev_inspect/abis/compound_v2/Comptroller.json
Normal file
1
mev_inspect/abis/compound_v2/Comptroller.json
Normal file
File diff suppressed because one or more lines are too long
@ -107,6 +107,26 @@ AAVE_SPEC = ClassifierSpec(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
COMPOUND_V2_CETH_SPEC = ClassifierSpec(
|
||||||
|
abi_name="CEther",
|
||||||
|
protocol=Protocol.compound_v2,
|
||||||
|
valid_contract_addresses=["0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5"],
|
||||||
|
classifications={
|
||||||
|
"liquidateBorrow(address,address)": Classification.liquidate,
|
||||||
|
"seize(address,address,uint)": Classification.seize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
COMPOUND_V2_CDAI_SPEC = ClassifierSpec(
|
||||||
|
abi_name="CToken",
|
||||||
|
protocol=Protocol.compound_v2,
|
||||||
|
valid_contract_addresses=["0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643"],
|
||||||
|
classifications={
|
||||||
|
"liquidateBorrow(address,uint256,address)": Classification.liquidate,
|
||||||
|
"seize(address,address,uint)": Classification.seize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
WETH_SPEC = ClassifierSpec(
|
WETH_SPEC = ClassifierSpec(
|
||||||
abi_name="WETH9",
|
abi_name="WETH9",
|
||||||
protocol=Protocol.weth,
|
protocol=Protocol.weth,
|
||||||
@ -126,4 +146,5 @@ CLASSIFIER_SPECS = [
|
|||||||
ERC20_SPEC,
|
ERC20_SPEC,
|
||||||
UNISWAP_V3_POOL_SPEC,
|
UNISWAP_V3_POOL_SPEC,
|
||||||
UNISWAPPY_V2_PAIR_SPEC,
|
UNISWAPPY_V2_PAIR_SPEC,
|
||||||
|
COMPOUND_V2_CETH_SPEC,
|
||||||
]
|
]
|
||||||
|
@ -6,9 +6,11 @@ from mev_inspect.schemas.classified_traces import ClassifiedTrace
|
|||||||
|
|
||||||
|
|
||||||
def delete_classified_traces_for_block(
|
def delete_classified_traces_for_block(
|
||||||
db_session, block_number: int,
|
db_session,
|
||||||
|
block_number: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
(db_session.query(ClassifiedTraceModel)
|
(
|
||||||
|
db_session.query(ClassifiedTraceModel)
|
||||||
.filter(ClassifiedTraceModel.block_number == block_number)
|
.filter(ClassifiedTraceModel.block_number == block_number)
|
||||||
.delete()
|
.delete()
|
||||||
)
|
)
|
||||||
|
@ -55,6 +55,19 @@ class Trace(CamelModel):
|
|||||||
error: Optional[str]
|
error: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(Web3Model):
|
||||||
|
from_address: str
|
||||||
|
to_address: str
|
||||||
|
value: int
|
||||||
|
tx_hash: str
|
||||||
|
tx_index: int
|
||||||
|
tx_input: str
|
||||||
|
tx_gas_used: int
|
||||||
|
tx_gas_price: int
|
||||||
|
tx_net_fees_paid: int
|
||||||
|
block_number: int
|
||||||
|
|
||||||
|
|
||||||
class Block(Web3Model):
|
class Block(Web3Model):
|
||||||
block_number: int
|
block_number: int
|
||||||
traces: List[Trace]
|
traces: List[Trace]
|
||||||
|
@ -12,6 +12,7 @@ class Classification(Enum):
|
|||||||
burn = "burn"
|
burn = "burn"
|
||||||
transfer = "transfer"
|
transfer = "transfer"
|
||||||
liquidate = "liquidate"
|
liquidate = "liquidate"
|
||||||
|
seize = "seize" # liquidate => attempt liquidation, seize => successful collateral transfer upon liquidation, following COMP naming
|
||||||
|
|
||||||
|
|
||||||
class Protocol(Enum):
|
class Protocol(Enum):
|
||||||
@ -19,6 +20,7 @@ class Protocol(Enum):
|
|||||||
uniswap_v3 = "uniswap_v3"
|
uniswap_v3 = "uniswap_v3"
|
||||||
sushiswap = "sushiswap"
|
sushiswap = "sushiswap"
|
||||||
aave = "aave"
|
aave = "aave"
|
||||||
|
compound_v2 = "compound_v2"
|
||||||
weth = "weth"
|
weth = "weth"
|
||||||
|
|
||||||
|
|
||||||
|
37
mev_inspect/schemas/liquidations.py
Normal file
37
mev_inspect/schemas/liquidations.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from .utils import CamelModel
|
||||||
|
|
||||||
|
|
||||||
|
class LiquidationType(Enum):
|
||||||
|
compound_v2_ceth_liquidation = "compound_v2_ceth_liquidation"
|
||||||
|
compound_v2_ctoken_liquidation = "compound_v2_ctoken_liquidation" # TODO: add logic to handle ctoken liquidations
|
||||||
|
|
||||||
|
|
||||||
|
class LiquidationStatus(Enum):
|
||||||
|
seized = "seized" # succesfully completed
|
||||||
|
check = "check" # just a liquidation check. i.e searcher only checks if opportunity is still available and reverts accordingly
|
||||||
|
out_of_gas = "out_of_gas" # tx ran out of gas
|
||||||
|
|
||||||
|
|
||||||
|
class LiquidationCollateralSource(Enum):
|
||||||
|
aave_flashloan = "aave_flashloan"
|
||||||
|
dydx_flashloan = "dydx_flashloan"
|
||||||
|
uniswap_flashloan = "uniswap_flashloan"
|
||||||
|
searcher_eoa = "searcher_eoa" # searchers own funds
|
||||||
|
searcher_contract = "searcher_contract"
|
||||||
|
other = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class Liquidation(CamelModel):
|
||||||
|
tx_hash: str
|
||||||
|
borrower: str # account that got liquidated
|
||||||
|
collateral_provided: str # collateral provided by searcher, 'ether' or token contract address
|
||||||
|
collateral_provided_amount: int # amount of collateral provided
|
||||||
|
asset_seized: str # asset that was given to searcher at a discount upon liquidation
|
||||||
|
asset_seized_amount: int # amount of asset that was given to searcher upon liquidation
|
||||||
|
profit_in_eth: int # profit estimated by strategy inspector
|
||||||
|
tokenflow_estimate_in_eth: int # profit estimated by tokenflow
|
||||||
|
tokenflow_diff: int # diff between tokenflow and strategy inspector
|
||||||
|
status: LiquidationStatus
|
||||||
|
type: LiquidationType
|
||||||
|
collateral_source: LiquidationCollateralSource
|
17
mev_inspect/schemas/tokenflow.py
Normal file
17
mev_inspect/schemas/tokenflow.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from .utils import CamelModel
|
||||||
|
|
||||||
|
|
||||||
|
class Tokenflow(CamelModel):
|
||||||
|
tx_hash: str
|
||||||
|
dollar_inflow: int
|
||||||
|
dollar_outflow: int
|
||||||
|
ether_inflow: int
|
||||||
|
ether_outflow: int
|
||||||
|
|
||||||
|
|
||||||
|
class TokenflowSpecifc(CamelModel):
|
||||||
|
tx_hash: str
|
||||||
|
token_address: str
|
||||||
|
token_inflow: int
|
||||||
|
token_outflow: int
|
116
mev_inspect/strategy_inspectors/compound_v2_ceth.py
Normal file
116
mev_inspect/strategy_inspectors/compound_v2_ceth.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
from mev_inspect.schemas.classified_traces import Classification, ClassifiedTrace
|
||||||
|
from mev_inspect.schemas.liquidations import (
|
||||||
|
Liquidation,
|
||||||
|
LiquidationType,
|
||||||
|
LiquidationStatus,
|
||||||
|
LiquidationCollateralSource,
|
||||||
|
)
|
||||||
|
from mev_inspect.block import _get_cache_path
|
||||||
|
from mev_inspect.schemas import Block
|
||||||
|
from mev_inspect.schemas.blocks import Transaction
|
||||||
|
from mev_inspect.trace_classifier import TraceClassifier
|
||||||
|
from mev_inspect.classifier_specs import CLASSIFIER_SPECS, Protocol
|
||||||
|
from mev_inspect.tokenflow import get_dollar_flows, get_tx_proxies
|
||||||
|
from mev_inspect.abi import get_raw_abi
|
||||||
|
from web3 import Web3
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
w3 = Web3(Web3.HTTPProvider(""))
|
||||||
|
|
||||||
|
|
||||||
|
comp_v2_comptroller_address = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"
|
||||||
|
c_ether = "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5"
|
||||||
|
|
||||||
|
# cToken=>Token mapping (cDAI=>DAI)
|
||||||
|
# useful for finding out the underlying asset address of the cToken seized
|
||||||
|
def get_all_comp_markets():
|
||||||
|
c_token_mapping = {}
|
||||||
|
comp_v2_comptroller_abi = get_raw_abi("Comptroller", Protocol.compound_v2)
|
||||||
|
comptroller_instance = w3.eth.contract(
|
||||||
|
address=comp_v2_comptroller_address, abi=comp_v2_comptroller_abi
|
||||||
|
)
|
||||||
|
markets = comptroller_instance.functions.getAllMarkets().call()
|
||||||
|
for c_token in markets:
|
||||||
|
# make an exception for cETH (as it has no .underlying())
|
||||||
|
if c_token != c_ether:
|
||||||
|
comp_v2_ctoken_abi = get_raw_abi("CToken", Protocol.compound_v2)
|
||||||
|
ctoken_instance = w3.eth.contract(address=c_token, abi=comp_v2_ctoken_abi)
|
||||||
|
underlying_token = ctoken_instance.functions.underlying().call()
|
||||||
|
c_token_mapping[
|
||||||
|
c_token.lower()
|
||||||
|
] = underlying_token.lower() # make k:v lowercase for consistancy
|
||||||
|
return c_token_mapping
|
||||||
|
|
||||||
|
|
||||||
|
# find if the searcher repays the loan from their own EOA, by buying it from a DEX, or w/ a flashloan
|
||||||
|
# TODO: add all flashloan providers and their origin address
|
||||||
|
def find_collateral_source(
|
||||||
|
classified_traces: list[ClassifiedTrace],
|
||||||
|
tx: Transaction,
|
||||||
|
liquidation_contract: Optional[str],
|
||||||
|
) -> LiquidationCollateralSource:
|
||||||
|
source = LiquidationCollateralSource.other # set other by default
|
||||||
|
for classified_trace in classified_traces:
|
||||||
|
# look for trace that liquidates and see from address
|
||||||
|
if (
|
||||||
|
classified_trace.to_address == liquidation_contract
|
||||||
|
and classified_trace.function_name == "liquidateBorrow"
|
||||||
|
):
|
||||||
|
## check if tx originates from searcher eoa or contract
|
||||||
|
if tx.to_address.lower() == classified_trace.from_address:
|
||||||
|
source = LiquidationCollateralSource.searcher_contract
|
||||||
|
elif tx.from_address.lower() == classified_trace.from_address:
|
||||||
|
source = LiquidationCollateralSource.searcher_eoa
|
||||||
|
## flashloan providers identified here
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: check tx status and assign accordingly
|
||||||
|
# i.e if a tx checks if the opportunity is still available ("liquidateBorrowAllowed")
|
||||||
|
# or if it calls the COMP oracle for price data ("getUnderlyingPrice(address")
|
||||||
|
# def is_pre_flight():
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# for cToken - differnt file?
|
||||||
|
# TODO: fetch historic price (in ETH) of any given token at the block height the tx occured
|
||||||
|
# to calculate the profit in ETH accurately, regardless of what token the profit was held in
|
||||||
|
# def get_historic_token_price():
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_compound_v2_ceth(
|
||||||
|
tx: Transaction, classified_traces: list[ClassifiedTrace]
|
||||||
|
) -> Liquidation:
|
||||||
|
# TODO: complete this logic after seized return type
|
||||||
|
# flow:
|
||||||
|
# 1. decide if it's a pre-flight check tx or an actual liquidation
|
||||||
|
# 2. parse `liquidateBorrow` and `seize` sub traces to determine actual amounts
|
||||||
|
# 3. calculate net profit by finding out the worth of seized tokens
|
||||||
|
# 4. use tokenflow module to find out profit independent of the inspector, calculate diff
|
||||||
|
# 5. prepare return object to get it ready for db processing
|
||||||
|
for classified_trace in classified_traces:
|
||||||
|
if (
|
||||||
|
classified_trace.function_name == "liquidateBorrow"
|
||||||
|
and classified_trace.inputs is not None
|
||||||
|
):
|
||||||
|
source = find_collateral_source(
|
||||||
|
classified_traces, tx, classified_trace.to_address
|
||||||
|
)
|
||||||
|
borrower = classified_trace.inputs["inputs"]
|
||||||
|
c_token_collateral = classified_trace.inputs["cTokenCollateral"]
|
||||||
|
liquidation = Liquidation(
|
||||||
|
tx_hash=tx.tx_hash,
|
||||||
|
borrower=borrower,
|
||||||
|
collateral_provided="ether",
|
||||||
|
collateral_provided_amount=classified_trace.value,
|
||||||
|
asset_seized=(get_all_comp_markets())[c_token_collateral],
|
||||||
|
asset_seized_amount=0,
|
||||||
|
profit_in_eth=0,
|
||||||
|
tokenflow_estimate_in_eth=0,
|
||||||
|
tokenflow_diff=0,
|
||||||
|
collateral_source=source,
|
||||||
|
status=LiquidationStatus.seized,
|
||||||
|
type=LiquidationType.compound_v2_ceth_liquidation,
|
||||||
|
)
|
||||||
|
return liquidation
|
||||||
|
return Liquidation()
|
@ -2,6 +2,7 @@ from typing import List, Optional
|
|||||||
|
|
||||||
from mev_inspect.config import load_config
|
from mev_inspect.config import load_config
|
||||||
from mev_inspect.schemas import Block, Trace, TraceType
|
from mev_inspect.schemas import Block, Trace, TraceType
|
||||||
|
from mev_inspect.schemas.tokenflow import Tokenflow, TokenflowSpecifc
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
@ -157,8 +158,6 @@ def get_dollar_flows(tx_traces, addresses_to_check):
|
|||||||
dollar_outflow = 0
|
dollar_outflow = 0
|
||||||
for trace in tx_traces:
|
for trace in tx_traces:
|
||||||
if trace.type == TraceType.call and is_stablecoin_address(trace.action["to"]):
|
if trace.type == TraceType.call and is_stablecoin_address(trace.action["to"]):
|
||||||
_ = int(trace.action["value"], 16) # converting from 0x prefix to decimal
|
|
||||||
|
|
||||||
# USD_GET1 & USD_GET2 (to account for both 'transfer' and 'transferFrom' methods)
|
# USD_GET1 & USD_GET2 (to account for both 'transfer' and 'transferFrom' methods)
|
||||||
# USD_GIVE1 & USD_GIVE2
|
# USD_GIVE1 & USD_GIVE2
|
||||||
|
|
||||||
@ -185,7 +184,70 @@ def get_dollar_flows(tx_traces, addresses_to_check):
|
|||||||
return [dollar_inflow, dollar_outflow]
|
return [dollar_inflow, dollar_outflow]
|
||||||
|
|
||||||
|
|
||||||
def run_tokenflow(tx_hash: str, block: Block):
|
def get_specifc_token_flows(tx_traces, addresses_to_check, token_address):
|
||||||
|
token_inflow = 0
|
||||||
|
token_outflow = 0
|
||||||
|
for trace in tx_traces:
|
||||||
|
if trace.type == TraceType.call and trace.action["to"] == token_address:
|
||||||
|
# transfer(address to,uint256 value) with args
|
||||||
|
print(len(trace.action["input"]))
|
||||||
|
print(trace.action["input"])
|
||||||
|
if len(trace.action["input"]) == 138:
|
||||||
|
if trace.action["input"][2:10] == "a9059cbb":
|
||||||
|
transfer_to = "0x" + trace.action["input"][34:74]
|
||||||
|
transfer_value = int("0x" + trace.action["input"][74:138], 16)
|
||||||
|
if transfer_to in addresses_to_check:
|
||||||
|
print(
|
||||||
|
"inflow from:",
|
||||||
|
trace["from"],
|
||||||
|
"to: ",
|
||||||
|
transfer_to,
|
||||||
|
"value:",
|
||||||
|
transfer_value,
|
||||||
|
)
|
||||||
|
token_inflow = token_inflow + transfer_value
|
||||||
|
token_inflow = token_inflow + transfer_value
|
||||||
|
elif trace.action["from"] in addresses_to_check:
|
||||||
|
print(
|
||||||
|
"outflow from:",
|
||||||
|
trace["from"],
|
||||||
|
"to: ",
|
||||||
|
transfer_to,
|
||||||
|
"value:",
|
||||||
|
transfer_value,
|
||||||
|
)
|
||||||
|
token_outflow = token_outflow + transfer_value
|
||||||
|
|
||||||
|
# transferFrom(address from,address to,uint256 value )
|
||||||
|
if len(trace.action["input"]) == 202:
|
||||||
|
if trace.action["input"][2:10] == "23b872dd":
|
||||||
|
transfer_from = "0x" + trace.action["input"][34:74]
|
||||||
|
transfer_to = "0x" + trace.action["input"][98:138]
|
||||||
|
transfer_value = int("0x" + trace.action["input"][138:202], 16)
|
||||||
|
if transfer_to in addresses_to_check:
|
||||||
|
print(
|
||||||
|
"inflow from:",
|
||||||
|
trace["from"],
|
||||||
|
"to: ",
|
||||||
|
transfer_to,
|
||||||
|
"value:",
|
||||||
|
transfer_value,
|
||||||
|
)
|
||||||
|
token_inflow = token_inflow + transfer_value
|
||||||
|
elif transfer_from in addresses_to_check:
|
||||||
|
print(
|
||||||
|
"outflow from:",
|
||||||
|
trace["from"],
|
||||||
|
"to: ",
|
||||||
|
transfer_to,
|
||||||
|
"value:",
|
||||||
|
transfer_value,
|
||||||
|
)
|
||||||
|
token_outflow = token_outflow + transfer_value
|
||||||
|
return [token_inflow, token_outflow]
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokenflow(tx_hash: str, block: Block) -> Tokenflow:
|
||||||
tx_traces = block.get_filtered_traces(tx_hash)
|
tx_traces = block.get_filtered_traces(tx_hash)
|
||||||
to_address = get_tx_to_address(tx_hash, block)
|
to_address = get_tx_to_address(tx_hash, block)
|
||||||
|
|
||||||
@ -209,10 +271,15 @@ def run_tokenflow(tx_hash: str, block: Block):
|
|||||||
|
|
||||||
ether_flows = get_ether_flows(tx_traces, addresses_to_check)
|
ether_flows = get_ether_flows(tx_traces, addresses_to_check)
|
||||||
dollar_flows = get_dollar_flows(tx_traces, addresses_to_check)
|
dollar_flows = get_dollar_flows(tx_traces, addresses_to_check)
|
||||||
# print(addresses_to_check)
|
tokenflow_result = Tokenflow(
|
||||||
# print('net eth flow', ether_flows[0] - ether_flows[1])
|
tx_hash=tx_hash,
|
||||||
# print('net dollar flow', dollar_flows )
|
ether_inflow=ether_flows[0],
|
||||||
return {"ether_flows": ether_flows, "dollar_flows": dollar_flows}
|
ether_outflow=ether_flows[1],
|
||||||
|
dollar_inflow=dollar_flows[0],
|
||||||
|
dollar_outflow=dollar_flows[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokenflow_result
|
||||||
|
|
||||||
|
|
||||||
# note: not the gas set by user, only gas consumed upon execution
|
# note: not the gas set by user, only gas consumed upon execution
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
# Fails precommit because these inspectors don't exist yet
|
|
||||||
# from mev_inspect import inspector_compound
|
|
||||||
# from mev_inspect import inspector_aave
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# class TestLiquidations(unittest.TestCase):
|
|
||||||
# def test_compound_liquidation(self):
|
|
||||||
# tx_hash = "0x0ec6d5044a47feb3ceb647bf7ea4ffc87d09244d629eeced82ba17ec66605012"
|
|
||||||
# block_no = 11338848
|
|
||||||
# res = inspector_compound.get_profit(tx_hash, block_no)
|
|
||||||
# # self.assertEqual(res['profit'], 0)
|
|
||||||
#
|
|
||||||
# def test_aave_liquidation(self):
|
|
||||||
# tx_hash = "0xc8d2501d28800b1557eb64c5d0e08fd6070c15b6c04c39ca05631f641d19ffb2"
|
|
||||||
# block_no = 10803840
|
|
||||||
# res = inspector_aave.get_profit(tx_hash, block_no)
|
|
||||||
# # self.assertEqual(res['profit'], 0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
63
tests/test_liquidations.py
Normal file
63
tests/test_liquidations.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import unittest
|
||||||
|
from mev_inspect.trace_classifier import TraceClassifier
|
||||||
|
from mev_inspect.classifier_specs import CLASSIFIER_SPECS
|
||||||
|
from mev_inspect.block import _get_cache_path
|
||||||
|
from mev_inspect.strategy_inspectors.compound_v2_ceth import inspect_compound_v2_ceth
|
||||||
|
|
||||||
|
from mev_inspect.schemas.blocks import Transaction
|
||||||
|
from mev_inspect.schemas.liquidations import (
|
||||||
|
LiquidationCollateralSource,
|
||||||
|
LiquidationType,
|
||||||
|
LiquidationStatus,
|
||||||
|
)
|
||||||
|
from mev_inspect.schemas import Block
|
||||||
|
from web3 import Web3
|
||||||
|
|
||||||
|
w3 = Web3(Web3.HTTPProvider(""))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompoundV2Liquidation(unittest.TestCase):
|
||||||
|
def test_compound_v2_ceth_liquidation(self):
|
||||||
|
tx_hash = "0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb"
|
||||||
|
block_no = 12900060
|
||||||
|
cache_path = _get_cache_path(block_no)
|
||||||
|
block_data = Block.parse_file(cache_path)
|
||||||
|
tx_data = w3.eth.get_transaction(tx_hash)
|
||||||
|
tx = Transaction(
|
||||||
|
from_address=tx_data["from"],
|
||||||
|
to_address=tx_data["to"],
|
||||||
|
value=tx_data["value"],
|
||||||
|
tx_hash=tx_hash,
|
||||||
|
tx_index=tx_data["transactionIndex"],
|
||||||
|
tx_input=tx_data["input"],
|
||||||
|
tx_gas_used=block_data.txs_gas_data[tx_hash]["gasUsed"],
|
||||||
|
tx_gas_price=block_data.txs_gas_data[tx_hash]["gasPrice"],
|
||||||
|
tx_net_fees_paid=block_data.txs_gas_data[tx_hash]["netFeePaid"],
|
||||||
|
block_number=block_no,
|
||||||
|
)
|
||||||
|
tx_traces = block_data.get_filtered_traces(tx_hash)
|
||||||
|
trace_clasifier = TraceClassifier(CLASSIFIER_SPECS)
|
||||||
|
classified_traces = trace_clasifier.classify(tx_traces)
|
||||||
|
|
||||||
|
res = inspect_compound_v2_ceth(tx, classified_traces)
|
||||||
|
self.assertEqual(
|
||||||
|
res.tx_hash,
|
||||||
|
"0xd09e499f2c2d6a900a974489215f25006a5a3fa401a10b8d67fa99480cbb62fb",
|
||||||
|
)
|
||||||
|
self.assertEqual(res.borrower, "0xc871095098488c17ae14cb898d46da631ad84b59")
|
||||||
|
self.assertEqual(res.collateral_provided, "ether")
|
||||||
|
self.assertEqual(res.collateral_provided_amount, 463900911985743409)
|
||||||
|
self.assertEqual(res.asset_seized, "0x6b175474e89094c44da98b954eedeac495271d0f")
|
||||||
|
self.assertEqual(res.asset_seized_amount, 0)
|
||||||
|
self.assertEqual(res.profit_in_eth, 0)
|
||||||
|
self.assertEqual(res.tokenflow_estimate_in_eth, 0)
|
||||||
|
self.assertEqual(res.tokenflow_diff, 0)
|
||||||
|
self.assertEqual(res.status, LiquidationStatus.seized)
|
||||||
|
self.assertEqual(res.type, LiquidationType.compound_v2_ceth_liquidation)
|
||||||
|
self.assertEqual(
|
||||||
|
res.collateral_source, LiquidationCollateralSource.searcher_contract
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
@ -1,7 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mev_inspect import tokenflow
|
from mev_inspect import tokenflow
|
||||||
|
from mev_inspect.schemas.tokenflow import Tokenflow, TokenflowSpecifc
|
||||||
from .utils import load_test_block
|
from .utils import load_test_block
|
||||||
|
|
||||||
|
|
||||||
@ -12,8 +12,18 @@ class TestTokenFlow(unittest.TestCase):
|
|||||||
|
|
||||||
block = load_test_block(block_no)
|
block = load_test_block(block_no)
|
||||||
res = tokenflow.run_tokenflow(tx_hash, block)
|
res = tokenflow.run_tokenflow(tx_hash, block)
|
||||||
self.assertEqual(res["ether_flows"], [3547869861992962562, 3499859860420296704])
|
self.assertEqual(
|
||||||
self.assertEqual(res["dollar_flows"], [0, 0])
|
res,
|
||||||
|
Tokenflow(
|
||||||
|
ether_inflow=3547869861992962562,
|
||||||
|
ether_outflow=3499859860420296704,
|
||||||
|
dollar_inflow=0,
|
||||||
|
dollar_outflow=0,
|
||||||
|
tx_hash=tx_hash,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# self.assertEqual(res["ether_flows"], [3547869861992962562, 3499859860420296704])
|
||||||
|
# self.assertEqual(res["dollar_flows"], [0, 0])
|
||||||
|
|
||||||
def test_arb_with_stable_flow(self):
|
def test_arb_with_stable_flow(self):
|
||||||
tx_hash = "0x496836e0bd1520388e36c79d587a31d4b3306e4f25352164178ca0667c7f9c29"
|
tx_hash = "0x496836e0bd1520388e36c79d587a31d4b3306e4f25352164178ca0667c7f9c29"
|
||||||
@ -21,16 +31,36 @@ class TestTokenFlow(unittest.TestCase):
|
|||||||
|
|
||||||
block = load_test_block(block_no)
|
block = load_test_block(block_no)
|
||||||
res = tokenflow.run_tokenflow(tx_hash, block)
|
res = tokenflow.run_tokenflow(tx_hash, block)
|
||||||
self.assertEqual(res["ether_flows"], [597044987302243493, 562445964778930176])
|
self.assertEqual(
|
||||||
self.assertEqual(res["dollar_flows"], [871839781, 871839781])
|
res,
|
||||||
|
Tokenflow(
|
||||||
|
ether_inflow=597044987302243493,
|
||||||
|
ether_outflow=562445964778930176,
|
||||||
|
dollar_inflow=871839781,
|
||||||
|
dollar_outflow=871839781,
|
||||||
|
tx_hash=tx_hash,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# self.assertEqual(res["ether_flows"], [597044987302243493, 562445964778930176])
|
||||||
|
# self.assertEqual(res["dollar_flows"], [871839781, 871839781])
|
||||||
|
|
||||||
def test_complex_cross_arb(self):
|
def test_complex_cross_arb(self):
|
||||||
tx_hash = "0x5ab21bfba50ad3993528c2828c63e311aafe93b40ee934790e545e150cb6ca73"
|
tx_hash = "0x5ab21bfba50ad3993528c2828c63e311aafe93b40ee934790e545e150cb6ca73"
|
||||||
block_no = 11931272
|
block_no = 11931272
|
||||||
block = load_test_block(block_no)
|
block = load_test_block(block_no)
|
||||||
res = tokenflow.run_tokenflow(tx_hash, block)
|
res = tokenflow.run_tokenflow(tx_hash, block)
|
||||||
self.assertEqual(res["ether_flows"], [3636400213125714803, 3559576672903063566])
|
self.assertEqual(
|
||||||
self.assertEqual(res["dollar_flows"], [0, 0])
|
res,
|
||||||
|
Tokenflow(
|
||||||
|
ether_inflow=3636400213125714803,
|
||||||
|
ether_outflow=3559576672903063566,
|
||||||
|
dollar_inflow=0,
|
||||||
|
dollar_outflow=0,
|
||||||
|
tx_hash=tx_hash,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# self.assertEqual(res["ether_flows"], [3636400213125714803, 3559576672903063566])
|
||||||
|
# self.assertEqual(res["dollar_flows"], [0, 0])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
Loading…
x
Reference in New Issue
Block a user