diff --git a/Cargo.lock b/Cargo.lock index 5b9c56a7a..68926547b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4797,6 +4797,7 @@ version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-contract", + "alloy-eips", "alloy-json-rpc", "alloy-node-bindings 0.4.2", "alloy-primitives", @@ -4869,6 +4870,7 @@ dependencies = [ name = "rundler-sim" version = "0.4.0" dependencies = [ + "alloy-eips", "alloy-primitives", "alloy-sol-types", "anyhow", @@ -4922,6 +4924,7 @@ dependencies = [ name = "rundler-types" version = "0.4.0" dependencies = [ + "alloy-eips", "alloy-primitives", "alloy-sol-types", "anyhow", @@ -4951,6 +4954,7 @@ name = "rundler-utils" version = "0.4.0" dependencies = [ "alloy-primitives", + "alloy-rpc-types-eth", "anyhow", "derive_more 0.99.18", "futures", diff --git a/crates/builder/src/signer/mod.rs b/crates/builder/src/signer/mod.rs index 66b7726e2..4d3855656 100644 --- a/crates/builder/src/signer/mod.rs +++ b/crates/builder/src/signer/mod.rs @@ -40,27 +40,40 @@ pub(crate) trait Signer: Send + Sync { .nonce .context("nonce should be set when transaction is filled")?; - let TypedTransaction::Eip1559(mut tx_1559) = tx - .build_typed_tx() - .expect("should build eip1559 transaction") - else { - bail!("transaction is not eip1559"); - }; + match tx.build_typed_tx().expect("unsupported transaction.") { + TypedTransaction::Eip1559(mut tx_1559) => { + tx_1559.set_chain_id(self.chain_id()); + let tx_hash = tx_1559.signature_hash(); + + let signature = self + .sign_hash(&tx_hash) + .await + .context("should sign transaction before sending")?; + + let signed: TxEnvelope = tx_1559.into_signed(signature).into(); - tx_1559.set_chain_id(self.chain_id()); - let tx_hash = tx_1559.signature_hash(); + let mut encoded = vec![]; + signed.encode_2718(&mut encoded); - let signature = self - .sign_hash(&tx_hash) - .await - .context("should sign transaction before sending")?; + return Ok((encoded.into(), nonce)); + } + TypedTransaction::Eip7702(mut tx_7702) => { + tx_7702.set_chain_id(self.chain_id()); + let tx_hash = tx_7702.signature_hash(); + let signature = self + .sign_hash(&tx_hash) + .await + .context("should sign transaction before sending")?; - let signed: TxEnvelope = tx_1559.into_signed(signature).into(); + let signed: TxEnvelope = tx_7702.into_signed(signature).into(); - let mut encoded = vec![]; - signed.encode_2718(&mut encoded); + let mut encoded = vec![]; + signed.encode_2718(&mut encoded); - Ok((encoded.into(), nonce)) + return Ok((encoded.into(), nonce)); + } + _ => bail!("transaction is either eip1559 nor eip7702."), + } } } diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 113d748f0..f8b2020d8 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -24,6 +24,18 @@ message UserOperation { } } +// Protocol Buffer representation of an 7702 authorization tuple. See the official +// specification at https://eips.ethereum.org/EIPS/eip-7702 +message AuthorizationTuple { + uint64 chain_id = 1; + uint64 nonce = 2; + bytes address = 3; + // signed authorization tuple. + uint32 y_parity = 4; + bytes r = 5; + bytes s = 6; +} + // Protocol Buffer representation of an ERC-4337 UserOperation. See the official // specification at https://eips.ethereum.org/EIPS/eip-4337#definitions message UserOperationV06 { @@ -52,6 +64,8 @@ message UserOperationV06 { bytes paymaster_and_data = 10; // Signature over the hash of the packed representation of the user operation bytes signature = 11; + // authorization tuple for 7702 txns + AuthorizationTuple authorization_tuple = 12; } message UserOperationV07 { @@ -90,6 +104,9 @@ message UserOperationV07 { // Extra data to compute the hash of the user operation bytes entry_point = 16; uint64 chain_id = 17; + + // authorization tuple for 7702 txns + AuthorizationTuple authorization_tuple = 18; } enum EntityType { @@ -592,6 +609,7 @@ message PrecheckViolationError { MaxFeePerGasTooLow max_fee_per_gas_too_low = 10; MaxPriorityFeePerGasTooLow max_priority_fee_per_gas_too_low = 11; CallGasLimitTooLow call_gas_limit_too_low = 12; + FactoryMustBeEmpty factory_must_be_empty = 13; } } @@ -659,6 +677,10 @@ message CallGasLimitTooLow { bytes min_gas_limit = 2; } +message FactoryMustBeEmpty{ + bytes factory_address = 1; +} + // SIMULATION VIOLATIONS message SimulationViolationError { oneof violation { diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 1b6cad5f4..386038b97 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -28,12 +28,12 @@ use super::protos::{ CallGasLimitEfficiencyTooLow, CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, EntryPointRevert, ExistingSenderWithInitCode, FactoryCalledCreate2Twice, - FactoryIsNotContract, InvalidAccountSignature, InvalidPaymasterSignature, InvalidSignature, - InvalidStorageAccess, InvalidTimeRange, MaxFeePerGasTooLow, MaxOperationsReachedError, - MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, - NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OperationRevert, OutOfGas, - PanicRevert, PaymasterBalanceTooLow, PaymasterDepositTooLow, PaymasterIsNotContract, - PreOpGasLimitEfficiencyTooLow, PreVerificationGasTooLow, + FactoryIsNotContract, FactoryMustBeEmpty, InvalidAccountSignature, InvalidPaymasterSignature, + InvalidSignature, InvalidStorageAccess, InvalidTimeRange, MaxFeePerGasTooLow, + MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, + MultipleRolesViolation, NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, + OperationRevert, OutOfGas, PanicRevert, PaymasterBalanceTooLow, PaymasterDepositTooLow, + PaymasterIsNotContract, PreOpGasLimitEfficiencyTooLow, PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, TotalGasLimitTooHigh, @@ -364,6 +364,13 @@ impl From for ProtoPrecheckViolationError { }, )), }, + PrecheckViolation::FactoryMustBeEmpty(addr) => ProtoPrecheckViolationError { + violation: Some(precheck_violation_error::Violation::FactoryMustBeEmpty( + FactoryMustBeEmpty { + factory_address: addr.to_proto_bytes(), + }, + )), + }, } } } @@ -433,6 +440,9 @@ impl TryFrom for PrecheckViolation { from_bytes(&e.min_gas_limit)?, ) } + Some(precheck_violation_error::Violation::FactoryMustBeEmpty(e)) => { + PrecheckViolation::FactoryMustBeEmpty(from_bytes(&e.factory_address)?) + } None => { bail!("unknown proto mempool precheck violation") } diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index 02591e49b..6c1cd9a2f 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -15,6 +15,7 @@ use alloy_primitives::{Address, B256}; use anyhow::{anyhow, Context}; use rundler_task::grpc::protos::{from_bytes, ConversionError, ToProtoBytes}; use rundler_types::{ + authorization::Authorization, chain::ChainSpec, da::{ BedrockDAGasUOData as RundlerBedrockDAGasUOData, DAGasUOData as RundlerDAGasUOData, @@ -25,7 +26,8 @@ use rundler_types::{ Reputation as PoolReputation, ReputationStatus as PoolReputationStatus, StakeStatus as RundlerStakeStatus, }, - v0_6, v0_7, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, + v0_6::{self, ExtendedUserOperation}, + v0_7, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, StakeInfo as RundlerStakeInfo, UserOperationVariant, ValidTimeRange, }; @@ -46,6 +48,17 @@ impl From<&UserOperationVariant> for UserOperation { impl From<&v0_6::UserOperation> for UserOperation { fn from(op: &v0_6::UserOperation) -> Self { + let authorization_tuple = + op.authorization_tuple + .as_ref() + .map(|authorization| AuthorizationTuple { + chain_id: authorization.chain_id, + address: authorization.address.to_proto_bytes(), + nonce: authorization.nonce, + y_parity: authorization.y_parity.into(), + r: authorization.r.to_proto_bytes(), + s: authorization.s.to_proto_bytes(), + }); let op = UserOperationV06 { sender: op.sender.to_proto_bytes(), nonce: op.nonce.to_proto_bytes(), @@ -58,6 +71,7 @@ impl From<&v0_6::UserOperation> for UserOperation { max_priority_fee_per_gas: op.max_priority_fee_per_gas.to_proto_bytes(), paymaster_and_data: op.paymaster_and_data.to_proto_bytes(), signature: op.signature.to_proto_bytes(), + authorization_tuple, }; UserOperation { uo: Some(user_operation::Uo::V06(op)), @@ -69,11 +83,28 @@ pub trait TryUoFromProto: Sized { fn try_uo_from_proto(value: T, chain_spec: &ChainSpec) -> Result; } +impl From for Authorization { + fn from(value: AuthorizationTuple) -> Self { + Authorization { + chain_id: value.chain_id, + address: from_bytes(&value.address).unwrap_or_default(), + nonce: value.nonce, + y_parity: value.y_parity as u8, + r: from_bytes(&value.r).unwrap_or_default(), + s: from_bytes(&value.s).unwrap_or_default(), + } + } +} impl TryUoFromProto for v0_6::UserOperation { fn try_uo_from_proto( op: UserOperationV06, chain_spec: &ChainSpec, ) -> Result { + let authorization_tuple = op + .authorization_tuple + .as_ref() + .map(|authorization| Authorization::from(authorization.clone())); + Ok(v0_6::UserOperationBuilder::new( chain_spec, v0_6::UserOperationRequiredFields { @@ -89,6 +120,9 @@ impl TryUoFromProto for v0_6::UserOperation { paymaster_and_data: op.paymaster_and_data.into(), signature: op.signature.into(), }, + ExtendedUserOperation { + authorization_tuple, + }, ) .build()) } @@ -114,6 +148,7 @@ impl From<&v0_7::UserOperation> for UserOperation { factory_data: op.factory_data.to_proto_bytes(), entry_point: op.entry_point.to_proto_bytes(), chain_id: op.chain_id, + authorization_tuple: None, }; UserOperation { uo: Some(user_operation::Uo::V07(op)), @@ -126,6 +161,11 @@ impl TryUoFromProto for v0_7::UserOperation { op: UserOperationV07, chain_spec: &ChainSpec, ) -> Result { + let authorization_tuple = op + .authorization_tuple + .as_ref() + .map(|authorization| Authorization::from(authorization.clone())); + let mut builder = v0_7::UserOperationBuilder::new( chain_spec, v0_7::UserOperationRequiredFields { @@ -150,6 +190,10 @@ impl TryUoFromProto for v0_7::UserOperation { ); } + if authorization_tuple.is_some() { + builder = builder.authorization_tuple(authorization_tuple); + } + if !op.factory.is_empty() { builder = builder.factory(from_bytes(&op.factory)?, op.factory_data.into()); } diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 2e73669d6..6f6319ee1 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -15,6 +15,7 @@ rundler-utils.workspace = true alloy-consensus.workspace = true alloy-contract.workspace = true +alloy-eips.workspace = true alloy-json-rpc.workspace = true alloy-primitives = { workspace = true, features = ["rand"] } alloy-provider = { workspace = true, features = ["debug-api"] } diff --git a/crates/provider/src/alloy/entry_point/v0_6.rs b/crates/provider/src/alloy/entry_point/v0_6.rs index 101fa566f..40a5fa98e 100644 --- a/crates/provider/src/alloy/entry_point/v0_6.rs +++ b/crates/provider/src/alloy/entry_point/v0_6.rs @@ -12,8 +12,9 @@ // If not, see https://www.gnu.org/licenses/. use alloy_contract::Error as ContractError; +use alloy_eips::eip7702::SignedAuthorization; use alloy_primitives::{Address, Bytes, U256}; -use alloy_provider::Provider as AlloyProvider; +use alloy_provider::{network::TransactionBuilder7702, Provider as AlloyProvider}; use alloy_rpc_types_eth::{state::StateOverride, BlockId, TransactionRequest}; use alloy_sol_types::{ContractError as SolContractError, SolCall, SolError, SolInterface}; use alloy_transport::{Transport, TransportError}; @@ -34,6 +35,7 @@ use rundler_types::{ v0_6::UserOperation, GasFees, UserOperation as _, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; +use rundler_utils::authorization_utils; use crate::{ AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasProvider, @@ -323,13 +325,16 @@ where block: BlockHashOrNumber, gas_price: u128, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { - let data = self + let au = user_op.authorization_tuple(); + let mut txn_request = self .i_entry_point .handleOps(vec![user_op.into()], Address::random()) - .into_transaction_request() - .input - .into_input() - .unwrap(); + .into_transaction_request(); + if let Some(authorization) = au { + txn_request = txn_request.with_authorization_list(vec![authorization.into()]); + } + + let data = txn_request.input.into_input().unwrap(); let bundle_data = super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); @@ -436,13 +441,21 @@ where target: Address, target_call_data: Bytes, block_id: BlockId, - state_override: StateOverride, + mut state_override: StateOverride, ) -> ProviderResult> { let da_gas: u64 = op .pre_verification_da_gas_limit(&self.chain_spec, Some(1)) .try_into() .unwrap_or(u64::MAX); + if let Some(authorization) = &op.authorization_tuple { + authorization_utils::apply_7702_overrides( + &mut state_override, + op.sender(), + authorization.address, + ); + } + let contract_error = self .i_entry_point .simulateHandleOp(op.into(), target, target_call_data) @@ -453,7 +466,6 @@ where .await .err() .context("simulateHandleOp succeeded, but should always revert")?; - match contract_error { ContractError::TransportError(TransportError::ErrorResp(resp)) => { match resp.as_revert_data() { @@ -519,25 +531,41 @@ fn get_handle_ops_call, T: Transport + Clone>( beneficiary: Address, gas: u64, ) -> TransactionRequest { + let mut authorization_list: Vec = vec![]; let mut ops_per_aggregator: Vec = ops_per_aggregator .into_iter() .map(|uoa| UserOpsPerAggregatorV0_6 { - userOps: uoa.user_ops.into_iter().map(Into::into).collect(), + userOps: uoa + .user_ops + .into_iter() + .map(|op| { + if let Some(authorization) = &op.authorization_tuple { + authorization_list.push(SignedAuthorization::from(authorization.clone())); + } + op.into() + }) + .collect(), aggregator: uoa.aggregator, signature: uoa.signature, }) .collect(); + + let mut txn_request: TransactionRequest; if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::ZERO { - entry_point + txn_request = entry_point .handleOps(ops_per_aggregator.swap_remove(0).userOps, beneficiary) .gas(gas) - .into_transaction_request() + .into_transaction_request(); } else { - entry_point + txn_request = entry_point .handleAggregatedOps(ops_per_aggregator, beneficiary) .gas(gas) - .into_transaction_request() + .into_transaction_request(); + } + if !authorization_list.is_empty() { + txn_request = txn_request.with_authorization_list(authorization_list); } + txn_request } impl TryFrom for ExecutionResult { diff --git a/crates/provider/src/alloy/entry_point/v0_7.rs b/crates/provider/src/alloy/entry_point/v0_7.rs index a72efd47c..4fb339739 100644 --- a/crates/provider/src/alloy/entry_point/v0_7.rs +++ b/crates/provider/src/alloy/entry_point/v0_7.rs @@ -12,9 +12,10 @@ // If not, see https://www.gnu.org/licenses/. use alloy_contract::Error as ContractError; +use alloy_eips::eip7702::SignedAuthorization; use alloy_json_rpc::ErrorPayload; use alloy_primitives::{Address, Bytes, U256}; -use alloy_provider::Provider as AlloyProvider; +use alloy_provider::{network::TransactionBuilder7702, Provider as AlloyProvider}; use alloy_rpc_types_eth::{ state::{AccountOverride, StateOverride}, BlockId, TransactionRequest, @@ -36,18 +37,19 @@ use rundler_contracts::v0_7::{ ENTRY_POINT_SIMULATIONS_V0_7_DEPLOYED_BYTECODE, }; use rundler_types::{ + authorization::Authorization, chain::ChainSpec, da::{DAGasBlockData, DAGasUOData}, v0_7::UserOperation, GasFees, UserOperation as _, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; +use rundler_utils::authorization_utils; use crate::{ AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider as EntryPointProviderTrait, EvmCall, ExecutionResult, HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, }; - /// Entry point provider for v0.7 #[derive(Clone)] pub struct EntryPointProvider { @@ -311,14 +313,17 @@ where block: BlockHashOrNumber, gas_price: u128, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { - let data = self + let au = user_op.authorization_tuple(); + + let mut txn_req = self .i_entry_point .handleOps(vec![user_op.pack()], Address::random()) - .into_transaction_request() - .input - .into_input() - .unwrap(); + .into_transaction_request(); + if let Some(authorization_tuple) = au { + txn_req = txn_req.with_authorization_list(vec![authorization_tuple.into()]); + } + let data = txn_req.input.into_input().unwrap(); let bundle_data = super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); @@ -350,6 +355,12 @@ where let mut override_ep = StateOverride::default(); add_simulations_override(&mut override_ep, addr); + add_authorization_tuple( + user_op.sender(), + &user_op.authorization_tuple, + &mut override_ep, + ); + let ep_simulations = IEntryPointSimulationsInstance::new(addr, self.i_entry_point.provider()); @@ -422,6 +433,9 @@ where .unwrap_or(u64::MAX); add_simulations_override(&mut state_override, *self.i_entry_point.address()); + + add_authorization_tuple(op.sender(), &op.authorization_tuple, &mut state_override); + let ep_simulations = IEntryPointSimulations::new( *self.i_entry_point.address(), self.i_entry_point.provider(), @@ -483,25 +497,40 @@ fn get_handle_ops_call, T: Transport + Clone>( beneficiary: Address, gas: u64, ) -> TransactionRequest { + let mut authorization_list: Vec = vec![]; let mut ops_per_aggregator: Vec = ops_per_aggregator .into_iter() .map(|uoa| UserOpsPerAggregatorV0_7 { - userOps: uoa.user_ops.into_iter().map(|op| op.pack()).collect(), + userOps: uoa + .user_ops + .into_iter() + .map(|op| { + if let Some(authorization) = &op.authorization_tuple { + authorization_list.push(SignedAuthorization::from(authorization.clone())); + } + op.pack() + }) + .collect(), aggregator: uoa.aggregator, signature: uoa.signature, }) .collect(); + let mut txn_request: TransactionRequest; if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::ZERO { - entry_point + txn_request = entry_point .handleOps(ops_per_aggregator.swap_remove(0).userOps, beneficiary) .gas(gas) - .into_transaction_request() + .into_transaction_request(); } else { - entry_point + txn_request = entry_point .handleAggregatedOps(ops_per_aggregator, beneficiary) .gas(gas) - .into_transaction_request() + .into_transaction_request(); + } + if !authorization_list.is_empty() { + txn_request = txn_request.with_authorization_list(authorization_list); } + txn_request } fn decode_validation_revert_payload(err: ErrorPayload) -> ValidationRevert { @@ -566,3 +595,13 @@ impl TryFrom for ExecutionResult { }) } } + +fn add_authorization_tuple( + sender: Address, + authorization: &Option, + state_override: &mut StateOverride, +) { + if let Some(authorization) = &authorization { + authorization_utils::apply_7702_overrides(state_override, sender, authorization.address); + } +} diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs index 5dafd4010..52229ab4c 100644 --- a/crates/rpc/src/eth/events/v0_6.rs +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -19,7 +19,7 @@ use rundler_contracts::v0_6::IEntryPoint::{ use rundler_provider::{Log, TransactionReceipt}; use rundler_types::{ chain::ChainSpec, - v0_6::{UserOperation, UserOperationBuilder}, + v0_6::{ExtendedUserOperation, UserOperation, UserOperationBuilder}, }; use super::common::{EntryPointEvents, UserOperationEventProviderImpl}; @@ -87,9 +87,15 @@ impl EntryPointEvents for EntryPointFiltersV0_6 { .ops .into_iter() .filter_map(|op| { - UserOperationBuilder::from_contract(chain_spec, op) - .ok() - .map(|b| b.build()) + UserOperationBuilder::from_contract( + chain_spec, + op, + ExtendedUserOperation { + authorization_tuple: None, + }, + ) + .ok() + .map(|b| b.build()) }) .collect(), IEntryPointCalls::handleAggregatedOps(handle_aggregated_ops_call) => { @@ -98,9 +104,15 @@ impl EntryPointEvents for EntryPointFiltersV0_6 { .into_iter() .flat_map(|ops| { ops.userOps.into_iter().filter_map(|op| { - UserOperationBuilder::from_contract(chain_spec, op) - .ok() - .map(|b| b.build()) + UserOperationBuilder::from_contract( + chain_spec, + op, + ExtendedUserOperation { + authorization_tuple: None, + }, + ) + .ok() + .map(|b| b.build()) }) }) .collect() diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs index 049bbf830..99f6d22fc 100644 --- a/crates/rpc/src/types/v0_6.rs +++ b/crates/rpc/src/types/v0_6.rs @@ -13,9 +13,11 @@ use alloy_primitives::{Address, Bytes, U128, U256}; use rundler_types::{ + authorization::Authorization, chain::ChainSpec, v0_6::{ - UserOperation, UserOperationBuilder, UserOperationOptionalGas, UserOperationRequiredFields, + ExtendedUserOperation, UserOperation, UserOperationBuilder, UserOperationOptionalGas, + UserOperationRequiredFields, }, GasEstimate, }; @@ -38,6 +40,7 @@ pub(crate) struct RpcUserOperation { max_priority_fee_per_gas: U128, paymaster_and_data: Bytes, signature: Bytes, + authorization_tuple: Option, } impl From for RpcUserOperation { @@ -54,6 +57,7 @@ impl From for RpcUserOperation { max_priority_fee_per_gas: U128::from(op.max_priority_fee_per_gas), paymaster_and_data: op.paymaster_and_data, signature: op.signature, + authorization_tuple: op.authorization_tuple, } } } @@ -75,6 +79,9 @@ impl FromRpc for UserOperation { paymaster_and_data: def.paymaster_and_data, signature: def.signature, }, + ExtendedUserOperation { + authorization_tuple: def.authorization_tuple, + }, ) .build() } @@ -94,6 +101,7 @@ pub(crate) struct RpcUserOperationOptionalGas { max_priority_fee_per_gas: Option, paymaster_and_data: Bytes, signature: Bytes, + authorization_contract: Option
, } impl From for UserOperationOptionalGas { @@ -110,6 +118,7 @@ impl From for UserOperationOptionalGas { max_priority_fee_per_gas: def.max_priority_fee_per_gas.map(|x| x.to()), paymaster_and_data: def.paymaster_and_data, signature: def.signature, + authorization_contract: def.authorization_contract, } } } diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs index bf2270e84..fab0dde71 100644 --- a/crates/rpc/src/types/v0_7.rs +++ b/crates/rpc/src/types/v0_7.rs @@ -13,6 +13,7 @@ use alloy_primitives::{Address, Bytes, B256, U128, U256}; use rundler_types::{ + authorization::Authorization, chain::ChainSpec, v0_7::{ UserOperation, UserOperationBuilder, UserOperationOptionalGas, UserOperationRequiredFields, @@ -48,6 +49,8 @@ pub(crate) struct RpcUserOperation { #[serde(skip_serializing_if = "Option::is_none")] paymaster_data: Option, signature: Bytes, + #[serde(skip_serializing_if = "Option::is_none")] + authorization_tuple: Option, } impl From for RpcUserOperation { @@ -85,6 +88,7 @@ impl From for RpcUserOperation { paymaster_post_op_gas_limit: paymaster_post_op_gas_limit.map(|x| U128::from(x)), paymaster_data, signature: op.signature, + authorization_tuple: op.authorization_tuple, } } } @@ -120,7 +124,9 @@ impl FromRpc for UserOperation { if def.factory.is_some() { builder = builder.factory(def.factory.unwrap(), def.factory_data.unwrap_or_default()); } - + if def.authorization_tuple.is_some() { + builder = builder.authorization_tuple(def.authorization_tuple); + } builder.build() } } @@ -154,6 +160,7 @@ pub(crate) struct RpcUserOperationOptionalGas { paymaster_post_op_gas_limit: Option, paymaster_data: Option, signature: Bytes, + authorization_contract: Option
, } impl From for UserOperationOptionalGas { @@ -174,6 +181,7 @@ impl From for UserOperationOptionalGas { paymaster_post_op_gas_limit: def.paymaster_post_op_gas_limit.map(|x| x.to()), paymaster_data: def.paymaster_data.unwrap_or_default(), signature: def.signature, + authorization_contract: def.authorization_contract, } } } diff --git a/crates/sim/Cargo.toml b/crates/sim/Cargo.toml index 7fb4ac4d4..82ed466be 100644 --- a/crates/sim/Cargo.toml +++ b/crates/sim/Cargo.toml @@ -13,6 +13,7 @@ rundler-provider.workspace = true rundler-types.workspace = true rundler-utils.workspace = true +alloy-eips.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true diff --git a/crates/sim/src/estimation/estimate_call_gas.rs b/crates/sim/src/estimation/estimate_call_gas.rs index 6f968e81d..fcfff1627 100644 --- a/crates/sim/src/estimation/estimate_call_gas.rs +++ b/crates/sim/src/estimation/estimate_call_gas.rs @@ -8,10 +8,10 @@ use rundler_contracts::{ }; use rundler_provider::{EntryPoint, SimulationProvider, StateOverride}; use rundler_types::UserOperation; +use rundler_utils::authorization_utils; use super::Settings; use crate::GasEstimationError; - /// Gas estimates will be rounded up to the next multiple of this. Increasing /// this value reduces the number of rounds of `eth_call` needed in binary /// search, e.g. a value of 1024 means ten fewer `eth_call`s needed for each of @@ -107,6 +107,14 @@ where let callless_op = self.specialization.get_op_with_no_call_gas(op.clone()); + if let Some(authorization_tuple) = op.authorization_tuple().clone() { + let authorization_contract = authorization_tuple.address; + authorization_utils::apply_7702_overrides( + &mut state_override, + op.sender(), + authorization_contract, + ); + } let mut min_gas = 0; let mut max_gas = self.settings.max_call_gas; let mut is_continuation = false; @@ -225,7 +233,6 @@ where let result = TestCallGasResult::abi_decode(&target_revert_data, false) .context("should decode revert data as TestCallGasResult")?; - if result.success { Ok(()) } else { diff --git a/crates/sim/src/estimation/estimate_verification_gas.rs b/crates/sim/src/estimation/estimate_verification_gas.rs index 933d6e274..157116575 100644 --- a/crates/sim/src/estimation/estimate_verification_gas.rs +++ b/crates/sim/src/estimation/estimate_verification_gas.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Context}; use async_trait::async_trait; use rundler_provider::{EntryPoint, EvmProvider, SimulationProvider, StateOverride}; use rundler_types::{chain::ChainSpec, UserOperation}; +use rundler_utils::authorization_utils; use super::Settings; use crate::GasEstimationError; @@ -73,8 +74,16 @@ where max_guess: u128, get_op_with_limit: F, ) -> Result { + let mut local_state_override = state_override.clone(); let timer = std::time::Instant::now(); let paymaster_gas_fee = self.settings.verification_estimation_gas_fee; + if let Some(au) = &op.authorization_tuple() { + authorization_utils::apply_7702_overrides( + &mut local_state_override, + op.sender(), + au.address, + ); + } // Fee logic for gas estimation: // @@ -101,7 +110,8 @@ where let initial_op = get_op(max_guess); let call = self .entry_point - .get_simulate_handle_op_call(initial_op, state_override.clone()); + .get_simulate_handle_op_call(initial_op, local_state_override.clone()); + let gas_used = self .provider .get_gas_used(call) @@ -127,7 +137,7 @@ where Address::ZERO, Bytes::new(), block_hash.into(), - state_override, + state_override.clone(), ) .await? .err(); @@ -162,7 +172,7 @@ where > (1.0 + GAS_ESTIMATION_ERROR_MARGIN) { num_rounds += 1; - if run_attempt_returning_error(guess, state_override.clone()).await? { + if run_attempt_returning_error(guess, local_state_override.clone()).await? { min_success_gas = guess; } else { max_failure_gas = guess; diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 4e8398477..c4ac8277f 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -31,7 +31,7 @@ use rundler_types::{ v0_6::{UserOperation, UserOperationBuilder, UserOperationOptionalGas}, GasEstimate, UserOperation as _, }; -use rundler_utils::math; +use rundler_utils::{authorization_utils, math}; use tokio::join; use super::{ @@ -71,6 +71,10 @@ where state_override: StateOverride, ) -> Result { self.check_provided_limits(&op)?; + let mut local_override = state_override.clone(); + if let Some(au) = &op.authorization_contract { + authorization_utils::apply_7702_overrides(&mut local_override, op.sender, *au); + } let (block_hash, _) = self .provider @@ -91,8 +95,8 @@ where .build(); let verification_future = - self.estimate_verification_gas(&op, &full_op, block_hash, state_override.clone()); - let call_future = self.estimate_call_gas(&op, full_op.clone(), block_hash, state_override); + self.estimate_verification_gas(&op, &full_op, block_hash, local_override.clone()); + let call_future = self.estimate_call_gas(&op, full_op.clone(), block_hash, local_override); // Not try_join! because then the output is nondeterministic if both // verification and call estimation fail. @@ -437,7 +441,10 @@ mod tests { }; use rundler_types::{ da::DAGasOracleType, - v0_6::{UserOperation, UserOperationOptionalGas, UserOperationRequiredFields}, + v0_6::{ + ExtendedUserOperation, UserOperation, UserOperationOptionalGas, + UserOperationRequiredFields, + }, GasFees, UserOperation as UserOperationTrait, ValidationRevert, }; use CallGasEstimationProxy::{ @@ -565,6 +572,7 @@ mod tests { max_priority_fee_per_gas: None, paymaster_and_data: Bytes::new(), signature: Bytes::new(), + authorization_contract: None, } } @@ -584,6 +592,9 @@ mod tests { paymaster_and_data: Bytes::new(), signature: Bytes::new(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build() } diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs index 37803707a..d9d103676 100644 --- a/crates/sim/src/estimation/v0_7.rs +++ b/crates/sim/src/estimation/v0_7.rs @@ -95,6 +95,7 @@ where let verification_gas_future = self.estimate_verification_gas(&op, &full_op, block_hash, state_override.clone()); + let paymaster_verification_gas_future = self.estimate_paymaster_verification_gas( &op, &full_op, @@ -613,6 +614,7 @@ mod tests { factory: None, factory_data: Bytes::new(), + authorization_contract: None, } } @@ -845,6 +847,7 @@ mod tests { factory: None, factory_data: Bytes::new(), + authorization_contract: None, }; let estimation = estimator diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 538501dbf..b9bf601d1 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -229,6 +229,13 @@ where .. } = async_data; let mut violations = ArrayVec::new(); + + if op.authorization_tuple().is_some() { + if op.factory().is_some() { + violations.push(PrecheckViolation::FactoryMustBeEmpty(op.factory().unwrap())); + } + return violations; + } if op.factory().is_none() { if !sender_exists { violations.push(PrecheckViolation::SenderIsNotContractAndNoInitCode( @@ -456,7 +463,9 @@ mod tests { use gas::MockFeeEstimator; use rundler_provider::{MockEntryPointV0_6, MockEvmProvider}; use rundler_types::{ - v0_6::{UserOperation, UserOperationBuilder, UserOperationRequiredFields}, + v0_6::{ + ExtendedUserOperation, UserOperation, UserOperationBuilder, UserOperationRequiredFields, + }, UserOperation as _, }; @@ -514,6 +523,9 @@ mod tests { paymaster_and_data: Bytes::default(), signature: Bytes::default(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); @@ -562,6 +574,9 @@ mod tests { paymaster_and_data: Bytes::default(), signature: Bytes::default(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); @@ -611,6 +626,9 @@ mod tests { ), signature: Bytes::default(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); diff --git a/crates/sim/src/simulation/simulator.rs b/crates/sim/src/simulation/simulator.rs index 5c3a415a8..e4bb0d172 100644 --- a/crates/sim/src/simulation/simulator.rs +++ b/crates/sim/src/simulation/simulator.rs @@ -671,7 +671,9 @@ mod tests { }; use rundler_types::{ chain::ChainSpec, - v0_6::{UserOperation, UserOperationBuilder, UserOperationRequiredFields}, + v0_6::{ + ExtendedUserOperation, UserOperation, UserOperationBuilder, UserOperationRequiredFields, + }, Opcode, StakeInfo, }; @@ -892,7 +894,11 @@ mod tests { max_priority_fee_per_gas: 105000000, paymaster_and_data: Bytes::default(), signature: bytes!("98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c"), - }).build(); + }, + ExtendedUserOperation{ + authorization_tuple: None, + }, + ).build(); let simulator = create_simulator(provider, entry_point, context); let res = simulator diff --git a/crates/sim/src/simulation/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs index 8209f0969..92e6656dc 100644 --- a/crates/sim/src/simulation/unsafe_sim.rs +++ b/crates/sim/src/simulation/unsafe_sim.rs @@ -118,10 +118,12 @@ where None }; - if validation_result.return_info.account_sig_failed - || validation_result.return_info.paymaster_sig_failed - { - violations.push(SimulationViolation::InvalidSignature); + if validation_result.return_info.account_sig_failed { + violations.push(SimulationViolation::InvalidAccountSignature); + } + + if validation_result.return_info.paymaster_sig_failed { + violations.push(SimulationViolation::InvalidPaymasterSignature); } if !violations.is_empty() { diff --git a/crates/sim/src/simulation/v0_6/context.rs b/crates/sim/src/simulation/v0_6/context.rs index a68e12003..6ba0de03b 100644 --- a/crates/sim/src/simulation/v0_6/context.rs +++ b/crates/sim/src/simulation/v0_6/context.rs @@ -202,7 +202,9 @@ mod tests { use rundler_contracts::v0_6::IEntryPoint::FailedOp; use rundler_types::{ chain::ChainSpec, - v0_6::{UserOperation, UserOperationBuilder, UserOperationRequiredFields}, + v0_6::{ + ExtendedUserOperation, UserOperation, UserOperationBuilder, UserOperationRequiredFields, + }, Opcode, }; use sim_context::ContractInfo; @@ -337,7 +339,11 @@ mod tests { max_priority_fee_per_gas: 105000000, paymaster_and_data: Bytes::default(), signature: bytes!("98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c"), - }).build(); + }, + ExtendedUserOperation { + authorization_tuple: None, + }, + ).build(); let context = ValidationContextProvider { simulate_validation_tracer: tracer, diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index c86172216..69fdb96e2 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -11,6 +11,7 @@ publish = false rundler-contracts.workspace = true rundler-utils.workspace = true +alloy-eips.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true diff --git a/crates/types/src/authorization.rs b/crates/types/src/authorization.rs new file mode 100644 index 000000000..d04f7766c --- /dev/null +++ b/crates/types/src/authorization.rs @@ -0,0 +1,47 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. +//! 7702 authorization list suport. + +use alloy_eips::eip7702::SignedAuthorization; +use alloy_primitives::{Address, U256}; +use serde::{Deserialize, Serialize}; + +/// authorization tuple for 7702 txn support +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct Authorization { + /// The chain ID of the authorization. + pub chain_id: u64, + /// The address of the authorization. + pub address: Address, + /// The nonce for the authorization. + pub nonce: u64, + /// signed authorizzation tuple. + pub y_parity: u8, + /// signed authorizzation tuple. + pub r: U256, + /// signed authorizzation tuple. + pub s: U256, +} + +impl From for alloy_eips::eip7702::SignedAuthorization { + fn from(value: Authorization) -> Self { + let authorization = alloy_eips::eip7702::Authorization { + chain_id: value.chain_id, + address: value.address, + nonce: value.nonce, + }; + + SignedAuthorization::new_unchecked(authorization, value.y_parity, value.r, value.s) + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index dccb061b0..87b65da2b 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -53,3 +53,5 @@ pub use validation_results::{ }; pub mod task; + +pub mod authorization; diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs index cdef5fcb4..d070ca923 100644 --- a/crates/types/src/pool/error.rs +++ b/crates/types/src/pool/error.rs @@ -145,6 +145,9 @@ pub enum PrecheckViolation { /// The call gas limit is too low to account for any possible call. #[display("callGasLimit is {0} but must be at least {1}")] CallGasLimitTooLow(u128, u128), + /// The Uo contains both factory and authorization tuple. + #[display("Factory must be empty when authorization contract is set")] + FactoryMustBeEmpty(Address), } /// All possible simulation violations diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 9cc7b3411..36b587a93 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -22,7 +22,7 @@ pub mod v0_6; /// User Operation types for Entry Point v0.7 pub mod v0_7; -use crate::{chain::ChainSpec, Entity}; +use crate::{authorization::Authorization, chain::ChainSpec, Entity}; /// A user op must be valid for at least this long into the future to be included. pub const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); @@ -110,6 +110,9 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { .min(base_fee + self.max_priority_fee_per_gas()) } + /// Return the authorization list of the UO. empty if it is not 7702 txn. + fn authorization_tuple(&self) -> Option; + /* * Enhanced functions */ @@ -277,9 +280,16 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { bundle_size: usize, da_gas: u128, ) -> u128 { + let authorization_gas = if self.authorization_tuple().is_some() { + alloy_eips::eip7702::constants::PER_AUTH_BASE_COST + + alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST + } else { + 0 + }; self.static_pre_verification_gas(chain_spec) .saturating_add(bundle_per_uo_shared_gas(chain_spec, bundle_size)) .saturating_add(da_gas) + .saturating_add(authorization_gas as u128) } /// Returns true if the user operation has enough pre-verification gas to be included in a bundle @@ -476,6 +486,13 @@ impl UserOperation for UserOperationVariant { UserOperationVariant::V0_7(op) => op.abi_encoded_size(), } } + + fn authorization_tuple(&self) -> Option { + match self { + UserOperationVariant::V0_6(op) => op.authorization_tuple(), + UserOperationVariant::V0_7(op) => op.authorization_tuple(), + } + } } impl UserOperationVariant { diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index bd3296900..ddf4c9397 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -22,6 +22,7 @@ use super::{ UserOperationVariant, }; use crate::{ + authorization::Authorization, chain::ChainSpec, entity::{Entity, EntityType}, EntryPointVersion, @@ -78,6 +79,9 @@ pub struct UserOperation { /// Signature pub signature: Bytes, + /// eip 7702 - list of authorities. + pub authorization_tuple: Option, + /// Cached calldata gas cost pub calldata_gas_cost: u128, } @@ -100,6 +104,9 @@ impl Default for UserOperation { max_fee_per_gas: 0, max_priority_fee_per_gas: 0, }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build() } @@ -272,6 +279,10 @@ impl UserOperationTrait for UserOperation { + super::byte_array_abi_len(&self.paymaster_and_data) + super::byte_array_abi_len(&self.signature) } + + fn authorization_tuple(&self) -> Option { + self.authorization_tuple.clone() + } } impl From for ContractUserOperation { @@ -381,6 +392,9 @@ pub struct UserOperationOptionalGas { pub paymaster_and_data: Bytes, /// Signature (required, dummy value for gas estimation) pub signature: Bytes, + + /// eip 7702 - tuple of authority. + pub authorization_contract: Option
, } impl UserOperationOptionalGas { @@ -405,6 +419,9 @@ impl UserOperationOptionalGas { max_priority_fee_per_gas: max_8, max_fee_per_gas: max_8, }, + ExtendedUserOperation { + authorization_tuple: None, + }, ); builder.build() @@ -435,6 +452,9 @@ impl UserOperationOptionalGas { max_fee_per_gas: u128::from_le_bytes(random_bytes_array::<16, 8>()), // 2^64 max max_priority_fee_per_gas: u128::from_le_bytes(random_bytes_array::<16, 8>()), // 2^64 max }, + ExtendedUserOperation { + authorization_tuple: None, + }, ); builder.build() @@ -455,6 +475,10 @@ impl UserOperationOptionalGas { super::default_if_none_or_equal(self.verification_gas_limit, max_verification_gas, 0); let pvg = super::default_if_none_or_equal(self.pre_verification_gas, max_call_gas, 0); + let authorization_tuple = self.authorization_contract.map(|address| Authorization { + address, + ..Default::default() + }); let required = UserOperationRequiredFields { sender: self.sender, nonce: self.nonce, @@ -469,8 +493,11 @@ impl UserOperationOptionalGas { max_fee_per_gas: self.max_fee_per_gas.unwrap_or_default(), max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or_default(), }; + let extended = ExtendedUserOperation { + authorization_tuple, + }; - UserOperationBuilder::new(chain_spec, required) + UserOperationBuilder::new(chain_spec, required, extended) } /// Abi encoded size of the user operation (with its dummy fields) @@ -506,6 +533,16 @@ pub struct UserOperationBuilder<'a> { // optional fields contract_uo: Option, + + // extended fields + extended: ExtendedUserOperation, +} + +/// Extended User Option fields. Fields introduced by other EIPs, not related +/// to UO directly but included for implementation's sake. +pub struct ExtendedUserOperation { + /// EIP 7702: authorization tuples. + pub authorization_tuple: Option, } /// User operation required fields @@ -556,11 +593,16 @@ impl TryFrom for UserOperationRequiredFields { impl<'a> UserOperationBuilder<'a> { /// Create a new builder - pub fn new(chain_spec: &'a ChainSpec, required: UserOperationRequiredFields) -> Self { + pub fn new( + chain_spec: &'a ChainSpec, + required: UserOperationRequiredFields, + extended: ExtendedUserOperation, + ) -> Self { Self { chain_spec, required, contract_uo: None, + extended, } } @@ -568,17 +610,21 @@ impl<'a> UserOperationBuilder<'a> { pub fn from_contract( chain_spec: &'a ChainSpec, contract_uo: ContractUserOperation, + extended: ExtendedUserOperation, ) -> Result> { let required = UserOperationRequiredFields::try_from(contract_uo.clone())?; Ok(Self { chain_spec, required, contract_uo: Some(contract_uo), + extended, }) } /// Create a builder from a user operation pub fn from_uo(uo: UserOperation, chain_spec: &'a ChainSpec) -> Self { + let authorization_tuple = uo.authorization_tuple; + Self { chain_spec, required: UserOperationRequiredFields { @@ -595,6 +641,9 @@ impl<'a> UserOperationBuilder<'a> { signature: uo.signature, }, contract_uo: None, + extended: ExtendedUserOperation { + authorization_tuple, + }, } } @@ -628,6 +677,11 @@ impl<'a> UserOperationBuilder<'a> { self } + /// Sets the authorization tuple. + pub fn authorization_tuple(mut self, authorization: Option) -> Self { + self.extended.authorization_tuple = authorization; + self + } /// Build the user operation pub fn build(self) -> UserOperation { let mut uo = UserOperation { @@ -642,6 +696,7 @@ impl<'a> UserOperationBuilder<'a> { max_priority_fee_per_gas: self.required.max_priority_fee_per_gas, paymaster_and_data: self.required.paymaster_and_data, signature: self.required.signature, + authorization_tuple: self.extended.authorization_tuple, calldata_gas_cost: 0, }; @@ -703,6 +758,9 @@ mod tests { paymaster_and_data: Bytes::default(), signature: Bytes::default(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); let entry_point = address!("66a15edcc3b50a663e72f1457ffd49b9ae284ddc"); @@ -760,6 +818,9 @@ mod tests { ), signature: bytes!("da0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a"), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); let entry_point = address!("66a15edcc3b50a663e72f1457ffd49b9ae284ddc"); @@ -809,6 +870,9 @@ mod tests { ), signature: bytes!("da0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a"), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); @@ -834,6 +898,9 @@ mod tests { paymaster_and_data: Bytes::default(), signature: Bytes::default(), }, + ExtendedUserOperation { + authorization_tuple: None, + }, ) .build(); let size = operation.abi_encoded_size(); @@ -863,6 +930,7 @@ mod tests { pre_verification_gas: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, + authorization_contract: None, } .max_fill(&ChainSpec::default()); diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index ca83c7456..e81e6b0b9 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -19,7 +19,7 @@ use super::{ random_bytes, random_bytes_array, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; -use crate::{chain::ChainSpec, Entity, EntryPointVersion}; +use crate::{authorization::Authorization, chain::ChainSpec, Entity, EntryPointVersion}; /// Gas overhead required by the entry point contract for the inner call pub const ENTRY_POINT_INNER_GAS_OVERHEAD: u128 = 10_000; @@ -82,6 +82,9 @@ pub struct UserOperation { pub paymaster_post_op_gas_limit: u128, /// Paymaster data pub paymaster_data: Bytes, + /// eip 7702 - tuple of authority. + pub authorization_tuple: Option, + /* * Cached fields, not part of the UO */ @@ -231,6 +234,10 @@ impl UserOperationTrait for UserOperation { + super::byte_array_abi_len(&self.packed.paymasterAndData) + super::byte_array_abi_len(&self.packed.signature) } + + fn authorization_tuple(&self) -> Option { + self.authorization_tuple.clone() + } } impl UserOperation { @@ -328,6 +335,8 @@ pub struct UserOperationOptionalGas { pub paymaster_post_op_gas_limit: Option, /// Paymaster data pub paymaster_data: Bytes, + /// 7702 authorization contract address. + pub authorization_contract: Option
, } impl UserOperationOptionalGas { @@ -360,6 +369,17 @@ impl UserOperationOptionalGas { vec![255_u8; self.paymaster_data.len()].into(), ); } + if self.authorization_contract.is_some() { + builder = builder.authorization_tuple(Some(Authorization { + address: self.authorization_contract.unwrap(), + chain_id: chain_spec.id, + // fake value for gas estimation. + nonce: 0, + y_parity: 0, + r: U256::from(0), + s: U256::from(0), + })); + } if self.factory.is_some() { builder = builder.factory( self.factory.unwrap(), @@ -460,6 +480,12 @@ impl UserOperationOptionalGas { self.paymaster_data, ); } + if let Some(contract) = self.authorization_contract { + builder = builder.authorization_tuple(Some(Authorization { + address: contract, + ..Default::default() + })); + } builder } @@ -510,6 +536,9 @@ pub struct UserOperationBuilder<'a> { paymaster_post_op_gas_limit: u128, paymaster_data: Bytes, packed_uo: Option, + + /// eip 7702 - tuple of authority. + authorization_tuple: Option, } /// Required fields for UserOperation v0.7 @@ -547,6 +576,7 @@ impl<'a> UserOperationBuilder<'a> { paymaster_post_op_gas_limit: 0, paymaster_data: Bytes::new(), packed_uo: None, + authorization_tuple: None, } } @@ -619,6 +649,7 @@ impl<'a> UserOperationBuilder<'a> { paymaster_post_op_gas_limit: uo.paymaster_post_op_gas_limit, paymaster_data: uo.paymaster_data, packed_uo: None, + authorization_tuple: uo.authorization_tuple, } } @@ -695,6 +726,12 @@ impl<'a> UserOperationBuilder<'a> { self } + /// Sets the authorization list + pub fn authorization_tuple(mut self, authorization_tuple: Option) -> Self { + self.authorization_tuple = authorization_tuple; + self + } + /// Builds the UserOperation pub fn build(self) -> UserOperation { let uo = UserOperation { @@ -712,6 +749,7 @@ impl<'a> UserOperationBuilder<'a> { paymaster_verification_gas_limit: self.paymaster_verification_gas_limit, paymaster_post_op_gas_limit: self.paymaster_post_op_gas_limit, paymaster_data: self.paymaster_data, + authorization_tuple: self.authorization_tuple, signature: self.required.signature, entry_point: self.chain_spec.entry_point_address_v0_7, chain_id: self.chain_spec.id, diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 5ec2b2125..2bc2caef2 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -9,6 +9,8 @@ publish = false [dependencies] alloy-primitives.workspace = true +alloy-rpc-types-eth.workspace = true + anyhow.workspace = true derive_more = "0.99.18" futures.workspace = true diff --git a/crates/utils/src/authorization_utils.rs b/crates/utils/src/authorization_utils.rs new file mode 100644 index 000000000..dae6a01de --- /dev/null +++ b/crates/utils/src/authorization_utils.rs @@ -0,0 +1,35 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! Utilities for 7702 authorization tuples + +use alloy_primitives::{fixed_bytes, Address, FixedBytes}; +use alloy_rpc_types_eth::state::{AccountOverride, StateOverride}; + +/// Apply a [7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) +/// overrides to `StateOverride`. +pub fn apply_7702_overrides( + state_override: &mut StateOverride, + sender: Address, + authorization_contract: Address, +) { + state_override.entry(sender).or_insert({ + let prefix: FixedBytes<3> = fixed_bytes!("ef0100"); + let code: FixedBytes<23> = prefix.concat_const(authorization_contract.into()); + + AccountOverride { + code: Some((code).into()), + ..Default::default() + } + }); +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 2bb709100..12677870c 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -20,6 +20,7 @@ //! Rundler utilities +pub mod authorization_utils; pub mod cache; pub mod emit; pub mod eth; diff --git a/test/spec-tests/v0_6/bundler-spec-tests b/test/spec-tests/v0_6/bundler-spec-tests index 939ae009a..bbae0cad2 160000 --- a/test/spec-tests/v0_6/bundler-spec-tests +++ b/test/spec-tests/v0_6/bundler-spec-tests @@ -1 +1 @@ -Subproject commit 939ae009a26820a43547071ded9f866a8704ede8 +Subproject commit bbae0cad201fc7c4c270012c597f28fbe8b037d8 diff --git a/test/spec-tests/v0_7/bundler-spec-tests b/test/spec-tests/v0_7/bundler-spec-tests index 996cbf9ac..b7ae47dfe 160000 --- a/test/spec-tests/v0_7/bundler-spec-tests +++ b/test/spec-tests/v0_7/bundler-spec-tests @@ -1 +1 @@ -Subproject commit 996cbf9ac1a5e766943511194fc4e90e3261f208 +Subproject commit b7ae47dfe670a6ee5596e3b3a10e69a548b2b324