Solver Call Data

What is solverOpData?

The solverOpData parameter is a crucial component that contains the transaction data required by your solver contract. This data ensures that your MEV strategy executes precisely and securely within the Atlas ecosystem.

Why is solverOpData Important?

  • Executes your MEV strategy on the solver contract

  • Atlas constructs the call to atlasSolverCall and passes the solverOpData along.

The simplest way to obtain the correct solverOpData is to create a transaction directly to your solver contract (as if Atlas didn't exist) and extract that transaction's data field.

There are two primary approaches to using solverOpData, and the choice depends on your specific implementation:


There are many approaches on how you want to use the solverOpData and they're ultimately up to you.

Potential Approaches:

Approach 1: Direct MEV Logic Implementation in Solver Contract

MEV logic alongside the solver contract which implements the ISolverContract

  • atlasSolverCall should be calling an internal function In this method, your solver contract contains all the MEV logic within its functions. The solverOpData is an encoding of a function call within your solver contract, such as solve()

We provide a helper SolverBase which redirects the callData. Your solverOpData should encode your function here solve()

// SolverBase.sol
 ...
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();
    }

Example Contract AtlasDirectSolver

// SPDX-License-Identifier: MIT

pragma solidity 0.8.25;

import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import { SolverBase } from "../lib/atlas/src/contracts/solver/SolverBase.sol";

/**
 * @title AtlasDirectSolver
 * @notice A simple example solver contract that inherits from SolverBase
 * @dev SolverBase is a helper contract that:
 * 1. Implements ISolverContract interface required for all solvers
 * 2. Handles bid payments to Atlas through the payBids modifier
 * 3. Provides security checks via safetyFirst modifier to ensure:
 *    - Only Atlas can call atlasSolverCall
 *    - Only the owner can initiate solver operations
 *    - Proper reconciliation of funds with Atlas escrow
 * 4. Manages WETH/ETH conversions as needed for bid payments
 */
contract AtlasDirectSolver is SolverBase, Ownable {
    bool internal s_shouldSucceed;

    constructor(address weth, address atlas) SolverBase(weth, atlas, msg.sender) Ownable(msg.sender) {
        s_shouldSucceed = true; // should succeed by default, can be set to false
    }

    fallback() external payable { }
    receive() external payable { }

    // Called during the SolverOperation phase
    // 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)
    }

    // ---------------------------------------------------- //
    //                      ONLY OWNER                      //
    // ---------------------------------------------------- //

    function withdrawETH(address recipient) public onlyOwner {
        SafeTransferLib.safeTransferETH(recipient, address(this).balance);
    }

    function withdrawERC20(address token, address recipient) public onlyOwner {
        SafeTransferLib.safeTransfer(token, recipient, IERC20(token).balanceOf(address(this)));
    }

    // ---------------------------------------------------- //
    //                      MODIFIERS                       //
    // ---------------------------------------------------- //

    // This ensures a function can only be called through atlasSolverCall
    // which includes security checks to work safely with Atlas
    modifier onlySelf() {
        require(msg.sender == address(this), "Not called via atlasSolverCall");
        _;
    }
}

How It Works:

  • The solve() function contains the MEV logic.

  • The atlasSolverCall() function (inherited from SolverBase) calls solve() by forwarding the callData.

  • solverOpData is simply the ABI-encoded call to solve().

Generating solverOpData:

const iface = new ethers.utils.Interface(["function solve()"]);
const solverOpData = iface.encodeFunctionData("solve");

Approach 2: Proxying Calls to an External Contract

This approach allows for modularity, enabling the use of a dedicated external contract for various MEV strategies.

The solverOpData includes both the function call to your solver contract and the data intended for the external contract.

// SolverBase.sol
 ...
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();
    }

Example: AtlasProxySolver

// SPDX-License-Identifier: MIT

pragma solidity 0.8.25;

import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { ReentrancyGuard } from "solady/utils/ReentrancyGuard.sol";
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import { SolverBase } from "../lib/atlas/src/contracts/solver/SolverBase.sol";

/**
 * @title ProxySolver
 * @notice An example solver contract that proxies calls to an external contract.
 * @dev This contract inherits from SolverBase and includes reentrancy guards.
 * It allows the owner to set an external contract to which MEV solution calls are proxied.
 * It includes helper functions to manage the external contract and approved EOAs.
 */
contract AtlasProxySolver is SolverBase, Ownable, ReentrancyGuard {
    address payable private searcherContract;
    mapping(address => bool) internal approvedEOAs;

    constructor(
        address weth,
        address atlas,
        address _searcherContract
    )
        SolverBase(weth, atlas, msg.sender)
        Ownable(msg.sender)
    {
        searcherContract = payable(_searcherContract);
    }

    fallback() external payable { }
    receive() external payable { }

    /**
     * @notice Called during the SolverOperation phase.
     * This function is called via atlasSolverCall(), which forwards the solverOpData calldata.
     * @param _searcherCallData The calldata to be forwarded to the external searcher contract.
     */
    function solve(bytes calldata _searcherCallData) public onlySelf nonReentrant {
        // Call the external searcher contract with the provided calldata.
        (bool success, bytes memory returnedData) = searcherContract.call(_searcherCallData);

        // Revert if the call was unsuccessful.
        require(success, "Searcher call unsuccessful");

        // SolverBase automatically handles paying the bid amount to the Execution Environment.
    }

    // ---------------------------------------------------- //
    //                      ONLY OWNER                      //
    // ---------------------------------------------------- //

    /**
     * @notice Sets the address of the external searcher contract.
     * @param _searcherContract The address of the new searcher contract.
     */
    function setSearcherContractAddress(address _searcherContract) public onlyOwner {
        searcherContract = payable(_searcherContract);
    }

    /**
     * @notice Withdraws all ETH from the contract to a recipient address.
     * @param recipient The address to receive the withdrawn ETH.
     */
    function withdrawETH(address recipient) public onlyOwner {
        SafeTransferLib.safeTransferETH(recipient, address(this).balance);
    }

    /**
     * @notice Withdraws all ERC20 tokens of a specific type from the contract to a recipient address.
     * @param token The address of the ERC20 token contract.
     * @param recipient The address to receive the withdrawn tokens.
     */
    function withdrawERC20(address token, address recipient) public onlyOwner {
        SafeTransferLib.safeTransfer(token, recipient, IERC20(token).balanceOf(address(this)));
    }

    // ---------------------------------------------------- //
    //                      MODIFIERS                       //
    // ---------------------------------------------------- //

    /**
     * @notice Ensures a function can only be called through atlasSolverCall,
     * which includes security checks to work safely with Atlas.
     */
    modifier onlySelf() {
        require(msg.sender == address(this), "Not called via atlasSolverCall");
        _;
    }
}

How It Works:

  • The solve(bytes calldata solverOpData) function receives the callData intended for the external contract.

  • It forwards this callData to the searcherContract using a low-level call.

  • solverOpData includes the encoded function call for the external contract.

Generating solverOpData:

Suppose your external searcher contract has a function:

function externalSolve(uint256 param1, string memory param2) public {
    // add origin validation here
    // MEV logic here
}

Step 1: Encode the External Contract Call

const externalIface = new ethers.utils.Interface(["function externalSolve(uint256, string)"]);
const externalCallData = externalIface.encodeFunctionData("externalSolve", [param1, param2]);

Step 2: Encode the Solver Contract Call

const solverIface = new ethers.utils.Interface(["function solve(bytes)"]);
const solverOpData = solverIface.encodeFunctionData("solve", [externalCallData]);

Last updated