Flashbots Integration

Walkthrough showing how Oval uses Flashbot's MEV-share on Ethereum Sepolia testnet.

This technical example shows a script and a series of sample contracts to illustrate a complete end-to-end Oval workflow in Sepolia with Flashbots. This demonstration will highlight how a searcher monitors a Price Feed update in a data feed oracle, such as Chainlink. The searcher then participates in the Oval auction specific to that price update and subsequently executes the actual liquidation. This technical example also presents numerical examples to explain how the searcher realizes a profit from the liquidation and how the protocol also earns a share of the liquidation proceeds.

You can find the scripts in the oval-quickstart repository.

Running the sample Flashbots integration on Ethereum Sepolia

To demonstrate the integration, we will run a set of scripts against Sepolia. To start with you will need a few things to proceed:

  1. Wallet with Private Key: For deploying on Sepolia, you can fund your wallet here, here or here.

  2. Foundry Installation: Install Foundry from here.

  3. Sepolia RPC URL: Use Infura or similar services.

  4. Etherscan API Key for Sepolia: Optional, obtainable here.

Then, run the following commands to clone the quickstart and install the dependencies.

git clone https://github.com/UMAprotocol/oval-quickstart.git
cd oval-quickstart
forge install
forge build

1. Deploy the demo contracts:

Run the following command in the root folder of the oval-quickstart repository.

forge script ./src/liquidation-demo/script/OvalLiquidationDemo.s.sol:OvalLiquidationDemoScript \
--rpc-url https://sepolia.infura.io/v3/<YOUR-INFURA-KEY> \
--private-key <YOUR-PRIVATE-KEY> \
--broadcast \
--verify \
--etherscan-api-key <YOUR-ETHERSCAN-API-KEY>

2. Run the liquidation script with Flashbots in Sepolia:

First fill in the contracts addresses deployed in previous steps in a .env file and the necessary keys and urls showed in /liquidation-demo-flashbots/.env.example:

NODE_URL_11155111=https://sepolia.infura.io/v3/<YOUR-INFURA-KEY>
CHAIN_ID=11155111
PRIVATE_KEY=<YOUR-PRIVATE-KEY> # Ensure you have some ETH in Sepolia for testing
OVAL_LIQUIDATION_DEMO_PRICE_FEED_ADDRESS=<YOUR-PRICE-FEED-ADDRESS>
CHAINLINK_OVAL_IMMUTABLE_ADDRESS=<YOUR-OVAL-ADDRESS>
OVAL_LIQUIDATION_DEMO_ADDRESS=<YOUR-LIQUIDATION-DEMO-ADDRESS>
PAY_BUILDER_ADDRESS=<YOUR-PAY-BUILDER-ADDRESS>

Then run the liquidation demo script from the /liquidation-demo-flashbots folder in oval-quickstart

  cd liquidation-demo-flashbots
  yarn install && yarn build 
  yarn start

Overview of the liquidation demo workflow

Here’s a streamlined overview of this workflow:

  1. A user creates a collateralized position in an example money market, named OvalLiquidationDemo.

  2. The price feed used by OvalLiquidationDemo is updated, rendering the user's position undercollateralized and eligible for liquidation .  

  3. A searcher identifies this opportunity and submits a bundle to liquidate the user. This action triggers an auction within MEV-Share, a process similar to what a traditional Liquidator would do without Oval. In this auction, Searchers compete against each other, with the highest bidder winning the right to execute the liquidation .

  4. The Searcher is required to include a payment to the builder within the bundle, representing their bid in the Order Flow Auction occurring in MEV-Share. This is a standard practice.

  5. When submitting the liquidation bundle to the Oval Node, a price unlock bundle is added in front. For this demonstration, the searcher's bundle already includes this price unlock bundle, but this will not be the case in a production environment where the Oval Node would do this role (see Oval Node). The price unlock bundle contains refund instructions to compensate the protocol with a portion of the winning auction bid.

  6. Once the winning bundle is selected, the Oval price feed update is used, making the new price available for use. Subsequently, the liquidation is executed, including both the payment to the builder and a kickback to the protocol. In this example, the protocol receives 90% of the builder's payment, equating to 90% of the MEV extracted from this transaction.

Contract Overview

This example involves the following contracts:

  1. OvalLiquidationDemo: This is a mock money market contract where users can create ETH collateralized positions using the updateCollateralisedPosition() function. The contract assesses the collateralized value by obtaining the price from an Oval of ChainlinkOvalImmutable, which reads from a mock Chainlink price feed, OvalLiquidationDemoPriceFeed

  2. OvalLiquidationDemoPriceFeed: This mock price feed functions similarly to a Chainlink price feed. The owner has the ability to push new prices, a feature especially useful for the purposes of this demo.

  3. ChainlinkOvalImmutable: This is the Oval contract that the money market references. It wraps the Chainlink price feed that we call OvalLiquidationDemoPriceFeed to supply prices to the consumer contract.

  4. PayBuilder: An auxiliary contract designed to facilitate the searcher in sending the builder payment transaction.

Liquidation script overview

We will now delve into the liquidation script, highlighting key aspects step by step:

Creating a Collateralized Position in a Mock Money Market: A user establishes a collateralized position using the following code:

  const collateralAmount = ethers.parseEther("0.01");
  await (await liquidationDemo.connect(user).updateCollateralisedPosition({ value: collateralAmount })).wait();
  console.log("User created as position in liquidation demo contract with ETH: ", ethers.formatEther(collateralAmount));

Updating the Price Feed in OvalLiquidationDemo: This step renders the user's position undercollateralized and subject to liquidation. In a production environment, this update would typically be sent by a Chainlink oracle and detected in the public mempool by the searcher.

  const signedUpdatePriceFeed = await getOvalPriceUpdateSignedTx(demoPriceFeed, priceOracleSigner, nonce + 1, baseFee);

Submission of Liquidation Bundle by the Searcher: The searcher identifies the liquidation opportunity and submits a bundle to the Oval Node. In this demo, the searcher's bundle includes a price unlock bundle, although this won't be necessary in a production setting as this is done by the Oval Node. The price unlock bundle includes instructions for refunding a portion of the winning auction bid to the protocol.

const ovalUnlockLatestValueBundle = await getUnlockBundle(oval, refundAddress, ovalSigner, nonce, currentBlock, baseFee);

Preparation of the Unlock and Liquidation Bundles: The searcher prepares the liquidation bundle, aiming to backrun the price update in the oracle. Notice the unlock price bundle prepended only needed in this testnet demo.

  const seacherBackrunBundleBody = [
    { bundle: ovalUnlockLatestValueBundle }, // In prod, this would be added by the Oval Node
    { tx: signedUpdatePriceFeed, canRevert: false },
    { tx: await searcher.signTransaction(liquidateTransaction), canRevert: false },
    { tx: await searcher.signTransaction(blockBuilderPaymentTransaction), canRevert: false },
  ]

Configuring the Refund in the Searcher's Bundle: The searcher sets up a refund configuration within their bundle to transfer a percentage of the builder payment (90% in this demo) to the protocol. This configuration is also done by the Oval Node in production.

  // Refund 90% of the winning bid to the protocol. 
  // In prod, this is configured in the Oval Node.
  const protocolRefundPercentage = 90;
  const searcherBundle: BundleParams = {
    inclusion: { block: currentBlock, maxBlock: currentBlock + 25 },
    body: seacherBackrunBundleBody,
    validity: { refund: [ { bodyIdx: 0, percent: protocolRefundPercentage } ] },

The following example output is run on Goerli, not Sepolia, but still illustrates the expected result.

Execution and Monitoring of the Liquidation Process: After submitting the searcher bundle to Flashbots, we wait for its inclusion. Once included, we can observe all the transactions involved in the liquidation. Here's an example log from this script on Goerli:

Updating price feed with roundId:  1  price:  100.0  updateTime:  1704890421
Creating position in liquidation demo contract with ETH:  0.01
Updating price feed with roundId:  2  price:  90.0  updateTime:  1704890502
Liquidation found!
Liquidation size eth:  0.06  eth
Searcher return:  0.006  eth
Builder return:  0.0054  eth
Protocol return: ~ 0.0486  eth
Liquidation bundle sent with hash:  0x3d95acf643953192fbbd8fc373dd2cdc57c1a31e48865bc62c3602e7e09ba5c8

Price feed update tx mined!
Tx hash: 0x41032cf97893f211f676d8f2c10d5df0b3823d644648766458fe166baa7d77b0

Liquidate tx mined!
Tx hash: 0xb1d8f0981b220811b57925ab018745d0dbdf8d830c8f5a01ca9cfd45aaa422ca

Pay builder tx mined!
Tx hash: 0x1f1d1fed288da8e27b0f3bf4d450c0e96c0d5763b448188ca7105de8eed4e8c9

On the block explorer here https://goerli.etherscan.io/txs?block=10351968&p=2, we can verify all the transactions executed in the expected order in block 10351968.

The liquidation process concludes with the builder receiving 90% of the liquidation size, and a 90% kickback of the builder payment goes to the protocol. The figures align with our script's calculations.

Last updated