diff --git a/src/LayerZeroSettler.sol b/src/LayerZeroSettler.sol index aa8d76a9..276b50ce 100644 --- a/src/LayerZeroSettler.sol +++ b/src/LayerZeroSettler.sol @@ -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)))); } diff --git a/src/Simulator.sol b/src/Simulator.sol index f70b05f4..1e468d6b 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -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 @@ -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 + // 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 @@ -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); + } + } } diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol new file mode 100644 index 00000000..c0ab86f2 --- /dev/null +++ b/src/interfaces/IMulticall3.sol @@ -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); +} diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 6b1b5c49..2ce04524 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -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(); diff --git a/test/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index 088bd597..a30c941e 100644 --- a/test/GuardedExecutor.t.sol +++ b/test/GuardedExecutor.t.sol @@ -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); @@ -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); diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 681266f7..099d6acc 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -4,13 +4,16 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import "./Base.t.sol"; import {MockGasBurner} from "./utils/mocks/MockGasBurner.sol"; +import {IMulticall3} from "../src/interfaces/IMulticall3.sol"; contract SimulateExecuteTest is BaseTest { MockGasBurner gasBurner; + MockMulticall3 multicall3; function setUp() public virtual override { super.setUp(); gasBurner = new MockGasBurner(); + multicall3 = new MockMulticall3(); } struct _SimulateExecuteTemps { @@ -19,9 +22,11 @@ contract SimulateExecuteTest is BaseTest { uint256 gExecute; uint256 gCombined; uint256 gUsed; + uint256 gMulticall3; bytes executionData; bool success; bytes result; + IMulticall3.Call3[] preCalls; } function _gasToBurn() internal returns (uint256) { @@ -318,4 +323,505 @@ contract SimulateExecuteTest is BaseTest { assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } + + function testSimulateMulticall3V1Logs() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + assertEq(_balanceOf(address(paymentToken), d.eoa), 0); + + paymentToken.mint(d.eoa, type(uint128).max); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); // Warm the storage first. + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + emit LogUint("gasToBurn", t.gasToBurn); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Create some preCalls + t.preCalls = new IMulticall3.Call3[](2); + t.preCalls[0] = IMulticall3.Call3({ + target: address(gasBurner), + allowFailure: false, + callData: abi.encodeWithSignature("setRandomness(uint256)", 42) + }); + t.preCalls[1] = IMulticall3.Call3({ + target: address(paymentToken), + allowFailure: false, + callData: abi.encodeWithSignature("approve(address,uint256)", address(this), 1000) + }); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = 0x112233112233112233112233; + i.paymentMaxAmount = 0x445566445566445566445566; + i.combinedGas = 20_000; + + { + // Just pass in a junk secp256k1 signature. + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + // If the caller does not have max balance, then the simulation should revert. + vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 0, 1, 11_000, 10_000, abi.encode(i) + ); + + uint256 snapshot = vm.snapshotState(); + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 11_000, 0, abi.encode(i) + ); + + vm.revertToStateAndDelete(snapshot); + i.combinedGas = t.gCombined; + + t.gExecute = t.gCombined + 10_000; + + i.signature = _sig(d, i); + + vm.expectRevert(bytes4(keccak256("InsufficientGas()"))); + oc.execute{gas: t.gExecute}(abi.encode(i)); + + t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); + + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + } + + function testSimulateMulticall3NoRevertUnderfundedReverts() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + assertEq(_balanceOf(address(paymentToken), d.eoa), 0); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); // Warm the storage first. + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + emit LogUint("gasToBurn", t.gasToBurn); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Empty preCalls for this test + t.preCalls = new IMulticall3.Call3[](0); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = 0x112233112233112233112233; + i.paymentMaxAmount = 0x445566445566445566445566; + i.combinedGas = 20_000; + + { + // Just pass in a junk secp256k1 signature. + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + vm.expectRevert(bytes4(keccak256("PaymentError()"))); + simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 0, 1, 11_000, 0, abi.encode(i) + ); + + deal(i.paymentToken, address(i.eoa), 0x112233112233112233112233); + vm.expectRevert(bytes4(keccak256("PaymentError()"))); + simulator.simulateMulticall3CombinedGas( + address(multicall3), t.preCalls, address(oc), 0, 1, 11_000, abi.encode(i) + ); + } + + function testSimulateMulticall3WithEOAKey(bytes32) public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + paymentToken.mint(d.eoa, 500 ether); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); // Warm the storage first. + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + emit LogUint("gasToBurn", t.gasToBurn); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Create a single preCall + t.preCalls = new IMulticall3.Call3[](1); + t.preCalls[0] = IMulticall3.Call3({ + target: address(gasBurner), + allowFailure: false, + callData: abi.encodeWithSignature("setRandomness(uint256)", 1) + }); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = _randomChance(2) ? 0 : 0.1 ether; + i.paymentMaxAmount = _bound(_random(), i.paymentAmount, 0.5 ether); + i.combinedGas = 20_000; + + { + // Just pass in a junk secp256k1 signature. + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + uint256 snapshot = vm.snapshotState(); + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 10_800, 0, abi.encode(i) + ); + + vm.revertToStateAndDelete(snapshot); + + i.combinedGas = t.gCombined; + // gExecute > (100k + combinedGas) * 64/63 + t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); + + i.signature = _sig(d, i); + + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(gasBurner.randomness(), t.randomness); + } + + function testSimulateMulticall3WithPassKey(bytes32) public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + vm.deal(d.eoa, 10 ether); + paymentToken.mint(d.eoa, 50 ether); + + PassKey memory k = _randomPassKey(); // Can be r1 or k1. + k.k.isSuperAdmin = true; + + vm.prank(d.eoa); + d.d.authorize(k.k); + + _SimulateExecuteTemps memory t; + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + emit LogUint("gasToBurn", t.gasToBurn); + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Create preCalls with token approval + t.preCalls = new IMulticall3.Call3[](1); + t.preCalls[0] = IMulticall3.Call3({ + target: address(paymentToken), + allowFailure: false, + callData: abi.encodeWithSignature( + "approve(address,uint256)", address(oc), type(uint256).max + ) + }); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = _randomChance(2) ? 0 : 0.1 ether; + i.paymentMaxAmount = _bound(_random(), i.paymentAmount, 0.5 ether); + i.combinedGas = 20_000; + + // Just fill with some non-zero junk P256 signature that contains the `keyHash`, + // so that the `simulateExecute` knows that + // it needs to add the variance for non-precompile P256 verification. + // We need the `keyHash` in the signature so that the simulation is able + // to hit all the gas for the GuardedExecutor stuff for the `keyHash`. + i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), k.keyHash, uint8(0)); + + uint256 snapshot = vm.snapshotState(); + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 12_000, 10_000, abi.encode(i) + ); + + vm.revertToStateAndDelete(snapshot); + + i.combinedGas = t.gCombined; + t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); + + i.signature = _sig(k, i); + + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(gasBurner.randomness(), t.randomness); + } + + function testSimulateMulticall3EmptyPreCalls() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + paymentToken.mint(d.eoa, type(uint128).max); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); // Warm the storage first. + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Empty preCalls - should behave similarly to regular simulate + t.preCalls = new IMulticall3.Call3[](0); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = 0x112233112233112233112233; + i.paymentMaxAmount = 0x445566445566445566445566; + i.combinedGas = 100_000; + + { + // Just pass in a junk secp256k1 signature. + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + uint256 snapshot = vm.snapshotState(); + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 11_000, 0, abi.encode(i) + ); + + vm.revertToStateAndDelete(snapshot); + + i.combinedGas = t.gCombined; + t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); + + i.signature = _sig(d, i); + + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(gasBurner.randomness(), t.randomness); + } + + function testSimulateMulticall3MultiplePreCalls() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + paymentToken.mint(d.eoa, type(uint128).max); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); // Warm the storage first. + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + // Multiple preCalls with different operations + t.preCalls = new IMulticall3.Call3[](3); + t.preCalls[0] = IMulticall3.Call3({ + target: address(gasBurner), + allowFailure: false, + callData: abi.encodeWithSignature("setRandomness(uint256)", 100) + }); + t.preCalls[1] = IMulticall3.Call3({ + target: address(paymentToken), + allowFailure: false, + callData: abi.encodeWithSignature("approve(address,uint256)", address(this), 5000) + }); + t.preCalls[2] = IMulticall3.Call3({ + target: address(gasBurner), + allowFailure: false, + callData: abi.encodeWithSignature("setRandomness(uint256)", 1) + }); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = 0.1 ether; + i.paymentMaxAmount = 0.5 ether; + i.combinedGas = 20_000; + + { + // Just pass in a junk secp256k1 signature. + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + uint256 snapshot = vm.snapshotState(); + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + (t.gUsed, t.gMulticall3, t.gCombined) = simulator.simulateMulticall3V1Logs( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 11_000, 0, abi.encode(i) + ); + + vm.revertToStateAndDelete(snapshot); + + i.combinedGas = t.gCombined; + t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); + + i.signature = _sig(d, i); + + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(gasBurner.randomness(), t.randomness); + } + + function testSimulateVsActualGas() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + paymentToken.mint(d.eoa, type(uint128).max); + + _SimulateExecuteTemps memory t; + + gasBurner.setRandomness(1); + + t.gasToBurn = _gasToBurn(); + do { + t.randomness = _randomUniform(); + } while (t.randomness == 0); + + t.executionData = _executionData( + address(gasBurner), + abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) + ); + + Orchestrator.Intent memory i; + i.eoa = d.eoa; + i.nonce = 0; + i.executionData = t.executionData; + i.payer = address(0x00); + i.paymentToken = address(paymentToken); + i.paymentRecipient = address(0x00); + i.paymentAmount = 0.1 ether; + i.paymentMaxAmount = 0.5 ether; + i.combinedGas = 20_000; + + { + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); + i.signature = abi.encodePacked(r, s, v); + } + + vm.deal(_ORIGIN_ADDRESS, type(uint192).max); + + // Get simulated gas using multicall3 with empty preCalls (simulation automatically reverts, no state changes persist) + t.preCalls = new IMulticall3.Call3[](0); + (uint256 simulatedGas, uint256 multicall3Gas, uint256 combinedGas) = simulator.simulateMulticall3CombinedGas( + address(multicall3), t.preCalls, address(oc), 2, 1e11, 11_000, abi.encode(i) + ); + + // Now execute through multicall3 with the calculated combinedGas and measure actual gas + i.combinedGas = combinedGas; + i.signature = _sig(d, i); + t.gExecute = Math.mulDiv(combinedGas + 110_000, 64, 63); + + // Build multicall3 calls array: empty preCalls + orchestrator execute call + IMulticall3.Call3[] memory executeCalls = new IMulticall3.Call3[](1); + executeCalls[0] = IMulticall3.Call3({ + target: address(oc), + allowFailure: false, + callData: abi.encodeWithSignature("execute(bytes)", abi.encode(i)) + }); + + IMulticall3.Result[] memory results = multicall3.aggregate3{gas: t.gExecute}(executeCalls); + + // Check that the orchestrator call succeeded and returned 0 (no error) + if (results.length > 0 && results[0].success) { + bytes4 err = abi.decode(results[0].returnData, (bytes4)); + assertEq(err, 0, "Execution should succeed"); + } else { + revert("Multicall3 execution failed"); + } + + assertEq(gasBurner.randomness(), t.randomness); + } +} + +// Mock Multicall3 implementation for testing +contract MockMulticall3 is IMulticall3 { + function aggregate(Call[] calldata calls) + external + payable + override + returns (uint256 blockNumber, bytes[] memory returnData) + { + blockNumber = block.number; + returnData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); + if (!success) revert("Multicall3: call failed"); + returnData[i] = ret; + } + } + + function aggregate3(Call3[] calldata calls) + external + payable + override + returns (Result[] memory returnData) + { + returnData = new Result[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); + returnData[i] = Result({success: success, returnData: ret}); + if (!calls[i].allowFailure && !success) { + revert("Multicall3: call failed"); + } + } + } }