Skip to main content

FastLane MEV Auction Handler - Searcher Integration Guide

Overview

The FastLane Auction Handler enables searchers to submit MEV bids on Monad for Top of Block ("TOB") or backrun opportunities.

Core Concepts

Bid Submission Flow

  1. Searcher bids are transactions that call the flashExecutionBid() function of the FastLane AuctionHandler contract; the transactions are submitted via eth_sendRawTransaction
tip

You can submit your transaction on any RPC service or node. The transaction will make its way to validators via RaptorCast just like any other Monad transaction.

  1. Auction handler executes searcher's contract via fastLaneCall()
  2. Searcher performs MEV operations
  3. Searcher repays bid amount to auction handler
  4. Proceeds distributed to validators

Auction Handler Interface

interface IFastLaneAuctionHandler {
// Entry point function
function flashExecutionBid(
uint256 bidAmount,
bytes32[] calldata txHashes,
bool executeOnLoss,
bool payBidOnFail,
address searcherToAddress,
bytes calldata searcherCallData
)
external
payable;
}

Parameters

  • bidAmount – Amount of native token (e.g., MON) you agree to pay on success (and potentially on failure if payBidOnFail = true).
  • txHashes – Array of txHashes that the MEV client will ensure your transaction is executed directly after. Use [bytes32(0)] for ToB.
  • executeOnLoss – If true, your transaction will be executed regardless if you win or lose the auction
  • payBidOnFail – If true, bid will be paid from shMON balance committed to the auction handler policy
  • searcherToAddress – The address the relay will call. Must be your Searcher Direct/Proxy contract.
  • searcherCallData – ABI-encoded call your Searcher executes, e.g. doStuff(...).

Smart Contract Integration

contract MySearcherBot is FastLaneSearcherDirectContract {
constructor() FastLaneSearcherDirectContract() {}

function myMEVFunction(/* params */) public payable returns (bool) {
// Security check
if (msg.sender != address(this)) {
require(approvedEOAs[msg.sender], "SenderEOANotApproved");
}

// MEV logic here

return true; // Return success status
}
}

Required Setup:

  1. Deploy your searcher contract
  2. Call setMFLAuctionAddress(auction_address)
  3. Call approveFastLaneEOA(your_eoa) for authorized callers

Submitting Bids

Submitting bids or bundles is the same as sending a regular onchain transaction via eth_sendRawTransaction.

You create a normal transaction with the 'to' address as the auctionHandler contract and the function call as a flashExecutionBid per below.

Auction Handler Contract Addresses:

  • Monad Testnet: 0x11f34d16BB4B898c3a489B40cD1024d89A313b88
  • Monad Mainnet: 0xD32EdF6642D917DbBE7B8BF8e5d6F5df6a9FFF58

Flash Execution Bid

// For atomic backruns
auctionHandler.flashExecutionBid{value: bidAmount}(
bidAmount, // Bid amount in native token
txHashes, // Array of tx hashes to backrun
false, // executeOnLoss
false, // payBidOnFail
searcherContract, // Your searcher contract
searcherCallData // abi.encodeWithSignature("myMEVFunction(...)", ...)
);

Security Requirements

Critical Checks

  1. Always verify caller: Use onlyRelayer modifier
  2. Check EOA permissions: Implement checkFastLaneEOA(_sender)
  3. Use reentrancy guards: Inherit ReentrancyGuard
  4. Validate balance: Ensure sufficient funds to repay bid

Example Security Pattern

function fastLaneCall(
address _sender,
uint256 _bidAmount,
bytes calldata _searcherCallData
) external payable onlyRelayer nonReentrant returns (bool, bytes memory) {
checkFastLaneEOA(_sender); // Verify caller authorization

(bool success, bytes memory data) = address(this).call(_searcherCallData);

if (!success) return (false, data);

require(address(this).balance >= _bidAmount, "Insufficient funds");
safeTransferETH(MFLAuction, _bidAmount);

return (true, data);
}

Full Example Contract

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;

import { ReentrancyGuard } from "@solady/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

abstract contract FastLaneSearcherDirectContract is ReentrancyGuard {
address public owner;
address payable private MFLAuction;

error WrongPermissions();
error OriginEOANotOwner();
error SearcherCallUnsuccessful(bytes retData);
error SearcherInsufficientFunds(uint256 amountToSend, uint256 currentBalance);

mapping(address => bool) internal approvedEOAs;

constructor() {
owner = msg.sender;
}

// The FastLane Auction contract will call this function
// The `onlyRelayer` modifier makes sure the calls can only come from MFL or will revert
// MFL will pass along the original msg.sender as _sender for the searcher to do additional checks
// Do NOT forget `onlyRelayer` and `checkFastLaneEOA(_sender);` or ANYONE will be able to call your contract with
// arbitrary calldata
function fastLaneCall(
address _sender, // Relay will always set this to msg.sender that called it. Ideally you (owner) or an
// approvedEOA.
uint256 _bidAmount,
bytes calldata _searcherCallData // contains func selector and calldata for your MEV transaction ie:
// abi.encodeWithSignature("doStuff(address,uint256)", 0xF00, 1212);
)
external
payable
onlyRelayer
nonReentrant
returns (bool, bytes memory)
{
// Make sure it's your own EOA that's calling your contract
checkFastLaneEOA(_sender);

// Execute the searcher's intended function
(bool success, bytes memory returnedData) = address(this).call(_searcherCallData);

if (!success) {
// If the call didn't turn out the way you wanted, revert either here or inside your MEV function itself
return (false, returnedData);
}

// Balance check then Repay MFL at the end
require(
(address(this).balance >= _bidAmount),
string(
abi.encodePacked(
"SearcherInsufficientFunds ",
Strings.toString(_bidAmount),
" ",
Strings.toString(address(this).balance)
)
)
);

safeTransferETH(MFLAuction, _bidAmount);

// /!\ Important to return success true or relay will revert.
// In case of success == false, `returnedData` will be used as revert message that can be decoded with
// `.humanizeError()`
return (success, returnedData);
}

// Other functions / modifiers that are necessary for FastLane integration:
// NOTE: you can use your own versions of these, or find alternative ways
// to implement similar safety checks. Please be careful when altering!
function safeTransferETH(address to, uint256 amount) internal {
bool success;

assembly {
// Transfer the ETH and store if it succeeded or not.
success := call(gas(), to, amount, 0, 0, 0, 0)
}

require(success, "ETH_TRANSFER_FAILED");
}

function setMFLAuctionAddress(address _mflAuction) public {
require(msg.sender == owner, "OriginEOANotOwner");
MFLAuction = payable(_mflAuction);
}

function approveFastLaneEOA(address _eoaAddress) public {
require(msg.sender == owner, "OriginEOANotOwner");
approvedEOAs[_eoaAddress] = true;
}

function revokeFastLaneEOA(address _eoaAddress) public {
require(msg.sender == owner, "OriginEOANotOwner");
approvedEOAs[_eoaAddress] = false;
}

function checkFastLaneEOA(address _eoaAddress) internal view {
require(approvedEOAs[_eoaAddress] || _eoaAddress == owner, "SenderEOANotApproved");
}

function isTrustedForwarder(address _forwarder) public view returns (bool) {
return _forwarder == MFLAuction;
}

// Be aware with a fallback fn that:
// `address(this).call(_searcherCallData);`
// Will hit this if _searcherCallData function is not implemented.
// And success will be true.
fallback() external payable { }
receive() external payable { }

modifier onlyRelayer() {
if (!isTrustedForwarder(msg.sender)) revert("InvalidPermissions");
_;
}
}

contract SearcherContractExample is FastLaneSearcherDirectContract {
// Your own MEV contract / functions here
// NOTE: its security checks must be compatible w/ calls from the FastLane Auction Contract

address public anAddress; // just a var to change for the placeholder MEV function
uint256 public anAmount; // another var to change for the placeholder MEV function

function doStuff(address _anAddress, uint256 _anAmount) public payable returns (bool) {
// NOTE: this function can't be external as the FastLaneCall func will call it internally
if (msg.sender != address(this)) {
// NOTE: msg.sender becomes address(this) if using call from inside contract per above example in
// `fasfastLaneCall`
require(approvedEOAs[msg.sender], "SenderEOANotApproved");
}

// Do MEV stuff here
// placeholder
anAddress = _anAddress;
anAmount = _anAmount;
bool isSuccessful = true;
return isSuccessful;
}

function doFail() public payable {
if (msg.sender != address(this)) {
// NOTE: msg.sender becomes address(this) if using call from inside contract per above example in
// `fasfastLaneCall`
require(approvedEOAs[msg.sender], "SenderEOANotApproved");
}
// Will cause Error(string) of:
// 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f4641494c5f4f4e5f505552504f53450000000000000000000000000000000000
// to bubble up to the relay contract.
// Use the read function `FastLaneRelay.humanizeError(bytes error)` to get a human readable version of an error
// should your searcher contract fail on a require.
require(false, "FAIL_ON_PURPOSE");
}
}