use revm::{
context_interface::{
journaled_state::account::JournaledAccountTr,
result::{HaltReason, InvalidTransaction},
transaction::{AuthorizationTr, TransactionType},
Block, Cfg, ContextTr, Database, JournalTr, LocalContextTr, Transaction,
},
handler::{
evm::FrameTr, handler::EvmTrError, pre_execution, validation, EthFrame, EvmTr, FrameResult,
Handler, MainnetHandler,
},
inspector::{Inspector, InspectorEvmTr, InspectorHandler},
interpreter::{
interpreter::EthInterpreter, interpreter_action::FrameInit, CallInput, CallInputs,
CallScheme, CallValue, CreateInputs, FrameInput, InitialAndFloorGas, SharedMemory,
},
primitives::{hardfork::SpecId, TxKind, U256},
state::Bytecode,
};
use crate::chain::MonadChainContext;
use crate::journal::MonadJournalTr;
use crate::reserve_balance::tracker::ReserveBalanceInit;
use crate::staking::constants::SYSTEM_ADDRESS;
use crate::api::exec::MonadContextTr;
#[derive(Debug, Clone)]
pub struct MonadHandler<EVM, ERROR, FRAME> {
pub mainnet: MainnetHandler<EVM, ERROR, FRAME>,
}
impl<EVM, ERROR, FRAME> MonadHandler<EVM, ERROR, FRAME> {
pub fn new() -> Self {
Self { mainnet: MainnetHandler::default() }
}
}
impl<EVM, ERROR, FRAME> Default for MonadHandler<EVM, ERROR, FRAME> {
fn default() -> Self {
Self::new()
}
}
fn validate_monad_against_state_and_deduct_caller<CTX, ERROR>(
context: &mut CTX,
) -> Result<(), ERROR>
where
CTX: ContextTr,
ERROR: From<InvalidTransaction> + From<<CTX::Db as Database>::Error>,
{
let (block, tx, cfg, journal, _, _) = context.all_mut();
let mut caller = journal.load_account_with_code_mut(tx.caller())?.data;
pre_execution::validate_account_nonce_and_code_with_components(
&caller.account().info,
tx,
cfg,
)?;
let is_balance_check_disabled = cfg.is_balance_check_disabled();
let gas_fee =
U256::from(tx.gas_limit()) * U256::from(tx.effective_gas_price(block.basefee() as u128));
let balance = *caller.balance();
if !is_balance_check_disabled && balance < gas_fee {
return Err(InvalidTransaction::Str("insufficient balance for fee".into()).into());
}
let mut new_balance = balance.saturating_sub(gas_fee);
if is_balance_check_disabled {
new_balance = new_balance.max(tx.value());
}
caller.set_balance(new_balance);
if tx.kind().is_call() {
caller.bump_nonce();
}
Ok(())
}
impl<EVM, ERROR, FRAME> Handler for MonadHandler<EVM, ERROR, FRAME>
where
EVM: EvmTr<Context: MonadContextTr, Frame = FRAME>,
ERROR: EvmTrError<EVM>,
FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
{
type Evm = EVM;
type Error = ERROR;
type HaltReason = HaltReason;
fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
let tx_type = TransactionType::from(evm.ctx().tx().tx_type());
if tx_type == TransactionType::Eip4844 {
return Err(InvalidTransaction::Eip4844NotSupported.into());
}
if tx_type == TransactionType::Eip7702 {
let has_system_authority = evm
.ctx()
.tx()
.authorization_list()
.any(|auth| auth.authority() == Some(SYSTEM_ADDRESS));
if has_system_authority {
return Err(InvalidTransaction::Str(
"system transaction sender is authority".into(),
)
.into());
}
}
let spec = evm.ctx().cfg().spec().into();
validation::validate_tx_env(evm.ctx(), spec).map_err(Into::into)
}
fn pre_execution(
&self,
evm: &mut Self::Evm,
init_and_floor_gas: &mut InitialAndFloorGas,
) -> Result<u64, Self::Error> {
validate_monad_against_state_and_deduct_caller::<_, Self::Error>(evm.ctx())?;
self.load_accounts(evm)?;
let gas = self.apply_eip7702_auth_list(evm, init_and_floor_gas)?;
let sender = evm.ctx().tx().caller();
let basefee = evm.ctx().block().basefee() as u128;
let effective_gas_price = evm.ctx().tx().effective_gas_price(basefee);
let gas_limit = evm.ctx().tx().gas_limit();
let spec = evm.ctx().cfg().spec();
let chain = evm.ctx().chain().clone();
let (sender_is_delegated, sender_account) = {
let sender_account = evm.ctx().journal_mut().load_account_with_code(sender)?.data;
(
sender_account.info.code.as_ref().is_some_and(revm::bytecode::Bytecode::is_eip7702),
sender_account.clone(),
)
};
evm.ctx().journal_mut().reserve_balance_mut().init(ReserveBalanceInit {
chain: &chain,
spec,
sender,
effective_gas_price,
gas_limit,
sender_is_delegated,
sender_account: Some(&sender_account),
});
Ok(gas)
}
fn refund(
&self,
_evm: &mut Self::Evm,
exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
_eip7702_refund: i64,
) {
exec_result.gas_mut().set_refund(0);
}
fn reimburse_caller(
&self,
_evm: &mut Self::Evm,
_exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<(), Self::Error> {
Ok(())
}
fn reward_beneficiary(
&self,
evm: &mut Self::Evm,
_exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
) -> Result<(), Self::Error> {
let ctx = evm.ctx();
let gas_limit = ctx.tx().gas_limit();
let basefee = ctx.block().basefee() as u128;
let effective_gas_price = ctx.tx().effective_gas_price(basefee);
let eth_spec: SpecId = ctx.cfg().spec().into();
let coinbase_gas_price = if eth_spec.is_enabled_in(SpecId::LONDON) {
effective_gas_price.saturating_sub(basefee)
} else {
effective_gas_price
};
let reward = coinbase_gas_price * gas_limit as u128;
let beneficiary = ctx.block().beneficiary();
ctx.journal_mut().balance_incr(beneficiary, U256::from(reward))?;
Ok(())
}
fn first_frame_input(
&mut self,
evm: &mut Self::Evm,
gas_limit: u64,
reservoir: u64,
) -> Result<FrameInit, Self::Error> {
let ctx = evm.ctx_mut();
let mut memory = SharedMemory::new_with_buffer(ctx.local().shared_memory_buffer().clone());
memory.set_memory_limit(ctx.cfg().memory_limit());
let (tx, journal) = ctx.tx_journal_mut();
let input = tx.input().clone();
let frame_input = match tx.kind() {
TxKind::Call(target_address) => {
let account = &journal.load_account_with_code(target_address)?.info;
let (known_bytecode, bytecode_address) = if let Some(delegated_address) =
account.code.as_ref().and_then(Bytecode::eip7702_address)
{
let account = &journal.load_account_with_code(delegated_address)?.info;
(
(account.code_hash(), account.code.clone().unwrap_or_default()),
delegated_address,
)
} else {
(
(account.code_hash(), account.code.clone().unwrap_or_default()),
target_address,
)
};
FrameInput::Call(Box::new(CallInputs {
input: CallInput::Bytes(input),
gas_limit,
reservoir,
bytecode_address,
target_address,
known_bytecode,
caller: tx.caller(),
value: CallValue::Transfer(tx.value()),
scheme: CallScheme::Call,
is_static: false,
return_memory_offset: 0..0,
}))
}
TxKind::Create => FrameInput::Create(Box::new(CreateInputs::new(
tx.caller(),
revm::context_interface::CreateScheme::Create,
tx.value(),
input,
gas_limit,
reservoir,
))),
};
Ok(FrameInit { depth: 0, memory, frame_input })
}
}
impl<EVM, ERROR> InspectorHandler for MonadHandler<EVM, ERROR, EthFrame<EthInterpreter>>
where
EVM: InspectorEvmTr<
Context: MonadContextTr<Chain = MonadChainContext, Journal: MonadJournalTr>,
Frame = EthFrame<EthInterpreter>,
Inspector: Inspector<<<Self as Handler>::Evm as EvmTr>::Context, EthInterpreter>,
>,
ERROR: EvmTrError<EVM>,
{
type IT = EthInterpreter;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
api::builder::MonadBuilder,
api::default_ctx::{monad_context_with_db, DefaultMonad},
};
use revm::{
context::{result::EVMError, Context, TxEnv},
context_interface::{
either::Either,
transaction::{Authorization, RecoveredAuthority, RecoveredAuthorization},
},
database::InMemoryDB,
inspector::NoOpInspector,
primitives::{Address, TxKind, B256},
ExecuteEvm,
};
#[test]
fn test_blob_transaction_rejected() {
let ctx = Context::monad();
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
let tx = TxEnv::builder()
.tx_type(Some(3)) .gas_priority_fee(Some(10))
.blob_hashes(vec![B256::from([5u8; 32])])
.build_fill();
let result = evm.transact(tx);
assert!(matches!(
result,
Err(EVMError::Transaction(InvalidTransaction::Eip4844NotSupported))
));
}
#[test]
fn test_reward_beneficiary_charges_full_gas_limit() {
let caller = Address::from([1u8; 20]);
let coinbase = Address::from([2u8; 20]);
let gas_limit = 100_000u64;
let gas_price = 1_000_000_000u128;
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo {
balance: U256::from(gas_limit as u128 * gas_price * 2),
..Default::default()
},
);
db.insert_account_info(coinbase, revm::state::AccountInfo::default());
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.beneficiary = coinbase;
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.to(Address::from([3u8; 20]))
.value(U256::from(1))
.gas_limit(gas_limit)
.gas_price(gas_price)
.build_fill();
let result = evm.transact(tx).expect("Transaction should succeed");
let coinbase_balance =
result.state.get(&coinbase).map(|a| a.info.balance).unwrap_or_default();
let expected_reward = U256::from(gas_limit as u128 * gas_price);
assert_eq!(
coinbase_balance, expected_reward,
"Coinbase should receive gas_limit * gas_price = {expected_reward}, got {coinbase_balance}"
);
}
#[test]
fn test_no_gas_refund_for_unused_gas() {
let caller = Address::from([1u8; 20]);
let gas_limit = 100_000u64;
let gas_price = 1_000_000_000u128; let initial_balance = U256::from(1_000_000_000_000_000_000u128);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo { balance: initial_balance, ..Default::default() },
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.to(Address::from([3u8; 20]))
.value(U256::from(1000))
.gas_limit(gas_limit)
.gas_price(gas_price)
.build_fill();
let result = evm.transact(tx).expect("Transaction should succeed");
let caller_balance = result.state.get(&caller).map(|a| a.info.balance).unwrap_or_default();
let gas_cost = U256::from(gas_limit as u128 * gas_price);
let value_sent = U256::from(1000);
let expected_balance = initial_balance - gas_cost - value_sent;
assert_eq!(
caller_balance,
expected_balance,
"Caller should be charged full gas_limit, not gas_used. \
Expected {}, got {}. Gas used was {}",
expected_balance,
caller_balance,
result.result.tx_gas_used()
);
assert!(
result.result.tx_gas_used() < gas_limit,
"Gas used ({}) should be less than gas_limit ({})",
result.result.tx_gas_used(),
gas_limit
);
}
#[test]
fn test_refund_counter_is_zero() {
use revm::context_interface::result::ExecutionResult;
let caller = Address::from([1u8; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u128),
..Default::default()
},
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.to(Address::from([3u8; 20]))
.value(U256::from(1))
.gas_limit(50_000)
.gas_price(1_000_000_000u128)
.build_fill();
let result = evm.transact(tx).expect("Transaction should succeed");
match result.result {
ExecutionResult::Success { gas, .. } => {
assert_eq!(
gas.inner_refunded(),
0,
"Refund should be 0 on Monad, got {}",
gas.inner_refunded()
);
}
_ => panic!("Expected successful transaction"),
}
}
#[test]
fn test_rejects_when_balance_cannot_cover_fee() {
let caller = Address::from([1u8; 20]);
let gas_limit = 100_000u64;
let gas_price = 1_000_000_000u128;
let gas_fee = U256::from(gas_limit as u128 * gas_price);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo { balance: gas_fee - U256::from(1), ..Default::default() },
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.to(Address::from([3u8; 20]))
.gas_limit(gas_limit)
.gas_price(gas_price)
.build_fill();
let result = evm.transact(tx);
assert!(
matches!(
result,
Err(EVMError::Transaction(InvalidTransaction::Str(ref msg)))
if msg.as_ref() == "insufficient balance for fee"
),
"Expected fee-only balance rejection, got: {result:?}"
);
}
#[test]
fn test_accepts_transaction_when_only_fee_is_covered() {
let caller = Address::from([1u8; 20]);
let gas_limit = 100_000u64;
let gas_price = 1_000_000_000u128;
let gas_fee = U256::from(gas_limit as u128 * gas_price);
let initial_balance = gas_fee + U256::from(1);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo { balance: initial_balance, ..Default::default() },
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.to(Address::from([3u8; 20]))
.value(U256::from(2))
.gas_limit(gas_limit)
.gas_price(gas_price)
.build_fill();
let result = evm.transact(tx);
assert!(
result.is_ok(),
"Transaction should pass fee-only admission even if value later fails, got: {result:?}"
);
let result = result.expect("fee-only validation should admit the transaction");
let caller_balance = result.state.get(&caller).map(|a| a.info.balance).unwrap_or_default();
assert_eq!(
caller_balance,
U256::from(1),
"Caller should only be charged gas upfront when value transfer fails later"
);
}
#[cfg(feature = "optional_balance_check")]
#[test]
fn test_balance_check_disabled_skips_fee_rejection_and_preserves_value_path() {
let caller = Address::from([1u8; 20]);
let recipient = Address::from([3u8; 20]);
let gas_limit = 100_000u64;
let gas_price = 1_000_000_000u128;
let value = U256::from(2);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo { balance: U256::from(1), ..Default::default() },
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
evm.ctx().cfg.0.disable_balance_check = true;
let tx = TxEnv::builder()
.caller(caller)
.to(recipient)
.value(value)
.gas_limit(gas_limit)
.gas_price(gas_price)
.build_fill();
let result =
evm.transact(tx).expect("disabled balance checks should admit the transaction");
let caller_balance = result.state.get(&caller).map(|a| a.info.balance).unwrap_or_default();
let recipient_balance =
result.state.get(&recipient).map(|a| a.info.balance).unwrap_or_default();
assert_eq!(
caller_balance,
U256::ZERO,
"caller balance should be topped up only enough to send value"
);
assert_eq!(recipient_balance, value, "recipient should receive the transfer value");
}
fn make_recovered_auth(authority: Address) -> RecoveredAuthorization {
RecoveredAuthorization::new_unchecked(
Authorization { chain_id: U256::from(1), address: Address::from([0xAA; 20]), nonce: 0 },
RecoveredAuthority::Valid(authority),
)
}
#[test]
fn test_eip7702_system_sender_authority_rejected() {
let caller = Address::from([1u8; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u128),
..Default::default()
},
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let auth = make_recovered_auth(SYSTEM_ADDRESS);
let tx = TxEnv::builder()
.caller(caller)
.gas_limit(100_000)
.gas_price(1_000_000_000u128)
.gas_priority_fee(Some(1_000_000_000u128))
.kind(TxKind::Call(Address::from([3u8; 20])))
.authorization_list(vec![Either::Right(auth)])
.build_fill();
let result = evm.transact(tx);
match result {
Err(EVMError::Transaction(InvalidTransaction::Str(msg))) => {
assert_eq!(
msg.as_ref(),
"system transaction sender is authority",
"Expected system authority error, got: {msg}"
);
}
Err(e) => panic!("Expected Str transaction error, got: {e:?}"),
Ok(_) => panic!("Expected transaction to be rejected, but it succeeded"),
}
}
#[test]
fn test_eip7702_non_system_authority_accepted() {
let caller = Address::from([1u8; 20]);
let normal_authority = Address::from([0xBB; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u128),
..Default::default()
},
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let auth = make_recovered_auth(normal_authority);
let tx = TxEnv::builder()
.caller(caller)
.gas_limit(100_000)
.gas_price(1_000_000_000u128)
.gas_priority_fee(Some(1_000_000_000u128))
.kind(TxKind::Call(Address::from([3u8; 20])))
.authorization_list(vec![Either::Right(auth)])
.build_fill();
let result = evm.transact(tx);
assert!(
result.is_ok(),
"EIP-7702 with normal authority should be accepted, got: {result:?}"
);
}
#[test]
fn test_eip7702_system_authority_among_multiple_rejected() {
let caller = Address::from([1u8; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
revm::state::AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u128),
..Default::default()
},
);
let ctx = monad_context_with_db(db);
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});
evm.ctx().block.basefee = 0;
let auth_normal = make_recovered_auth(Address::from([0xCC; 20]));
let auth_system = make_recovered_auth(SYSTEM_ADDRESS);
let tx = TxEnv::builder()
.caller(caller)
.gas_limit(100_000)
.gas_price(1_000_000_000u128)
.gas_priority_fee(Some(1_000_000_000u128))
.kind(TxKind::Call(Address::from([3u8; 20])))
.authorization_list(vec![Either::Right(auth_normal), Either::Right(auth_system)])
.build_fill();
let result = evm.transact(tx);
assert!(
matches!(
result,
Err(EVMError::Transaction(InvalidTransaction::Str(ref msg)))
if msg.as_ref() == "system transaction sender is authority"
),
"Expected system authority error, got: {result:?}"
);
}
}