# 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](https://docs.moonwell.fi/moonwell/developers/protocol/oev).

## Step 1: Find Liquidatable Positions

Use the Comptroller to check which accounts are underwater.

```solidity
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
```

{% hint style="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.
{% endhint %}

## Step 2: Calculate Repay Amount

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

```solidity
MErc20 mTokenLoan = MErc20(0x...); // The market the borrower owes

// Get the borrower's current debt in this market
uint borrowBalance = mTokenLoan.borrowBalanceCurrent(borrowerAddress);

// Close factor is a mantissa (e.g. 0.5e18 = 50%)
uint closeFactor = comptroller.closeFactorMantissa();

// Maximum repay amount
uint maxRepay = borrowBalance * closeFactor / 1e18;
```

## 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.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IChainlinkOEVWrapper {
    function updatePriceEarlyAndLiquidate(
        address borrower,
        uint256 repayAmount,
        address mTokenCollateral,
        address mTokenLoan
    ) external;
}

interface IMToken {
    function underlying() external view returns (address);
}

contract OEVLiquidator {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    /// @notice Execute an OEV liquidation
    /// @param wrapper The ChainlinkOEVWrapper for the collateral asset
    /// @param borrower The underwater account to liquidate
    /// @param repayAmount Amount of loan tokens to repay
    /// @param mTokenCollateral The mToken market for collateral being seized
    /// @param mTokenLoan The mToken market for the loan being repaid
    function liquidate(
        address wrapper,
        address borrower,
        uint256 repayAmount,
        address mTokenCollateral,
        address mTokenLoan
    ) external {
        require(msg.sender == owner, "not owner");

        // 1. Get the loan token
        address loanToken = IMToken(mTokenLoan).underlying();

        // 2. Transfer loan tokens from caller
        IERC20(loanToken).transferFrom(msg.sender, address(this), repayAmount);

        // 3. Approve wrapper to spend loan tokens
        IERC20(loanToken).approve(wrapper, repayAmount);

        // 4. Execute OEV liquidation - unlocks fresh price + liquidates + splits profit
        IChainlinkOEVWrapper(wrapper).updatePriceEarlyAndLiquidate(
            borrower,
            repayAmount,
            mTokenCollateral,
            mTokenLoan
        );

        // 5. Seized mTokens (your share) are now in this contract
        //    Transfer them to the caller or redeem for underlying
    }

    /// @notice Withdraw any tokens from this contract
    function sweep(address token, address to) external {
        require(msg.sender == owner, "not owner");
        IERC20(token).transfer(to, IERC20(token).balanceOf(address(this)));
    }
}
```

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

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

struct MarketParams {
    address loanToken;
    address collateralToken;
    address oracle;
    address irm;
    uint256 lltv;
}

interface IChainlinkOEVMorphoWrapper {
    function updatePriceEarlyAndLiquidate(
        MarketParams memory marketParams,
        address borrower,
        uint256 seizedAssets,
        uint256 maxRepayAmount
    ) external;
}

contract MorphoOEVLiquidator {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function liquidate(
        address wrapper,
        MarketParams calldata marketParams,
        address borrower,
        uint256 seizedAssets,
        uint256 maxRepayAmount
    ) external {
        require(msg.sender == owner, "not owner");
        address loanToken = marketParams.loanToken;

        // 1. Transfer max loan tokens from caller
        IERC20(loanToken).transferFrom(msg.sender, address(this), maxRepayAmount);

        // 2. Approve wrapper
        IERC20(loanToken).approve(wrapper, maxRepayAmount);

        // 3. Execute - excess loan tokens are returned automatically
        IChainlinkOEVMorphoWrapper(wrapper).updatePriceEarlyAndLiquidate(
            marketParams,
            borrower,
            seizedAssets,
            maxRepayAmount
        );

        // 4. Collateral tokens (not mTokens) are now in this contract
        IERC20(marketParams.collateralToken).transfer(
            msg.sender,
            IERC20(marketParams.collateralToken).balanceOf(address(this))
        );

        // 5. Return any remaining loan tokens
        uint256 remaining = IERC20(loanToken).balanceOf(address(this));
        if (remaining > 0) {
            IERC20(loanToken).transfer(msg.sender, remaining);
        }
    }
}
```

{% hint style="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.
{% endhint %}

For the full Morpho wrapper reference, see [OEV - Morpho Markets](https://docs.moonwell.fi/moonwell/developers/protocol/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](https://docs.moonwell.fi/moonwell/protocol/mtokens#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](https://docs.moonwell.fi/moonwell/protocol/oev/core-markets#deployed-contracts) and [OEV - Morpho Markets](https://docs.moonwell.fi/moonwell/protocol/oev/morpho-markets#deployed-contracts) for all deployed wrapper addresses.
