use crate::MonadSpecId;
use revm::{
context_interface::cfg::{GasId, GasParams},
handler::instructions::EthInstructions,
interpreter::{
instructions::{instruction_table_gas_changes_spec, Instruction},
interpreter::EthInterpreter,
Host,
},
};
pub type MonadInstructions<CTX> = EthInstructions<EthInterpreter, CTX>;
pub fn monad_gas_params(spec: MonadSpecId) -> GasParams {
let eth_spec = spec.into_eth_spec();
let mut params = GasParams::new_spec(eth_spec);
if MonadSpecId::MonadEight.is_enabled_in(spec) {
params.override_gas([
(GasId::cold_storage_cost(), COLD_SLOAD_COST),
(GasId::cold_storage_additional_cost(), COLD_SLOAD_COST - WARM_STORAGE_READ_COST),
(
GasId::cold_account_additional_cost(),
COLD_ACCOUNT_ACCESS_COST - WARM_STORAGE_READ_COST,
),
]);
}
params
}
pub fn monad_instructions<CTX: Host>(spec: MonadSpecId) -> MonadInstructions<CTX> {
let eth_spec = spec.into_eth_spec();
let mut instructions =
EthInstructions::new(instruction_table_gas_changes_spec(eth_spec), eth_spec);
use crate::memory::opcodes;
use revm::bytecode::opcode::*;
instructions.insert_instruction(CREATE, Instruction::new(opcodes::create::<_, false, _>, 0));
instructions.insert_instruction(CREATE2, Instruction::new(opcodes::create::<_, true, _>, 0));
if MonadSpecId::MonadNine.is_enabled_in(spec) {
use revm::interpreter::instructions::gas;
instructions.insert_instruction(MLOAD, Instruction::new(opcodes::mload, 3));
instructions.insert_instruction(MSTORE, Instruction::new(opcodes::mstore, 3));
instructions.insert_instruction(MSTORE8, Instruction::new(opcodes::mstore8, 3));
instructions.insert_instruction(MCOPY, Instruction::new(opcodes::mcopy, 3));
instructions
.insert_instruction(KECCAK256, Instruction::new(opcodes::keccak256, gas::KECCAK256));
instructions.insert_instruction(CALLDATACOPY, Instruction::new(opcodes::calldatacopy, 3));
instructions.insert_instruction(CODECOPY, Instruction::new(opcodes::codecopy, 3));
instructions
.insert_instruction(RETURNDATACOPY, Instruction::new(opcodes::returndatacopy, 3));
instructions.insert_instruction(
EXTCODECOPY,
Instruction::new(opcodes::extcodecopy, gas::WARM_STORAGE_READ_COST),
);
instructions.insert_instruction(LOG0, Instruction::new(opcodes::log::<0, _>, gas::LOG));
instructions.insert_instruction(LOG1, Instruction::new(opcodes::log::<1, _>, gas::LOG));
instructions.insert_instruction(LOG2, Instruction::new(opcodes::log::<2, _>, gas::LOG));
instructions.insert_instruction(LOG3, Instruction::new(opcodes::log::<3, _>, gas::LOG));
instructions.insert_instruction(LOG4, Instruction::new(opcodes::log::<4, _>, gas::LOG));
instructions
.insert_instruction(CALL, Instruction::new(opcodes::call, gas::WARM_STORAGE_READ_COST));
instructions.insert_instruction(
CALLCODE,
Instruction::new(opcodes::call_code, gas::WARM_STORAGE_READ_COST),
);
instructions.insert_instruction(
DELEGATECALL,
Instruction::new(opcodes::delegate_call, gas::WARM_STORAGE_READ_COST),
);
instructions.insert_instruction(
STATICCALL,
Instruction::new(opcodes::static_call, gas::WARM_STORAGE_READ_COST),
);
instructions.insert_instruction(RETURN, Instruction::new(opcodes::ret, 0));
instructions.insert_instruction(REVERT, Instruction::new(opcodes::revert, 0));
}
instructions
}
pub const COLD_SLOAD_COST: u64 = 8100;
pub const COLD_ACCOUNT_ACCESS_COST: u64 = 10100;
pub const WARM_STORAGE_READ_COST: u64 = 100;
#[cfg(test)]
mod tests {
use super::*;
use crate::{
api::{builder::MonadBuilder, default_ctx::monad_context_with_db},
MonadCfgEnv,
};
use revm::primitives::hardfork::SpecId;
use revm::{
bytecode::opcode,
context::TxEnv,
context_interface::result::{ExecutionResult, HaltReason},
database::InMemoryDB,
handler::EvmTr,
primitives::{Address, Bytes, TxKind, U256},
state::{AccountInfo, Bytecode},
ExecuteEvm,
};
const DUPN_OPCODE: u8 = 0xE6;
const SWAPN_OPCODE: u8 = 0xE7;
const EXCHANGE_OPCODE: u8 = 0xE8;
#[test]
fn test_monad_gas_params_cold_storage_cost() {
let params = monad_gas_params(MonadSpecId::MonadEight);
assert_eq!(params.get(GasId::cold_storage_cost()), COLD_SLOAD_COST);
}
#[test]
fn test_monad_gas_params_cold_storage_additional_cost() {
let params = monad_gas_params(MonadSpecId::MonadEight);
assert_eq!(
params.get(GasId::cold_storage_additional_cost()),
COLD_SLOAD_COST - WARM_STORAGE_READ_COST
);
}
#[test]
fn test_monad_gas_params_cold_account_additional_cost() {
let params = monad_gas_params(MonadSpecId::MonadEight);
assert_eq!(
params.get(GasId::cold_account_additional_cost()),
COLD_ACCOUNT_ACCESS_COST - WARM_STORAGE_READ_COST
);
}
#[test]
fn test_monad_gas_params_warm_storage_unchanged() {
let params = monad_gas_params(MonadSpecId::MonadEight);
assert_eq!(params.get(GasId::warm_storage_read_cost()), WARM_STORAGE_READ_COST);
}
#[test]
fn test_monad_vs_ethereum_cold_costs() {
let monad = monad_gas_params(MonadSpecId::MonadEight);
let eth = GasParams::new_spec(SpecId::PRAGUE);
assert_eq!(monad.get(GasId::cold_storage_cost()), 8100);
assert_eq!(eth.get(GasId::cold_storage_cost()), 2100);
assert_eq!(monad.get(GasId::cold_account_additional_cost()), 10000);
assert_eq!(eth.get(GasId::cold_account_additional_cost()), 2500);
}
fn run_contract(spec: MonadSpecId, code: Vec<u8>) -> ExecutionResult<HaltReason> {
let caller = Address::from([0x11; 20]);
let contract = Address::from([0x22; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo { balance: U256::from(1_000_000u64), ..Default::default() },
);
db.insert_account_info(
contract,
AccountInfo::default().with_code(Bytecode::new_raw(Bytes::from(code))),
);
let ctx = monad_context_with_db(db).with_cfg(MonadCfgEnv::new_with_spec(spec));
let mut evm = ctx.build_monad();
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.kind(TxKind::Call(contract))
.gas_limit(100_000)
.gas_price(0)
.build_fill();
evm.transact(tx).expect("contract call should execute").result
}
fn run_delegated_contract(
spec: MonadSpecId,
target_code: Bytecode,
delegated_address: Address,
delegated_code: Vec<u8>,
extra_accounts: &[(Address, Bytecode)],
) -> ExecutionResult<HaltReason> {
let caller = Address::from([0x11; 20]);
let target = Address::from([0x22; 20]);
let mut db = InMemoryDB::default();
db.insert_account_info(
caller,
AccountInfo { balance: U256::from(1_000_000u64), ..Default::default() },
);
db.insert_account_info(target, AccountInfo::default().with_code(target_code));
db.insert_account_info(
delegated_address,
AccountInfo::default().with_code(Bytecode::new_raw(Bytes::from(delegated_code))),
);
for (address, code) in extra_accounts {
db.insert_account_info(*address, AccountInfo::default().with_code(code.clone()));
}
let ctx = monad_context_with_db(db).with_cfg(MonadCfgEnv::new_with_spec(spec));
let mut evm = ctx.build_monad();
evm.ctx().block.basefee = 0;
let tx = TxEnv::builder()
.caller(caller)
.kind(TxKind::Call(target))
.gas_limit(1_000_000)
.gas_price(0)
.build_fill();
evm.transact(tx).expect("delegated contract call should execute").result
}
#[test]
fn test_clz_is_only_available_on_monad_nine() {
let clz_contract = vec![
opcode::PUSH1,
0x01,
opcode::CLZ,
opcode::PUSH1,
0x00,
opcode::MSTORE,
opcode::PUSH1,
0x20,
opcode::PUSH1,
0x00,
opcode::RETURN,
];
let monad_eight_result = run_contract(MonadSpecId::MonadEight, clz_contract.clone());
assert!(
matches!(
monad_eight_result,
ExecutionResult::Halt { reason: HaltReason::NotActivated, .. }
),
"CLZ should be unavailable before MonadNine, got {monad_eight_result:?}"
);
let monad_nine_result = run_contract(MonadSpecId::MonadNine, clz_contract);
let output = monad_nine_result.output().expect("CLZ should return data on MonadNine");
assert_eq!(
U256::from_be_slice(output.as_ref()),
U256::from(255),
"CLZ(1) should return 255 on MonadNine"
);
}
#[test]
fn test_extended_stack_opcode_bytes_are_unknown_on_monad_nine_and_next() {
for spec in [MonadSpecId::MonadNine, MonadSpecId::MonadNext] {
for opcode in [DUPN_OPCODE, SWAPN_OPCODE, EXCHANGE_OPCODE] {
let result = run_contract(spec, vec![opcode]);
assert!(
matches!(
result,
ExecutionResult::Halt { reason: HaltReason::OpcodeNotFound, .. }
),
"opcode 0x{opcode:02x} should be unavailable on {spec:?}, got {result:?}"
);
}
}
}
#[test]
fn test_jumpdest_after_unknown_extended_stack_opcode_byte_is_reachable() {
let contract = vec![
opcode::PUSH1,
0x04,
opcode::JUMP,
DUPN_OPCODE,
opcode::JUMPDEST,
opcode::PUSH1,
0x2a,
opcode::PUSH1,
0x00,
opcode::MSTORE,
opcode::PUSH1,
0x20,
opcode::PUSH1,
0x00,
opcode::RETURN,
];
for spec in [MonadSpecId::MonadNine, MonadSpecId::MonadNext] {
let result = run_contract(spec, contract.clone());
let output = result.output().expect("jump target should execute successfully");
assert_eq!(
U256::from_be_slice(output.as_ref()),
U256::from(42),
"jumpdest after 0xE6 should remain reachable on {spec:?}"
);
}
}
#[test]
fn test_create_is_rejected_for_delegated_accounts() {
let delegated_address = Address::from([0x33; 20]);
let delegated_code = vec![opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::CREATE];
for spec in [MonadSpecId::MonadEight, MonadSpecId::MonadNine, MonadSpecId::MonadNext] {
let result = run_delegated_contract(
spec,
Bytecode::new_eip7702(delegated_address),
delegated_address,
delegated_code.clone(),
&[],
);
assert!(
matches!(
result,
ExecutionResult::Halt { reason: HaltReason::NotActivated, .. }
),
"CREATE should halt with NotActivated for delegated accounts on {spec:?}, got {result:?}"
);
}
}
#[test]
fn test_create2_is_rejected_for_delegated_accounts() {
let delegated_address = Address::from([0x33; 20]);
let delegated_code =
vec![opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::CREATE2];
for spec in [MonadSpecId::MonadEight, MonadSpecId::MonadNine, MonadSpecId::MonadNext] {
let result = run_delegated_contract(
spec,
Bytecode::new_eip7702(delegated_address),
delegated_address,
delegated_code.clone(),
&[],
);
assert!(
matches!(
result,
ExecutionResult::Halt { reason: HaltReason::NotActivated, .. }
),
"CREATE2 should halt with NotActivated for delegated accounts on {spec:?}, got {result:?}"
);
}
}
#[test]
fn test_nested_delegatecall_to_create2_only_fails_for_delegated_accounts() {
let delegated_address = Address::from([0x33; 20]);
let creator = Address::from([0x44; 20]);
let mut delegated_code =
vec![opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::PUSH20];
delegated_code.extend_from_slice(creator.as_slice());
delegated_code.extend_from_slice(&[
opcode::GAS,
opcode::DELEGATECALL,
opcode::PUSH1,
0x1f,
opcode::JUMPI,
opcode::INVALID,
opcode::JUMPDEST,
opcode::STOP,
]);
let creator_code = Bytecode::new_raw(Bytes::from(vec![
opcode::PUSH0,
opcode::PUSH0,
opcode::PUSH0,
opcode::PUSH0,
opcode::CREATE2,
]));
for spec in [MonadSpecId::MonadEight, MonadSpecId::MonadNine, MonadSpecId::MonadNext] {
let delegated_result = run_delegated_contract(
spec,
Bytecode::new_eip7702(delegated_address),
delegated_address,
delegated_code.clone(),
&[(creator, creator_code.clone())],
);
assert!(
matches!(
delegated_result,
ExecutionResult::Halt { reason: HaltReason::InvalidFEOpcode, .. }
),
"nested delegatecall should hit the INVALID sentinel when delegated CREATE2 fails on {spec:?}, got {delegated_result:?}"
);
let regular_result = run_delegated_contract(
spec,
Bytecode::new_raw(Bytes::from(delegated_code.clone())),
delegated_address,
delegated_code.clone(),
&[(creator, creator_code.clone())],
);
assert!(
matches!(regular_result, ExecutionResult::Success { .. }),
"nested delegatecall should succeed for a regular contract on {spec:?}, got {regular_result:?}"
);
}
}
#[test]
fn test_create_still_succeeds_for_regular_contracts() {
let result = run_contract(
MonadSpecId::MonadNine,
vec![opcode::PUSH0, opcode::PUSH0, opcode::PUSH0, opcode::CREATE, opcode::STOP],
);
assert!(
matches!(result, ExecutionResult::Success { .. }),
"regular CREATE should still succeed, got {result:?}"
);
}
}