use crate::{
api::exec::OpContextTr,
constants::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT},
transaction::{deposit::DEPOSIT_TRANSACTION_TYPE, OpTransactionError, OpTxTr},
L1BlockInfo, OpHaltReason, OpSpecId,
};
use revm::{
context::{result::InvalidTransaction, LocalContextTr},
context_interface::{
context::ContextError,
result::{EVMError, ExecutionResult, FromStringError},
Block, Cfg, ContextTr, JournalTr, Transaction,
},
handler::{
evm::FrameTr,
handler::EvmTrError,
post_execution::{self, reimburse_caller},
pre_execution::validate_account_nonce_and_code,
EthFrame, EvmTr, FrameResult, Handler, MainnetHandler,
},
inspector::{Inspector, InspectorEvmTr, InspectorHandler},
interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInit, Gas},
primitives::{hardfork::SpecId, U256},
};
use std::boxed::Box;
#[derive(Debug, Clone)]
pub struct OpHandler<EVM, ERROR, FRAME> {
pub mainnet: MainnetHandler<EVM, ERROR, FRAME>,
}
impl<EVM, ERROR, FRAME> OpHandler<EVM, ERROR, FRAME> {
pub fn new() -> Self {
Self {
mainnet: MainnetHandler::default(),
}
}
}
impl<EVM, ERROR, FRAME> Default for OpHandler<EVM, ERROR, FRAME> {
fn default() -> Self {
Self::new()
}
}
pub trait IsTxError {
fn is_tx_error(&self) -> bool;
}
impl<DB, TX> IsTxError for EVMError<DB, TX> {
fn is_tx_error(&self) -> bool {
matches!(self, EVMError::Transaction(_))
}
}
impl<EVM, ERROR, FRAME> Handler for OpHandler<EVM, ERROR, FRAME>
where
EVM: EvmTr<Context: OpContextTr, Frame = FRAME>,
ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
{
type Evm = EVM;
type Error = ERROR;
type HaltReason = OpHaltReason;
fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
let ctx = evm.ctx();
let tx = ctx.tx();
let tx_type = tx.tx_type();
if tx_type == DEPOSIT_TRANSACTION_TYPE {
if tx.is_system_transaction()
&& evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH)
{
return Err(OpTransactionError::DepositSystemTxPostRegolith.into());
}
return Ok(());
}
self.mainnet.validate_env(evm)
}
fn validate_against_state_and_deduct_caller(
&self,
evm: &mut Self::Evm,
) -> Result<(), Self::Error> {
let ctx = evm.ctx();
let basefee = ctx.block().basefee() as u128;
let blob_price = ctx.block().blob_gasprice().unwrap_or_default();
let is_deposit = ctx.tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
let spec = ctx.cfg().spec();
let block_number = ctx.block().number();
let is_balance_check_disabled = ctx.cfg().is_balance_check_disabled();
let is_eip3607_disabled = ctx.cfg().is_eip3607_disabled();
let is_nonce_check_disabled = ctx.cfg().is_nonce_check_disabled();
if is_deposit {
let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut();
let basefee = block.basefee() as u128;
let blob_price = block.blob_gasprice().unwrap_or_default();
let caller_account = journal.load_account_code(tx.caller())?.data;
let effective_balance_spending = tx
.effective_balance_spending(basefee, blob_price)
.expect("Deposit transaction effective balance spending overflow")
- tx.value();
let mut new_balance = caller_account
.info
.balance
.saturating_add(U256::from(tx.mint().unwrap_or_default()))
.saturating_sub(effective_balance_spending);
if cfg.is_balance_check_disabled() {
new_balance = new_balance.max(tx.value());
}
let old_balance =
caller_account.caller_initial_modification(new_balance, tx.kind().is_call());
journal.caller_accounting_journal_entry(tx.caller(), old_balance, tx.kind().is_call());
return Ok(());
}
let mut additional_cost = U256::ZERO;
if ctx.chain().l2_block != Some(block_number) {
*ctx.chain_mut() = L1BlockInfo::try_fetch(ctx.db_mut(), block_number, spec)?;
}
if !ctx.cfg().is_fee_charge_disabled() {
let Some(enveloped_tx) = ctx.tx().enveloped_tx().cloned() else {
return Err(ERROR::from_string(
"[OPTIMISM] Failed to load enveloped transaction.".into(),
));
};
additional_cost = ctx.chain_mut().calculate_tx_l1_cost(&enveloped_tx, spec);
if spec.is_enabled_in(OpSpecId::ISTHMUS) {
let gas_limit = U256::from(ctx.tx().gas_limit());
let operator_fee_charge =
ctx.chain()
.operator_fee_charge(&enveloped_tx, gas_limit, spec);
additional_cost = additional_cost.saturating_add(operator_fee_charge);
}
}
let (tx, journal) = ctx.tx_journal_mut();
let caller_account = journal.load_account_code(tx.caller())?.data;
validate_account_nonce_and_code(
&mut caller_account.info,
tx.nonce(),
is_eip3607_disabled,
is_nonce_check_disabled,
)?;
let mut new_balance = caller_account.info.balance;
if !is_balance_check_disabled {
let Some(balance) = new_balance.checked_sub(additional_cost) else {
return Err(InvalidTransaction::LackOfFundForMaxFee {
fee: Box::new(additional_cost),
balance: Box::new(new_balance),
}
.into());
};
tx.ensure_enough_balance(balance)?;
}
let gas_balance_spending = tx
.gas_balance_spending(basefee, blob_price)
.expect("effective balance is always smaller than max balance so it can't overflow");
let op_gas_balance_spending = gas_balance_spending.saturating_add(additional_cost);
new_balance = new_balance.saturating_sub(op_gas_balance_spending);
if is_balance_check_disabled {
new_balance = new_balance.max(tx.value());
}
let old_balance =
caller_account.caller_initial_modification(new_balance, tx.kind().is_call());
journal.caller_accounting_journal_entry(tx.caller(), old_balance, tx.kind().is_call());
Ok(())
}
fn last_frame_result(
&mut self,
evm: &mut Self::Evm,
frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<(), Self::Error> {
let ctx = evm.ctx();
let tx = ctx.tx();
let is_deposit = tx.tx_type() == DEPOSIT_TRANSACTION_TYPE;
let tx_gas_limit = tx.gas_limit();
let is_regolith = ctx.cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
let instruction_result = frame_result.interpreter_result().result;
let gas = frame_result.gas_mut();
let remaining = gas.remaining();
let refunded = gas.refunded();
*gas = Gas::new_spent(tx_gas_limit);
if instruction_result.is_ok() {
if !is_deposit || is_regolith {
gas.erase_cost(remaining);
gas.record_refund(refunded);
} else if is_deposit {
let tx = ctx.tx();
if tx.is_system_transaction() {
gas.erase_cost(tx_gas_limit);
}
}
} else if instruction_result.is_revert() {
if !is_deposit || is_regolith {
gas.erase_cost(remaining);
}
}
Ok(())
}
fn reimburse_caller(
&self,
evm: &mut Self::Evm,
frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<(), Self::Error> {
let mut additional_refund = U256::ZERO;
if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE
&& !evm.ctx().cfg().is_fee_charge_disabled()
{
let spec = evm.ctx().cfg().spec();
additional_refund = evm
.ctx()
.chain()
.operator_fee_refund(frame_result.gas(), spec);
}
reimburse_caller(evm.ctx(), frame_result.gas(), additional_refund).map_err(From::from)
}
fn refund(
&self,
evm: &mut Self::Evm,
frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
eip7702_refund: i64,
) {
frame_result.gas_mut().record_refund(eip7702_refund);
let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH);
let is_gas_refund_disabled = is_deposit && !is_regolith;
if !is_gas_refund_disabled {
frame_result.gas_mut().set_final_refund(
evm.ctx()
.cfg()
.spec()
.into_eth_spec()
.is_enabled_in(SpecId::LONDON),
);
}
}
fn reward_beneficiary(
&self,
evm: &mut Self::Evm,
frame_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<(), Self::Error> {
let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
if is_deposit {
return Ok(());
}
self.mainnet.reward_beneficiary(evm, frame_result)?;
let basefee = evm.ctx().block().basefee() as u128;
let ctx = evm.ctx();
let enveloped = ctx.tx().enveloped_tx().cloned();
let spec = ctx.cfg().spec();
let l1_block_info = ctx.chain_mut();
let Some(enveloped_tx) = &enveloped else {
return Err(ERROR::from_string(
"[OPTIMISM] Failed to load enveloped transaction.".into(),
));
};
let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec);
let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) {
l1_block_info.operator_fee_charge(
enveloped_tx,
U256::from(frame_result.gas().used()),
spec,
)
} else {
U256::ZERO
};
let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
for (recipient, amount) in [
(L1_FEE_RECIPIENT, l1_cost),
(BASE_FEE_RECIPIENT, base_fee_amount),
(OPERATOR_FEE_RECIPIENT, operator_fee_cost),
] {
ctx.journal_mut().balance_incr(recipient, amount)?;
}
Ok(())
}
fn execution_result(
&mut self,
evm: &mut Self::Evm,
frame_result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
match core::mem::replace(evm.ctx().error(), Ok(())) {
Err(ContextError::Db(e)) => return Err(e.into()),
Err(ContextError::Custom(e)) => return Err(Self::Error::from_string(e)),
Ok(_) => (),
}
let exec_result =
post_execution::output(evm.ctx(), frame_result).map_haltreason(OpHaltReason::Base);
if exec_result.is_halt() {
let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
if is_deposit && evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH) {
return Err(ERROR::from(OpTransactionError::HaltedDepositPostRegolith));
}
}
evm.ctx().journal_mut().commit_tx();
evm.ctx().chain_mut().clear_tx_l1_cost();
evm.ctx().local_mut().clear();
evm.frame_stack().clear();
Ok(exec_result)
}
fn catch_error(
&self,
evm: &mut Self::Evm,
error: Self::Error,
) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE;
let output = if error.is_tx_error() && is_deposit {
let ctx = evm.ctx();
let spec = ctx.cfg().spec();
let tx = ctx.tx();
let caller = tx.caller();
let mint = tx.mint();
let is_system_tx = tx.is_system_transaction();
let gas_limit = tx.gas_limit();
evm.ctx().journal_mut().discard_tx();
let acc: &mut revm::state::Account = evm.ctx().journal_mut().load_account(caller)?.data;
let old_balance = acc.info.balance;
acc.transaction_id -= 1;
acc.info.nonce = acc.info.nonce.saturating_add(1);
acc.info.balance = acc
.info
.balance
.saturating_add(U256::from(mint.unwrap_or_default()));
acc.mark_touch();
evm.ctx()
.journal_mut()
.caller_accounting_journal_entry(caller, old_balance, true);
let gas_used = if spec.is_enabled_in(OpSpecId::REGOLITH) || !is_system_tx {
gas_limit
} else {
0
};
Ok(ExecutionResult::Halt {
reason: OpHaltReason::FailedDeposit,
gas_used,
})
} else {
Err(error)
};
evm.ctx().chain_mut().clear_tx_l1_cost();
evm.ctx().local_mut().clear();
evm.frame_stack().clear();
output
}
}
impl<EVM, ERROR> InspectorHandler for OpHandler<EVM, ERROR, EthFrame<EthInterpreter>>
where
EVM: InspectorEvmTr<
Context: OpContextTr,
Frame = EthFrame<EthInterpreter>,
Inspector: Inspector<<<Self as Handler>::Evm as EvmTr>::Context, EthInterpreter>,
>,
ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError,
{
type IT = EthInterpreter;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
api::default_ctx::OpContext,
constants::{
BASE_FEE_SCALAR_OFFSET, DA_FOOTPRINT_GAS_SCALAR_SLOT, ECOTONE_L1_BLOB_BASE_FEE_SLOT,
ECOTONE_L1_FEE_SCALARS_SLOT, L1_BASE_FEE_SLOT, L1_BLOCK_CONTRACT,
OPERATOR_FEE_SCALARS_SLOT,
},
DefaultOp, OpBuilder, OpTransaction,
};
use alloy_primitives::uint;
use revm::{
context::{BlockEnv, Context, TxEnv},
context_interface::result::InvalidTransaction,
database::InMemoryDB,
database_interface::EmptyDB,
handler::EthFrame,
interpreter::{CallOutcome, InstructionResult, InterpreterResult},
primitives::{bytes, Address, Bytes, B256},
state::AccountInfo,
};
use rstest::rstest;
use std::boxed::Box;
fn call_last_frame_return(
ctx: OpContext<EmptyDB>,
instruction_result: InstructionResult,
gas: Gas,
) -> Gas {
let mut evm = ctx.build_op();
let mut exec_result = FrameResult::Call(CallOutcome::new(
InterpreterResult {
result: instruction_result,
output: Bytes::new(),
gas,
},
0..0,
));
let mut handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.last_frame_result(&mut evm, &mut exec_result)
.unwrap();
handler.refund(&mut evm, &mut exec_result, 0);
*exec_result.gas()
}
#[test]
fn test_revert_gas() {
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
let gas = call_last_frame_return(ctx, InstructionResult::Revert, Gas::new(90));
assert_eq!(gas.remaining(), 90);
assert_eq!(gas.spent(), 10);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_consume_gas() {
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
assert_eq!(gas.remaining(), 90);
assert_eq!(gas.spent(), 10);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_consume_gas_with_refund() {
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.source_hash(B256::from([1u8; 32]))
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut ret_gas = Gas::new(90);
ret_gas.record_refund(20);
let gas = call_last_frame_return(ctx.clone(), InstructionResult::Stop, ret_gas);
assert_eq!(gas.remaining(), 90);
assert_eq!(gas.spent(), 10);
assert_eq!(gas.refunded(), 2);
let gas = call_last_frame_return(ctx, InstructionResult::Revert, ret_gas);
assert_eq!(gas.remaining(), 90);
assert_eq!(gas.spent(), 10);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_consume_gas_deposit_tx() {
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.source_hash(B256::from([1u8; 32]))
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
assert_eq!(gas.remaining(), 0);
assert_eq!(gas.spent(), 100);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_consume_gas_sys_deposit_tx() {
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.source_hash(B256::from([1u8; 32]))
.is_system_transaction()
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK);
let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90));
assert_eq!(gas.remaining(), 100);
assert_eq!(gas.spent(), 0);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_commit_mint_value() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(1000),
..Default::default()
},
);
let mut ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l1_base_fee: U256::from(1_000),
l1_fee_overhead: Some(U256::from(1_000)),
l1_base_fee_scalar: U256::from(1_000),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
ctx.modify_tx(|tx| {
tx.deposit.source_hash = B256::from([1u8; 32]);
tx.deposit.mint = Some(10);
});
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
let account = evm.ctx().journal_mut().load_account(caller).unwrap();
assert_eq!(account.info.balance, U256::from(1010));
}
#[test]
fn test_remove_l1_cost_non_deposit() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(1058), ..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l1_base_fee: U256::from(1_000),
l1_fee_overhead: Some(U256::from(1_000)),
l1_base_fee_scalar: U256::from(1_000),
l2_block: Some(U256::from(0)),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.enveloped_tx(Some(bytes!("FACADE")))
.source_hash(B256::ZERO)
.build()
.unwrap(),
);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
let account = evm.ctx().journal_mut().load_account(caller).unwrap();
assert_eq!(account.info.balance, U256::from(10)); }
#[test]
fn test_reload_l1_block_info_isthmus() {
const BLOCK_NUM: U256 = uint!(100_U256);
const L1_BASE_FEE: U256 = uint!(1_U256);
const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
const L1_BASE_FEE_SCALAR: u64 = 3;
const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
const L1_FEE_SCALARS: U256 = U256::from_limbs([
0,
(L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
0,
0,
]);
const OPERATOR_FEE_SCALAR: u64 = 5;
const OPERATOR_FEE_CONST: u64 = 6;
const OPERATOR_FEE: U256 =
U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
let mut db = InMemoryDB::default();
let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
l1_block_contract
.storage
.insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
l1_block_contract
.storage
.insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
db.insert_account_info(
Address::ZERO,
AccountInfo {
balance: U256::from(1000),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l2_block: Some(BLOCK_NUM + U256::from(1)), ..Default::default()
})
.with_block(BlockEnv {
number: BLOCK_NUM,
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
let mut evm = ctx.build_op();
assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert_eq!(
*evm.ctx().chain(),
L1BlockInfo {
l2_block: Some(BLOCK_NUM),
l1_base_fee: L1_BASE_FEE,
l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
empty_ecotone_scalars: false,
l1_fee_overhead: None,
operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
tx_l1_cost: Some(U256::ZERO),
da_footprint_gas_scalar: None
}
);
}
#[test]
fn test_parse_da_footprint_gas_scalar_jovian() {
const BLOCK_NUM: U256 = uint!(100_U256);
const L1_BASE_FEE: U256 = uint!(1_U256);
const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
const L1_BASE_FEE_SCALAR: u64 = 3;
const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
const L1_FEE_SCALARS: U256 = U256::from_limbs([
0,
(L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
0,
0,
]);
const OPERATOR_FEE_SCALAR: u64 = 5;
const OPERATOR_FEE_CONST: u64 = 6;
const OPERATOR_FEE: U256 =
U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
const DA_FOOTPRINT_GAS_SCALAR: u16 = 7;
const DA_FOOTPRINT_GAS_SCALAR_U64: u64 =
u64::from_be_bytes([0, DA_FOOTPRINT_GAS_SCALAR as u8, 0, 0, 0, 0, 0, 0]);
const DA_FOOTPRINT_GAS_SCALAR_SLOT_VALUE: U256 =
U256::from_limbs([0, 0, 0, DA_FOOTPRINT_GAS_SCALAR_U64]);
let mut db = InMemoryDB::default();
let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
l1_block_contract
.storage
.insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
l1_block_contract
.storage
.insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
l1_block_contract.storage.insert(
DA_FOOTPRINT_GAS_SCALAR_SLOT,
DA_FOOTPRINT_GAS_SCALAR_SLOT_VALUE,
);
db.insert_account_info(
Address::ZERO,
AccountInfo {
balance: U256::from(6000),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l2_block: Some(BLOCK_NUM + U256::from(1)), operator_fee_scalar: Some(U256::from(2)),
operator_fee_constant: Some(U256::from(50)),
..Default::default()
})
.with_block(BlockEnv {
number: BLOCK_NUM,
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::JOVIAN)
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(10))
.enveloped_tx(Some(bytes!("FACADE")))
.build_fill(),
);
let mut evm = ctx.build_op();
assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert_eq!(
*evm.ctx().chain(),
L1BlockInfo {
l2_block: Some(BLOCK_NUM),
l1_base_fee: L1_BASE_FEE,
l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
empty_ecotone_scalars: false,
l1_fee_overhead: None,
operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
tx_l1_cost: Some(U256::ZERO),
da_footprint_gas_scalar: Some(DA_FOOTPRINT_GAS_SCALAR),
}
);
}
#[test]
fn test_reload_l1_block_info_regolith() {
const BLOCK_NUM: U256 = uint!(200_U256);
const L1_BASE_FEE: U256 = uint!(7_U256);
const L1_FEE_OVERHEAD: U256 = uint!(9_U256);
const L1_BASE_FEE_SCALAR: u64 = 11;
let mut db = InMemoryDB::default();
let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
l1_block_contract
.storage
.insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
use crate::constants::{L1_OVERHEAD_SLOT, L1_SCALAR_SLOT};
l1_block_contract
.storage
.insert(L1_OVERHEAD_SLOT, L1_FEE_OVERHEAD);
l1_block_contract
.storage
.insert(L1_SCALAR_SLOT, U256::from(L1_BASE_FEE_SCALAR));
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l2_block: Some(BLOCK_NUM + U256::from(1)),
..Default::default()
})
.with_block(BlockEnv {
number: BLOCK_NUM,
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut evm = ctx.build_op();
assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert_eq!(
*evm.ctx().chain(),
L1BlockInfo {
l2_block: Some(BLOCK_NUM),
l1_base_fee: L1_BASE_FEE,
l1_fee_overhead: Some(L1_FEE_OVERHEAD),
l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
tx_l1_cost: Some(U256::ZERO),
..Default::default()
}
);
}
#[test]
fn test_reload_l1_block_info_ecotone_pre_isthmus() {
const BLOCK_NUM: U256 = uint!(300_U256);
const L1_BASE_FEE: U256 = uint!(13_U256);
const L1_BLOB_BASE_FEE: U256 = uint!(17_U256);
const L1_BASE_FEE_SCALAR: u64 = 19;
const L1_BLOB_BASE_FEE_SCALAR: u64 = 23;
const L1_FEE_SCALARS: U256 = U256::from_limbs([
0,
(L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
0,
0,
]);
let mut db = InMemoryDB::default();
let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
l1_block_contract
.storage
.insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l2_block: Some(BLOCK_NUM + U256::from(1)),
..Default::default()
})
.with_block(BlockEnv {
number: BLOCK_NUM,
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ECOTONE);
let mut evm = ctx.build_op();
assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert_eq!(
*evm.ctx().chain(),
L1BlockInfo {
l2_block: Some(BLOCK_NUM),
l1_base_fee: L1_BASE_FEE,
l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
empty_ecotone_scalars: false,
l1_fee_overhead: None,
tx_l1_cost: Some(U256::ZERO),
..Default::default()
}
);
}
#[test]
fn test_load_l1_block_info_isthmus_none() {
const BLOCK_NUM: U256 = uint!(100_U256);
const L1_BASE_FEE: U256 = uint!(1_U256);
const L1_BLOB_BASE_FEE: U256 = uint!(2_U256);
const L1_BASE_FEE_SCALAR: u64 = 3;
const L1_BLOB_BASE_FEE_SCALAR: u64 = 4;
const L1_FEE_SCALARS: U256 = U256::from_limbs([
0,
(L1_BASE_FEE_SCALAR << (64 - BASE_FEE_SCALAR_OFFSET * 2)) | L1_BLOB_BASE_FEE_SCALAR,
0,
0,
]);
const OPERATOR_FEE_SCALAR: u64 = 5;
const OPERATOR_FEE_CONST: u64 = 6;
const OPERATOR_FEE: U256 =
U256::from_limbs([OPERATOR_FEE_CONST, OPERATOR_FEE_SCALAR, 0, 0]);
let mut db = InMemoryDB::default();
let l1_block_contract = db.load_account(L1_BLOCK_CONTRACT).unwrap();
l1_block_contract
.storage
.insert(L1_BASE_FEE_SLOT, L1_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_BLOB_BASE_FEE_SLOT, L1_BLOB_BASE_FEE);
l1_block_contract
.storage
.insert(ECOTONE_L1_FEE_SCALARS_SLOT, L1_FEE_SCALARS);
l1_block_contract
.storage
.insert(OPERATOR_FEE_SCALARS_SLOT, OPERATOR_FEE);
db.insert_account_info(
Address::ZERO,
AccountInfo {
balance: U256::from(1000),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_block(BlockEnv {
number: BLOCK_NUM,
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
let mut evm = ctx.build_op();
assert_ne!(evm.ctx().chain().l2_block, Some(BLOCK_NUM));
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert_eq!(
*evm.ctx().chain(),
L1BlockInfo {
l2_block: Some(BLOCK_NUM),
l1_base_fee: L1_BASE_FEE,
l1_base_fee_scalar: U256::from(L1_BASE_FEE_SCALAR),
l1_blob_base_fee: Some(L1_BLOB_BASE_FEE),
l1_blob_base_fee_scalar: Some(U256::from(L1_BLOB_BASE_FEE_SCALAR)),
empty_ecotone_scalars: false,
l1_fee_overhead: None,
operator_fee_scalar: Some(U256::from(OPERATOR_FEE_SCALAR)),
operator_fee_constant: Some(U256::from(OPERATOR_FEE_CONST)),
tx_l1_cost: Some(U256::ZERO),
..Default::default()
}
);
}
#[test]
fn test_remove_l1_cost() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(1049),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l1_base_fee: U256::from(1_000),
l1_fee_overhead: Some(U256::from(1_000)),
l1_base_fee_scalar: U256::from(1_000),
l2_block: Some(U256::from(0)),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(100))
.source_hash(B256::ZERO)
.enveloped_tx(Some(bytes!("FACADE")))
.build()
.unwrap(),
);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
let account = evm.ctx().journal_mut().load_account(caller).unwrap();
assert_eq!(account.info.balance, U256::from(1));
}
#[test]
fn test_remove_operator_cost_isthmus() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(151),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
operator_fee_scalar: Some(U256::from(10_000_000)),
operator_fee_constant: Some(U256::from(50)),
l2_block: Some(U256::from(0)),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS)
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(10))
.enveloped_tx(Some(bytes!("FACADE")))
.build_fill(),
);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
let account = evm.ctx().journal_mut().load_account(caller).unwrap();
assert_eq!(account.info.balance, U256::from(1));
}
#[test]
fn test_remove_operator_cost_jovian() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(2_051),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
operator_fee_scalar: Some(U256::from(2)),
operator_fee_constant: Some(U256::from(50)),
l2_block: Some(U256::from(0)),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::JOVIAN)
.with_tx(
OpTransaction::builder()
.base(TxEnv::builder().gas_limit(10))
.enveloped_tx(Some(bytes!("FACADE")))
.build_fill(),
);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
let account = evm.ctx().journal_mut().load_account(caller).unwrap();
assert_eq!(account.info.balance, U256::from(1));
}
#[test]
fn test_remove_l1_cost_lack_of_funds() {
let caller = Address::ZERO;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(48),
..Default::default()
},
);
let ctx = Context::op()
.with_db(db)
.with_chain(L1BlockInfo {
l1_base_fee: U256::from(1_000),
l1_fee_overhead: Some(U256::from(1_000)),
l1_base_fee_scalar: U256::from(1_000),
l2_block: Some(U256::from(0)),
..Default::default()
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH)
.modify_tx_chained(|tx| {
tx.enveloped_tx = Some(bytes!("FACADE"));
});
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
assert_eq!(
handler.validate_against_state_and_deduct_caller(&mut evm),
Err(EVMError::Transaction(
InvalidTransaction::LackOfFundForMaxFee {
fee: Box::new(U256::from(1048)),
balance: Box::new(U256::from(48)),
}
.into(),
))
);
}
#[test]
fn test_validate_sys_tx() {
let ctx = Context::op()
.modify_tx_chained(|tx| {
tx.deposit.source_hash = B256::from([1u8; 32]);
tx.deposit.is_system_transaction = true;
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
assert_eq!(
handler.validate_env(&mut evm),
Err(EVMError::Transaction(
OpTransactionError::DepositSystemTxPostRegolith
))
);
evm.ctx().modify_cfg(|cfg| cfg.spec = OpSpecId::BEDROCK);
assert!(handler.validate_env(&mut evm).is_ok());
}
#[test]
fn test_validate_deposit_tx() {
let ctx = Context::op()
.modify_tx_chained(|tx| {
tx.deposit.source_hash = B256::from([1u8; 32]);
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
assert!(handler.validate_env(&mut evm).is_ok());
}
#[test]
fn test_validate_tx_against_state_deposit_tx() {
let ctx = Context::op()
.modify_tx_chained(|tx| {
tx.deposit.source_hash = B256::from([1u8; 32]);
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
assert!(handler.validate_env(&mut evm).is_ok());
}
#[test]
fn test_halted_deposit_tx_post_regolith() {
let ctx = Context::op()
.modify_tx_chained(|tx| {
tx.deposit.source_hash = B256::from([1u8; 32]);
})
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH);
let mut evm = ctx.build_op();
let mut handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
assert_eq!(
handler.execution_result(
&mut evm,
FrameResult::Call(CallOutcome {
result: InterpreterResult {
result: InstructionResult::OutOfGas,
output: Default::default(),
gas: Default::default(),
},
memory_offset: Default::default(),
})
),
Err(EVMError::Transaction(
OpTransactionError::HaltedDepositPostRegolith
))
)
}
#[test]
fn test_tx_zero_value_touch_caller() {
let ctx = Context::op();
let mut evm = ctx.build_op();
assert!(!evm
.0
.ctx
.journal_mut()
.load_account(Address::ZERO)
.unwrap()
.is_touched());
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
handler
.validate_against_state_and_deduct_caller(&mut evm)
.unwrap();
assert!(evm
.0
.ctx
.journal_mut()
.load_account(Address::ZERO)
.unwrap()
.is_touched());
}
#[rstest]
#[case::deposit(true)]
#[case::dyn_fee(false)]
fn test_operator_fee_refund(#[case] is_deposit: bool) {
const SENDER: Address = Address::ZERO;
const GAS_PRICE: u128 = 0xFF;
const OP_FEE_MOCK_PARAM: u128 = 0xFFFF;
let ctx = Context::op()
.with_tx(
OpTransaction::builder()
.base(
TxEnv::builder()
.gas_price(GAS_PRICE)
.gas_priority_fee(None)
.caller(SENDER),
)
.enveloped_tx(if is_deposit {
None
} else {
Some(bytes!("FACADE"))
})
.source_hash(if is_deposit {
B256::from([1u8; 32])
} else {
B256::ZERO
})
.build_fill(),
)
.modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
evm.ctx().chain.operator_fee_scalar = Some(U256::from(OP_FEE_MOCK_PARAM));
evm.ctx().chain.operator_fee_constant = Some(U256::from(OP_FEE_MOCK_PARAM));
let mut gas = Gas::new(100);
gas.set_spent(10);
let mut exec_result = FrameResult::Call(CallOutcome::new(
InterpreterResult {
result: InstructionResult::Return,
output: Default::default(),
gas,
},
0..0,
));
handler
.reimburse_caller(&mut evm, &mut exec_result)
.unwrap();
let mut expected_refund =
U256::from(GAS_PRICE * (gas.remaining() + gas.refunded() as u64) as u128);
let op_fee_refund = evm
.ctx()
.chain()
.operator_fee_refund(&gas, OpSpecId::ISTHMUS);
assert!(op_fee_refund > U256::ZERO);
if !is_deposit {
expected_refund += op_fee_refund;
}
let account = evm.ctx().journal_mut().load_account(SENDER).unwrap();
assert_eq!(account.info.balance, expected_refund);
}
#[test]
fn test_tx_low_balance_nonce_unchanged() {
let ctx = Context::op().with_tx(
OpTransaction::builder()
.base(TxEnv::builder().value(U256::from(1000)))
.build_fill(),
);
let mut evm = ctx.build_op();
let handler =
OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame<EthInterpreter>>::new();
let result = handler.validate_against_state_and_deduct_caller(&mut evm);
assert!(matches!(
result.err().unwrap(),
EVMError::Transaction(OpTransactionError::Base(
InvalidTransaction::LackOfFundForMaxFee { .. }
))
));
assert_eq!(
evm.0
.ctx
.journal_mut()
.load_account(Address::ZERO)
.unwrap()
.info
.nonce,
0
);
}
}