Feb 19, 2023
mev
rust

Improve liquidation bots with price update predictions 2/2

dark-forest

Here we are with the 2nd part of this blog post. Ensure you checked out the first one for a better understanding. As the image suggests we are going one step ahead into the "dark forest". We will see some code that inspects the mempool to predict the price change of an asset on AAVE.

The complete source can be found on GitHub.

The code I'll show uses the RPC method eth_newPendingTransactionFilter to inspect the mempool. This is generally not exposed by RPC service providers (e.g. Infura). Anyway if you are going to be a "searcher", you need a top performance environment. A self hosted Ethereum node is then a must.

AAVE oracles

Each reserve on AAVE has a oracle that implements the ChainLink AggregatorInterface. AAVE oracles are proxy contracts that hide a "real" aggregator behind them.

Price updates will be sent from the ChainLink network to the real aggregator, so we are going to retrieve its address from the proxy contract.

The oracles.rs module exports a function to get all the real price oracles available on AAVE.

pub async fn find_all(&self) -> Result<Vec<Oracle>, Box<dyn Error>> {
    ...
}

NOTE that real aggregators can be updated over time, so your bot needs to handle this properly.

The oracles.rs module requires 3 ABIs to retrieve the ChainLink oracles for all reserves on AAVE.

// oracles.rs

// To retrieve all available AAVE reserves.
abigen!(AaveProtocolDataProvider, "./abi/AaveProtocolDataProvider.json",);

// To retrieve the proxy oracle given a reserve.
abigen!(AaveOracle, "./abi/AaveOracle.json",);

// To retrieve the real aggregator behind a proxy oracle.
abigen!(AaveOracle, "./abi/AggregatorInterface.json",);

🔥 Price prediction

This is the hot part of this guide. Given a price oracle we are going to intercept all price update transactions from the mempool. This task is implemented in mempool.rs module.

When a price update is pushed by the ChainLink network, it will call the transmit() function on the oracle's smart contract.

// mempool.rs

abigen!(
    OffchainAggregator,
    r#"[
        function transmit(bytes calldata report, bytes32[] calldata rs, bytes32[] calldata ss, bytes32 rawVs) external
    ]"#
);

The steps are pretty simple:

  • Spawn an independent task that scans all pending transactions in the mempool.
  • For each transaction check:
    • Target address: The "to" field is equal to the price oracle's address.
    • Still pending: The transaction has not been mined already.
    • It's a price update: It's calling the transmit() method

If all checks pass then extract the report argument of the call and get the price update data.

function transmit(bytes calldata report, ...) external

Looking at the solidity implementation of transmit(), we see that report is decoded as a tuple:

(r.rawReportContext, rawObservers, r.observations) = abi.decode(
  report, (bytes32, bytes32, int192[])
);

We need the last item of this tuple (observations array). It represents an ordered list of prices observed by the ChainLink network. The smart contract will then accept the median value as the new price for the asset.

We need to replicate this logic in our Rust program and extract the future price of our asset.

// mempool.rs

// Try to decode the transaction and extract the new price
if let Ok(decoded) = TransmitCall::decode(txn.input.clone()) {
    let report: Bytes = decoded.report;
    let mut observations: Vec<U256> = vec![];
    const WORD_SIZE: usize = 32;

    for word in report.to_vec().chunks(WORD_SIZE).into_iter().skip(4) {
        observations.push(U256::from(word));
    }

    if let Some(median) = observations.get(observations.len() / 2) {

        // We use a communication channel to push 
        // the predicted price to the main loop
        tx.send(PricePrediction {
            new_price: *median,
            transaction: txn,
        })
        .await
        .unwrap();
    }
}

Conclusions

This strategy will give you the privileged information that an asset price is going to change in a bunch of seconds. You know the predicted price, and the exact transaction that is going to apply that change in a future block. Use it to update user's HF in memory and predict if a user is going to fall below the liquidation threshold in the near future.

Do you feel ready to backrun the price oracle and compete for the top liquidators leaderboard!

If you liked this post, follow me on GitHub and stay tuned for updates.

Credits

Forest image by Beorn via hallofbeorn.