Skip to content

Commit

Permalink
fix(sim): handle v0.7 executeUserOp special call in call gas estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
dancoombs committed Jun 4, 2024
1 parent 07f6699 commit 7990f85
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 116 deletions.
122 changes: 39 additions & 83 deletions crates/sim/src/estimation/estimate_call_gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -100,9 +110,8 @@ where
mut state_override: spoof::State,
) -> Result<U128, GasEstimationError> {
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());

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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::<usize>::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);
}
}
100 changes: 89 additions & 11 deletions crates/sim/src/estimation/v0_6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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(),
Expand All @@ -338,33 +363,71 @@ 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.sender, callless_op.call_data, 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;
use ethers::{
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,
Expand All @@ -373,7 +436,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,
};
Expand Down Expand Up @@ -1361,4 +1427,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::<usize>::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);
}
}
Loading

0 comments on commit 7990f85

Please sign in to comment.