Polygon FastLane
  • What is Polygon FastLane?
    • Overview
    • Design Principles
    • Components
    • Component Diagram
  • Getting Started as a Validator
    • Getting Started as a Validator
    • Connecting to a FastLane Sentry Node
      • Finding Your Enode Address & Peer ID
      • Adding FastLane as a Static Peer
    • Patching Your Sentry Nodes With The FastLane Patch
      • Installing from source
        • Patch Download
        • Patch Installation
      • Installing from packages
  • Withdrawing Validator Revenue
    • Validator Vault
      • Connect an Eligible Wallet
      • Revenue Redemption (withdrawal)
  • Searcher Guides
    • Getting Started as a Searcher
      • Solver Call Data
      • Submission Methods
      • Migration Guide for Searchers
    • Bundles (Backruns)
      • Bundle Format
      • Bid Submission
      • Bundle Requirements
      • Full Example
      • Subscribe Events
    • 4337 Bundles Integration Guide
      • Overview
      • How it works
      • RPC Reference
      • Examples
    • Searcher Contract Integration
      • Safety Considerations
      • atlasSolverCall
      • Direct Implementation
      • Proxy Implementation
      • Solver Concepts
      • Altas Bonding Concept
      • Bond atlETH
      • Estimating Solver Gas Charges
    • Addresses & Endpoints
    • Helpers
    • Common Mistakes
    • Atlas SDK's
  • Tools and Analytics
    • FastLane Bundle Explorer
      • Features Overview
      • Key Components
      • Usage Example
      • Error Codes & Troubleshooting
  • Key Concepts
    • Transaction Encoding
  • INFRASTRUCTURE
    • Health Status Endpoint
  • Reference
    • Relay JSON-RPC API
    • Relay REST API
    • Glossary of Terms
Powered by GitBook
On this page
  • Submitting the Signed Message
  • 1. Generate CallData for the Solver contract function
  • 2. Generate SolverOperation to Submit for
  • Encoding of the Bundle
  1. Searcher Guides
  2. Bundles (Backruns)

Bid Submission

FastLane Searchers submit their bids and solutions to the FastLane Protocol using EIP-712 signed messages. This method allows for off-chain data signing and on-chain verification, reducing gas costs and improving efficiency. The submission to the PFL-Auction will be performed by Bundler EOA's. To establish a trusted a PFL-Auction transaction is constructed of 3 parts here:

  • UserOperation. : (opportunity transaction converted on PFL-Auction system)

  • SolverOperation : (generated and signed by searcher)

  • dAppOperation : (generated and signed by dAppSigner to hash userOperation and selected solverOperations)

The above definitions are simplified to for more technical details we encourage to read our Atlas documentation

Submitting the Signed Message

This will be a two step process

Atlas will call the atlasSolverCall on the solver/searcher contracts

// Opionanted atlasSolverCall implementation which forwards
// to a internal call using the solverOpData
function atlasSolverCall(
    address solverOpFrom,
    address executionEnvironment,
    address bidToken,
    uint256 bidAmount,
    bytes calldata solverOpData,
    bytes calldata
)
    external
    payable
    virtual
    safetyFirst(executionEnvironment, solverOpFrom)
    payBids(executionEnvironment, bidToken, bidAmount)
{
    (bool success,) = address(this).call{ value: msg.value }(solverOpData);
    if (!success) revert SolverCallUnsuccessful();
}

1. Generate CallData for the Solver contract function

The first step is to generate and encode the backrun for a particular opportunity transaction

  • In our example the solve() function

  • Responsibilities:

    • perform backrun operation

    • makes sure the the contract has bidToken in bidAmount quantity (POL)


// This function is called by atlasSolverCall() which forwards the solverOpData calldata
// by doing: address(this).call{value: msg.value}(solverOpData)
// where solverOpData contains the ABI-encoded call to solve()
function solve() public view onlySelf {
    // SolverBase automatically handles paying the bid amount to the Execution Environment through
    // the payBids modifier, as long as this contract has sufficient balance (ETH or WETH)
}

2. Generate SolverOperation to Submit for

The second part will be generating the a EIP-712 signed messages with a atlas specific format

EIP-712 like Message Structure

Searchers need to construct and sign a message containing the bid and operation details according to an Atlas format similar to the EIP-712 standard. A SolverOperation contains the following important details

The message includes:

  • Bid Details: Bid token address, bid amount.

  • Operation Data: Encoded data for the solver operation.

  • dAppControl: PFL-Auction specific implementation of the Atlas hooks (pre-solver, post-solver)

  • dAppSigner: Account which will be signing the dAppControl operation

  • userOpHash: hash of User's Operation, for verification

  • deadline: is provided in block number not timestamp!

struct SolverOperation {
    address from; // Solver address
    address to; // Atlas address
    uint256 value; // Amount of ETH(POL) required for the solver operation (used in `value` field of the solver call)
    uint256 gas; // Gas limit for the solver operation
    uint256 maxFeePerGas; // maxFeePerGas matching the opportunity tx
    uint256 deadline; // block.number deadline for the solver operation
    address solver; // Nested "to" address (used in `to` field of the solver call)
    address control; // PFL Auction DAppControl (see documentation)
    bytes32 userOpHash; // hash of User's Operation, for verification of user's tx (if not matched, solver wont be
        // charged for gas)
    address bidToken; // address(0) for ETH(POL)
    uint256 bidAmount; // Amount of bidToken that the solver bids
    bytes data; // Solver op calldata (used in `data` field of the solver call)
    bytes signature; // Solver operation signature signed by SolverOperation.from
}
  • dAppControl Contract Address

    • Description: This is the current address of the dAppControl contract, which is responsible for generating the userOpHash needed for the solver operation.

    • PFL-Auctions implementation for Atlas hooks (pre-solver, post-solver) and value distribution

  • dAppOpSigner Address

    • Description: This is the address of the dAppOpSigner, which acts as the signer for the bundled dAppOperation

  • atlasVerification Contract Address

    • Description: This contract is used for EIP-712 domain verification when signing the solver operation.

  • atlas Contract Address

    • Description: Atlas main entryPoint contract metacall will be used to submit PFL-Bundles (handled by FastLane bundler)

Encoding of the Bundle

When encoding the bundle, represent all BigInt values as hex strings (prefixed with 0x), and serialize the entire JSON object as a string.

Example Solver Operation as struct:

{
  from: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  to: '0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0',
  value: '0x0',
  gas: '0x7a120',
  maxFeePerGas: '0x848788e91',
  deadline: '0x0',
  solver: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
  control: '0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F',
  userOpHash: '0x31ebaf2b454ec7b665697554b8be6422d39e775befa93768f36a101d4653ae8c',
  bidToken: '0x0000000000000000000000000000000000000000',
  bidAmount: '0x2386f26fc10000',
  data: '0x890d6908',
  signature: '0xe1a0e01491f5a45110ab8d777d7feeec596b8ccdea086383a04af80bacd4c67038c0b9d9d6139e7818dd4069d23fc96bf1cdd63073e37ff44287e6722f5299c21b'
}

Example Bundle payload what we expect:

  id: 1,
  jsonrpc: '2.0',
  method: 'pfl_addSearcherBundle',
  params: [
    '0x02f8778189820be28506c6b450bc8508052c48a9830186a0940000000000000000000000000000000000000000872386f26fc1000080c001a064d1594494fddeaa9412ed29975cbe6f349da4a5f39448659d9520c78f6588fca06cfea84e288b0e9861ce01f01465389d511c412acb02222c7e00f4299abd0fc1',
    '{"from":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","to":"0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0","value":"0x0","gas":"0x7a120","maxFeePerGas":"0x8052c48a9","deadline":"0x0","solver":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","control":"0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F","userOpHash":"0xcc1d143ccb5365daf7cb5c8b3fdd3fad51dc8c2db10ee23f1e47f41894a89713","bidToken":"0x0000000000000000000000000000000000000000","bidAmount":"0x2386f26fc10000","data":"0x890d6908","signature":"0xf71884b9b567b2b212194054577fa402a4c5469d751766e3eb8fe3c21056661972e7f482f20b8f461caaa952ab5879e88a45ef9e998bbe2c86a0030dbf253c961b"}'
  ]
}

Example:

import { OperationBuilder, SolverOperation } from "@fastlane-labs/atlas-sdk";
import axios, { AxiosInstance } from "axios";
import { Contract, Interface, JsonRpcProvider, keccak256, parseEther, TypedDataDomain, Wallet } from "ethers";

const dappControlAddr = "0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F"; // current dappControl address (review docs)
const dAppOpSignerAddr = "0x96D501A4C52669283980dc5648EEC6437e2E6346"; // current dAppOpSigner address (review docs)
const atlasVerificationAddr = "0xf31cf8740Dc4438Bb89a56Ee2234Ba9d5595c0E9"; // current atlasVerification address (review docs)
const atlasAddr = '0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0';

const PFLControlAbi = [
  {
    "inputs": [
      { "internalType": "bytes32", "name": "oppTxHash", "type": "bytes32" },
      { "internalType": "uint256", "name": "oppTxMaxFeePerGas", "type": "uint256" },
      { "internalType": "uint256", "name": "oppTxMaxPriorityFeePerGas", "type": "uint256" },
      { "internalType": "address", "name": "fastLaneSigner", "type": "address" }
    ],
    "name": "getBackrunUserOpHash",
    "outputs": [{ "internalType": "bytes32", "name": "userOpHash", "type": "bytes32" }],
    "stateMutability": "view",
    "type": "function"
  },
];

const eip712Domain: TypedDataDomain = {
    name: "AtlasVerification",
    version: "1.0",
    chainId: 137,
    verifyingContract: atlasVerificationAddr,
}

interface PflBundle {
  id: number;
  jsonrpc: string;
  method: string;
  params: string[];
}

const provider = new JsonRpcProvider("https://polygon.llamarpc.com");
// PK are foundry default PKs
const userSigner = new Wallet("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider);
const solverSigner = new Wallet("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", provider);

const dappControl = new Contract(dappControlAddr, PFLControlAbi, provider);

const httpFastlaneEndpoint = "https://polygon-rpc.fastlane.xyz";
const opportunityIsLegacyTx = false;

// Mock values for gas prices for opportunity tx
const maxFeePerGas = BigInt(10000000000000000);
const maxPriorityFeePerGas = BigInt(10000000000000000);

const createOpportunityRawTx = async (
    userSigner: Wallet,
    bidAmount: bigint,
    toAddress: string, 
    legacyTx: boolean, 
    maxFeePerGas: bigint | null, 
    maxPriorityFeePerGas: bigint | null
) => {
    if (!maxFeePerGas) {
        throw new Error("maxFeePerGas is required");
    }

    const txData = await userSigner.populateTransaction({
        to: toAddress,
        gasLimit: 100000,
        ...(legacyTx 
            ? { gasPrice: maxFeePerGas } 
            : { 
                maxFeePerGas, 
                maxPriorityFeePerGas: maxPriorityFeePerGas || maxFeePerGas
            }),
        value: bidAmount
    });
    return await userSigner.signTransaction(txData);
}


// Generate the solver call data for the solver operation
const generateSolverCallData = () => {
    const searcherAbi = [
        `function doMEV(uint256, string)`,
    ];

    const iface = new Interface(searcherAbi);

    // Grab bytes for doMEV(uint256, string)
    const searcherCallDataBytes = iface.encodeFunctionData("doMEV", [parseEther("1.0"), "hello"]);
    return searcherCallDataBytes;
}

// helper function to generate the solver signature using eip712Domain
const generateSolverSignature = async (solverOp: SolverOperation) => {
    return await signer.signTypedData(eip712Domain, solverOp.toTypedDataTypes(), solverOp.toTypedDataValues());
}

// helper function to generate the solver operation
const generateSolverOperation = async (userOpHash: string, bidAmount: bigint, maxFeePerGas: bigint, maxPriorityFeePerGas: bigint):Promise<SolverOperation> => {

    // Generate the solver call data
    const solverCallData = generateSolverCallData();

    // Generate the solver operation
    const solverOp = OperationBuilder.newSolverOperation({
        from: solverSigner.address, // solver address
        to: atlasAddr, // atlasAddr address
        value: BigInt(0), // 0 value
        gas: BigInt(500000), // 500,000 gasLimit
        maxFeePerGas: maxFeePerGas,
        deadline: BigInt(0), // 0 deadline
        solver: solverSigner.address, // dAppOpSigner address
        control: dappControlAddr, // dappControl address
        userOpHash: userOpHash, 
        bidToken: "0x0000000000000000000000000000000000000000", // POL
        bidAmount: bidAmount,
        data: solverCallData,
        signature: "0x" // empty signature
    });
    
    // Generate the solver signature
    const solverSignature = await generateSolverSignature(solverOp);

    // Set the solver signature
    solverOp.setField("signature", solverSignature);
    return solverOp;
}


const generatePflBundle = (solverOp: SolverOperation, opportunityRawTx: string, bundleId: number): PflBundle => {
    return {
        id: bundleId,
        jsonrpc: "2.0",
        method: "pfl_addSearcherBundle",
        params: [`${opportunityRawTx}`, `${JSON.stringify(solverOp.toStruct())}`]
    }
}

// main function to submit the bundle to the fastlane endpoint
async function main() {
    
    const gasPrice = await provider.getFeeData();
    //match opportunity tx transaction type
    const maxFeePerGas = opportunityIsLegacyTx ? gasPrice.gasPrice : gasPrice.maxFeePerGas;
    const maxPriorityFeePerGas = opportunityIsLegacyTx ? gasPrice.gasPrice : gasPrice.maxPriorityFeePerGas;

    if (!maxFeePerGas || !maxPriorityFeePerGas) {
        throw new Error("Failed to get gas price data");
    }
    
    const solverContract = "0x0000000000000000000000000000000000000000";
    const bidAmount = BigInt(10000000000000000); // 0.01 POL
    
    // self transfer to solver contract
    const opportunityRawTx = await createOpportunityRawTx(userSigner, bidAmount, solverContract, opportunityIsLegacyTx, maxFeePerGas, maxPriorityFeePerGas);
    //match opportunity tx transaction type
    const maxFeePerGas = isLegacyTx ? feeData.gasPrice: feeData.maxFeePerGas;
    const maxPriorityFeePerGas = isLegacyTx ? feeData.gasPrice : feeData.maxPriorityFeePerGas,

    // Generate the solver operation
    const solverOp = await generateSolverOperation(userOpHash);

    const pflBundle = generatePflBundle(solverOp, opportunityRawTx, 1   );

    // Submit the bundle to the fastlane endpoint
    await submitHttpBundle(pflBundle);

}

main().catch(console.error);

import requests
import json
from web3.middleware import geth_poa_middleware

# Set up the provider
provider_url = "https://polygon.llamarpc.com"
web3 = Web3(HTTPProvider(provider_url))

# Inject PoA middleware if needed
web3.middleware_onion.inject(geth_poa_middleware, layer=0)

def submit_http_bundle(bundle: PflBundle):
    try:
        resp = requests.post(
            httpFastlaneEndpoint,
            json={
                "id": bundle.id,
                "jsonrpc": bundle.jsonrpc,
                "method": bundle.method,
                "params": bundle.params,
            },
            timeout=10,
        )
        data = resp.json()
        if "error" in data:
            print(f"Error submitting bundle {bundle.id}: {data}")
        else:
            print(f"Response received for bundle {bundle.id}: {data}")
    except Exception as e:
        print(f"Error submitting bundle {bundle.id}: {e}")
PreviousBundle FormatNextBundle Requirements

Last updated 5 months ago

Address:

Address:

Address:

Address:

Please see section on how to target your own transaction instead of one from a provider.

For details on how to create a bundle review the

0x3e23e4282FcE0cF42DCd0E9bdf39056434E65C1F
0x96D501A4C52669283980dc5648EEC6437e2E6346
0xf31cf8740Dc4438Bb89a56Ee2234Ba9d5595c0E9
0x4A394bD4Bc2f4309ac0b75c052b242ba3e0f32e0
Full Example
Full Example