Skip to content

Commit 226f059

Browse files
authored
feat: handle isthmus operator fee (#1960)
* feat: handle isthmus operator fee * fix: inverse if condition
1 parent 811bc7d commit 226f059

File tree

3 files changed

+217
-17
lines changed

3 files changed

+217
-17
lines changed

crates/revm/src/optimism.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ mod precompile;
88

99
pub use handler_register::{
1010
deduct_caller, end, last_frame_return, load_accounts, load_precompiles,
11-
optimism_handle_register, output, refund, reward_beneficiary, validate_env,
11+
optimism_handle_register, output, refund, reimburse_caller, reward_beneficiary, validate_env,
1212
validate_tx_against_state,
1313
};
14-
pub use l1block::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT};
14+
pub use l1block::{
15+
L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT,
16+
};

crates/revm/src/optimism/handler_register.rs

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use revm_precompile::PrecompileSpecId;
1919
use std::string::ToString;
2020
use std::sync::Arc;
2121

22+
use super::l1block::OPERATOR_FEE_RECIPIENT;
23+
2224
pub fn optimism_handle_register<DB: Database, EXT>(handler: &mut EvmHandler<'_, EXT, DB>) {
2325
spec_to_generic!(handler.cfg.spec_id, {
2426
// validate environment
@@ -34,6 +36,7 @@ pub fn optimism_handle_register<DB: Database, EXT>(handler: &mut EvmHandler<'_,
3436
// Refund is calculated differently then mainnet.
3537
handler.execution.last_frame_return = Arc::new(last_frame_return::<SPEC, EXT, DB>);
3638
handler.post_execution.refund = Arc::new(refund::<SPEC, EXT, DB>);
39+
handler.post_execution.reimburse_caller = Arc::new(reimburse_caller::<SPEC, EXT, DB>);
3740
handler.post_execution.reward_beneficiary = Arc::new(reward_beneficiary::<SPEC, EXT, DB>);
3841
// In case of halt of deposit transaction return Error.
3942
handler.post_execution.output = Arc::new(output::<SPEC, EXT, DB>);
@@ -160,6 +163,40 @@ pub fn refund<SPEC: Spec, EXT, DB: Database>(
160163
}
161164
}
162165

166+
/// Reimburse the transaction caller.
167+
#[inline]
168+
pub fn reimburse_caller<SPEC: Spec, EXT, DB: Database>(
169+
context: &mut Context<EXT, DB>,
170+
gas: &Gas,
171+
) -> Result<(), EVMError<DB::Error>> {
172+
mainnet::reimburse_caller::<SPEC, EXT, DB>(context, gas)?;
173+
174+
if context.evm.inner.env.tx.optimism.source_hash.is_none() {
175+
let caller_account = context
176+
.evm
177+
.inner
178+
.journaled_state
179+
.load_account(context.evm.inner.env.tx.caller, &mut context.evm.inner.db)?;
180+
let operator_fee_refund = context
181+
.evm
182+
.inner
183+
.l1_block_info
184+
.as_ref()
185+
.expect("L1BlockInfo should be loaded")
186+
.operator_fee_refund(gas, SPEC::SPEC_ID);
187+
188+
// In additional to the normal transaction fee, additionally refund the caller
189+
// for the operator fee.
190+
caller_account.data.info.balance = caller_account
191+
.data
192+
.info
193+
.balance
194+
.saturating_add(operator_fee_refund);
195+
}
196+
197+
Ok(())
198+
}
199+
163200
/// Load precompiles for Optimism chain.
164201
#[inline]
165202
pub fn load_precompiles<SPEC: Spec, EXT, DB: Database>() -> ContextPrecompiles<DB> {
@@ -216,6 +253,7 @@ pub fn deduct_caller<SPEC: Spec, EXT, DB: Database>(
216253

217254
// If the transaction is not a deposit transaction, subtract the L1 data fee from the
218255
// caller's balance directly after minting the requested amount of ETH.
256+
// Additionally deduct the operator fee from the caller's account.
219257
if context.evm.inner.env.tx.optimism.source_hash.is_none() {
220258
// get envelope
221259
let Some(enveloped_tx) = &context.evm.inner.env.tx.optimism.enveloped_tx else {
@@ -240,6 +278,22 @@ pub fn deduct_caller<SPEC: Spec, EXT, DB: Database>(
240278
));
241279
}
242280
caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost);
281+
282+
// Deduct the operator fee from the caller's account.
283+
let gas_limit = U256::from(context.evm.inner.env.tx.gas_limit);
284+
285+
let operator_fee_charge = context
286+
.evm
287+
.inner
288+
.l1_block_info
289+
.as_ref()
290+
.expect("L1BlockInfo should be loaded")
291+
.operator_fee_charge(gas_limit, SPEC::SPEC_ID);
292+
293+
caller_account.info.balance = caller_account
294+
.info
295+
.balance
296+
.saturating_sub(operator_fee_charge);
243297
}
244298
Ok(())
245299
}
@@ -273,6 +327,10 @@ pub fn reward_beneficiary<SPEC: Spec, EXT, DB: Database>(
273327
};
274328

275329
let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::SPEC_ID);
330+
let operator_fee_cost = l1_block_info.operator_fee_charge(
331+
U256::from(gas.spent() - gas.refunded() as u64),
332+
SPEC::SPEC_ID,
333+
);
276334

277335
// Send the L1 cost of the transaction to the L1 Fee Vault.
278336
let mut l1_fee_vault_account = context
@@ -297,6 +355,16 @@ pub fn reward_beneficiary<SPEC: Spec, EXT, DB: Database>(
297355
.block
298356
.basefee
299357
.mul(U256::from(gas.spent() - gas.refunded() as u64));
358+
359+
// Send the operator fee of the transaction to the coinbase.
360+
let mut operator_fee_vault_account = context
361+
.evm
362+
.inner
363+
.journaled_state
364+
.load_account(OPERATOR_FEE_RECIPIENT, &mut context.evm.inner.db)?;
365+
366+
operator_fee_vault_account.mark_touch();
367+
operator_fee_vault_account.data.info.balance += operator_fee_cost;
300368
}
301369
Ok(())
302370
}
@@ -399,8 +467,8 @@ mod tests {
399467
use crate::{
400468
db::{EmptyDB, InMemoryDB},
401469
primitives::{
402-
bytes, state::AccountInfo, Address, BedrockSpec, Bytes, Env, LatestSpec, RegolithSpec,
403-
B256,
470+
bytes, state::AccountInfo, Address, BedrockSpec, Bytes, Env, IsthmusSpec, LatestSpec,
471+
RegolithSpec, B256,
404472
},
405473
L1BlockInfo,
406474
};
@@ -590,6 +658,40 @@ mod tests {
590658
assert_eq!(account.info.balance, U256::from(1));
591659
}
592660

661+
#[test]
662+
fn test_remove_operator_cost() {
663+
let caller = Address::ZERO;
664+
let mut db = InMemoryDB::default();
665+
db.insert_account_info(
666+
caller,
667+
AccountInfo {
668+
balance: U256::from(151),
669+
..Default::default()
670+
},
671+
);
672+
let mut context: Context<(), InMemoryDB> = Context::new_with_db(db);
673+
context.evm.l1_block_info = Some(L1BlockInfo {
674+
operator_fee_scalar: Some(U256::from(10_000_000)),
675+
operator_fee_constant: Some(U256::from(50)),
676+
..Default::default()
677+
});
678+
context.evm.inner.env.tx.gas_limit = 10;
679+
680+
// operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant
681+
// 10_000_000 * 10 / 1_000_000 + 50 = 150
682+
context.evm.inner.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE"));
683+
deduct_caller::<IsthmusSpec, (), _>(&mut context).unwrap();
684+
685+
// Check the account balance is updated.
686+
let account = context
687+
.evm
688+
.inner
689+
.journaled_state
690+
.load_account(caller, &mut context.evm.inner.db)
691+
.unwrap();
692+
assert_eq!(account.info.balance, U256::from(1));
693+
}
694+
593695
#[test]
594696
fn test_remove_l1_cost_lack_of_funds() {
595697
let caller = Address::ZERO;

crates/revm/src/optimism/l1block.rs

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use revm_interpreter::Gas;
2+
13
use crate::optimism::fast_lz::flz_compress_len;
24
use crate::primitives::{address, db::Database, Address, SpecId, U256};
35
use core::ops::Mul;
@@ -11,6 +13,17 @@ const BASE_FEE_SCALAR_OFFSET: usize = 16;
1113
/// The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte sequence number.
1214
/// Byte offset within the storage slot of the 4-byte blobBaseFeeScalar attribute.
1315
const BLOB_BASE_FEE_SCALAR_OFFSET: usize = 20;
16+
/// The Isthmus operator fee scalar values are similarly packed. Byte offset within
17+
/// the storage slot of the 4-byte operatorFeeScalar attribute.
18+
const OPERATOR_FEE_SCALAR_OFFSET: usize = 20;
19+
/// The Isthmus operator fee scalar values are similarly packed. Byte offset within
20+
/// the storage slot of the 8-byte operatorFeeConstant attribute.
21+
const OPERATOR_FEE_CONSTANT_OFFSET: usize = 24;
22+
23+
/// The fixed point decimal scaling factor associated with the operator fee scalar.
24+
///
25+
/// Allows users to use 6 decimal points of precision when specifying the operator_fee_scalar.
26+
const OPERATOR_FEE_SCALAR_DECIMAL: u64 = 1_000_000;
1427

1528
const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]);
1629
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]);
2336
/// offsets [BASE_FEE_SCALAR_OFFSET] and [BLOB_BASE_FEE_SCALAR_OFFSET] respectively.
2437
const ECOTONE_L1_FEE_SCALARS_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]);
2538

39+
/// This storage slot stores the 32-bit operatorFeeScalar and operatorFeeConstant attributes at
40+
/// offsets [OPERATOR_FEE_SCALAR_OFFSET] and [OPERATOR_FEE_CONSTANT_OFFSET] respectively.
41+
const OPERATOR_FEE_SCALARS_SLOT: U256 = U256::from_limbs([8u64, 0, 0, 0]);
42+
2643
/// An empty 64-bit set of scalar values.
2744
const EMPTY_SCALARS: [u8; 8] = [0u8; 8];
2845

2946
/// The address of L1 fee recipient.
3047
pub const L1_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001A");
3148

49+
/// The address of the operator fee recipient.
50+
pub const OPERATOR_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001B");
51+
3252
/// The address of the base fee recipient.
3353
pub const BASE_FEE_RECIPIENT: Address = address!("4200000000000000000000000000000000000019");
3454

@@ -68,8 +88,12 @@ pub struct L1BlockInfo {
6888
pub l1_blob_base_fee: Option<U256>,
6989
/// The current L1 blob base fee scalar. None if Ecotone is not activated.
7090
pub l1_blob_base_fee_scalar: Option<U256>,
91+
/// The current L1 blob base fee. None if Isthmus is not activated, except if `empty_scalars` is `true`.
92+
pub operator_fee_scalar: Option<U256>,
93+
/// The current L1 blob base fee scalar. None if Isthmus is not activated.
94+
pub operator_fee_constant: Option<U256>,
7195
/// True if Ecotone is activated, but the L1 fee scalars have not yet been set.
72-
pub(crate) empty_scalars: bool,
96+
pub(crate) empty_ecotone_scalars: bool,
7397
}
7498

7599
impl L1BlockInfo {
@@ -109,22 +133,94 @@ impl L1BlockInfo {
109133

110134
// Check if the L1 fee scalars are empty. If so, we use the Bedrock cost function. The L1 fee overhead is
111135
// only necessary if `empty_scalars` is true, as it was deprecated in Ecotone.
112-
let empty_scalars = l1_blob_base_fee.is_zero()
136+
let empty_ecotone_scalars = l1_blob_base_fee.is_zero()
113137
&& l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4]
114138
== EMPTY_SCALARS;
115-
let l1_fee_overhead = empty_scalars
139+
let l1_fee_overhead = empty_ecotone_scalars
116140
.then(|| db.storage(L1_BLOCK_CONTRACT, L1_OVERHEAD_SLOT))
117141
.transpose()?;
118142

119-
Ok(L1BlockInfo {
120-
l1_base_fee,
121-
l1_base_fee_scalar,
122-
l1_blob_base_fee: Some(l1_blob_base_fee),
123-
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
124-
empty_scalars,
125-
l1_fee_overhead,
126-
})
143+
if spec_id.is_enabled_in(SpecId::ISTHMUS) {
144+
let operator_fee_scalars = db
145+
.storage(L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT)?
146+
.to_be_bytes::<32>();
147+
148+
// Post-isthmus L1 block info
149+
// The `operator_fee_scalar` is stored as a big endian u32 at
150+
// OPERATOR_FEE_SCALAR_OFFSET.
151+
let operator_fee_scalar = U256::from_be_slice(
152+
operator_fee_scalars
153+
[OPERATOR_FEE_SCALAR_OFFSET..OPERATOR_FEE_SCALAR_OFFSET + 4]
154+
.as_ref(),
155+
);
156+
// The `operator_fee_constant` is stored as a big endian u64 at
157+
// OPERATOR_FEE_CONSTANT_OFFSET.
158+
let operator_fee_constant = U256::from_be_slice(
159+
operator_fee_scalars
160+
[OPERATOR_FEE_CONSTANT_OFFSET..OPERATOR_FEE_CONSTANT_OFFSET + 8]
161+
.as_ref(),
162+
);
163+
Ok(L1BlockInfo {
164+
l1_base_fee,
165+
l1_base_fee_scalar,
166+
l1_blob_base_fee: Some(l1_blob_base_fee),
167+
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
168+
empty_ecotone_scalars,
169+
l1_fee_overhead,
170+
operator_fee_scalar: Some(operator_fee_scalar),
171+
operator_fee_constant: Some(operator_fee_constant),
172+
})
173+
} else {
174+
// Pre-isthmus L1 block info
175+
Ok(L1BlockInfo {
176+
l1_base_fee,
177+
l1_base_fee_scalar,
178+
l1_blob_base_fee: Some(l1_blob_base_fee),
179+
l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar),
180+
empty_ecotone_scalars,
181+
l1_fee_overhead,
182+
..Default::default()
183+
})
184+
}
185+
}
186+
}
187+
188+
/// Calculate the operator fee for executing this transaction.
189+
///
190+
/// Introduced in isthmus. Prior to isthmus, the operator fee is always zero.
191+
pub fn operator_fee_charge(&self, gas_limit: U256, spec_id: SpecId) -> U256 {
192+
if !spec_id.is_enabled_in(SpecId::ISTHMUS) {
193+
return U256::ZERO;
194+
}
195+
let operator_fee_scalar = self
196+
.operator_fee_scalar
197+
.expect("Missing operator fee scalar for isthmus L1 Block");
198+
let operator_fee_constant = self
199+
.operator_fee_constant
200+
.expect("Missing operator fee constant for isthmus L1 Block");
201+
202+
let product = gas_limit.saturating_mul(operator_fee_scalar)
203+
/ (U256::from(OPERATOR_FEE_SCALAR_DECIMAL));
204+
205+
product.saturating_add(operator_fee_constant)
206+
}
207+
208+
/// Calculate the operator fee for executing this transaction.
209+
///
210+
/// Introduced in isthmus. Prior to isthmus, the operator fee is always zero.
211+
pub fn operator_fee_refund(&self, gas: &Gas, spec_id: SpecId) -> U256 {
212+
if !spec_id.is_enabled_in(SpecId::ISTHMUS) {
213+
return U256::ZERO;
127214
}
215+
216+
let operator_fee_scalar = self
217+
.operator_fee_scalar
218+
.expect("Missing operator fee scalar for isthmus L1 Block");
219+
220+
// We're computing the difference between two operator fees, so no need to include the
221+
// constant.
222+
223+
operator_fee_scalar.saturating_mul(U256::from(gas.remaining() + gas.refunded() as u64))
128224
}
129225

130226
/// Calculate the data gas for posting the transaction on L1. Calldata costs 16 gas per byte
@@ -213,7 +309,7 @@ impl L1BlockInfo {
213309
// There is an edgecase where, for the very first Ecotone block (unless it is activated at Genesis), we must
214310
// use the Bedrock cost function. To determine if this is the case, we can check if the Ecotone parameters are
215311
// unset.
216-
if self.empty_scalars {
312+
if self.empty_ecotone_scalars {
217313
return self.calculate_tx_l1_cost_bedrock(input, spec_id);
218314
}
219315

@@ -371,7 +467,7 @@ mod tests {
371467
assert_eq!(gas_cost, U256::ZERO);
372468

373469
// If the scalars are empty, the bedrock cost function should be used.
374-
l1_block_info.empty_scalars = true;
470+
l1_block_info.empty_ecotone_scalars = true;
375471
let input = bytes!("FACADE");
376472
let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE);
377473
assert_eq!(gas_cost, U256::from(1048));

0 commit comments

Comments
 (0)