use crate::{
optimism_spec_to_generic,
transaction::{
deposit::DepositTransaction, error::OpTransactionError, OpTransactionType, OpTxTrait,
},
wiring::{OptimismContextTrait, OptimismWiring},
OptimismHaltReason, OptimismSpec, OptimismSpecId,
};
use crate::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT};
use core::ops::Mul;
use revm::{
database_interface::Database,
handler::{
mainnet::{self, deduct_caller_inner, validate_block_env, validate_tx_env},
register::EvmHandler,
},
interpreter::{return_ok, return_revert, Gas},
precompile::{secp256r1, PrecompileSpecId},
primitives::{HashMap, U256},
state::Account,
transaction::CommonTxFields,
wiring::{
default::EnvWiring,
result::{
EVMError, EVMResult, EVMResultGeneric, ExecutionResult, InvalidTransaction,
ResultAndState,
},
Block, Transaction,
},
Context, ContextPrecompiles, FrameResult,
};
use std::sync::Arc;
pub fn optimism_handle_register<EvmWiringT>(handler: &mut EvmHandler<'_, EvmWiringT>)
where
EvmWiringT: OptimismWiring,
{
optimism_spec_to_generic!(handler.spec_id, {
handler.validation.env = Arc::new(validate_env::<EvmWiringT, SPEC>);
handler.validation.tx_against_state =
Arc::new(validate_tx_against_state::<EvmWiringT, SPEC>);
handler.pre_execution.load_precompiles = Arc::new(load_precompiles::<EvmWiringT, SPEC>);
handler.pre_execution.load_accounts = Arc::new(load_accounts::<EvmWiringT, SPEC>);
handler.pre_execution.deduct_caller = Arc::new(deduct_caller::<EvmWiringT, SPEC>);
handler.execution.last_frame_return = Arc::new(last_frame_return::<EvmWiringT, SPEC>);
handler.post_execution.refund = Arc::new(refund::<EvmWiringT, SPEC>);
handler.post_execution.reward_beneficiary =
Arc::new(reward_beneficiary::<EvmWiringT, SPEC>);
handler.post_execution.output = Arc::new(output::<EvmWiringT, SPEC>);
handler.post_execution.end = Arc::new(end::<EvmWiringT, SPEC>);
});
}
pub fn validate_env<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
env: &EnvWiring<EvmWiringT>,
) -> EVMResultGeneric<(), EvmWiringT> {
let tx_type = env.tx.tx_type();
if tx_type == OpTransactionType::Deposit {
let tx = env.tx.deposit();
if tx.is_system_transaction() && SPEC::optimism_enabled(OptimismSpecId::REGOLITH) {
return Err(OpTransactionError::DepositSystemTxPostRegolith.into());
}
return Ok(());
}
validate_block_env::<EvmWiringT, SPEC>(&env.block).map_err(EVMError::Header)?;
validate_tx_env::<EvmWiringT, SPEC>(&env.tx, &env.block, &env.cfg)
.map_err(OpTransactionError::Base)?;
Ok(())
}
pub fn validate_tx_against_state<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
) -> EVMResultGeneric<(), EvmWiringT> {
if context.evm.env.tx.tx_type() == OpTransactionType::Deposit {
return Ok(());
}
mainnet::validate_tx_against_state::<EvmWiringT, SPEC>(context)
}
#[inline]
pub fn last_frame_return<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
frame_result: &mut FrameResult,
) -> EVMResultGeneric<(), EvmWiringT> {
let env = context.evm.inner.env();
let is_deposit = env.tx.tx_type() == OpTransactionType::Deposit;
let tx_gas_limit = env.tx.common_fields().gas_limit();
let is_regolith = SPEC::optimism_enabled(OptimismSpecId::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);
match instruction_result {
return_ok!() => {
if !is_deposit || is_regolith {
gas.erase_cost(remaining);
gas.record_refund(refunded);
} else if is_deposit {
let tx = env.tx.deposit();
if tx.is_system_transaction() {
gas.erase_cost(tx_gas_limit);
}
}
}
return_revert!() => {
if !is_deposit || is_regolith {
gas.erase_cost(remaining);
}
}
_ => {}
}
Ok(())
}
#[inline]
pub fn refund<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
gas: &mut Gas,
eip7702_refund: i64,
) {
gas.record_refund(eip7702_refund);
let env = context.evm.inner.env();
let is_deposit = env.tx.tx_type() == OpTransactionType::Deposit;
let is_regolith = SPEC::optimism_enabled(OptimismSpecId::REGOLITH);
let is_gas_refund_disabled = env.cfg.is_gas_refund_disabled() || (is_deposit && !is_regolith);
if !is_gas_refund_disabled {
gas.set_final_refund(SPEC::OPTIMISM_SPEC_ID.is_enabled_in(OptimismSpecId::LONDON));
}
}
#[inline]
pub fn load_precompiles<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
) -> ContextPrecompiles<EvmWiringT> {
let mut precompiles = ContextPrecompiles::new(PrecompileSpecId::from_spec_id(SPEC::SPEC_ID));
if SPEC::optimism_enabled(OptimismSpecId::FJORD) {
precompiles.extend([
secp256r1::P256VERIFY,
])
}
if SPEC::optimism_enabled(OptimismSpecId::GRANITE) {
precompiles.extend([
crate::bn128::pair::GRANITE,
])
}
precompiles
}
#[inline]
pub fn load_accounts<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
) -> EVMResultGeneric<(), EvmWiringT> {
if context.evm.env.tx.tx_type() != OpTransactionType::Deposit {
let l1_block_info =
super::L1BlockInfo::try_fetch(&mut context.evm.inner.db, SPEC::OPTIMISM_SPEC_ID)
.map_err(EVMError::Database)?;
*context.evm.chain.l1_block_info_mut() = Some(l1_block_info);
}
mainnet::load_accounts::<EvmWiringT, SPEC>(context)
}
#[inline]
pub fn deduct_caller<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
) -> EVMResultGeneric<(), EvmWiringT> {
let caller = context.evm.inner.env.tx.common_fields().caller();
let mut caller_account = context
.evm
.inner
.journaled_state
.load_account(caller, &mut context.evm.inner.db)
.map_err(EVMError::Database)?;
let is_deposit = context.evm.inner.env.tx.tx_type() == OpTransactionType::Deposit;
if is_deposit {
let tx = context.evm.inner.env.tx.deposit();
if let Some(mint) = tx.mint() {
caller_account.info.balance += U256::from(mint);
}
}
deduct_caller_inner::<EvmWiringT, SPEC>(caller_account.data, &context.evm.inner.env);
if !is_deposit {
let enveloped_tx = context
.evm
.inner
.env
.tx
.enveloped_tx()
.expect("all not deposit tx have enveloped tx");
let tx_l1_cost = context
.evm
.inner
.chain
.l1_block_info()
.expect("L1BlockInfo should be loaded")
.calculate_tx_l1_cost(enveloped_tx, SPEC::OPTIMISM_SPEC_ID);
if tx_l1_cost.gt(&caller_account.info.balance) {
return Err(EVMError::Transaction(
InvalidTransaction::LackOfFundForMaxFee {
fee: tx_l1_cost.into(),
balance: caller_account.info.balance.into(),
}
.into(),
));
}
caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost);
}
Ok(())
}
#[inline]
pub fn reward_beneficiary<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
gas: &Gas,
) -> EVMResultGeneric<(), EvmWiringT> {
let is_deposit = context.evm.inner.env.tx.tx_type() == OpTransactionType::Deposit;
if !is_deposit {
mainnet::reward_beneficiary::<EvmWiringT, SPEC>(context, gas)?;
}
if !is_deposit {
let l1_block_info = context
.evm
.chain
.l1_block_info()
.expect("L1BlockInfo should be loaded");
let Some(enveloped_tx) = &context.evm.inner.env.tx.enveloped_tx() else {
return Err(EVMError::Custom(
"[OPTIMISM] Failed to load enveloped transaction.".into(),
));
};
let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::OPTIMISM_SPEC_ID);
let mut l1_fee_vault_account = context
.evm
.inner
.journaled_state
.load_account(L1_FEE_RECIPIENT, &mut context.evm.inner.db)
.map_err(EVMError::Database)?;
l1_fee_vault_account.mark_touch();
l1_fee_vault_account.info.balance += l1_cost;
let mut base_fee_vault_account = context
.evm
.inner
.journaled_state
.load_account(BASE_FEE_RECIPIENT, &mut context.evm.inner.db)
.map_err(EVMError::Database)?;
base_fee_vault_account.mark_touch();
base_fee_vault_account.info.balance += context
.evm
.inner
.env
.block
.basefee()
.mul(U256::from(gas.spent() - gas.refunded() as u64));
}
Ok(())
}
#[inline]
pub fn output<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
frame_result: FrameResult,
) -> EVMResult<EvmWiringT> {
let result = mainnet::output::<EvmWiringT>(context, frame_result)?;
if result.result.is_halt() {
let is_deposit = context.evm.inner.env.tx.tx_type() == OpTransactionType::Deposit;
if is_deposit && SPEC::optimism_enabled(OptimismSpecId::REGOLITH) {
return Err(EVMError::Transaction(
OpTransactionError::HaltedDepositPostRegolith,
));
}
}
Ok(result)
}
#[inline]
pub fn end<EvmWiringT: OptimismWiring, SPEC: OptimismSpec>(
context: &mut Context<EvmWiringT>,
evm_output: EVMResult<EvmWiringT>,
) -> EVMResult<EvmWiringT> {
let is_deposit = context.evm.inner.env.tx.tx_type() == OpTransactionType::Deposit;
evm_output.or_else(|err| {
if matches!(err, EVMError::Transaction(_)) && is_deposit {
let tx = context.evm.inner.env.tx.deposit();
let account = {
let mut acc = Account::from(
context
.evm
.inner
.db
.basic(tx.caller())
.unwrap_or_default()
.unwrap_or_default(),
);
acc.info.nonce = acc.info.nonce.saturating_add(1);
acc.info.balance = acc
.info
.balance
.saturating_add(U256::from(tx.mint().unwrap_or_default()));
acc.mark_touch();
acc
};
let state = HashMap::from_iter([(tx.caller(), account)]);
let gas_used = if SPEC::optimism_enabled(OptimismSpecId::REGOLITH)
|| !tx.is_system_transaction()
{
tx.gas_limit()
} else {
0
};
Ok(ResultAndState {
result: ExecutionResult::Halt {
reason: OptimismHaltReason::FailedDeposit,
gas_used,
},
state,
})
} else {
Err(err)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
transaction::deposit::TxDeposit, wiring::OptimismEvmWiring, BedrockSpec, L1BlockInfo,
LatestSpec, OpTransaction, RegolithSpec,
};
use database::InMemoryDB;
use revm::{
database_interface::EmptyDB,
interpreter::{CallOutcome, InstructionResult, InterpreterResult},
primitives::{bytes, Address, Bytes, B256},
state::AccountInfo,
wiring::default::{block::BlockEnv, Env, TxEnv},
};
use std::boxed::Box;
type TestEmptyOpWiring = OptimismEvmWiring<EmptyDB, ()>;
type TestMemOpWiring = OptimismEvmWiring<InMemoryDB, ()>;
fn call_last_frame_return<SPEC>(
env: EnvWiring<TestEmptyOpWiring>,
instruction_result: InstructionResult,
gas: Gas,
) -> Gas
where
SPEC: OptimismSpec,
{
let mut ctx = Context::<TestEmptyOpWiring>::new_with_db(EmptyDB::default());
ctx.evm.inner.env = Box::new(env);
let mut first_frame = FrameResult::Call(CallOutcome::new(
InterpreterResult {
result: instruction_result,
output: Bytes::new(),
gas,
},
0..0,
));
last_frame_return::<TestEmptyOpWiring, SPEC>(&mut ctx, &mut first_frame).unwrap();
refund::<TestEmptyOpWiring, SPEC>(&mut ctx, first_frame.gas_mut(), 0);
*first_frame.gas()
}
#[test]
fn test_revert_gas() {
let mut env = EnvWiring::<TestEmptyOpWiring>::default();
let tx = TxEnv {
gas_limit: 100,
..Default::default()
};
env.tx = OpTransaction::Base {
tx,
enveloped_tx: None,
};
let gas =
call_last_frame_return::<BedrockSpec>(env, 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 mut env = EnvWiring::<TestEmptyOpWiring>::default();
let deposit = TxDeposit {
gas_limit: 100,
source_hash: B256::ZERO,
..Default::default()
};
env.tx = OpTransaction::Deposit(deposit);
let gas =
call_last_frame_return::<RegolithSpec>(env, 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 mut env = EnvWiring::<TestEmptyOpWiring>::default();
let deposit = TxDeposit {
gas_limit: 100,
source_hash: B256::ZERO,
..Default::default()
};
env.tx = OpTransaction::Deposit(deposit);
let mut ret_gas = Gas::new(90);
ret_gas.record_refund(20);
let gas =
call_last_frame_return::<RegolithSpec>(env.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::<RegolithSpec>(env, InstructionResult::Revert, ret_gas);
assert_eq!(gas.remaining(), 90);
assert_eq!(gas.spent(), 10);
assert_eq!(gas.refunded(), 0);
}
#[test]
fn test_consume_gas_sys_deposit_tx() {
let mut env = EnvWiring::<TestEmptyOpWiring>::default();
let deposit = TxDeposit {
gas_limit: 100,
source_hash: B256::ZERO,
..Default::default()
};
env.tx = OpTransaction::Deposit(deposit);
let gas = call_last_frame_return::<BedrockSpec>(env, InstructionResult::Stop, Gas::new(90));
assert_eq!(gas.remaining(), 0);
assert_eq!(gas.spent(), 100);
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 context = Context::<TestMemOpWiring>::new_with_db(db);
*context.evm.chain.l1_block_info_mut() = Some(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()
});
let deposit = TxDeposit {
gas_limit: 100,
mint: Some(10),
source_hash: B256::ZERO,
..Default::default()
};
context.evm.inner.env.tx = OpTransaction::Deposit(deposit);
deduct_caller::<TestMemOpWiring, RegolithSpec>(&mut context).unwrap();
let account = context
.evm
.inner
.journaled_state
.load_account(caller, &mut context.evm.inner.db)
.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(1000),
..Default::default()
},
);
let mut context = Context::<TestMemOpWiring>::new_with_db(db);
*context.evm.chain.l1_block_info_mut() = Some(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()
});
let deposit = TxDeposit {
mint: Some(10),
source_hash: B256::ZERO,
..Default::default()
};
context.evm.inner.env.tx = OpTransaction::Deposit(deposit);
deduct_caller::<TestMemOpWiring, RegolithSpec>(&mut context).unwrap();
let account = context
.evm
.inner
.journaled_state
.load_account(caller, &mut context.evm.inner.db)
.unwrap();
assert_eq!(account.info.balance, U256::from(1010));
}
#[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 mut context = Context::<TestMemOpWiring>::new_with_db(db);
*context.evm.chain.l1_block_info_mut() = Some(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()
});
context.evm.inner.env.tx = OpTransaction::Base {
tx: TxEnv::default(),
enveloped_tx: Some(bytes!("FACADE")),
};
deduct_caller::<TestMemOpWiring, RegolithSpec>(&mut context).unwrap();
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;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo {
balance: U256::from(48),
..Default::default()
},
);
let mut context = Context::<TestMemOpWiring>::new_with_db(db);
*context.evm.chain.l1_block_info_mut() = Some(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()
});
context.evm.inner.env.tx = OpTransaction::Base {
tx: TxEnv::default(),
enveloped_tx: Some(bytes!("FACADE")),
};
assert_eq!(
deduct_caller::<TestMemOpWiring, RegolithSpec>(&mut context),
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 tx = TxDeposit {
is_system_transaction: true,
..Default::default()
};
let env = Env::<BlockEnv, OpTransaction<TxEnv>> {
tx: OpTransaction::Deposit(tx),
..Default::default()
};
assert_eq!(
validate_env::<TestEmptyOpWiring, RegolithSpec>(&env),
Err(EVMError::Transaction(
OpTransactionError::DepositSystemTxPostRegolith
))
);
assert!(validate_env::<TestEmptyOpWiring, BedrockSpec>(&env).is_ok());
}
#[test]
fn test_validate_deposit_tx() {
let tx = TxDeposit {
source_hash: B256::ZERO,
..Default::default()
};
let env = Env::<BlockEnv, OpTransaction<TxEnv>> {
tx: OpTransaction::Deposit(tx),
..Default::default()
};
assert!(validate_env::<TestEmptyOpWiring, RegolithSpec>(&env).is_ok());
}
#[test]
fn test_validate_tx_against_state_deposit_tx() {
let tx = TxDeposit {
source_hash: B256::ZERO,
..Default::default()
};
let env = Env::<BlockEnv, OpTransaction<TxEnv>> {
tx: OpTransaction::Deposit(tx),
..Default::default()
};
assert!(validate_env::<TestEmptyOpWiring, LatestSpec>(&env).is_ok());
}
}