diff --git a/crates/sim/src/estimation/estimate_call_gas.rs b/crates/sim/src/estimation/estimate_call_gas.rs index 1d818bc5f..9ee336aa1 100644 --- a/crates/sim/src/estimation/estimate_call_gas.rs +++ b/crates/sim/src/estimation/estimate_call_gas.rs @@ -2,16 +2,16 @@ use anyhow::{anyhow, Context}; use async_trait::async_trait; use ethers::{ abi::AbiDecode, - contract::EthCall, types::{spoof, Address, Bytes, H256, U128, U256}, }; -use rand::Rng; use rundler_provider::{EntryPoint, SimulationProvider}; use rundler_types::{ - contracts::utils::call_gas_estimation_proxy::{ - EstimateCallGasArgs, EstimateCallGasCall, EstimateCallGasContinuation, - EstimateCallGasResult, EstimateCallGasRevertAtMax, TestCallGasCall, TestCallGasResult, - CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE, + contracts::v0_7::call_gas_estimation_proxy::{ + // Errors are shared between v0.6 and v0.7 proxies + EstimateCallGasContinuation, + EstimateCallGasResult, + EstimateCallGasRevertAtMax, + TestCallGasResult, }, UserOperation, }; @@ -26,12 +26,10 @@ use crate::GasEstimationError; /// verification gas and call gas. const GAS_ROUNDING: u64 = 4096; -/// Offset at which the proxy target address appears in the proxy bytecode. Must -/// be updated whenever `CallGasEstimationProxy.sol` changes. -/// -/// The easiest way to get the updated value is to run this module's tests. The -/// failure will tell you the new value. -const PROXY_TARGET_OFFSET: usize = 163; +/// Must match the constant in `CallGasEstimationProxyTypes.sol`. +#[allow(dead_code)] +pub(crate) const PROXY_IMPLEMENTATION_ADDRESS_MARKER: &str = + "A13dB4eCfbce0586E57D1AeE224FbE64706E8cd3"; /// Estimates the gas limit for a user operation #[async_trait] @@ -75,13 +73,25 @@ pub trait CallGasEstimatorSpecialization: Send + Sync + 'static { /// The user operation type estimated by this specialization type UO: UserOperation; + /// Add the required CallGasEstimation proxy to the overrides at the given entrypoint address + fn add_proxy_to_overrides(&self, ep_to_override: Address, state_override: &mut spoof::State); + /// Returns the input user operation, modified to have limits but zero for the call gas limits. /// The intent is that the modified operation should run its validation but do nothing during execution fn get_op_with_no_call_gas(&self, op: Self::UO) -> Self::UO; - /// Returns the deployed bytecode of the entry point contract with - /// simulation methods - fn entry_point_simulations_code(&self) -> Bytes; + /// Returns the calldata for the `estimateCallGas` function of the proxy + fn get_estimate_call_gas_calldata( + &self, + callless_op: Self::UO, + min_gas: U256, + max_gas: U256, + rounding: U256, + is_continuation: bool, + ) -> Bytes; + + /// Returns the calldata for the `testCallGas` function of the proxy + fn get_test_call_gas_calldata(&self, callless_op: Self::UO, call_gas_limit: U256) -> Bytes; } #[async_trait] @@ -100,9 +110,8 @@ where mut state_override: spoof::State, ) -> Result { let timer = std::time::Instant::now(); - // For an explanation of what's going on here, see the comment at the - // top of `CallGasEstimationProxy.sol`. - self.add_proxy_to_overrides(&mut state_override); + self.specialization + .add_proxy_to_overrides(self.entry_point.address(), &mut state_override); let callless_op = self.specialization.get_op_with_no_call_gas(op.clone()); @@ -111,16 +120,12 @@ where let mut is_continuation = false; let mut num_rounds = U256::zero(); loop { - let target_call_data = eth::call_data_of( - EstimateCallGasCall::selector(), - (EstimateCallGasArgs { - sender: op.sender(), - call_data: Bytes::clone(op.call_data()), - min_gas, - max_gas, - rounding: GAS_ROUNDING.into(), - is_continuation, - },), + let target_call_data = self.specialization.get_estimate_call_gas_calldata( + callless_op.clone(), + min_gas, + max_gas, + GAS_ROUNDING.into(), + is_continuation, ); let target_revert_data = self .entry_point @@ -184,18 +189,14 @@ where block_hash: H256, mut state_override: spoof::State, ) -> Result<(), GasEstimationError> { - self.add_proxy_to_overrides(&mut state_override); - - let target_call_data = eth::call_data_of( - TestCallGasCall::selector(), - ( - op.sender(), - Bytes::clone(op.call_data()), - op.call_gas_limit(), - ), - ); + self.specialization + .add_proxy_to_overrides(self.entry_point.address(), &mut state_override); + let call_gas_limit = op.call_gas_limit(); let callless_op = self.specialization.get_op_with_no_call_gas(op); + let target_call_data = self + .specialization + .get_test_call_gas_calldata(callless_op.clone(), call_gas_limit); let target_revert_data = self .entry_point @@ -241,49 +242,4 @@ where specialization, } } - - fn add_proxy_to_overrides(&self, state_override: &mut spoof::State) { - // Use a random address for the moved entry point so that users can't - // intentionally get bad estimates by interacting with the hardcoded - // address. - let moved_entry_point_address: Address = rand::thread_rng().gen(); - let estimation_proxy_bytecode = - estimation_proxy_bytecode_with_target(moved_entry_point_address); - state_override - .account(moved_entry_point_address) - .code(self.specialization.entry_point_simulations_code()); - state_override - .account(self.entry_point.address()) - .code(estimation_proxy_bytecode); - } -} - -/// Replaces the address of the proxy target where it appears in the proxy -/// bytecode so we don't need the same fixed address every time. -fn estimation_proxy_bytecode_with_target(target: Address) -> Bytes { - let mut vec = CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.to_vec(); - vec[PROXY_TARGET_OFFSET..PROXY_TARGET_OFFSET + 20].copy_from_slice(target.as_bytes()); - vec.into() -} - -#[cfg(test)] -mod tests { - use ethers::utils::hex; - - use super::*; - - /// Must match the constant in `CallGasEstimationProxy.sol`. - const PROXY_TARGET_CONSTANT: &str = "A13dB4eCfbce0586E57D1AeE224FbE64706E8cd3"; - - #[test] - fn test_proxy_target_offset() { - let proxy_target_bytes = hex::decode(PROXY_TARGET_CONSTANT).unwrap(); - let mut offsets = Vec::::new(); - for i in 0..CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.len() - 20 { - if CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE[i..i + 20] == proxy_target_bytes { - offsets.push(i); - } - } - assert_eq!(vec![PROXY_TARGET_OFFSET], offsets); - } } diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 41e179d13..ba81db875 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -14,17 +14,25 @@ use std::{cmp, ops::Add, sync::Arc}; use ethers::{ + contract::EthCall, providers::spoof, - types::{Bytes, H256, U256}, + types::{Address, Bytes, H256, U256}, }; +use rand::Rng; use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; use rundler_types::{ chain::ChainSpec, - contracts::ENTRY_POINT_V0_6_DEPLOYED_BYTECODE, + contracts::{ + v0_6::call_gas_estimation_proxy::{ + EstimateCallGasArgs, EstimateCallGasCall, TestCallGasCall, + CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE, + }, + ENTRY_POINT_V0_6_DEPLOYED_BYTECODE, + }, v0_6::{UserOperation, UserOperationOptionalGas}, GasEstimate, }; -use rundler_utils::math; +use rundler_utils::{eth, math}; use tokio::join; use super::{ @@ -330,6 +338,23 @@ pub struct CallGasEstimatorSpecializationV06; impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV06 { type UO = UserOperation; + fn add_proxy_to_overrides(&self, ep_to_override: Address, state_override: &mut spoof::State) { + // For an explanation of what's going on here, see the comment at the + // top of `CallGasEstimationProxy.sol`. + // Use a random address for the moved entry point so that users can't + // intentionally get bad estimates by interacting with the hardcoded + // address. + let moved_entry_point_address: Address = rand::thread_rng().gen(); + let estimation_proxy_bytecode = + estimation_proxy_bytecode_with_target(moved_entry_point_address); + state_override + .account(moved_entry_point_address) + .code(ENTRY_POINT_V0_6_DEPLOYED_BYTECODE.clone()); + state_override + .account(ep_to_override) + .code(estimation_proxy_bytecode); + } + fn get_op_with_no_call_gas(&self, op: Self::UO) -> Self::UO { UserOperation { call_gas_limit: 0.into(), @@ -338,13 +363,47 @@ impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV06 { } } - fn entry_point_simulations_code(&self) -> Bytes { - // In v0.6, the entry point code contains the simulations code, so we - // just return the entry point code. - Bytes::clone(&ENTRY_POINT_V0_6_DEPLOYED_BYTECODE) + fn get_estimate_call_gas_calldata( + &self, + callless_op: Self::UO, + min_gas: U256, + max_gas: U256, + rounding: U256, + is_continuation: bool, + ) -> Bytes { + eth::call_data_of( + EstimateCallGasCall::selector(), + (EstimateCallGasArgs { + call_data: callless_op.call_data, + sender: callless_op.sender, + min_gas, + max_gas, + rounding, + is_continuation, + },), + ) + } + + fn get_test_call_gas_calldata(&self, callless_op: Self::UO, call_gas_limit: U256) -> Bytes { + eth::call_data_of(TestCallGasCall::selector(), (callless_op, call_gas_limit)) } } +/// Offset at which the proxy target address appears in the proxy bytecode. Must +/// be updated whenever `CallGasEstimationProxy.sol` changes. +/// +/// The easiest way to get the updated value is to run this module's tests. The +/// failure will tell you the new value. +const PROXY_TARGET_OFFSET: usize = 163; + +// Replaces the address of the proxy target where it appears in the proxy +// bytecode so we don't need the same fixed address every time. +fn estimation_proxy_bytecode_with_target(target: Address) -> Bytes { + let mut vec = CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.to_vec(); + vec[PROXY_TARGET_OFFSET..PROXY_TARGET_OFFSET + 20].copy_from_slice(target.as_bytes()); + vec.into() +} + #[cfg(test)] mod tests { use anyhow::anyhow; @@ -352,19 +411,20 @@ mod tests { abi::{AbiEncode, Address}, contract::EthCall, types::{U128, U64}, + utils::hex, }; use rundler_provider::{ExecutionResult, MockEntryPointV0_6, MockProvider, SimulateOpCallData}; use rundler_types::{ chain::L1GasOracleContractType, contracts::{ - utils::{ + utils::get_gas_used::GasUsedResult, + v0_6::{ call_gas_estimation_proxy::{ EstimateCallGasContinuation, EstimateCallGasResult, EstimateCallGasRevertAtMax, TestCallGasResult, }, - get_gas_used::GasUsedResult, + i_entry_point, }, - v0_6::i_entry_point, }, v0_6::{UserOperation, UserOperationOptionalGas}, UserOperation as UserOperationTrait, ValidationRevert, @@ -373,7 +433,10 @@ mod tests { use super::*; use crate::{ - estimation::{CALL_GAS_BUFFER_VALUE, VERIFICATION_GAS_BUFFER_PERCENT}, + estimation::{ + estimate_call_gas::PROXY_IMPLEMENTATION_ADDRESS_MARKER, CALL_GAS_BUFFER_VALUE, + VERIFICATION_GAS_BUFFER_PERCENT, + }, simulation::v0_6::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, PriorityFeeMode, VerificationGasEstimatorImpl, }; @@ -1361,4 +1424,16 @@ mod tests { GasEstimationError::RevertInCallWithMessage(msg) if msg == revert_msg )); } + + #[test] + fn test_proxy_target_offset() { + let proxy_target_bytes = hex::decode(PROXY_IMPLEMENTATION_ADDRESS_MARKER).unwrap(); + let mut offsets = Vec::::new(); + for i in 0..CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.len() - 20 { + if CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE[i..i + 20] == proxy_target_bytes { + offsets.push(i); + } + } + assert_eq!(vec![PROXY_TARGET_OFFSET], offsets); + } } diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs index ce9d5f6c6..c4ebb0541 100644 --- a/crates/sim/src/estimation/v0_7.rs +++ b/crates/sim/src/estimation/v0_7.rs @@ -13,15 +13,25 @@ use std::{cmp, ops::Add, sync::Arc}; -use ethers::types::{spoof, Bytes, H256, U128, U256}; +use ethers::{ + contract::EthCall, + types::{spoof, Address, Bytes, H256, U128, U256}, +}; +use rand::Rng; use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; use rundler_types::{ chain::ChainSpec, - contracts::v0_7::entry_point_simulations, + contracts::v0_7::{ + call_gas_estimation_proxy::{ + EstimateCallGasArgs, EstimateCallGasCall, TestCallGasCall, + CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE, + }, + entry_point_simulations::ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE, + }, v0_7::{UserOperation, UserOperationOptionalGas}, GasEstimate, }; -use rundler_utils::math; +use rundler_utils::{eth, math}; use tokio::join; use super::{estimate_verification_gas::GetOpWithLimitArgs, GasEstimationError, Settings}; @@ -389,6 +399,23 @@ pub struct CallGasEstimatorSpecializationV07; impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV07 { type UO = UserOperation; + fn add_proxy_to_overrides(&self, ep_to_override: Address, state_override: &mut spoof::State) { + // For an explanation of what's going on here, see the comment at the + // top of `CallGasEstimationProxy.sol`. + // Use a random address for the moved entry point so that users can't + // intentionally get bad estimates by interacting with the hardcoded + // address. + let moved_entry_point_address: Address = rand::thread_rng().gen(); + let estimation_proxy_bytecode = + estimation_proxy_bytecode_with_target(moved_entry_point_address); + state_override + .account(moved_entry_point_address) + .code(ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone()); + state_override + .account(ep_to_override) + .code(estimation_proxy_bytecode); + } + fn get_op_with_no_call_gas(&self, op: Self::UO) -> Self::UO { op.into_builder() .call_gas_limit(U128::zero()) @@ -396,30 +423,72 @@ impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV07 { .build() } - fn entry_point_simulations_code(&self) -> Bytes { - entry_point_simulations::ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone() + fn get_estimate_call_gas_calldata( + &self, + callless_op: Self::UO, + min_gas: U256, + max_gas: U256, + rounding: U256, + is_continuation: bool, + ) -> Bytes { + eth::call_data_of( + EstimateCallGasCall::selector(), + (EstimateCallGasArgs { + user_op: callless_op.pack(), + min_gas, + max_gas, + rounding, + is_continuation, + },), + ) + } + + fn get_test_call_gas_calldata(&self, callless_op: Self::UO, call_gas_limit: U256) -> Bytes { + eth::call_data_of( + TestCallGasCall::selector(), + (callless_op.pack(), call_gas_limit), + ) } } +/// Offset at which the proxy target address appears in the proxy bytecode. Must +/// be updated whenever `CallGasEstimationProxy.sol` changes. +/// +/// The easiest way to get the updated value is to run this module's tests. The +/// failure will tell you the new value. +const PROXY_TARGET_OFFSET: usize = 163; + +// Replaces the address of the proxy target where it appears in the proxy +// bytecode so we don't need the same fixed address every time. +fn estimation_proxy_bytecode_with_target(target: Address) -> Bytes { + let mut vec = CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.to_vec(); + vec[PROXY_TARGET_OFFSET..PROXY_TARGET_OFFSET + 20].copy_from_slice(target.as_bytes()); + vec.into() +} + #[cfg(test)] mod tests { use ethers::{ abi::AbiEncode, contract::EthCall, types::{Address, U64}, + utils::hex, }; use rundler_provider::{ExecutionResult, MockEntryPointV0_7, MockProvider, SimulateOpCallData}; use rundler_types::{ - contracts::{ - utils::call_gas_estimation_proxy::TestCallGasResult, - v0_7::entry_point_simulations::SimulateHandleOpCall, + contracts::v0_7::{ + call_gas_estimation_proxy::TestCallGasResult, + entry_point_simulations::SimulateHandleOpCall, }, v0_7::UserOperationOptionalGas, }; use rundler_utils::eth::{self, ContractRevertError}; use super::*; - use crate::{GasEstimator as _, PriorityFeeMode}; + use crate::{ + estimation::estimate_call_gas::PROXY_IMPLEMENTATION_ADDRESS_MARKER, GasEstimator as _, + PriorityFeeMode, + }; // Alises for complex types (which also satisfy Clippy) type VerificationGasEstimatorWithMocks = @@ -729,4 +798,16 @@ mod tests { GasEstimationError::RevertInCallWithMessage(msg) if msg == revert_msg )); } + + #[test] + fn test_proxy_target_offset() { + let proxy_target_bytes = hex::decode(PROXY_IMPLEMENTATION_ADDRESS_MARKER).unwrap(); + let mut offsets = Vec::::new(); + for i in 0..CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE.len() - 20 { + if CALLGASESTIMATIONPROXY_DEPLOYED_BYTECODE[i..i + 20] == proxy_target_bytes { + offsets.push(i); + } + } + assert_eq!(vec![PROXY_TARGET_OFFSET], offsets); + } } diff --git a/crates/types/build.rs b/crates/types/build.rs index 9366ba23c..6bbb22fa4 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -44,6 +44,7 @@ fn generate_v0_6_bindings() -> Result<(), Box> { abigen_of("v0_6", "SimpleAccount")?, abigen_of("v0_6", "SimpleAccountFactory")?, abigen_of("v0_6", "VerifyingPaymaster")?, + abigen_of("v0_6", "CallGasEstimationProxy")?, ]) .build()? .write_to_module("src/contracts/v0_6", false)?; @@ -68,6 +69,7 @@ fn generate_v0_7_bindings() -> Result<(), Box> { abigen_of("v0_7", "IStakeManager")?, abigen_of("v0_7", "GetBalances")?, abigen_of("v0_7", "EntryPointSimulations")?, + abigen_of("v0_7", "CallGasEstimationProxy")?, abigen_of("v0_7", "SenderCreator")?, ]) .build()? @@ -84,7 +86,6 @@ fn generate_utils_bindings() -> Result<(), Box> { )?; MultiAbigen::from_abigens([ - abigen_of("utils", "CallGasEstimationProxy")?, abigen_of("utils", "GetCodeHashes")?, abigen_of("utils", "GetGasUsed")?, ]) diff --git a/crates/types/contracts/src/utils/CallGasEstimationProxyTypes.sol b/crates/types/contracts/src/utils/CallGasEstimationProxyTypes.sol new file mode 100644 index 000000000..5f466f8c0 --- /dev/null +++ b/crates/types/contracts/src/utils/CallGasEstimationProxyTypes.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +error EstimateCallGasResult(uint256 gasEstimate, uint256 numRounds); + +error EstimateCallGasContinuation(uint256 minGas, uint256 maxGas, uint256 numRounds); + +error EstimateCallGasRevertAtMax(bytes revertData); + +error TestCallGasResult(bool success, uint256 gasUsed, bytes revertData); + +// keccak("CallGasEstimationProxy")[:20] +// Don't use an immutable constant. We want the "deployedBytecode" in +// the generated JSON to contain this constant. +address constant IMPLEMENTATION_ADDRESS_MARKER = 0xA13dB4eCfbce0586E57D1AeE224FbE64706E8cd3; diff --git a/crates/types/contracts/src/utils/CallGasEstimationProxy.sol b/crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol similarity index 93% rename from crates/types/contracts/src/utils/CallGasEstimationProxy.sol rename to crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol index 4657da93e..d28765de1 100644 --- a/crates/types/contracts/src/utils/CallGasEstimationProxy.sol +++ b/crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts-versions/v5_0/contracts/proxy/Proxy.sol"; import "openzeppelin-contracts-versions/v5_0/contracts/utils/math/Math.sol"; +import "../utils/CallGasEstimationProxyTypes.sol"; + /** * Contract used in `eth_call`'s "overrides" parameter in order to estimate the * required `callGasLimit` for a user operation. @@ -36,10 +38,7 @@ contract CallGasEstimationProxy is Proxy { using Math for uint256; function _implementation() internal pure virtual override returns (address) { - // keccak("CallGasEstimationProxy")[:20] - // Don't use an immutable constant. We want the "deployedBytecode" in - // the generated JSON to contain this constant. - return 0xA13dB4eCfbce0586E57D1AeE224FbE64706E8cd3; + return IMPLEMENTATION_ADDRESS_MARKER; } struct EstimateCallGasArgs { @@ -51,14 +50,6 @@ contract CallGasEstimationProxy is Proxy { bool isContinuation; } - error EstimateCallGasResult(uint256 gasEstimate, uint256 numRounds); - - error EstimateCallGasContinuation(uint256 minGas, uint256 maxGas, uint256 numRounds); - - error EstimateCallGasRevertAtMax(bytes revertData); - - error TestCallGasResult(bool success, uint256 gasUsed, bytes revertData); - /** * Runs a binary search to find the smallest amount of gas at which the call * succeeds. diff --git a/crates/types/contracts/src/v0_7/CallGasEstimationProxy.sol b/crates/types/contracts/src/v0_7/CallGasEstimationProxy.sol new file mode 100644 index 000000000..22283f7d1 --- /dev/null +++ b/crates/types/contracts/src/v0_7/CallGasEstimationProxy.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "openzeppelin-contracts-versions/v5_0/contracts/proxy/Proxy.sol"; +import "openzeppelin-contracts-versions/v5_0/contracts/utils/math/Math.sol"; +import "account-abstraction/v0_7/interfaces/IAccountExecute.sol"; +import "account-abstraction/v0_7/interfaces/PackedUserOperation.sol"; +import "account-abstraction/v0_7/interfaces/IEntryPoint.sol"; + +import "../utils/CallGasEstimationProxyTypes.sol"; + +/** + * Contract used in `eth_call`'s "overrides" parameter in order to estimate the + * required `callGasLimit` for a user operation. + * + * This contract is solving the problem that the entry point's + * `simulateHandleOp` doesn't return whether the op's call succeeded, thus + * making it impossible to use directly for trying call gas limits to see if + * they work. We could call the sender directly with its call data, but that + * fails because we do need to run the validation step first, as it may cause + * changes to the sender's state or even deploy the sender in the first place. + * We can use `simulateHandleOp`s optional `target` and `targetData` parameters + * to run code after the validation step, but we need to watch out for the + * restriction that a typical sender will reject calls not coming from the + * entry point address. + * + * The solution is to create a proxy contract which delegates to the entry point + * but also exposes a method for estimating call gas by binary searching. + * We then call `simulateHandleOp` on this contract and use `target` and + * `targetData` to have this contract call itself to run a binary search to + * discover the call gas estimate. Thus when we call `simulateHandleOp`, we call + * it on this contract, using `eth_call`s overrides to move the original entry + * point code to a different address, then putting this contract's code at the + * original entry point address and having it's proxy target be the address to + * which we moved the entry point code. + * + * Note that this contract is never deployed. It is only used for its compiled + * bytecode, which is passed as an override in `eth_call`. + */ +contract CallGasEstimationProxy is Proxy { + using Math for uint256; + + function _implementation() internal pure virtual override returns (address) { + return IMPLEMENTATION_ADDRESS_MARKER; + } + + struct EstimateCallGasArgs { + PackedUserOperation userOp; + uint256 minGas; + uint256 maxGas; + uint256 rounding; + bool isContinuation; + } + + /** + * Runs a binary search to find the smallest amount of gas at which the call + * succeeds. + * + * Always reverts with its result, which is one of the following: + * + * - The successful gas estimate + * - That the call fails even with max gas + * - A new min and max gas to be used in a follow-up call, if we ran out of + * gas before completing the binary search. + * + * Takes a `rounding` parameter which rounds all guesses and the final + * result to a multiple of that parameter. + * + * As an optimization, if a round of binary search just completed + * successfully and used N gas, then the next round will try 2N gas if it's + * lower than the next (low + high) / 2 guess. This helps us quickly narrow + * down the common case where the gas needed is much smaller than the + * initial upper bound. + */ + function estimateCallGas(EstimateCallGasArgs calldata args) external { + // Will only be violated if the op is doing shinanigans where it tries + // to call this method on the entry point to throw off gas estimates. + require(msg.sender == address(this)); + uint256 scaledMaxFailureGas = args.minGas / args.rounding; + uint256 scaledMinSuccessGas = args.maxGas.ceilDiv(args.rounding); + uint256 scaledGasUsedInSuccess = scaledMinSuccessGas; + uint256 scaledGuess = 0; + bytes32 userOpHash = _getUserOpHashInternal(args.userOp); + if (!args.isContinuation) { + // Make one call at full gas to make sure success is even possible. + (bool success, uint256 gasUsed, bytes memory revertData) = innerCall(args.userOp, userOpHash, args.maxGas); + if (!success) { + revert EstimateCallGasRevertAtMax(revertData); + } + scaledGuess = (gasUsed * 2) / args.rounding; + } else { + scaledGuess = chooseGuess(scaledMaxFailureGas, scaledMinSuccessGas, scaledGasUsedInSuccess); + } + uint256 numRounds = 0; + while (scaledMaxFailureGas + 1 < scaledMinSuccessGas) { + numRounds++; + uint256 guess = scaledGuess * args.rounding; + if (!isEnoughGasForGuess(guess)) { + uint256 nextMin = scaledMaxFailureGas * args.rounding; + uint256 nextMax = scaledMinSuccessGas * args.rounding; + revert EstimateCallGasContinuation(nextMin, nextMax, numRounds); + } + (bool success, uint256 gasUsed,) = innerCall(args.userOp, userOpHash, guess); + if (success) { + scaledGasUsedInSuccess = scaledGasUsedInSuccess.min(gasUsed.ceilDiv(args.rounding)); + scaledMinSuccessGas = scaledGuess; + } else { + scaledMaxFailureGas = scaledGuess; + } + + scaledGuess = chooseGuess(scaledMaxFailureGas, scaledMinSuccessGas, scaledGasUsedInSuccess); + } + revert EstimateCallGasResult(args.maxGas.min(scaledMinSuccessGas * args.rounding), numRounds); + } + + /** + * A helper function for testing execution at a given gas limit. + */ + function testCallGas(PackedUserOperation calldata userOp, uint256 callGasLimit) external { + bytes32 userOpHash = _getUserOpHashInternal(userOp); + (bool success, uint256 gasUsed, bytes memory revertData) = innerCall(userOp, userOpHash, callGasLimit); + revert TestCallGasResult(success, gasUsed, revertData); + } + + function chooseGuess(uint256 highestFailureGas, uint256 lowestSuccessGas, uint256 lowestGasUsedInSuccess) + private + pure + returns (uint256) + { + uint256 average = (highestFailureGas + lowestSuccessGas) / 2; + if (lowestGasUsedInSuccess <= highestFailureGas) { + // Handle pathological cases where the contract requires a lot of + // gas but uses very little, which without this branch could cause + // the guesses to inch up a tiny bit at a time. + return average; + } else { + return average.min(2 * lowestGasUsedInSuccess); + } + } + + function isEnoughGasForGuess(uint256 guess) private view returns (bool) { + // Because of the 1/64 rule and the fact that we need two levels of + // calls, we need + // + // guess < (63/64)^2 * (gas - some_overhead) + // + // We'll take the overhead to be 50000, which should leave plenty left + // over for us to hand the result back to the EntryPoint to return. + return (64 * 64 * guess) / (63 * 63) + 50000 < gasleft(); + } + + error _InnerCallResult(bool success, uint256 gasUsed, bytes revertData); + + function innerCall(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 gas) + private + returns (bool success, uint256 gasUsed, bytes memory revertData) + { + bytes calldata callData = userOp.callData; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len, 3) { methodSig := calldataload(callData.offset) } + } + + bytes memory executeCall; + if (methodSig == IAccountExecute.executeUserOp.selector) { + executeCall = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, userOpHash)); + } else { + executeCall = callData; + } + + try this._innerCall(userOp.sender, executeCall, gas) { + // Should never happen. _innerCall should always revert. + revert(); + } catch (bytes memory innerCallRevertData) { + require(bytes4(innerCallRevertData) == _InnerCallResult.selector); + assembly { + innerCallRevertData := add(innerCallRevertData, 0x04) + } + (success, gasUsed, revertData) = abi.decode(innerCallRevertData, (bool, uint256, bytes)); + } + } + + function _innerCall(address sender, bytes calldata callData, uint256 gas) external { + uint256 preGas = gasleft(); + (bool success, bytes memory data) = sender.call{gas: gas}(callData); + uint256 gasUsed = preGas - gasleft(); + bytes memory revertData = success ? bytes("") : data; + revert _InnerCallResult(success, gasUsed, revertData); + } + + function _getUserOpHashInternal(PackedUserOperation calldata userOp) internal returns (bytes32) { + (bool success, bytes memory data) = + address(this).call(abi.encodeWithSelector(IEntryPoint.getUserOpHash.selector, userOp)); + require(success, "Call to getUserOpHash failed"); + return abi.decode(data, (bytes32)); + } +}