|
2 | 2 | pragma solidity ^0.8.23; |
3 | 3 |
|
4 | 4 | import {ICommon} from "./interfaces/ICommon.sol"; |
| 5 | +import {IMulticall3} from "./interfaces/IMulticall3.sol"; |
5 | 6 | import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; |
6 | 7 |
|
7 | 8 | /// @title Simulator |
@@ -125,6 +126,99 @@ contract Simulator { |
125 | 126 | return _callOrchestrator(oc, isStateOverride, data); |
126 | 127 | } |
127 | 128 |
|
| 129 | + /// @dev Performs a call to Multicall3 with calls followed by an orchestrator call. |
| 130 | + /// Returns the gas used parsed from the last Result in the multicall3 response. |
| 131 | + /// If parsing fails (gasUsed == 0), this function stores the orchestrator's error in memory |
| 132 | + /// so the caller can bubble it up using bubbleUpMulticall3Error. |
| 133 | + /// @param multicall3 The multicall3 contract address |
| 134 | + /// @param calls Array of Call3 structs to execute before the orchestrator call |
| 135 | + /// @param oc The orchestrator address |
| 136 | + /// @param isStateOverride Whether to use state override mode for the orchestrator call |
| 137 | + /// @param combinedGasOverride The combined gas override value |
| 138 | + /// @param u The Intent struct to pass to the orchestrator |
| 139 | + /// @return gasUsed The gas used by the orchestrator call (parsed from SimulationPassed or state override result) |
| 140 | + /// @return multicall3Gas The gas spent on the aggregate3 call itself |
| 141 | + /// @return lastReturnData The return data from the orchestrator call |
| 142 | + function _callMulticall3( |
| 143 | + address multicall3, |
| 144 | + IMulticall3.Call3[] memory calls, |
| 145 | + address oc, |
| 146 | + bool isStateOverride, |
| 147 | + uint256 combinedGasOverride, |
| 148 | + ICommon.Intent memory u |
| 149 | + ) |
| 150 | + internal |
| 151 | + freeTempMemory |
| 152 | + returns (uint256 gasUsed, uint256 multicall3Gas, bytes memory lastReturnData) |
| 153 | + { |
| 154 | + // Build the orchestrator call data |
| 155 | + bytes memory orchestratorData = abi.encodeWithSignature( |
| 156 | + "simulateExecute(bool,uint256,bytes)", |
| 157 | + isStateOverride, |
| 158 | + combinedGasOverride, |
| 159 | + abi.encode(u) |
| 160 | + ); |
| 161 | + |
| 162 | + // Construct the full Call3[] array: calls + orchestrator call |
| 163 | + IMulticall3.Call3[] memory allCalls = new IMulticall3.Call3[](calls.length + 1); |
| 164 | + for (uint256 i = 0; i < calls.length; i++) { |
| 165 | + allCalls[i] = calls[i]; |
| 166 | + } |
| 167 | + // Last call is to the orchestrator |
| 168 | + allCalls[calls.length] = |
| 169 | + IMulticall3.Call3({target: oc, allowFailure: true, callData: orchestratorData}); |
| 170 | + |
| 171 | + // Measure gas before and after aggregate3 call |
| 172 | + uint256 gasBefore = gasleft(); |
| 173 | + IMulticall3.Result[] memory results = IMulticall3(multicall3).aggregate3(allCalls); |
| 174 | + multicall3Gas = gasBefore - gasleft(); |
| 175 | + |
| 176 | + // Get the last result (orchestrator call result) |
| 177 | + // Check all calls for failures (all results except the last one, which is the orchestrator call) |
| 178 | + if (results.length > 1) { |
| 179 | + for (uint256 i = 0; i < results.length - 1; i++) { |
| 180 | + // If any call failed, we return gasUsed = 0, multicall3Gas, and the error data from that call |
| 181 | + if (!results[i].success) { |
| 182 | + return (0, 0, results[i].returnData); |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + IMulticall3.Result memory lastResult = results[results.length - 1]; |
| 187 | + lastReturnData = lastResult.returnData; |
| 188 | + |
| 189 | + // Parse gasUsed from the orchestrator's return data |
| 190 | + // Assembly required for low-level parsing of return data |
| 191 | + assembly ("memory-safe") { |
| 192 | + let returnDataPtr := add(lastReturnData, 0x20) |
| 193 | + |
| 194 | + switch isStateOverride |
| 195 | + case 0 { |
| 196 | + // If `isStateOverride` is false, the orchestrator reverts with SimulationPassed |
| 197 | + // so success will be false in multicall3's Result, but we still need to parse the revert data |
| 198 | + // Check for SimulationPassed selector (0x4f0c028c) |
| 199 | + // The `gasUsed` will be in the revert data at offset 0x04 |
| 200 | + if eq(shr(224, mload(returnDataPtr)), 0x4f0c028c) { |
| 201 | + gasUsed := mload(add(returnDataPtr, 0x04)) |
| 202 | + } |
| 203 | + } |
| 204 | + default { |
| 205 | + // If isStateOverride is true and call succeeded, gasUsed is at offset 0x00 |
| 206 | + let lastSuccess := mload(lastResult) |
| 207 | + if lastSuccess { |
| 208 | + gasUsed := mload(returnDataPtr) |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + /// @dev Bubbles up the error from the multicall3's last result returnData. |
| 215 | + /// This is called when _callMulticall3 returns gasUsed == 0. |
| 216 | + function _bubbleUpMulticall3Error(bytes memory errorData) internal pure { |
| 217 | + assembly ("memory-safe") { |
| 218 | + revert(add(errorData, 0x20), mload(errorData)) |
| 219 | + } |
| 220 | + } |
| 221 | + |
128 | 222 | /// @dev Simulate the gas usage for a user operation. This function reverts if the simulation fails. |
129 | 223 | /// @param oc The orchestrator address |
130 | 224 | /// @param overrideCombinedGas Whether to override the combined gas for the intent to type(uint256).max |
@@ -262,4 +356,151 @@ contract Simulator { |
262 | 356 | } |
263 | 357 | } |
264 | 358 | } |
| 359 | + |
| 360 | + /// @dev Simulates the execution of an intent through Multicall3, with calls executed before the orchestrator call. |
| 361 | + /// Finds the combined gas by iteratively increasing it until the simulation passes. |
| 362 | + /// The start value for combinedGas is gasUsed + original combinedGas. |
| 363 | + /// Set u.combinedGas to add some starting offset to the gasUsed value. |
| 364 | + /// @param multicall3 The multicall3 contract address |
| 365 | + /// @param calls Array of Call3 structs to execute before the orchestrator call |
| 366 | + /// @param oc The orchestrator address |
| 367 | + /// @param paymentPerGasPrecision The precision of the payment per gas value. |
| 368 | + /// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision) |
| 369 | + /// @param paymentPerGas The amount of `paymentToken` to be added per gas unit. |
| 370 | + /// Total payment is calculated as paymentAmount += gasUsed * paymentPerGas. |
| 371 | + /// @dev Set paymentAmount to include any static offset to the gas value. |
| 372 | + /// @param combinedGasIncrement Basis Points increment to be added for each iteration of searching for combined gas. |
| 373 | + /// @dev The closer this number is to 10_000, the more precise combined gas will be. But more iterations will be needed. |
| 374 | + /// @dev This number should always be > 10_000, to get correct results. |
| 375 | + /// If the increment is too small, the function might run out of gas while finding the combined gas value. |
| 376 | + /// @param encodedIntent The encoded user operation |
| 377 | + /// @return gasUsed The gas used in the successful simulation |
| 378 | + /// @return multicall3Gas The gas spent on the aggregate3 call |
| 379 | + /// @return combinedGas The first combined gas value that gives a successful simulation. |
| 380 | + /// This function reverts if the primary simulation run with max combinedGas fails. |
| 381 | + /// If the primary run is successful, it iteratively increases u.combinedGas by `combinedGasIncrement` until the simulation passes. |
| 382 | + /// All failing simulations during this run are ignored. |
| 383 | + function simulateMulticall3CombinedGas( |
| 384 | + address multicall3, |
| 385 | + IMulticall3.Call3[] memory calls, |
| 386 | + address oc, |
| 387 | + uint8 paymentPerGasPrecision, |
| 388 | + uint256 paymentPerGas, |
| 389 | + uint256 combinedGasIncrement, |
| 390 | + bytes calldata encodedIntent |
| 391 | + ) public payable virtual returns (uint256 gasUsed, uint256 multicall3Gas, uint256 combinedGas) { |
| 392 | + // Decode the intent first |
| 393 | + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); |
| 394 | + |
| 395 | + bytes memory errorData; |
| 396 | + |
| 397 | + // 1. Primary Simulation Run to get initial gasUsed value with combinedGasOverride |
| 398 | + |
| 399 | + (gasUsed, multicall3Gas, errorData) = |
| 400 | + _callMulticall3(multicall3, calls, oc, false, type(uint256).max, u); |
| 401 | + |
| 402 | + // If the simulation failed, bubble up the orchestrator's error. |
| 403 | + if (gasUsed == 0) { |
| 404 | + _bubbleUpMulticall3Error(errorData); |
| 405 | + } |
| 406 | + |
| 407 | + u.combinedGas += gasUsed; |
| 408 | + |
| 409 | + _updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas); |
| 410 | + |
| 411 | + while (true) { |
| 412 | + (gasUsed, multicall3Gas, errorData) = |
| 413 | + _callMulticall3(multicall3, calls, oc, false, 0, u); |
| 414 | + |
| 415 | + // If the simulation failed, check if it's a PaymentError and bubble it up. |
| 416 | + // PaymentError is given special treatment here, as it comes from |
| 417 | + // the account not having enough funds, and cannot be recovered from, |
| 418 | + // since the paymentAmount will keep increasing in this loop. |
| 419 | + if (gasUsed == 0 && errorData.length >= 4) { |
| 420 | + // errorData is a bytes memory containing the revert data from the orchestrator |
| 421 | + // Layout: [length (32 bytes)][selector (4 bytes)][additional data...] |
| 422 | + bytes4 errorSelector; |
| 423 | + assembly ("memory-safe") { |
| 424 | + // Load 32 bytes starting from errorData+32 |
| 425 | + // bytes4 values are already right-aligned, no shift needed |
| 426 | + errorSelector := mload(add(errorData, 32)) |
| 427 | + } |
| 428 | + if (errorSelector == 0xabab8fc9) { |
| 429 | + // PaymentError() |
| 430 | + |
| 431 | + // Revert with just the selector (0x20 bytes = 4 bytes selector + 28 bytes padding) |
| 432 | + assembly ("memory-safe") { |
| 433 | + mstore(0x00, errorSelector) |
| 434 | + revert(0x00, 0x20) |
| 435 | + } |
| 436 | + } |
| 437 | + } |
| 438 | + |
| 439 | + if (gasUsed != 0) { |
| 440 | + return (gasUsed, multicall3Gas, u.combinedGas); |
| 441 | + } |
| 442 | + |
| 443 | + uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000); |
| 444 | + |
| 445 | + _updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas); |
| 446 | + |
| 447 | + // Step up the combined gas, until we see a simulation passing |
| 448 | + u.combinedGas += gasIncrement; |
| 449 | + } |
| 450 | + } |
| 451 | + |
| 452 | + /// @dev Same as simulateMulticall3CombinedGas, but with an additional verification run |
| 453 | + /// that generates a successful non reverting state override simulation. |
| 454 | + /// Which can be used in eth_simulateV1 to get the trace. |
| 455 | + /// @param multicall3 The multicall3 contract address |
| 456 | + /// @param calls Array of Call3 structs to execute before the orchestrator call |
| 457 | + /// @param oc The orchestrator address |
| 458 | + /// @param paymentPerGasPrecision The precision of the payment per gas value. |
| 459 | + /// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision) |
| 460 | + /// @param paymentPerGas The amount of `paymentToken` to be added per gas unit. |
| 461 | + /// @param combinedGasIncrement Basis Points increment to be added for each iteration of searching for combined gas. |
| 462 | + /// @param combinedGasVerificationOffset is a static value that is added after a successful combinedGas is found. |
| 463 | + /// This can be used to account for variations in sig verification gas, for keytypes like P256. |
| 464 | + /// @param encodedIntent The encoded user operation |
| 465 | + /// @return gasUsed The gas used in the successful simulation |
| 466 | + /// @return multicall3Gas The gas spent on the aggregate3 call |
| 467 | + /// @return combinedGas The combined gas value including the verification offset |
| 468 | + function simulateMulticall3V1Logs( |
| 469 | + address multicall3, |
| 470 | + IMulticall3.Call3[] memory calls, |
| 471 | + address oc, |
| 472 | + uint8 paymentPerGasPrecision, |
| 473 | + uint256 paymentPerGas, |
| 474 | + uint256 combinedGasIncrement, |
| 475 | + uint256 combinedGasVerificationOffset, |
| 476 | + bytes calldata encodedIntent |
| 477 | + ) public payable virtual returns (uint256 gasUsed, uint256 multicall3Gas, uint256 combinedGas) { |
| 478 | + (gasUsed, multicall3Gas, combinedGas) = simulateMulticall3CombinedGas( |
| 479 | + multicall3, |
| 480 | + calls, |
| 481 | + oc, |
| 482 | + paymentPerGasPrecision, |
| 483 | + paymentPerGas, |
| 484 | + combinedGasIncrement, |
| 485 | + encodedIntent |
| 486 | + ); |
| 487 | + |
| 488 | + combinedGas += combinedGasVerificationOffset; |
| 489 | + |
| 490 | + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); |
| 491 | + |
| 492 | + _updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas); |
| 493 | + |
| 494 | + u.combinedGas = combinedGas; |
| 495 | + |
| 496 | + bytes memory errorData; |
| 497 | + |
| 498 | + // Verification Run to generate the logs with the correct combinedGas and payment amounts. |
| 499 | + (gasUsed, multicall3Gas, errorData) = _callMulticall3(multicall3, calls, oc, true, 0, u); |
| 500 | + |
| 501 | + // If the simulation failed, bubble up the orchestrator's error |
| 502 | + if (gasUsed == 0) { |
| 503 | + _bubbleUpMulticall3Error(errorData); |
| 504 | + } |
| 505 | + } |
265 | 506 | } |
0 commit comments