@@ -1732,7 +1732,10 @@ impl<UO: UserOperation> ProposalContext<UO> {
17321732 }
17331733
17341734 fn get_bundle_gas_limit_inner ( & self , chain_spec : & ChainSpec , include_da_gas : bool ) -> u128 {
1735- let mut gas_limit = rundler_types:: bundle_shared_gas ( chain_spec) ;
1735+ let mut authorization_gas = 0_u128 ;
1736+ let mut standard_gas_limit = 0_u128 ;
1737+ let mut calldata_floor_gas_limit = 0_u128 ;
1738+ let mut da_gas_limit = 0_u128 ;
17361739
17371740 // Per aggregator fixed gas
17381741 for agg in self . groups_by_aggregator . keys ( ) {
@@ -1743,36 +1746,45 @@ impl<UO: UserOperation> ProposalContext<UO> {
17431746 // this should be checked prior to calling this function
17441747 panic ! ( "BUG: aggregator {agg:?} not found in chain spec" ) ;
17451748 } ;
1746- gas_limit += agg. costs ( ) . execution_fixed_gas ;
1749+ standard_gas_limit = standard_gas_limit . saturating_add ( agg. costs ( ) . execution_fixed_gas ) ;
17471750
17481751 // NOTE: this assumes that the DA cost for the aggregated signature is covered by the gas limits
17491752 // from the UOs (on chains that have DA gas in gas limit). This is enforced during fee check phase.
17501753 }
17511754
1752- // per UO gas, bundle_size == None to signal to exclude shared gas
1753- gas_limit += self
1754- . iter_ops_with_simulations ( )
1755- . map ( |sim_op| {
1756- if include_da_gas {
1757- sim_op. op . bundle_gas_limit ( chain_spec, None ) + sim_op. sponsored_da_gas
1758- } else {
1759- sim_op. op . bundle_computation_gas_limit ( chain_spec, None )
1760- }
1761- } )
1762- . sum :: < u128 > ( ) ;
1755+ for sim_op in self . iter_ops_with_simulations ( ) {
1756+ let op_authorization_gas = sim_op. op . authorization_gas_limit ( ) ;
1757+ authorization_gas = authorization_gas. saturating_add ( op_authorization_gas) ;
17631758
1764- let calldata_floor_gas_limit = self . bundle_overhead_bytes ( chain_spec)
1765- * chain_spec. calldata_floor_non_zero_byte_gas ( )
1766- + self
1767- . iter_ops_with_simulations ( )
1768- . map ( |sim_op| sim_op. op . calldata_floor_gas_limit ( ) )
1769- . sum :: < u128 > ( ) ;
1759+ // EIP-7623 calculations require this to be done without the EIP-7702 intrinsic gas. Its added later in the final calculation.
1760+ standard_gas_limit = standard_gas_limit. saturating_add (
1761+ sim_op. op . bundle_computation_gas_limit ( chain_spec, None ) - op_authorization_gas,
1762+ ) ;
1763+
1764+ calldata_floor_gas_limit =
1765+ calldata_floor_gas_limit. saturating_add ( sim_op. op . calldata_floor_gas_limit ( ) ) ;
17701766
1771- if calldata_floor_gas_limit > gas_limit {
1772- return calldata_floor_gas_limit;
1767+ if include_da_gas {
1768+ da_gas_limit = da_gas_limit. saturating_add (
1769+ sim_op. op . pre_verification_da_gas_limit ( chain_spec, None )
1770+ + sim_op. sponsored_da_gas ,
1771+ ) ;
1772+ }
17731773 }
17741774
1775- gas_limit
1775+ calldata_floor_gas_limit = calldata_floor_gas_limit. saturating_add (
1776+ self . bundle_overhead_bytes ( chain_spec)
1777+ . saturating_mul ( chain_spec. calldata_floor_non_zero_byte_gas ( ) ) ,
1778+ ) ;
1779+
1780+ // EIP-7623 maximum between standard and calldata floor
1781+ let execution_gas_limit = standard_gas_limit. max ( calldata_floor_gas_limit) ;
1782+
1783+ chain_spec
1784+ . transaction_intrinsic_gas ( )
1785+ . saturating_add ( authorization_gas)
1786+ . saturating_add ( da_gas_limit)
1787+ . saturating_add ( execution_gas_limit)
17761788 }
17771789
17781790 fn iter_ops_with_simulations ( & self ) -> impl Iterator < Item = & OpWithSimulation < UO > > + ' _ {
@@ -3084,6 +3096,148 @@ mod tests {
30843096 assert_eq ! ( gas_limit, expected_gas_limit) ;
30853097 }
30863098
3099+ #[ tokio:: test]
3100+ async fn test_bundle_gas_limit_calldata_floor_with_authorization ( ) {
3101+ // Enable EIP-7623 so the calldata floor is non-zero
3102+ let cs = ChainSpec {
3103+ eip7623_enabled : true ,
3104+ ..Default :: default ( )
3105+ } ;
3106+
3107+ // Create an op with an authorization tuple
3108+ let op = UserOperationBuilder :: new (
3109+ & cs,
3110+ UserOperationRequiredFields {
3111+ // Use small gas limits so the calldata floor wins over execution gas
3112+ pre_verification_gas : 1_000 ,
3113+ call_gas_limit : 1_000 ,
3114+ verification_gas_limit : 1_000 ,
3115+ // Large calldata to ensure the calldata floor exceeds execution gas
3116+ call_data : Bytes :: from ( vec ! [ 1u8 ; 4096 ] ) ,
3117+ ..Default :: default ( )
3118+ } ,
3119+ )
3120+ . authorization_tuple ( rundler_types:: authorization:: Eip7702Auth :: new_dummy (
3121+ cs. id ,
3122+ address ( 1 ) ,
3123+ ) )
3124+ . build ( ) ;
3125+
3126+ let auth_gas = op. authorization_gas_limit ( ) ;
3127+ assert ! ( auth_gas > 0 , "op must have authorization gas" ) ;
3128+
3129+ let mut groups_by_aggregator = LinkedHashMap :: new ( ) ;
3130+ groups_by_aggregator. insert (
3131+ Address :: ZERO ,
3132+ AggregatorGroup {
3133+ ops_with_simulations : vec ! [ OpWithSimulation {
3134+ op: op. clone( ) ,
3135+ simulation: SimulationResult {
3136+ requires_post_op: false ,
3137+ ..Default :: default ( )
3138+ } ,
3139+ sponsored_da_gas: 0 ,
3140+ } ] ,
3141+ signature : Default :: default ( ) ,
3142+ } ,
3143+ ) ;
3144+ let context = ProposalContext {
3145+ groups_by_aggregator,
3146+ rejected_ops : vec ! [ ] ,
3147+ entity_updates : BTreeMap :: new ( ) ,
3148+ bundle_expected_storage : BundleExpectedStorage :: default ( ) ,
3149+ } ;
3150+
3151+ let gas_limit = context. get_bundle_gas_limit ( & cs) ;
3152+
3153+ // Verify the calldata floor path is being taken by checking that
3154+ // the gas limit is higher than what execution gas alone would give
3155+ let execution_gas = op. bundle_gas_limit ( & cs, None ) + rundler_types:: bundle_shared_gas ( & cs) ;
3156+ let calldata_floor = context. bundle_overhead_bytes ( & cs)
3157+ * cs. calldata_floor_non_zero_byte_gas ( )
3158+ + op. calldata_floor_gas_limit ( ) ;
3159+ assert ! (
3160+ calldata_floor > execution_gas,
3161+ "calldata floor ({calldata_floor}) must exceed execution gas ({execution_gas}) for this test to be valid"
3162+ ) ;
3163+
3164+ // The gas limit must include transaction intrinsic and authorization gas
3165+ // even when calldata floor wins.
3166+ assert_eq ! (
3167+ gas_limit,
3168+ cs. transaction_intrinsic_gas( ) + calldata_floor + auth_gas
3169+ ) ;
3170+ }
3171+
3172+ #[ tokio:: test]
3173+ async fn test_bundle_gas_limit_authorization_delta_is_empty_account_cost ( ) {
3174+ let cs = ChainSpec :: default ( ) ;
3175+ let required = UserOperationRequiredFields {
3176+ pre_verification_gas : 100_000 ,
3177+ call_gas_limit : 100_000 ,
3178+ verification_gas_limit : 100_000 ,
3179+ ..Default :: default ( )
3180+ } ;
3181+
3182+ let op_without_authorization = UserOperationBuilder :: new ( & cs, required. clone ( ) ) . build ( ) ;
3183+ let op_with_authorization = UserOperationBuilder :: new ( & cs, required)
3184+ . authorization_tuple ( rundler_types:: authorization:: Eip7702Auth :: new_dummy (
3185+ cs. id ,
3186+ address ( 1 ) ,
3187+ ) )
3188+ . build ( ) ;
3189+
3190+ assert_eq ! ( op_without_authorization. authorization_gas_limit( ) , 0 ) ;
3191+ assert_eq ! ( op_with_authorization. authorization_gas_limit( ) , 25_000 ) ;
3192+
3193+ let mut groups_without_authorization = LinkedHashMap :: new ( ) ;
3194+ groups_without_authorization. insert (
3195+ Address :: ZERO ,
3196+ AggregatorGroup {
3197+ ops_with_simulations : vec ! [ OpWithSimulation {
3198+ op: op_without_authorization,
3199+ simulation: SimulationResult :: default ( ) ,
3200+ sponsored_da_gas: 0 ,
3201+ } ] ,
3202+ signature : Default :: default ( ) ,
3203+ } ,
3204+ ) ;
3205+ let context_without_authorization = ProposalContext {
3206+ groups_by_aggregator : groups_without_authorization,
3207+ rejected_ops : vec ! [ ] ,
3208+ entity_updates : BTreeMap :: new ( ) ,
3209+ bundle_expected_storage : BundleExpectedStorage :: default ( ) ,
3210+ } ;
3211+
3212+ let mut groups_with_authorization = LinkedHashMap :: new ( ) ;
3213+ groups_with_authorization. insert (
3214+ Address :: ZERO ,
3215+ AggregatorGroup {
3216+ ops_with_simulations : vec ! [ OpWithSimulation {
3217+ op: op_with_authorization,
3218+ simulation: SimulationResult :: default ( ) ,
3219+ sponsored_da_gas: 0 ,
3220+ } ] ,
3221+ signature : Default :: default ( ) ,
3222+ } ,
3223+ ) ;
3224+ let context_with_authorization = ProposalContext {
3225+ groups_by_aggregator : groups_with_authorization,
3226+ rejected_ops : vec ! [ ] ,
3227+ entity_updates : BTreeMap :: new ( ) ,
3228+ bundle_expected_storage : BundleExpectedStorage :: default ( ) ,
3229+ } ;
3230+
3231+ let gas_without_authorization = context_without_authorization. get_bundle_gas_limit ( & cs) ;
3232+ let gas_with_authorization = context_with_authorization. get_bundle_gas_limit ( & cs) ;
3233+
3234+ assert_eq ! (
3235+ gas_with_authorization - gas_without_authorization,
3236+ 25_000 ,
3237+ "authorization should add only EIP-7702 empty account intrinsic gas"
3238+ ) ;
3239+ }
3240+
30873241 #[ tokio:: test]
30883242 async fn test_post_op_revert ( ) {
30893243 let op1 = op_with_sender ( address ( 1 ) ) ;
0 commit comments