use crate::{
handler::{
mainnet::{self, deduct_caller_inner},
register::EvmHandler,
},
interpreter::{return_ok, return_revert, Gas, InstructionResult},
optimism,
primitives::{
db::Database, spec_to_generic, Account, EVMError, Env, ExecutionResult, HaltReason,
HashMap, InvalidTransaction, OptimismInvalidTransaction, ResultAndState, Spec, SpecId,
SpecId::REGOLITH, U256,
},
Context, ContextPrecompiles, FrameResult,
};
use core::ops::Mul;
use revm_precompile::{secp256r1, PrecompileSpecId};
use std::string::ToString;
use std::sync::Arc;
pub fn optimism_handle_register<DB: Database, EXT>(handler: &mut EvmHandler<'_, EXT, DB>) {
spec_to_generic!(handler.cfg.spec_id, {
handler.validation.env = Arc::new(validate_env::<SPEC, DB>);
handler.validation.tx_against_state = Arc::new(validate_tx_against_state::<SPEC, EXT, DB>);
handler.pre_execution.load_precompiles = Arc::new(load_precompiles::<SPEC, EXT, DB>);
handler.pre_execution.load_accounts = Arc::new(load_accounts::<SPEC, EXT, DB>);
handler.pre_execution.deduct_caller = Arc::new(deduct_caller::<SPEC, EXT, DB>);
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.reward_beneficiary = Arc::new(reward_beneficiary::<SPEC, EXT, DB>);
handler.post_execution.output = Arc::new(output::<SPEC, EXT, DB>);
handler.post_execution.end = Arc::new(end::<SPEC, EXT, DB>);
});
}
pub fn validate_env<SPEC: Spec, DB: Database>(env: &Env) -> Result<(), EVMError<DB::Error>> {
if env.tx.optimism.source_hash.is_some() {
return Ok(());
}
env.validate_block_env::<SPEC>()?;
let tx = &env.tx.optimism;
if tx.is_system_transaction.unwrap_or(false) && SPEC::enabled(SpecId::REGOLITH) {
return Err(InvalidTransaction::OptimismError(
OptimismInvalidTransaction::DepositSystemTxPostRegolith,
)
.into());
}
env.validate_tx::<SPEC>()?;
Ok(())
}
pub fn validate_tx_against_state<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
) -> Result<(), EVMError<DB::Error>> {
if context.evm.inner.env.tx.optimism.source_hash.is_some() {
return Ok(());
}
mainnet::validate_tx_against_state::<SPEC, EXT, DB>(context)
}
#[inline]
pub fn last_frame_return<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
frame_result: &mut FrameResult,
) -> Result<(), EVMError<DB::Error>> {
let env = context.evm.inner.env();
let is_deposit = env.tx.optimism.source_hash.is_some();
let tx_system = env.tx.optimism.is_system_transaction;
let tx_gas_limit = env.tx.gas_limit;
let is_regolith = SPEC::enabled(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 && tx_system.unwrap_or(false) {
gas.erase_cost(tx_gas_limit);
}
}
return_revert!() => {
if !is_deposit || is_regolith {
gas.erase_cost(remaining);
}
}
_ => {}
}
Ok(())
}
#[inline]
pub fn refund<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
gas: &mut Gas,
eip7702_refund: i64,
) {
gas.record_refund(eip7702_refund);
let env = context.evm.inner.env();
let is_deposit = env.tx.optimism.source_hash.is_some();
let is_regolith = SPEC::enabled(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::SPEC_ID.is_enabled_in(SpecId::LONDON));
}
}
#[inline]
pub fn load_precompiles<SPEC: Spec, EXT, DB: Database>() -> ContextPrecompiles<DB> {
let mut precompiles = ContextPrecompiles::new(PrecompileSpecId::from_spec_id(SPEC::SPEC_ID));
if SPEC::enabled(SpecId::FJORD) {
precompiles.extend([
secp256r1::P256VERIFY,
])
}
if SPEC::enabled(SpecId::GRANITE) {
precompiles.extend([
optimism::bn128::pair::GRANITE,
])
}
precompiles
}
#[inline]
pub fn load_accounts<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
) -> Result<(), EVMError<DB::Error>> {
if context.evm.inner.env.tx.optimism.source_hash.is_none() {
let l1_block_info =
crate::optimism::L1BlockInfo::try_fetch(&mut context.evm.inner.db, SPEC::SPEC_ID)
.map_err(EVMError::Database)?;
context.evm.inner.l1_block_info = Some(l1_block_info);
}
mainnet::load_accounts::<SPEC, EXT, DB>(context)
}
#[inline]
pub fn deduct_caller<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
) -> Result<(), EVMError<DB::Error>> {
let mut caller_account = context
.evm
.inner
.journaled_state
.load_account(context.evm.inner.env.tx.caller, &mut context.evm.inner.db)?;
if let Some(mint) = context.evm.inner.env.tx.optimism.mint {
caller_account.info.balance += U256::from(mint);
}
deduct_caller_inner::<SPEC>(caller_account.data, &context.evm.inner.env);
if context.evm.inner.env.tx.optimism.source_hash.is_none() {
let Some(enveloped_tx) = &context.evm.inner.env.tx.optimism.enveloped_tx else {
return Err(EVMError::Custom(
"[OPTIMISM] Failed to load enveloped transaction.".to_string(),
));
};
let tx_l1_cost = context
.evm
.inner
.l1_block_info
.as_ref()
.expect("L1BlockInfo should be loaded")
.calculate_tx_l1_cost(enveloped_tx, SPEC::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(),
},
));
}
caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost);
}
Ok(())
}
#[inline]
pub fn reward_beneficiary<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
gas: &Gas,
) -> Result<(), EVMError<DB::Error>> {
let is_deposit = context.evm.inner.env.tx.optimism.source_hash.is_some();
if !is_deposit {
mainnet::reward_beneficiary::<SPEC, EXT, DB>(context, gas)?;
}
if !is_deposit {
let Some(l1_block_info) = &context.evm.inner.l1_block_info else {
return Err(EVMError::Custom(
"[OPTIMISM] Failed to load L1 block information.".to_string(),
));
};
let Some(enveloped_tx) = &context.evm.inner.env.tx.optimism.enveloped_tx else {
return Err(EVMError::Custom(
"[OPTIMISM] Failed to load enveloped transaction.".to_string(),
));
};
let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::SPEC_ID);
let mut l1_fee_vault_account = context
.evm
.inner
.journaled_state
.load_account(optimism::L1_FEE_RECIPIENT, &mut context.evm.inner.db)?;
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(optimism::BASE_FEE_RECIPIENT, &mut context.evm.inner.db)?;
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<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
frame_result: FrameResult,
) -> Result<ResultAndState, EVMError<DB::Error>> {
let result = mainnet::output::<EXT, DB>(context, frame_result)?;
if result.result.is_halt() {
let is_deposit = context.evm.inner.env.tx.optimism.source_hash.is_some();
if is_deposit && SPEC::enabled(REGOLITH) {
return Err(EVMError::Transaction(InvalidTransaction::OptimismError(
OptimismInvalidTransaction::HaltedDepositPostRegolith,
)));
}
}
Ok(result)
}
#[inline]
pub fn end<SPEC: Spec, EXT, DB: Database>(
context: &mut Context<EXT, DB>,
evm_output: Result<ResultAndState, EVMError<DB::Error>>,
) -> Result<ResultAndState, EVMError<DB::Error>> {
evm_output.or_else(|err| {
if matches!(err, EVMError::Transaction(_))
&& context.evm.inner.env().tx.optimism.source_hash.is_some()
{
let caller = context.evm.inner.env().tx.caller;
let account = {
let mut acc = Account::from(
context
.evm
.db
.basic(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(
context.evm.inner.env().tx.optimism.mint.unwrap_or(0),
));
acc.mark_touch();
acc
};
let state = HashMap::from_iter([(caller, account)]);
let is_system_tx = context
.evm
.env()
.tx
.optimism
.is_system_transaction
.unwrap_or(false);
let gas_used = if SPEC::enabled(REGOLITH) || !is_system_tx {
context.evm.inner.env().tx.gas_limit
} else {
0
};
Ok(ResultAndState {
result: ExecutionResult::Halt {
reason: HaltReason::FailedDeposit,
gas_used,
},
state,
})
} else {
Err(err)
}
})
}
#[cfg(test)]
mod tests {
use revm_interpreter::{CallOutcome, InterpreterResult};
use super::*;
use crate::{
db::{EmptyDB, InMemoryDB},
primitives::{
bytes, state::AccountInfo, Address, BedrockSpec, Bytes, Env, LatestSpec, RegolithSpec,
B256,
},
L1BlockInfo,
};
fn call_last_frame_return<SPEC: Spec>(
env: Env,
instruction_result: InstructionResult,
gas: Gas,
) -> Gas {
let mut ctx = Context::new_empty();
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::<SPEC, _, _>(&mut ctx, &mut first_frame).unwrap();
refund::<SPEC, _, _>(&mut ctx, first_frame.gas_mut(), 0);
*first_frame.gas()
}
#[test]
fn test_revert_gas() {
let mut env = Env::default();
env.tx.gas_limit = 100;
env.tx.optimism.source_hash = 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 = Env::default();
env.tx.gas_limit = 100;
env.tx.optimism.source_hash = Some(B256::ZERO);
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 = Env::default();
env.tx.gas_limit = 100;
env.tx.optimism.source_hash = Some(B256::ZERO);
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 = Env::default();
env.tx.gas_limit = 100;
env.tx.optimism.source_hash = Some(B256::ZERO);
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<(), InMemoryDB> = Context::new_with_db(db);
context.evm.inner.l1_block_info = 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.optimism.enveloped_tx = Some(bytes!(""));
context.evm.inner.env.tx.optimism.mint = Some(10);
deduct_caller::<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<(), InMemoryDB> = Context::new_with_db(db);
context.evm.inner.l1_block_info = 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.optimism.enveloped_tx = Some(bytes!("FACADE"));
context.evm.inner.env.tx.optimism.mint = Some(10);
context.evm.inner.env.tx.optimism.source_hash = Some(B256::ZERO);
deduct_caller::<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<(), InMemoryDB> = Context::new_with_db(db);
context.evm.inner.l1_block_info = 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.optimism.enveloped_tx = Some(bytes!("FACADE"));
deduct_caller::<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<(), InMemoryDB> = Context::new_with_db(db);
context.evm.inner.l1_block_info = 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.optimism.enveloped_tx = Some(bytes!("FACADE"));
assert_eq!(
deduct_caller::<RegolithSpec, (), _>(&mut context),
Err(EVMError::Transaction(
InvalidTransaction::LackOfFundForMaxFee {
fee: Box::new(U256::from(1048)),
balance: Box::new(U256::from(48)),
},
))
);
}
#[test]
fn test_validate_sys_tx() {
let mut env = Env::default();
env.tx.optimism.is_system_transaction = Some(true);
assert_eq!(
validate_env::<RegolithSpec, EmptyDB>(&env),
Err(EVMError::Transaction(InvalidTransaction::OptimismError(
OptimismInvalidTransaction::DepositSystemTxPostRegolith
)))
);
assert!(validate_env::<BedrockSpec, EmptyDB>(&env).is_ok());
}
#[test]
fn test_validate_deposit_tx() {
let mut env = Env::default();
env.tx.optimism.source_hash = Some(B256::ZERO);
assert!(validate_env::<RegolithSpec, EmptyDB>(&env).is_ok());
}
#[test]
fn test_validate_tx_against_state_deposit_tx() {
let mut env = Env::default();
env.tx.optimism.source_hash = Some(B256::ZERO);
assert!(validate_env::<LatestSpec, EmptyDB>(&env).is_ok());
}
}