diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 9faa2eb3e..40598ddba 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -384,9 +384,19 @@ where required_op_fees: GasFees, ) -> Option { let op_hash = op.uo.hash(); + + let mut required_max_fee_per_gas = required_op_fees.max_fee_per_gas; + let mut required_max_priority_fee_per_gas = required_op_fees.max_priority_fee_per_gas; + + if let Some(pct) = op.perms.underpriced_bundle_pct { + required_max_fee_per_gas = math::percent_ceil(required_max_fee_per_gas, pct); + required_max_priority_fee_per_gas = + math::percent_ceil(required_max_priority_fee_per_gas, pct); + } + // filter by fees - if op.uo.max_fee_per_gas() < required_op_fees.max_fee_per_gas - || op.uo.max_priority_fee_per_gas() < required_op_fees.max_priority_fee_per_gas + if op.uo.max_fee_per_gas() < required_max_fee_per_gas + || op.uo.max_priority_fee_per_gas() < required_max_priority_fee_per_gas { self.emit(BuilderEvent::skipped_op( self.builder_tag.clone(), @@ -455,12 +465,16 @@ where } }; - let required_pvg = op.uo.required_pre_verification_gas( + let mut required_pvg = op.uo.required_pre_verification_gas( &self.settings.chain_spec, bundle_size, required_da_gas, ); + if let Some(pct) = op.perms.underpriced_bundle_pct { + required_pvg = math::percent_ceil(required_pvg, pct); + } + if op.uo.pre_verification_gas() < required_pvg { self.emit(BuilderEvent::skipped_op( self.builder_tag.clone(), diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 1f649a83a..41c61080b 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -27,6 +27,8 @@ message UserOperation { message UserOperationPermissions { bool trusted = 1; optional uint64 max_allowed_in_pool_for_sender = 2; + optional uint32 underpriced_accept_pct = 3; + optional uint32 underpriced_bundle_pct = 4; } // Protocol Buffer representation of an 7702 authorization tuple. See the official diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 89a3f1158..4dfa0d2f7 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -285,11 +285,15 @@ where op.uo().extra_data_len(bundle_size), ); - let required_pvg = op.uo().required_pre_verification_gas( + let mut required_pvg = op.uo().required_pre_verification_gas( &self.config.chain_spec, bundle_size, required_da_gas, ); + if let Some(pct) = op.po.perms.underpriced_bundle_pct { + required_pvg = math::percent_ceil(required_pvg, pct); + } + let actual_pvg = op.uo().pre_verification_gas(); if actual_pvg < required_pvg { diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 259fe115d..a1507c658 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -584,7 +584,7 @@ where let precheck_ret = self .pool_providers .prechecker() - .check(&versioned_op, block_hash.into()) + .check(&versioned_op, &perms, block_hash.into()) .await?; // Only let ops with successful simulations through @@ -2383,7 +2383,7 @@ mod tests { .returning(|| Ok(FeeUpdate::default())); for op in ops { - prechecker.expect_check().returning(move |_, _| { + prechecker.expect_check().returning(move |_, _, _| { if let Some(error) = &op.precheck_error { Err(PrecheckError::Violations(vec![error.clone()])) } else { diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index c3e67dcf3..8c2611f75 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -579,6 +579,8 @@ impl From for RundlerUserOperationPermissions { max_allowed_in_pool_for_sender: permissions .max_allowed_in_pool_for_sender .map(|c| c as usize), + underpriced_accept_pct: permissions.underpriced_accept_pct, + underpriced_bundle_pct: permissions.underpriced_bundle_pct, } } } @@ -590,6 +592,8 @@ impl From for UserOperationPermissions { max_allowed_in_pool_for_sender: permissions .max_allowed_in_pool_for_sender .map(|c| c as u64), + underpriced_accept_pct: permissions.underpriced_accept_pct, + underpriced_bundle_pct: permissions.underpriced_bundle_pct, } } } diff --git a/crates/rpc/src/types/permissions.rs b/crates/rpc/src/types/permissions.rs index 3cb62c83f..787d2217c 100644 --- a/crates/rpc/src/types/permissions.rs +++ b/crates/rpc/src/types/permissions.rs @@ -27,6 +27,12 @@ pub(crate) struct RpcUserOperationPermissions { /// The maximum sender allowed in the pool #[serde(default)] pub(crate) max_allowed_in_pool_for_sender: Option, + /// The allowed percentage of underpriced fees that is accepted into the pool + #[serde(default)] + pub(crate) underpriced_accept_pct: Option, + /// The allowed percentage of fees underpriced that is bundled + #[serde(default)] + pub(crate) underpriced_bundle_pct: Option, } impl FromRpc for UserOperationPermissions { @@ -34,6 +40,8 @@ impl FromRpc for UserOperationPermissions { UserOperationPermissions { trusted: rpc.trusted, max_allowed_in_pool_for_sender: rpc.max_allowed_in_pool_for_sender.map(|c| c.to()), + underpriced_accept_pct: rpc.underpriced_accept_pct.map(|c| c.to()), + underpriced_bundle_pct: rpc.underpriced_bundle_pct.map(|c| c.to()), } } } diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index e3e0b63db..40b00c204 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use std::{marker::PhantomData, sync::RwLock}; +use std::{cmp, marker::PhantomData, sync::RwLock}; use alloy_primitives::{Address, U256}; use anyhow::Context; @@ -23,7 +23,7 @@ use rundler_types::{ chain::ChainSpec, da::DAGasUOData, pool::{MempoolError, PrecheckViolation}, - GasFees, UserOperation, + GasFees, UserOperation, UserOperationPermissions, }; use rundler_utils::math; use tracing::instrument; @@ -67,6 +67,7 @@ pub trait Prechecker: Send + Sync { async fn check( &self, op: &Self::UO, + perms: &UserOperationPermissions, block: BlockHashOrNumber, ) -> Result; @@ -174,12 +175,13 @@ where async fn check( &self, op: &Self::UO, + perms: &UserOperationPermissions, block: BlockHashOrNumber, ) -> Result { let async_data = self.load_async_data(op, block).await?; let mut violations: Vec = vec![]; violations.extend(self.check_init_code(op, &async_data)); - violations.extend(self.check_gas(op, &async_data)); + violations.extend(self.check_gas(op, &async_data, perms)); violations.extend(self.check_payer(op, &async_data)); if !violations.is_empty() { Err(violations)? @@ -266,7 +268,12 @@ where violations } - fn check_gas(&self, op: &UO, async_data: &AsyncData) -> ArrayVec { + fn check_gas( + &self, + op: &UO, + async_data: &AsyncData, + perms: &UserOperationPermissions, + ) -> ArrayVec { let Settings { max_verification_gas, max_total_execution_gas, @@ -299,10 +306,12 @@ where // if preVerificationGas is dynamic, then allow for the percentage buffer // and check if the preVerificationGas is at least the minimum. if self.chain_spec.da_pre_verification_gas { - min_pre_verification_gas = math::percent( - min_pre_verification_gas, + let accept_pct = cmp::min( + perms.underpriced_accept_pct.unwrap_or(100), self.settings.pre_verification_gas_accept_percent, ); + + min_pre_verification_gas = math::percent_ceil(min_pre_verification_gas, accept_pct); } if op.pre_verification_gas() < min_pre_verification_gas { violations.push(PrecheckViolation::PreVerificationGasTooLow( @@ -312,10 +321,14 @@ where } // check that the max fee per gas and max priority fee per gas are at least the required fees - let min_base_fee = math::percent(base_fee, self.settings.base_fee_accept_percent); + let min_base_fee_accept_pct = cmp::min( + perms.underpriced_accept_pct.unwrap_or(100), + self.settings.base_fee_accept_percent, + ); + let min_base_fee = math::percent_ceil(base_fee, min_base_fee_accept_pct); let min_priority_fee = self.settings.priority_fee_mode.minimum_priority_fee( base_fee, - self.settings.base_fee_accept_percent, + min_base_fee_accept_pct, self.chain_spec.min_max_priority_fee_per_gas(), self.settings.bundle_priority_fee_overhead_percent, ); @@ -591,7 +604,11 @@ mod tests { ) .build(); - let res = prechecker.check_gas(&op, &get_test_async_data()); + let res = prechecker.check_gas( + &op, + &get_test_async_data(), + &UserOperationPermissions::default(), + ); let total_gas_limit = op.gas_limit(&cs, Some(1)); @@ -685,7 +702,7 @@ mod tests { ) .build(); - let res = prechecker.check_gas(&op, &async_data); + let res = prechecker.check_gas(&op, &async_data, &UserOperationPermissions::default()); assert!(res.is_empty()); } @@ -718,7 +735,7 @@ mod tests { ) .build(); - let res = prechecker.check_gas(&op, &async_data); + let res = prechecker.check_gas(&op, &async_data, &UserOperationPermissions::default()); let mut expected = ArrayVec::::new(); expected.push(PrecheckViolation::MaxFeePerGasTooLow( math::percent(5_000, settings.base_fee_accept_percent - 10), @@ -763,7 +780,7 @@ mod tests { ) .build(); - let res = prechecker.check_gas(&op, &async_data); + let res = prechecker.check_gas(&op, &async_data, &UserOperationPermissions::default()); let mut expected = ArrayVec::::new(); expected.push(PrecheckViolation::MaxPriorityFeePerGasTooLow( mintip - 1, @@ -805,7 +822,7 @@ mod tests { ) .build(); - let res = prechecker.check_gas(&op, &async_data); + let res = prechecker.check_gas(&op, &async_data, &UserOperationPermissions::default()); let mut expected = ArrayVec::::new(); expected.push(PrecheckViolation::PreVerificationGasTooLow( math::percent(1_000, settings.pre_verification_gas_accept_percent - 10), diff --git a/crates/types/src/user_operation/permissions.rs b/crates/types/src/user_operation/permissions.rs index e83392217..e0ec6c3f7 100644 --- a/crates/types/src/user_operation/permissions.rs +++ b/crates/types/src/user_operation/permissions.rs @@ -18,4 +18,8 @@ pub struct UserOperationPermissions { pub trusted: bool, /// The maximum number of user operations allowed for a sender in the mempool pub max_allowed_in_pool_for_sender: Option, + /// The allowed percentage of underpriced fees that is accepted into the pool + pub underpriced_accept_pct: Option, + /// The allowed percentage of fees underpriced that is bundled + pub underpriced_bundle_pct: Option, } diff --git a/crates/utils/src/math.rs b/crates/utils/src/math.rs index ffa0527d4..c38f4b823 100644 --- a/crates/utils/src/math.rs +++ b/crates/utils/src/math.rs @@ -68,6 +68,14 @@ where (n * T::from(percent)) / T::from(100) } +/// Take a percentage of a number, rounding up +pub fn percent_ceil(n: T, percent: u32) -> T +where + T: Add + Mul + Div + From, +{ + (n * T::from(percent) + T::from(99)) / T::from(100) +} + #[cfg(test)] mod tests { use alloy_primitives::U256; @@ -106,4 +114,9 @@ mod tests { fn test_percent() { assert_eq!(percent(3123_u32, 10), 312); } + + #[test] + fn test_percent_ceil() { + assert_eq!(percent_ceil(3123_u32, 10), 313); + } }