⚑OEV Liquidator Bot

This guide walks through building a liquidation bot that uses Moonwell's OEV (Oracle Extractable Value) system to capture liquidation profit. Instead of competing with MEV searchers, you route liquidations through the OEV wrapper to access fresh Chainlink prices before everyone else.

How OEV Liquidation Works

  1. Chainlink posts a new price update

  2. The OEV wrapper delays the new price by ~10 seconds

  3. During this window, only the updatePriceEarlyAndLiquidate() function can use the fresh price

  4. Your bot calls this function, which unlocks the price, executes the liquidation, and splits the profit

  5. Liquidation profits are split between you and the protocol based on liquidatorFeeBps (currently 40% to liquidator)

For the full OEV mechanism, see OEV.

Step 1: Find Liquidatable Positions

Use the Comptroller to check which accounts are underwater.

Comptroller comptroller = Comptroller(0x...);

// Returns (error, liquidity, shortfall) - all in USD with 18 decimals
(uint err, uint liquidity, uint shortfall) =
    comptroller.getAccountLiquidity(borrowerAddress);

// shortfall > 0 means the account is liquidatable
circle-info

In practice, you'll monitor on-chain events or index account positions off-chain. When a new Chainlink round is posted and an account becomes underwater at the new price, that's your OEV opportunity - you have ~10 seconds before the price becomes public.

Step 2: Calculate Repay Amount

The maximum you can repay in a single liquidation is limited by the close factor (typically 50%).

Step 3: Execute the OEV Liquidation

Deploy a contract that calls updatePriceEarlyAndLiquidate() on the OEV wrapper. The wrapper handles everything - price update, liquidation execution, and profit splitting.

Step 4: Profit

After a successful liquidation, your contract receives mTokens representing your share of the seized collateral. You get your full repayment back plus a share of the profit based on liquidatorFeeBps (configurable, currently 4000 = 40%):

Amount

Repayment

1,000 USDC

Collateral seized (10% liquidation incentive)

1,100 USDC

Gross profit

100 USDC

Your bonus (40% of profit)

40 USDC worth of mTokens

Your total

1,040 USDC worth of mTokens

Protocol share (60% of profit)

60 USDC worth of mTokens

You can hold the mTokens (they earn interest) or redeem them for the underlying collateral token.

Morpho Market Liquidations

The Morpho OEV wrapper (ChainlinkOEVMorphoWrapper) works differently from the core wrapper:

Core Markets
Morpho Markets

Collateral received

mTokens

Underlying tokens

Market identification

mToken addresses

MarketParams struct

Slippage protection

Fixed repay amount

maxRepayAmount parameter

You specify

Repay amount

Seized collateral amount

circle-info

The Morpho wrapper's oracle must have the OEV wrapper set as BASE_FEED_1. The contract verifies this and reverts if the oracle isn't configured correctly.

For the full Morpho wrapper reference, see OEV - Morpho Markets.

Key Considerations

Use the right wrapper. Each OEV wrapper is tied to a specific collateral asset's Chainlink feed. The contract verifies the feed matches and reverts if you use the wrong wrapper. See Core Markets - Deployed Contracts for wrapper addresses.

The ~10 second window is competitive. Multiple liquidators may target the same opportunity. Your transaction needs to land before the delay expires, at which point the price becomes public and standard liquidation bots can compete.

Repay amount cannot be zero or type(uint).max. The wrapper rejects zero amounts directly. Passing type(uint).max is rejected by the underlying liquidateBorrow call, which causes the wrapper to revert with "ChainlinkOEVWrapper: liquidation failed". Use borrowBalanceCurrent() and closeFactorMantissa() to calculate a valid amount.

Error handling. If the underlying liquidateBorrow fails (e.g. the account isn't actually underwater at the fresh price), the wrapper reverts with "ChainlinkOEVWrapper: liquidation failed".

Deployed OEV Wrappers

See OEV - Core Markets and OEV - Morpho Markets for all deployed wrapper addresses.

Last updated