use std::convert::Infallible;
use alloy_primitives::{address, Address, Bytes, TxKind, U256};
use mega_evm::{
constants::rex::NEW_ACCOUNT_STORAGE_GAS_BASE,
test_utils::{BytecodeBuilder, ErrorInjectingDatabase, InjectedDbError, MemoryDatabase},
BucketId, EVMError, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvs, MegaContext, MegaEvm,
MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionError, SaltEnv, TestExternalEnvs,
MIN_BUCKET_SIZE,
};
use revm::{
bytecode::opcode::{CALL, CALLCODE, STOP},
context::{result::ResultAndState, TxEnv},
database::AccountState,
state::Bytecode,
};
const CALLER: Address = address!("2000000000000000000000000000000000000001");
const CALLEE: Address = address!("1000000000000000000000000000000000000001");
const EMPTY_TARGET: Address = address!("3000000000000000000000000000000000000001");
const DELEGATE: Address = address!("4000000000000000000000000000000000000001");
fn set_eip7702_delegation(db: &mut MemoryDatabase, address: Address, delegate_to: Address) {
let bytecode = Bytecode::new_eip7702(delegate_to);
let code_hash = bytecode.hash_slow();
let account = db.load_account(address).unwrap();
account.info.code = Some(bytecode);
account.info.code_hash = code_hash;
account.account_state = AccountState::None;
}
fn callcode_bytecode(target: Address) -> Bytes {
BytecodeBuilder::default()
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(1_u64) .push_address(target)
.push_number(100_000_u64) .append(CALLCODE)
.append(STOP)
.build()
}
fn call_bytecode(target: Address) -> Bytes {
BytecodeBuilder::default()
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(1_u64) .push_address(target)
.push_number(100_000_u64) .append(CALL)
.append(STOP)
.build()
}
#[allow(clippy::too_many_arguments)]
fn transact(
spec: MegaSpecId,
db: &mut MemoryDatabase,
external_envs: &TestExternalEnvs,
caller: Address,
callee: Address,
value: U256,
gas_limit: u64,
) -> Result<ResultAndState<MegaHaltReason>, EVMError<Infallible, MegaTransactionError>> {
let mut context =
MegaContext::new(db, spec).with_external_envs(external_envs.into()).with_tx_runtime_limits(
EvmTxRuntimeLimits::no_limits()
.with_tx_data_size_limit(u64::MAX)
.with_tx_kv_updates_limit(u64::MAX),
);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let mut evm = MegaEvm::new(context);
let tx = TxEnv {
caller,
kind: TxKind::Call(callee),
data: Bytes::new(),
value,
gas_limit,
..Default::default()
};
let mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
alloy_evm::Evm::transact_raw(&mut evm, tx)
}
fn run_with_target_multiplier(spec: MegaSpecId, bytecode: Bytes, target_multiplier: u64) -> u64 {
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000_000_000u64))
.account_balance(CALLEE, U256::from(1_000_000_000u64))
.account_code(CALLEE, bytecode);
let target_bucket = TestExternalEnvs::<Infallible>::bucket_id_for_account(EMPTY_TARGET);
let external_envs = TestExternalEnvs::new()
.with_bucket_capacity(target_bucket, MIN_BUCKET_SIZE as u64 * target_multiplier);
let result = transact(spec, &mut db, &external_envs, CALLER, CALLEE, U256::ZERO, 10_000_000)
.expect("transaction must succeed");
assert!(result.result.is_success(), "execution must succeed: {:?}", result.result);
result.result.gas_used()
}
#[test]
fn test_rex5_callcode_to_empty_no_new_account_storage_gas() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX5, bytecode.clone(), 1);
let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX5, bytecode, 10);
assert_eq!(
gas_mult10, gas_mult1,
"Rex5 CALLCODE must not charge new-account storage gas based on the code-source bucket",
);
}
#[test]
fn test_rex5_callcode_from_eip7702_authority_no_storage_gas() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let run = |authority_multiplier: u64, target_multiplier: u64| -> u64 {
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000_000_000u64))
.account_balance(CALLEE, U256::from(1_000_000_000u64))
.account_code(DELEGATE, bytecode.clone());
set_eip7702_delegation(&mut db, CALLEE, DELEGATE);
let authority_bucket = TestExternalEnvs::<Infallible>::bucket_id_for_account(CALLEE);
let target_bucket = TestExternalEnvs::<Infallible>::bucket_id_for_account(EMPTY_TARGET);
let external_envs = TestExternalEnvs::new()
.with_bucket_capacity(authority_bucket, MIN_BUCKET_SIZE as u64 * authority_multiplier)
.with_bucket_capacity(target_bucket, MIN_BUCKET_SIZE as u64 * target_multiplier);
let result = transact(
MegaSpecId::REX5,
&mut db,
&external_envs,
CALLER,
CALLEE,
U256::ZERO,
10_000_000,
)
.expect("transaction must succeed");
assert!(result.result.is_success(), "execution must succeed: {:?}", result.result);
result.result.gas_used()
};
let gas_baseline = run(1, 1);
let gas_high_authority = run(10, 1);
let gas_high_target = run(1, 10);
assert_eq!(
gas_high_authority, gas_baseline,
"authority bucket multiplier must not affect gas — no new-account charge fires against the authority",
);
assert_eq!(
gas_high_target, gas_baseline,
"code-source bucket multiplier must not affect gas — Rex5 CALLCODE meters against the caller, not the code-source",
);
}
#[test]
fn test_rex4_callcode_to_empty_charges_new_account_storage_gas() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX4, bytecode.clone(), 1);
let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX4, bytecode, 10);
let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9;
assert_eq!(
gas_mult10 - gas_mult1,
expected_extra,
"Rex4 (frozen) must keep charging new-account storage gas against the code-source bucket",
);
}
#[test]
fn test_rex5_call_to_empty_still_charges_new_account_storage_gas() {
let bytecode = call_bytecode(EMPTY_TARGET);
let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX5, bytecode.clone(), 1);
let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX5, bytecode, 10);
let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9;
assert_eq!(
gas_mult10 - gas_mult1,
expected_extra,
"Rex5 CALL must continue to charge new-account storage gas against the target bucket",
);
}
#[test]
fn test_rex4_call_to_empty_charges_new_account_storage_gas() {
let bytecode = call_bytecode(EMPTY_TARGET);
let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX4, bytecode.clone(), 1);
let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX4, bytecode, 10);
let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9;
assert_eq!(
gas_mult10 - gas_mult1,
expected_extra,
"Rex4 CALL must charge new-account storage gas against the target bucket",
);
}
#[derive(Debug)]
struct FailingSaltEnv;
impl SaltEnv for FailingSaltEnv {
type Error = String;
fn get_bucket_capacity(&self, _bucket_id: BucketId) -> Result<u64, String> {
Err("injected salt error".into())
}
fn bucket_id_for_account(_account: Address) -> BucketId {
0
}
fn bucket_id_for_slot(_address: Address, _key: U256) -> BucketId {
0
}
}
fn transact_with_error_db(
spec: MegaSpecId,
db: ErrorInjectingDatabase,
caller: Address,
callee: Address,
gas_limit: u64,
) -> Result<ResultAndState<MegaHaltReason>, EVMError<InjectedDbError, MegaTransactionError>> {
let external_envs = TestExternalEnvs::<Infallible>::new();
let mut context =
MegaContext::new(db, spec).with_external_envs(external_envs.into()).with_tx_runtime_limits(
EvmTxRuntimeLimits::no_limits()
.with_tx_data_size_limit(u64::MAX)
.with_tx_kv_updates_limit(u64::MAX),
);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let mut evm = MegaEvm::new(context);
let tx = TxEnv {
caller,
kind: TxKind::Call(callee),
data: Bytes::new(),
value: U256::ZERO,
gas_limit,
..Default::default()
};
let mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
alloy_evm::Evm::transact_raw(&mut evm, tx)
}
fn transact_with_failing_salt(
spec: MegaSpecId,
db: &mut MemoryDatabase,
caller: Address,
callee: Address,
gas_limit: u64,
) -> Result<ResultAndState<MegaHaltReason>, EVMError<Infallible, MegaTransactionError>> {
let envs: ExternalEnvs<(FailingSaltEnv, EmptyExternalEnv)> =
ExternalEnvs { salt_env: FailingSaltEnv, oracle_env: EmptyExternalEnv };
let mut context = MegaContext::new(db, spec).with_external_envs(envs).with_tx_runtime_limits(
EvmTxRuntimeLimits::no_limits()
.with_tx_data_size_limit(u64::MAX)
.with_tx_kv_updates_limit(u64::MAX),
);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let mut evm = MegaEvm::new(context);
let tx = TxEnv {
caller,
kind: TxKind::Call(callee),
data: Bytes::new(),
value: U256::ZERO,
gas_limit,
..Default::default()
};
let mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
alloy_evm::Evm::transact_raw(&mut evm, tx)
}
#[test]
fn test_callcode_db_error_on_inspect_account() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let inner_db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000_000_000u64))
.account_balance(CALLEE, U256::from(1_000_000_000u64))
.account_code(CALLEE, bytecode);
let mut db = ErrorInjectingDatabase::new(inner_db);
db.fail_on_account = Some(EMPTY_TARGET);
let result = transact_with_error_db(MegaSpecId::REX4, db, CALLER, CALLEE, 1_000_000);
match result {
Err(EVMError::Custom(msg)) => {
assert!(
msg.contains("injected basic()"),
"error message should contain injected error, got: {msg}"
);
}
Err(other) => panic!("expected EVMError::Custom, got: {other:?}"),
Ok(result) => panic!("expected error, got success: {:?}", result.result),
}
}
#[test]
fn test_rex5_callcode_db_error_on_inspect_account() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let inner_db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000_000_000u64))
.account_balance(CALLEE, U256::from(1_000_000_000u64))
.account_code(CALLEE, bytecode);
let mut db = ErrorInjectingDatabase::new(inner_db);
db.fail_on_account = Some(CALLEE);
let result = transact_with_error_db(MegaSpecId::REX5, db, CALLER, CALLEE, 1_000_000);
match result {
Err(EVMError::Database(err)) => {
assert!(
err.to_string().contains("injected basic()"),
"error message should contain injected error, got: {err}"
);
}
Err(other) => panic!("expected EVMError::Database, got: {other:?}"),
Ok(result) => panic!("expected error, got success: {:?}", result.result),
}
}
#[test]
fn test_callcode_salt_error_on_new_account_storage_gas() {
let bytecode = callcode_bytecode(EMPTY_TARGET);
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000_000_000u64))
.account_balance(CALLEE, U256::from(1_000_000_000u64))
.account_code(CALLEE, bytecode);
let result = transact_with_failing_salt(MegaSpecId::REX4, &mut db, CALLER, CALLEE, 1_000_000);
match result {
Err(EVMError::Custom(msg)) => {
assert!(
msg.contains("injected salt error"),
"error message should contain salt error, got: {msg}"
);
}
Err(other) => panic!("expected EVMError::Custom, got: {other:?}"),
Ok(result) => panic!("expected error, got success: {:?}", result.result),
}
}