Skip to content

Commit

Permalink
feat: handle isthmus operator fee (#1960)
Browse files Browse the repository at this point in the history
* feat: handle isthmus operator fee

* fix: inverse if condition
  • Loading branch information
leruaa authored Jan 3, 2025
1 parent 811bc7d commit 226f059
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 17 deletions.
6 changes: 4 additions & 2 deletions crates/revm/src/optimism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
106 changes: 104 additions & 2 deletions crates/revm/src/optimism/handler_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DB: Database, EXT>(handler: &mut EvmHandler<'_, EXT, DB>) {
spec_to_generic!(handler.cfg.spec_id, {
// validate environment
Expand All @@ -34,6 +36,7 @@ pub fn optimism_handle_register<DB: Database, EXT>(handler: &mut EvmHandler<'_,
// Refund is calculated differently then mainnet.
handler.execution.last_frame_return = Arc::new(last_frame_return::<SPEC, EXT, DB>);
handler.post_execution.refund = Arc::new(refund::<SPEC, EXT, DB>);
handler.post_execution.reimburse_caller = Arc::new(reimburse_caller::<SPEC, EXT, DB>);
handler.post_execution.reward_beneficiary = Arc::new(reward_beneficiary::<SPEC, EXT, DB>);
// In case of halt of deposit transaction return Error.
handler.post_execution.output = Arc::new(output::<SPEC, EXT, DB>);
Expand Down Expand Up @@ -160,6 +163,40 @@ pub fn refund<SPEC: Spec, EXT, DB: Database>(
}
}

/// Reimburse the transaction caller.
#[inline]
pub fn reimburse_caller<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
gas: &Gas,
) -> Result<(), EVMError<DB::Error>> {
mainnet::reimburse_caller::<SPEC, EXT, DB>(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<SPEC: Spec, EXT, DB: Database>() -> ContextPrecompiles<DB> {
Expand Down Expand Up @@ -216,6 +253,7 @@ pub fn deduct_caller<SPEC: Spec, EXT, DB: Database>(

// 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 {
Expand All @@ -240,6 +278,22 @@ pub fn deduct_caller<SPEC: Spec, EXT, DB: Database>(
));
}
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(())
}
Expand Down Expand Up @@ -273,6 +327,10 @@ pub fn reward_beneficiary<SPEC: Spec, EXT, DB: Database>(
};

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
Expand All @@ -297,6 +355,16 @@ pub fn reward_beneficiary<SPEC: Spec, EXT, DB: Database>(
.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(())
}
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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::<IsthmusSpec, (), _>(&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;
Expand Down
122 changes: 109 additions & 13 deletions crates/revm/src/optimism/l1block.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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]);
Expand All @@ -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");

Expand Down Expand Up @@ -68,8 +88,12 @@ pub struct L1BlockInfo {
pub l1_blob_base_fee: Option<U256>,
/// The current L1 blob base fee scalar. None if Ecotone is not activated.
pub l1_blob_base_fee_scalar: Option<U256>,
/// The current L1 blob base fee. None if Isthmus is not activated, except if `empty_scalars` is `true`.
pub operator_fee_scalar: Option<U256>,
/// The current L1 blob base fee scalar. None if Isthmus is not activated.
pub operator_fee_constant: Option<U256>,
/// 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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit 226f059

Please sign in to comment.