From 226f0591f7b1adb84817c870b15a5cd23001d1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <3535019+leruaa@users.noreply.github.com> Date: Fri, 3 Jan 2025 02:59:13 -0800 Subject: [PATCH] feat: handle isthmus operator fee (#1960) * feat: handle isthmus operator fee * fix: inverse if condition --- crates/revm/src/optimism.rs | 6 +- crates/revm/src/optimism/handler_register.rs | 106 +++++++++++++++- crates/revm/src/optimism/l1block.rs | 122 +++++++++++++++++-- 3 files changed, 217 insertions(+), 17 deletions(-) diff --git a/crates/revm/src/optimism.rs b/crates/revm/src/optimism.rs index 61e24cb842..64bff5295e 100644 --- a/crates/revm/src/optimism.rs +++ b/crates/revm/src/optimism.rs @@ -8,7 +8,9 @@ mod precompile; pub use handler_register::{ deduct_caller, end, last_frame_return, load_accounts, load_precompiles, - optimism_handle_register, output, refund, reward_beneficiary, validate_env, + optimism_handle_register, output, refund, reimburse_caller, reward_beneficiary, validate_env, validate_tx_against_state, }; -pub use l1block::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT}; +pub use l1block::{ + L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT, +}; diff --git a/crates/revm/src/optimism/handler_register.rs b/crates/revm/src/optimism/handler_register.rs index 43fcca8f60..7185dbc287 100644 --- a/crates/revm/src/optimism/handler_register.rs +++ b/crates/revm/src/optimism/handler_register.rs @@ -19,6 +19,8 @@ use revm_precompile::PrecompileSpecId; use std::string::ToString; use std::sync::Arc; +use super::l1block::OPERATOR_FEE_RECIPIENT; + pub fn optimism_handle_register(handler: &mut EvmHandler<'_, EXT, DB>) { spec_to_generic!(handler.cfg.spec_id, { // validate environment @@ -34,6 +36,7 @@ pub fn optimism_handle_register(handler: &mut EvmHandler<'_, // Refund is calculated differently then mainnet. handler.execution.last_frame_return = Arc::new(last_frame_return::); handler.post_execution.refund = Arc::new(refund::); + handler.post_execution.reimburse_caller = Arc::new(reimburse_caller::); handler.post_execution.reward_beneficiary = Arc::new(reward_beneficiary::); // In case of halt of deposit transaction return Error. handler.post_execution.output = Arc::new(output::); @@ -160,6 +163,40 @@ pub fn refund( } } +/// Reimburse the transaction caller. +#[inline] +pub fn reimburse_caller( + context: &mut Context, + gas: &Gas, +) -> Result<(), EVMError> { + mainnet::reimburse_caller::(context, gas)?; + + if context.evm.inner.env.tx.optimism.source_hash.is_none() { + let caller_account = context + .evm + .inner + .journaled_state + .load_account(context.evm.inner.env.tx.caller, &mut context.evm.inner.db)?; + let operator_fee_refund = context + .evm + .inner + .l1_block_info + .as_ref() + .expect("L1BlockInfo should be loaded") + .operator_fee_refund(gas, SPEC::SPEC_ID); + + // In additional to the normal transaction fee, additionally refund the caller + // for the operator fee. + caller_account.data.info.balance = caller_account + .data + .info + .balance + .saturating_add(operator_fee_refund); + } + + Ok(()) +} + /// Load precompiles for Optimism chain. #[inline] pub fn load_precompiles() -> ContextPrecompiles { @@ -216,6 +253,7 @@ pub fn deduct_caller( // If the transaction is not a deposit transaction, subtract the L1 data fee from the // caller's balance directly after minting the requested amount of ETH. + // Additionally deduct the operator fee from the caller's account. if context.evm.inner.env.tx.optimism.source_hash.is_none() { // get envelope let Some(enveloped_tx) = &context.evm.inner.env.tx.optimism.enveloped_tx else { @@ -240,6 +278,22 @@ pub fn deduct_caller( )); } caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost); + + // Deduct the operator fee from the caller's account. + let gas_limit = U256::from(context.evm.inner.env.tx.gas_limit); + + let operator_fee_charge = context + .evm + .inner + .l1_block_info + .as_ref() + .expect("L1BlockInfo should be loaded") + .operator_fee_charge(gas_limit, SPEC::SPEC_ID); + + caller_account.info.balance = caller_account + .info + .balance + .saturating_sub(operator_fee_charge); } Ok(()) } @@ -273,6 +327,10 @@ pub fn reward_beneficiary( }; let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::SPEC_ID); + let operator_fee_cost = l1_block_info.operator_fee_charge( + U256::from(gas.spent() - gas.refunded() as u64), + SPEC::SPEC_ID, + ); // Send the L1 cost of the transaction to the L1 Fee Vault. let mut l1_fee_vault_account = context @@ -297,6 +355,16 @@ pub fn reward_beneficiary( .block .basefee .mul(U256::from(gas.spent() - gas.refunded() as u64)); + + // Send the operator fee of the transaction to the coinbase. + let mut operator_fee_vault_account = context + .evm + .inner + .journaled_state + .load_account(OPERATOR_FEE_RECIPIENT, &mut context.evm.inner.db)?; + + operator_fee_vault_account.mark_touch(); + operator_fee_vault_account.data.info.balance += operator_fee_cost; } Ok(()) } @@ -399,8 +467,8 @@ mod tests { use crate::{ db::{EmptyDB, InMemoryDB}, primitives::{ - bytes, state::AccountInfo, Address, BedrockSpec, Bytes, Env, LatestSpec, RegolithSpec, - B256, + bytes, state::AccountInfo, Address, BedrockSpec, Bytes, Env, IsthmusSpec, LatestSpec, + RegolithSpec, B256, }, L1BlockInfo, }; @@ -590,6 +658,40 @@ mod tests { assert_eq!(account.info.balance, U256::from(1)); } + #[test] + fn test_remove_operator_cost() { + let caller = Address::ZERO; + let mut db = InMemoryDB::default(); + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(151), + ..Default::default() + }, + ); + let mut context: Context<(), InMemoryDB> = Context::new_with_db(db); + context.evm.l1_block_info = Some(L1BlockInfo { + operator_fee_scalar: Some(U256::from(10_000_000)), + operator_fee_constant: Some(U256::from(50)), + ..Default::default() + }); + context.evm.inner.env.tx.gas_limit = 10; + + // operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant + // 10_000_000 * 10 / 1_000_000 + 50 = 150 + context.evm.inner.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE")); + deduct_caller::(&mut context).unwrap(); + + // Check the account balance is updated. + let account = context + .evm + .inner + .journaled_state + .load_account(caller, &mut context.evm.inner.db) + .unwrap(); + assert_eq!(account.info.balance, U256::from(1)); + } + #[test] fn test_remove_l1_cost_lack_of_funds() { let caller = Address::ZERO; diff --git a/crates/revm/src/optimism/l1block.rs b/crates/revm/src/optimism/l1block.rs index cfe3e6bb9c..780b5726da 100644 --- a/crates/revm/src/optimism/l1block.rs +++ b/crates/revm/src/optimism/l1block.rs @@ -1,3 +1,5 @@ +use revm_interpreter::Gas; + use crate::optimism::fast_lz::flz_compress_len; use crate::primitives::{address, db::Database, Address, SpecId, U256}; use core::ops::Mul; @@ -11,6 +13,17 @@ const BASE_FEE_SCALAR_OFFSET: usize = 16; /// The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte sequence number. /// Byte offset within the storage slot of the 4-byte blobBaseFeeScalar attribute. const BLOB_BASE_FEE_SCALAR_OFFSET: usize = 20; +/// The Isthmus operator fee scalar values are similarly packed. Byte offset within +/// the storage slot of the 4-byte operatorFeeScalar attribute. +const OPERATOR_FEE_SCALAR_OFFSET: usize = 20; +/// The Isthmus operator fee scalar values are similarly packed. Byte offset within +/// the storage slot of the 8-byte operatorFeeConstant attribute. +const OPERATOR_FEE_CONSTANT_OFFSET: usize = 24; + +/// The fixed point decimal scaling factor associated with the operator fee scalar. +/// +/// Allows users to use 6 decimal points of precision when specifying the operator_fee_scalar. +const OPERATOR_FEE_SCALAR_DECIMAL: u64 = 1_000_000; const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]); const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([5u64, 0, 0, 0]); @@ -23,12 +36,19 @@ const ECOTONE_L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); /// offsets [BASE_FEE_SCALAR_OFFSET] and [BLOB_BASE_FEE_SCALAR_OFFSET] respectively. const ECOTONE_L1_FEE_SCALARS_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]); +/// This storage slot stores the 32-bit operatorFeeScalar and operatorFeeConstant attributes at +/// offsets [OPERATOR_FEE_SCALAR_OFFSET] and [OPERATOR_FEE_CONSTANT_OFFSET] respectively. +const OPERATOR_FEE_SCALARS_SLOT: U256 = U256::from_limbs([8u64, 0, 0, 0]); + /// An empty 64-bit set of scalar values. const EMPTY_SCALARS: [u8; 8] = [0u8; 8]; /// The address of L1 fee recipient. pub const L1_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001A"); +/// The address of the operator fee recipient. +pub const OPERATOR_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001B"); + /// The address of the base fee recipient. pub const BASE_FEE_RECIPIENT: Address = address!("4200000000000000000000000000000000000019"); @@ -68,8 +88,12 @@ pub struct L1BlockInfo { pub l1_blob_base_fee: Option, /// The current L1 blob base fee scalar. None if Ecotone is not activated. pub l1_blob_base_fee_scalar: Option, + /// The current L1 blob base fee. None if Isthmus is not activated, except if `empty_scalars` is `true`. + pub operator_fee_scalar: Option, + /// The current L1 blob base fee scalar. None if Isthmus is not activated. + pub operator_fee_constant: Option, /// True if Ecotone is activated, but the L1 fee scalars have not yet been set. - pub(crate) empty_scalars: bool, + pub(crate) empty_ecotone_scalars: bool, } impl L1BlockInfo { @@ -109,22 +133,94 @@ impl L1BlockInfo { // Check if the L1 fee scalars are empty. If so, we use the Bedrock cost function. The L1 fee overhead is // only necessary if `empty_scalars` is true, as it was deprecated in Ecotone. - let empty_scalars = l1_blob_base_fee.is_zero() + let empty_ecotone_scalars = l1_blob_base_fee.is_zero() && l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4] == EMPTY_SCALARS; - let l1_fee_overhead = empty_scalars + let l1_fee_overhead = empty_ecotone_scalars .then(|| db.storage(L1_BLOCK_CONTRACT, L1_OVERHEAD_SLOT)) .transpose()?; - Ok(L1BlockInfo { - l1_base_fee, - l1_base_fee_scalar, - l1_blob_base_fee: Some(l1_blob_base_fee), - l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), - empty_scalars, - l1_fee_overhead, - }) + if spec_id.is_enabled_in(SpecId::ISTHMUS) { + let operator_fee_scalars = db + .storage(L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT)? + .to_be_bytes::<32>(); + + // Post-isthmus L1 block info + // The `operator_fee_scalar` is stored as a big endian u32 at + // OPERATOR_FEE_SCALAR_OFFSET. + let operator_fee_scalar = U256::from_be_slice( + operator_fee_scalars + [OPERATOR_FEE_SCALAR_OFFSET..OPERATOR_FEE_SCALAR_OFFSET + 4] + .as_ref(), + ); + // The `operator_fee_constant` is stored as a big endian u64 at + // OPERATOR_FEE_CONSTANT_OFFSET. + let operator_fee_constant = U256::from_be_slice( + operator_fee_scalars + [OPERATOR_FEE_CONSTANT_OFFSET..OPERATOR_FEE_CONSTANT_OFFSET + 8] + .as_ref(), + ); + Ok(L1BlockInfo { + l1_base_fee, + l1_base_fee_scalar, + l1_blob_base_fee: Some(l1_blob_base_fee), + l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), + empty_ecotone_scalars, + l1_fee_overhead, + operator_fee_scalar: Some(operator_fee_scalar), + operator_fee_constant: Some(operator_fee_constant), + }) + } else { + // Pre-isthmus L1 block info + Ok(L1BlockInfo { + l1_base_fee, + l1_base_fee_scalar, + l1_blob_base_fee: Some(l1_blob_base_fee), + l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), + empty_ecotone_scalars, + l1_fee_overhead, + ..Default::default() + }) + } + } + } + + /// Calculate the operator fee for executing this transaction. + /// + /// Introduced in isthmus. Prior to isthmus, the operator fee is always zero. + pub fn operator_fee_charge(&self, gas_limit: U256, spec_id: SpecId) -> U256 { + if !spec_id.is_enabled_in(SpecId::ISTHMUS) { + return U256::ZERO; + } + let operator_fee_scalar = self + .operator_fee_scalar + .expect("Missing operator fee scalar for isthmus L1 Block"); + let operator_fee_constant = self + .operator_fee_constant + .expect("Missing operator fee constant for isthmus L1 Block"); + + let product = gas_limit.saturating_mul(operator_fee_scalar) + / (U256::from(OPERATOR_FEE_SCALAR_DECIMAL)); + + product.saturating_add(operator_fee_constant) + } + + /// Calculate the operator fee for executing this transaction. + /// + /// Introduced in isthmus. Prior to isthmus, the operator fee is always zero. + pub fn operator_fee_refund(&self, gas: &Gas, spec_id: SpecId) -> U256 { + if !spec_id.is_enabled_in(SpecId::ISTHMUS) { + return U256::ZERO; } + + let operator_fee_scalar = self + .operator_fee_scalar + .expect("Missing operator fee scalar for isthmus L1 Block"); + + // We're computing the difference between two operator fees, so no need to include the + // constant. + + operator_fee_scalar.saturating_mul(U256::from(gas.remaining() + gas.refunded() as u64)) } /// Calculate the data gas for posting the transaction on L1. Calldata costs 16 gas per byte @@ -213,7 +309,7 @@ impl L1BlockInfo { // There is an edgecase where, for the very first Ecotone block (unless it is activated at Genesis), we must // use the Bedrock cost function. To determine if this is the case, we can check if the Ecotone parameters are // unset. - if self.empty_scalars { + if self.empty_ecotone_scalars { return self.calculate_tx_l1_cost_bedrock(input, spec_id); } @@ -371,7 +467,7 @@ mod tests { assert_eq!(gas_cost, U256::ZERO); // If the scalars are empty, the bedrock cost function should be used. - l1_block_info.empty_scalars = true; + l1_block_info.empty_ecotone_scalars = true; let input = bytes!("FACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); assert_eq!(gas_cost, U256::from(1048));