Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/LayerZeroSettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,15 @@ contract LayerZeroSettler is OApp, ISettler, EIP712 {
}
}

function _getPeerOrRevert(uint32 /* _eid */) internal view virtual override returns (bytes32) {
function _getPeerOrRevert(
uint32 /* _eid */
)
internal
view
virtual
override
returns (bytes32)
{
// The peer address for all chains is automatically set to `address(this)`
return bytes32(uint256(uint160(address(this))));
}
Expand Down
241 changes: 241 additions & 0 deletions src/Simulator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import {ICommon} from "./interfaces/ICommon.sol";
import {IMulticall3} from "./interfaces/IMulticall3.sol";
import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol";

/// @title Simulator
Expand Down Expand Up @@ -125,6 +126,99 @@ contract Simulator {
return _callOrchestrator(oc, isStateOverride, data);
}

/// @dev Performs a call to Multicall3 with calls followed by an orchestrator call.
/// Returns the gas used parsed from the last Result in the multicall3 response.
/// If parsing fails (gasUsed == 0), this function stores the orchestrator's error in memory
/// so the caller can bubble it up using bubbleUpMulticall3Error.
/// @param multicall3 The multicall3 contract address
/// @param calls Array of Call3 structs to execute before the orchestrator call
/// @param oc The orchestrator address
/// @param isStateOverride Whether to use state override mode for the orchestrator call
/// @param combinedGasOverride The combined gas override value
/// @param u The Intent struct to pass to the orchestrator
/// @return gasUsed The gas used by the orchestrator call (parsed from SimulationPassed or state override result)
/// @return multicall3Gas The gas spent on the aggregate3 call itself
/// @return lastReturnData The return data from the orchestrator call
function _callMulticall3(
address multicall3,
IMulticall3.Call3[] memory calls,
address oc,
bool isStateOverride,
uint256 combinedGasOverride,
ICommon.Intent memory u
)
internal
freeTempMemory
returns (uint256 gasUsed, uint256 multicall3Gas, bytes memory lastReturnData)
{
// Build the orchestrator call data
bytes memory orchestratorData = abi.encodeWithSignature(
"simulateExecute(bool,uint256,bytes)",
isStateOverride,
combinedGasOverride,
abi.encode(u)
);

// Construct the full Call3[] array: calls + orchestrator call
IMulticall3.Call3[] memory allCalls = new IMulticall3.Call3[](calls.length + 1);
for (uint256 i = 0; i < calls.length; i++) {
allCalls[i] = calls[i];
}
// Last call is to the orchestrator
allCalls[calls.length] =
IMulticall3.Call3({target: oc, allowFailure: true, callData: orchestratorData});

// Measure gas before and after aggregate3 call
uint256 gasBefore = gasleft();
IMulticall3.Result[] memory results = IMulticall3(multicall3).aggregate3(allCalls);
multicall3Gas = gasBefore - gasleft();

// Get the last result (orchestrator call result)
// Check all calls for failures (all results except the last one, which is the orchestrator call)
if (results.length > 1) {
for (uint256 i = 0; i < results.length - 1; i++) {
// If any call failed, we return gasUsed = 0, multicall3Gas, and the error data from that call
if (!results[i].success) {
return (0, 0, results[i].returnData);
}
}
}
IMulticall3.Result memory lastResult = results[results.length - 1];
lastReturnData = lastResult.returnData;

// Parse gasUsed from the orchestrator's return data

@joshieDo joshieDo Oct 16, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only returning the gasUsed by the intent right? would be interested in the overal gasUsed of the whole multicall including the intent and precalls

@joshieDo joshieDo Oct 16, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually both. used by intent (so we can cap its execution) and by intent+precalls so we can price it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, i think the current gasUsed is the value to cap, we need another totalGas or something with the preCall

// Assembly required for low-level parsing of return data
assembly ("memory-safe") {
let returnDataPtr := add(lastReturnData, 0x20)

switch isStateOverride
case 0 {
// If `isStateOverride` is false, the orchestrator reverts with SimulationPassed
// so success will be false in multicall3's Result, but we still need to parse the revert data
// Check for SimulationPassed selector (0x4f0c028c)
// The `gasUsed` will be in the revert data at offset 0x04
if eq(shr(224, mload(returnDataPtr)), 0x4f0c028c) {
gasUsed := mload(add(returnDataPtr, 0x04))
}
}
default {
// If isStateOverride is true and call succeeded, gasUsed is at offset 0x00
let lastSuccess := mload(lastResult)
if lastSuccess {
gasUsed := mload(returnDataPtr)
}
}
}
}

/// @dev Bubbles up the error from the multicall3's last result returnData.
/// This is called when _callMulticall3 returns gasUsed == 0.
function _bubbleUpMulticall3Error(bytes memory errorData) internal pure {
assembly ("memory-safe") {
revert(add(errorData, 0x20), mload(errorData))
}
}

/// @dev Simulate the gas usage for a user operation. This function reverts if the simulation fails.
/// @param oc The orchestrator address
/// @param overrideCombinedGas Whether to override the combined gas for the intent to type(uint256).max
Expand Down Expand Up @@ -262,4 +356,151 @@ contract Simulator {
}
}
}

/// @dev Simulates the execution of an intent through Multicall3, with calls executed before the orchestrator call.
/// Finds the combined gas by iteratively increasing it until the simulation passes.
/// The start value for combinedGas is gasUsed + original combinedGas.
/// Set u.combinedGas to add some starting offset to the gasUsed value.
/// @param multicall3 The multicall3 contract address
/// @param calls Array of Call3 structs to execute before the orchestrator call
/// @param oc The orchestrator address
/// @param paymentPerGasPrecision The precision of the payment per gas value.
/// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision)
/// @param paymentPerGas The amount of `paymentToken` to be added per gas unit.
/// Total payment is calculated as paymentAmount += gasUsed * paymentPerGas.
/// @dev Set paymentAmount to include any static offset to the gas value.
/// @param combinedGasIncrement Basis Points increment to be added for each iteration of searching for combined gas.
/// @dev The closer this number is to 10_000, the more precise combined gas will be. But more iterations will be needed.
/// @dev This number should always be > 10_000, to get correct results.
/// If the increment is too small, the function might run out of gas while finding the combined gas value.
/// @param encodedIntent The encoded user operation
/// @return gasUsed The gas used in the successful simulation
/// @return multicall3Gas The gas spent on the aggregate3 call
/// @return combinedGas The first combined gas value that gives a successful simulation.
/// This function reverts if the primary simulation run with max combinedGas fails.
/// If the primary run is successful, it iteratively increases u.combinedGas by `combinedGasIncrement` until the simulation passes.
/// All failing simulations during this run are ignored.
function simulateMulticall3CombinedGas(
address multicall3,
IMulticall3.Call3[] memory calls,
address oc,
uint8 paymentPerGasPrecision,
uint256 paymentPerGas,
uint256 combinedGasIncrement,
bytes calldata encodedIntent
) public payable virtual returns (uint256 gasUsed, uint256 multicall3Gas, uint256 combinedGas) {
// Decode the intent first
ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent));

bytes memory errorData;

// 1. Primary Simulation Run to get initial gasUsed value with combinedGasOverride

(gasUsed, multicall3Gas, errorData) =
_callMulticall3(multicall3, calls, oc, false, type(uint256).max, u);

// If the simulation failed, bubble up the orchestrator's error.
if (gasUsed == 0) {
_bubbleUpMulticall3Error(errorData);
}

u.combinedGas += gasUsed;

_updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas);

while (true) {
(gasUsed, multicall3Gas, errorData) =
_callMulticall3(multicall3, calls, oc, false, 0, u);

// If the simulation failed, check if it's a PaymentError and bubble it up.
// PaymentError is given special treatment here, as it comes from
// the account not having enough funds, and cannot be recovered from,
// since the paymentAmount will keep increasing in this loop.
if (gasUsed == 0 && errorData.length >= 4) {
// errorData is a bytes memory containing the revert data from the orchestrator
// Layout: [length (32 bytes)][selector (4 bytes)][additional data...]
bytes4 errorSelector;
assembly ("memory-safe") {
// Load 32 bytes starting from errorData+32
// bytes4 values are already right-aligned, no shift needed
errorSelector := mload(add(errorData, 32))
}
if (errorSelector == 0xabab8fc9) {
// PaymentError()

// Revert with just the selector (0x20 bytes = 4 bytes selector + 28 bytes padding)
assembly ("memory-safe") {
mstore(0x00, errorSelector)
revert(0x00, 0x20)
}
}
}

if (gasUsed != 0) {
return (gasUsed, multicall3Gas, u.combinedGas);
}

uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000);

_updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas);

// Step up the combined gas, until we see a simulation passing
u.combinedGas += gasIncrement;
}
}

/// @dev Same as simulateMulticall3CombinedGas, but with an additional verification run
/// that generates a successful non reverting state override simulation.
/// Which can be used in eth_simulateV1 to get the trace.
/// @param multicall3 The multicall3 contract address
/// @param calls Array of Call3 structs to execute before the orchestrator call
/// @param oc The orchestrator address
/// @param paymentPerGasPrecision The precision of the payment per gas value.
/// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision)
/// @param paymentPerGas The amount of `paymentToken` to be added per gas unit.
/// @param combinedGasIncrement Basis Points increment to be added for each iteration of searching for combined gas.
/// @param combinedGasVerificationOffset is a static value that is added after a successful combinedGas is found.
/// This can be used to account for variations in sig verification gas, for keytypes like P256.
/// @param encodedIntent The encoded user operation
/// @return gasUsed The gas used in the successful simulation
/// @return multicall3Gas The gas spent on the aggregate3 call
/// @return combinedGas The combined gas value including the verification offset
function simulateMulticall3V1Logs(
address multicall3,
IMulticall3.Call3[] memory calls,
address oc,
uint8 paymentPerGasPrecision,
uint256 paymentPerGas,
uint256 combinedGasIncrement,
uint256 combinedGasVerificationOffset,
bytes calldata encodedIntent
) public payable virtual returns (uint256 gasUsed, uint256 multicall3Gas, uint256 combinedGas) {
(gasUsed, multicall3Gas, combinedGas) = simulateMulticall3CombinedGas(
multicall3,
calls,
oc,
paymentPerGasPrecision,
paymentPerGas,
combinedGasIncrement,
encodedIntent
);

combinedGas += combinedGasVerificationOffset;

ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent));

_updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas);

u.combinedGas = combinedGas;

bytes memory errorData;

// Verification Run to generate the logs with the correct combinedGas and payment amounts.
(gasUsed, multicall3Gas, errorData) = _callMulticall3(multicall3, calls, oc, true, 0, u);

// If the simulation failed, bubble up the orchestrator's error
if (gasUsed == 0) {
_bubbleUpMulticall3Error(errorData);
}
}
}
38 changes: 38 additions & 0 deletions src/interfaces/IMulticall3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/// @notice Interface for Multicall3 contract
interface IMulticall3 {
struct Call {
address target;
bytes callData;
}

struct Call3 {
address target;
bool allowFailure;
bytes callData;
}

struct Call3Value {
address target;
bool allowFailure;
uint256 value;
bytes callData;
}

struct Result {
bool success;
bytes returnData;
}

function aggregate(Call[] calldata calls)
external
payable
returns (uint256 blockNumber, bytes[] memory returnData);

function aggregate3(Call3[] calldata calls)
external
payable
returns (Result[] memory returnData);
}
13 changes: 9 additions & 4 deletions test/Benchmark.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1772,11 +1772,16 @@ contract BenchmarkTest is BaseTest {
vm.startPrank(d.eoa);
d.d.authorize(k.k);
d.d
.setCanExecute(
k.keyHash, address(paymentToken), bytes4(keccak256("transfer(address,uint256)")), true
);
.setCanExecute(
k.keyHash,
address(paymentToken),
bytes4(keccak256("transfer(address,uint256)")),
true
);
d.d
.setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, 1 ether);
.setSpendLimit(
k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, 1 ether
);
d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether);
vm.stopPrank();

Expand Down
8 changes: 6 additions & 2 deletions test/GuardedExecutor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ contract GuardedExecutorTest is BaseTest {

vm.startPrank(d.eoa);
d.d
.setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether);
.setSpendLimit(
k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether
);
vm.stopPrank();

u.nonce = d.d.getNonce(0);
Expand Down Expand Up @@ -265,7 +267,9 @@ contract GuardedExecutorTest is BaseTest {

vm.startPrank(d.eoa);
d.d
.setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether);
.setSpendLimit(
k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether
);
vm.stopPrank();

u.nonce = d.d.getNonce(0);
Expand Down
Loading