Skip to content

OpenMEV

  • Intro

  • Benefits

  • Issues/Support links

User Example

Info

order than sent, we need a mechanism for choosing a canonical id from a list that doesn't depend on the order. This chooses the "minimum" id by an arbitrary ordering: the smallest string if possible, otherwise the smallest number, otherwise null.

Example

order = {
    Give: ETH,
    Want: DAI,
    SlippageLimit: 10%,
    Amount: 1000ETH,
    OpenMEV: 0xabc...,
    FeesIn: DAI,
    TargetDEX: SushiSwap,
    Deadline: time.Now() + 1*time.Minute
    Signature: sign(order.SignBytes())
}

When we broadcast this transaction with an arbitrage order, the transaction contains 2 orders:

Example

transactions = [
    {
        Give: ETH,
        Want: DAI,
        SlippageLimit: 10%,
        Amount: 1000ETH,
        OpenMEV: 0xabc...,
        FeesIn: DAI,
        TargetDEX: SushiSwap,
        Deadline: time.Now() + 1*time.Minute
        Signature: sign(order.SignBytes())
    },
    {
        Give: DAI,
        Want: ETH,
        SlippageLimit: 1%,
        Amount: 10ETH,
        OpenMEV: 0xabc...,
        FeesIn: DAI,
        TargetDEX: SushiSwap,
        Deadline: time.Now() + 1*time.Minute
        Signature: sign(order.SignBytes()),
        IsFlashbots OpenMEV: true,
        TransferProfitTo: transactions[0].signer
    }
]

The arbitrage profit generated by second order is sent to the msg.sender of the first order.

The first order will still lose 5%(assumption) in slippage.

Arbitrage profits will rarely be more than the slippage loss.

If someone front runs the transaction sent by the OpenMEV:

  1. They pay for the gas while post confirmation of transaction the fees for order1 goes to the relayer in the signed order.
  2. They lose 5% in slippage as our real user does.

Engine

OpenMEV uses a batch auction-based matching engine to execute orders.

  1. All orders for the given market are collected.

  2. Orders beyond their time-in-force are canceled.

  3. Orders are placed into separate lists by market side, and aggregate supply and demand curves are calculated.

  4. The matching engine discovers the price at which the aggregate supply and demand curves cross, which yields the clearing price. If there is a horizontal cross - i.e., two prices for which aggregate supply and demand are equal - then the clearing price is the midpoint between the two prices.

  5. If both sides of the market have equal volume, then all orders are completely filled. If one side has more volume than the other, then the side with higher volume is rationed pro-rata based on how much its volume exceeds the other side. For example, if aggregate demand is 100 and aggregate supply is 90, then every order on the demand side of the market will be matched by 90%.

Orders are sorted based on their price, and order ID. Order IDs are generated at post time and is the only part of the matching engine that is time-dependent. However, the oldest order IDs are matched first so there is no incentive to post an order ahead of someone else’s.

Summary

  1. Bundle the frontend transactions into blocks
  2. Finds miner extracted value (MEV), and then sends them to miners through a direct connection
  3. Redistribute the resulting value back to Manifold users who submitted trades by eliminating their transaction cost and to the greater SushiSwap community by further enriching their rewards pool

Application structure

  • Route paths: acceptable trading pairs/whitelisted tokens. See manifold.tokenlist.json
  • Subroutes can be defined in separate files within the routes folder and referenced in
  • Controllers should be used to handle HTTP/WS/RPC requests
  • Services handle business logic

Endpoints v1

POST

/v1/quote

A user can get a quote based on the current market conditions and potentially the other transactions that are queued. The only reason for this to be on the backend is if we find value in quoting based on the other transactions in the queue. Details of this quote calculation formula need to be researched.

POST

/v1/transact

A user submits their transaction call after signing it. This is an asynchronous request that will insert the swap transaction in a queue. The call returns a TransactionId. The user can then listen for the result of their transaction via the websocket feed which will report all completed and failed transactions.

Sequencing

On any additions to the queue, the server runs a sequencing algorithm to optimize MEV, then decides if it is time to submit the transactions to a miner for the current block.

!!! attention GC (Garbage Collection) is only on public submitted transactions, not SushiSwap sent transactions!

Warning

remove transactions that are not going to be successful due to timeout or slippage.

On successful and failed (garbage collected) transactions, the backend will send WebSocket messages to the frontend to notify users.

Transaction Price Service

!!! tip Visit txprice.com and API

The TxPrice Service is an important part of the overall system. Since Gwei pricing is the most important portion of the overall system efficacy it is decoupled from the application itself and run in a separate stack entirely. We inject the Gwei pricing service by loading at runtime via startGasWorker(). note we use the term GasWorker to draw a distinction between gwei and gas. Whereas gwei is understood as a specific SI unit, gas is more abstract.

Gas Pricing Service

Note: EIP1559 Gas Pricing Compatible

For accurate pricing, we trim off the lowest price with the fastest time and highest price with the slowest times until 80% of the data is represented; these are outliers.

See the API Service here: https://api.txprice.com

!! warning This is for Legacy Transactions

/**
*
* @summary filters transactions from blocks
* @note transaction wait duration and gas price taken into consideration
*/
blocks.forEach((block) => {
       block.transactions.forEach((tx) => {
           const price = parseFloat(ethers.utils.formatUnits(tx.gasPrice, "gwei"));
           const duration = tx.waitDuration;
 /**
 *
 *  @summary Purge anything that takes over 5 minutes
 *  @param duration
 *  @type {seconds}
 *  @exports TransactionTimeDuration
 */
   if (duration > (60 * 60)) { return; }

   if (duration < (1 * 60)) {
               data.fast.push(price);
       } else if (duration < (5 * 60)) {
               data.medium.push(price);
       } else {
               data.slow.push(price);
       }

Transaction Details

/**
 * Add the transaction details
 *  @const diff
 *  @param waitDuration
 *  @param dataLength
 *  @param gasLimit
 *  @param value
 */
const diff = timestamp - seenTime;
txs.push({
  w: diff, // waitDuration, not a delta but difference
  d: ethers.utils.hexDataLength(tx.data), // dataLength
  l: tx.gasLimit.toString(), // gasLimit
  p: ethers.utils.formatUnits(tx.gasPrice, 'gwei'), // gasPrice
  v: ethers.utils.formatUnits(tx.value), // value
});

Canary Scanning

Failsafe guard

// Canary scanning (check every second)
// If we go too long without a ne block or a new transaction, it indicates the
// underlying connection to a backend has probably disconnected.
setInterval(() => {
  const delta = getTime() - canaryTimer;
  if (delta > MAX_DISCONNECT) {
    console.log(`Canary: forcing restart...`);
    process.exit();
  }
}, 1000).unref();

How to subscribe to gas price changes

import { Container } from 'typedi';
import EventConstants from '@constants/events';
import EventEmitter from 'events';

const { GAS_CHANGE } = EventConstants;

const events: EventEmitter = Container.get('eventEmitter');
events.on(GAS_CHANGE, (newGasPrice) => {});
Back to top