use std::convert::Infallible;
use alloy_primitives::{address, Address, Bytes, U256};
use alloy_sol_types::{SolCall, SolError};
use mega_evm::{
test_utils::{BytecodeBuilder, MemoryDatabase},
IMegaAccessControl, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction,
MegaTransactionError, TestExternalEnvs, VolatileDataAccessType, ACCESS_CONTROL_ADDRESS,
ORACLE_CONTRACT_ADDRESS,
};
use revm::{
bytecode::opcode::*,
context::{
result::{EVMError, ResultAndState},
tx::TxEnvBuilder,
ContextTr, TxEnv,
},
interpreter::{CallInputs, CallOutcome, InterpreterTypes},
Inspector,
};
const CALLER: Address = address!("0000000000000000000000000000000000200000");
const PARENT: Address = address!("0000000000000000000000000000000000200001");
const CHILD: Address = address!("0000000000000000000000000000000000200002");
const GRANDCHILD: Address = address!("0000000000000000000000000000000000200003");
const SIBLING: Address = address!("0000000000000000000000000000000000200004");
const DISABLE_VOLATILE_DATA_ACCESS_SELECTOR: [u8; 4] =
IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR;
const ENABLE_VOLATILE_DATA_ACCESS_SELECTOR: [u8; 4] =
IMegaAccessControl::enableVolatileDataAccessCall::SELECTOR;
const IS_VOLATILE_DATA_ACCESS_DISABLED_SELECTOR: [u8; 4] =
IMegaAccessControl::isVolatileDataAccessDisabledCall::SELECTOR;
const VOLATILE_DATA_ACCESS_DISABLED_SELECTOR: [u8; 4] =
IMegaAccessControl::VolatileDataAccessDisabled::SELECTOR;
const DISABLED_BY_PARENT_SELECTOR: [u8; 4] = IMegaAccessControl::DisabledByParent::SELECTOR;
const NOT_INTERCEPTED_SELECTOR: [u8; 4] = IMegaAccessControl::NotIntercepted::SELECTOR;
const NON_ZERO_TRANSFER_SELECTOR: [u8; 4] = IMegaAccessControl::NonZeroTransfer::SELECTOR;
fn decode_volatile_data_access_disabled(
data: &[u8],
) -> IMegaAccessControl::VolatileDataAccessDisabled {
<IMegaAccessControl::VolatileDataAccessDisabled as SolError>::abi_decode(data)
.expect("valid VolatileDataAccessDisabled revert data")
}
fn transact(
db: &mut MemoryDatabase,
tx: TxEnv,
) -> Result<ResultAndState<MegaHaltReason>, EVMError<Infallible, MegaTransactionError>> {
let mut context = MegaContext::new(db, MegaSpecId::REX4);
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 mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
alloy_evm::Evm::transact_raw(&mut evm, tx)
}
fn default_tx(to: Address) -> TxEnv {
TxEnvBuilder::default().caller(CALLER).call(to).gas_limit(100_000_000).build_fill()
}
fn call_disable_volatile_data_access(builder: BytecodeBuilder) -> BytecodeBuilder {
let builder = builder.mstore(0x0, DISABLE_VOLATILE_DATA_ACCESS_SELECTOR);
builder
.push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64) .append(CALL)
.append(POP) }
fn call_enable_volatile_data_access(builder: BytecodeBuilder) -> BytecodeBuilder {
let builder = builder.mstore(0x0, ENABLE_VOLATILE_DATA_ACCESS_SELECTOR);
builder
.push_number(0_u64)
.push_number(0_u64)
.push_number(4_u64)
.push_number(0_u64)
.push_number(0_u64)
.push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64)
.append(CALL)
.append(POP)
}
fn call_is_volatile_data_access_disabled(builder: BytecodeBuilder) -> BytecodeBuilder {
let builder = builder.mstore(0x0, IS_VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
builder
.push_number(32_u64) .push_number(0x20_u64) .push_number(4_u64) .push_number(0_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64)
.append(CALL)
.append(POP) .push_number(32_u64) .push_number(0x20_u64) .append(RETURN)
}
fn append_call(builder: BytecodeBuilder, target: Address, gas: u64) -> BytecodeBuilder {
builder
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_address(target)
.push_number(gas)
.append(CALL)
}
fn append_staticcall(builder: BytecodeBuilder, target: Address, gas: u64) -> BytecodeBuilder {
builder
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_address(target)
.push_number(gas)
.append(STATICCALL)
}
fn append_access_control_staticcall(
builder: BytecodeBuilder,
selector: [u8; 4],
gas: u64,
) -> BytecodeBuilder {
let builder = builder.mstore(0x0, selector);
builder
.push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(gas)
.append(STATICCALL)
}
fn append_delegatecall(builder: BytecodeBuilder, target: Address, gas: u64) -> BytecodeBuilder {
builder
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_address(target)
.push_number(gas)
.append(DELEGATECALL)
}
fn append_log_call_status(builder: BytecodeBuilder) -> BytecodeBuilder {
builder
.push_number(0_u64) .append(MSTORE) .push_number(32_u64) .push_number(0_u64) .append(LOG0)
}
fn assert_log_call_status(
result: &ResultAndState<MegaHaltReason>,
log_index: usize,
expected_success: bool,
) {
let logs = result.result.logs();
assert!(
logs.len() > log_index,
"Expected at least {} log(s), got {}",
log_index + 1,
logs.len()
);
let value = U256::from_be_slice(logs[log_index].data.data.as_ref());
let expected = if expected_success { U256::from(1) } else { U256::ZERO };
assert_eq!(value, expected, "Log[{log_index}] call status: expected {expected}, got {value}");
}
fn append_call_and_return_data(
builder: BytecodeBuilder,
target: Address,
gas: u64,
) -> BytecodeBuilder {
append_call(builder, target, gas)
.append(POP) .append(RETURNDATASIZE) .push_number(0_u64) .push_number(0_u64) .append(RETURNDATACOPY)
.append(RETURNDATASIZE)
.push_number(0_u64) .append(RETURN)
}
#[test]
fn test_inner_call_timestamp_reverts() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
}
#[test]
fn test_caller_frame_is_restricted() {
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = parent_code.append(TIMESTAMP).append(POP).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(
!result.result.is_success(),
"Caller should revert when accessing TIMESTAMP after disabling, got: {:?}",
result.result
);
}
#[test]
fn test_inner_call_without_volatile_access_succeeds() {
let child_code = BytecodeBuilder::default()
.push_number(1_u64)
.push_number(2_u64)
.append(ADD)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Inner call without volatile access should succeed");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_volatile_opcodes_all_revert_in_inner_call() {
let volatile_opcodes: &[(u8, &str)] = &[
(TIMESTAMP, "TIMESTAMP"),
(NUMBER, "NUMBER"),
(COINBASE, "COINBASE"),
(DIFFICULTY, "DIFFICULTY"),
(GASLIMIT, "GASLIMIT"),
(BASEFEE, "BASEFEE"),
(BLOCKHASH, "BLOCKHASH"),
(BLOBBASEFEE, "BLOBBASEFEE"),
(BLOBHASH, "BLOBHASH"),
];
for &(opcode, name) in volatile_opcodes {
let child_code = if opcode == BLOCKHASH || opcode == BLOBHASH {
BytecodeBuilder::default().push_number(0_u64).append(opcode).append(POP).stop().build()
} else {
BytecodeBuilder::default().append(opcode).append(POP).stop().build()
};
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed for opcode {name}");
assert_log_call_status(&result, 0, false);
}
}
#[test]
fn test_revert_data_contains_error_with_access_type() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR, "Selector mismatch");
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Timestamp,
"Access type should be Timestamp (1)"
);
}
#[test]
fn test_nested_call_volatile_access_reverts() {
let grandchild_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let child_code = append_call(BytecodeBuilder::default(), GRANDCHILD, 40_000_000);
let child_code = append_log_call_status(child_code).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code)
.account_code(GRANDCHILD, grandchild_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed even though grandchild reverted");
assert_log_call_status(&result, 0, false);
assert_log_call_status(&result, 1, true);
}
#[test]
fn test_reverted_inner_call_returns_gas() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 10_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let gas_used = result.result.gas_used();
assert!(gas_used < 5_000_000, "Gas used ({gas_used}) should be relatively small");
}
#[test]
fn test_staticcall_also_restricted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_staticcall(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed with STATICCALL");
assert_log_call_status(&result, 0, false);
}
#[test]
fn test_delegatecall_also_restricted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_delegatecall(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed with DELEGATECALL");
assert_log_call_status(&result, 0, false);
}
#[test]
fn test_pre_rex4_no_interception() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX3);
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 mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "On Rex3, volatile access should NOT be disabled");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_balance_non_beneficiary_not_restricted() {
let child_code = BytecodeBuilder::default()
.push_address(CHILD) .append(BALANCE)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "BALANCE on non-beneficiary should not be restricted");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_balance_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code = BytecodeBuilder::default()
.push_address(beneficiary)
.append(BALANCE)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"Access type should be Beneficiary (10)"
);
}
#[test]
fn test_extcodesize_non_beneficiary_not_restricted() {
let child_code = BytecodeBuilder::default()
.push_address(CHILD)
.append(EXTCODESIZE)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "EXTCODESIZE on non-beneficiary should not be restricted");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_parent_accesses_volatile_then_child_restricted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP);
let parent_code = call_disable_volatile_data_access(parent_code);
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Timestamp,
"Child should revert with access type Timestamp even though parent already accessed it"
);
}
#[test]
fn test_sibling_call_not_restricted() {
let c1_code = call_disable_volatile_data_access(BytecodeBuilder::default()).stop().build();
let grandchild_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let c2_code = append_call(BytecodeBuilder::default(), GRANDCHILD, 40_000_000)
.push_number(0_u64) .append(MSTORE) .push_number(32_u64) .push_number(0_u64) .append(RETURN)
.build();
let parent_code = append_call(BytecodeBuilder::default(), CHILD, 50_000_000)
.append(POP) .push_number(32_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_address(SIBLING)
.push_number(50_000_000_u64) .append(CALL)
.append(POP) .push_number(32_u64) .push_number(0_u64) .append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, c1_code) .account_code(SIBLING, c2_code) .account_code(GRANDCHILD, grandchild_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let grandchild_success = U256::from_be_slice(output.as_ref());
assert_eq!(
grandchild_success,
U256::from(1),
"GRANDCHILD should succeed: C2's child should NOT be restricted by C1's disableVolatileDataAccess()"
);
}
#[test]
fn test_enable_after_disable_succeeds() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = call_enable_volatile_data_access(parent_code);
let parent_code = append_call(parent_code, CHILD, 50_000_000)
.push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let child_success = U256::from_be_slice(output.as_ref());
assert_eq!(
child_success,
U256::from(1),
"Child should succeed after parent re-enabled volatile data access"
);
}
#[test]
fn test_enable_by_child_reverts_when_parent_disabled() {
let child_code = call_enable_volatile_data_access(BytecodeBuilder::default());
let child_code = child_code
.append(RETURNDATASIZE)
.push_number(0_u64)
.push_number(0_u64)
.append(RETURNDATACOPY)
.append(RETURNDATASIZE)
.push_number(0_u64)
.append(RETURN)
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(
output.as_ref(),
&DISABLED_BY_PARENT_SELECTOR,
"Child's enable call should revert with DisabledByParent()"
);
}
#[test]
fn test_enable_when_not_disabled_is_noop() {
let parent_code = call_enable_volatile_data_access(BytecodeBuilder::default());
let parent_code = parent_code.stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(
result.result.is_success(),
"enableVolatileDataAccess() when not disabled should succeed"
);
}
#[test]
fn test_staticcall_disable_volatile_data_access_is_intercepted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = append_access_control_staticcall(
BytecodeBuilder::default(),
DISABLE_VOLATILE_DATA_ACCESS_SELECTOR,
100_000,
);
let parent_code = append_log_call_status(parent_code);
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, true);
assert_log_call_status(&result, 1, false);
}
#[test]
fn test_staticcall_enable_volatile_data_access_is_intercepted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_access_control_staticcall(
parent_code,
ENABLE_VOLATILE_DATA_ACCESS_SELECTOR,
100_000,
);
let parent_code = append_log_call_status(parent_code);
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, true);
assert_log_call_status(&result, 1, true);
}
#[test]
fn test_query_returns_false_when_not_disabled() {
let parent_code = call_is_volatile_data_access_disabled(BytecodeBuilder::default()).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let disabled = U256::from_be_slice(output.as_ref());
assert_eq!(disabled, U256::ZERO, "Should return false when not disabled");
}
#[test]
fn test_query_returns_true_when_parent_disabled() {
let child_code = call_is_volatile_data_access_disabled(BytecodeBuilder::default()).build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let disabled = U256::from_be_slice(output.as_ref());
assert_eq!(disabled, U256::from(1), "Should return true when parent disabled");
}
#[test]
fn test_query_returns_true_for_disabling_frame() {
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = call_is_volatile_data_access_disabled(parent_code).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let disabled = U256::from_be_slice(output.as_ref());
assert_eq!(
disabled,
U256::from(1),
"Should return true when queried in the frame that called disable"
);
}
fn direct_access_control_tx(selector: &[u8; 4]) -> TxEnv {
direct_access_control_tx_with_value(selector, U256::ZERO)
}
fn direct_access_control_tx_with_value(selector: &[u8; 4], value: U256) -> TxEnv {
TxEnvBuilder::default()
.caller(CALLER)
.call(ACCESS_CONTROL_ADDRESS)
.value(value)
.gas_limit(100_000_000)
.data(Bytes::copy_from_slice(selector))
.build_fill()
}
#[test]
fn test_direct_tx_disable_volatile_data_access() {
let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(1_000_000));
let result =
transact(&mut db, direct_access_control_tx(&DISABLE_VOLATILE_DATA_ACCESS_SELECTOR))
.unwrap();
assert!(
result.result.is_success(),
"Direct TX to disableVolatileDataAccess should succeed, got: {:?}",
result.result
);
}
#[test]
fn test_direct_tx_disable_volatile_data_access_with_value_reverts() {
let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(1_000_000));
let result = transact(
&mut db,
direct_access_control_tx_with_value(
&DISABLE_VOLATILE_DATA_ACCESS_SELECTOR,
U256::from(1_u64),
),
)
.unwrap();
assert!(
!result.result.is_success(),
"Direct TX with non-zero value should revert, got: {:?}",
result.result
);
let output = result.result.output().expect("Should have output");
assert_eq!(output.len(), 4, "non-zero transfer revert should return selector only");
assert_eq!(
&output[..4],
&NON_ZERO_TRANSFER_SELECTOR,
"non-zero transfer should revert with NonZeroTransfer()"
);
}
#[test]
fn test_direct_tx_enable_volatile_data_access() {
let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(1_000_000));
let result =
transact(&mut db, direct_access_control_tx(&ENABLE_VOLATILE_DATA_ACCESS_SELECTOR)).unwrap();
assert!(
result.result.is_success(),
"Direct TX to enableVolatileDataAccess should succeed, got: {:?}",
result.result
);
}
#[test]
fn test_direct_tx_is_volatile_data_access_disabled() {
let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(1_000_000));
let result =
transact(&mut db, direct_access_control_tx(&IS_VOLATILE_DATA_ACCESS_DISABLED_SELECTOR))
.unwrap();
assert!(
result.result.is_success(),
"Direct TX to isVolatileDataAccessDisabled should succeed, got: {:?}",
result.result
);
let output = result.result.output().expect("Should have output");
let disabled = U256::from_be_slice(output.as_ref());
assert_eq!(disabled, U256::ZERO, "Should return false when called directly by TX");
}
#[test]
fn test_direct_tx_unknown_selector_falls_through_and_reverts_not_intercepted() {
let unknown_selector = [0xde, 0xad, 0xbe, 0xef];
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(ACCESS_CONTROL_ADDRESS, mega_evm::ACCESS_CONTROL_CODE);
let result = transact(&mut db, direct_access_control_tx(&unknown_selector)).unwrap();
assert!(
!result.result.is_success(),
"Unknown selector should fall through and revert, got: {:?}",
result.result
);
let output = result.result.output().expect("Should have output");
assert_eq!(
output.len(),
4,
"Fallback should return only NotIntercepted selector, got {} bytes",
output.len()
);
assert_eq!(
&output[..4],
&NOT_INTERCEPTED_SELECTOR,
"Unknown selector should revert with NotIntercepted()"
);
}
#[derive(Default)]
struct CallTrackingInspector {
calls: Vec<Address>,
call_ends: Vec<Address>,
}
impl<CTX: ContextTr, INTR: InterpreterTypes> Inspector<CTX, INTR> for CallTrackingInspector {
fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
self.calls.push(inputs.target_address);
None
}
fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, _outcome: &mut CallOutcome) {
self.call_ends.push(inputs.target_address);
}
}
#[test]
fn test_inspector_sees_system_contract_call() {
let child_code = BytecodeBuilder::default()
.push_number(1_u64)
.push_number(2_u64)
.append(ADD)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let mut inspector = CallTrackingInspector::default();
let mut evm = MegaEvm::new(context).with_inspector(&mut inspector);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Transaction should succeed");
assert_eq!(
inspector.calls.len(),
3,
"Inspector should see 3 call hooks: top-level + access control + child, got: {:?}",
inspector.calls
);
assert_eq!(inspector.calls[0], PARENT, "First call should be to PARENT");
assert_eq!(
inspector.calls[1], ACCESS_CONTROL_ADDRESS,
"Second call should be to ACCESS_CONTROL_ADDRESS"
);
assert_eq!(inspector.calls[2], CHILD, "Third call should be to CHILD");
assert_eq!(
inspector.call_ends.len(),
3,
"Inspector should see 3 call_end hooks, got: {:?}",
inspector.call_ends
);
assert_eq!(
inspector.call_ends[0], ACCESS_CONTROL_ADDRESS,
"First call_end should be ACCESS_CONTROL_ADDRESS (innermost, returned first)"
);
assert_eq!(inspector.call_ends[1], CHILD, "Second call_end should be CHILD");
assert_eq!(inspector.call_ends[2], PARENT, "Third call_end should be PARENT (outermost)");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_system_contract_call_gas_cost() {
let parent_code = BytecodeBuilder::default().mstore(0x0, DISABLE_VOLATILE_DATA_ACCESS_SELECTOR);
let parent_code = parent_code
.append(GAS) .push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64) .append(CALL)
.append(POP) .append(GAS) .append(SWAP1)
.append(SUB) .push_number(0x20_u64)
.append(MSTORE)
.push_number(32_u64) .push_number(0x20_u64) .append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Transaction should succeed");
let output = result.result.output().expect("Should have output");
let gas_consumed = U256::from_be_slice(output.as_ref()).to::<u64>();
assert!(
gas_consumed < 3000,
"System contract call gas delta should be small (CALL overhead only), got: {gas_consumed}. \
If this is close to 100,000, the child frame gas was not refunded."
);
assert!(
gas_consumed >= 2600,
"Gas consumed ({gas_consumed}) should be at least 2600 (cold CALL cost)"
);
}
#[test]
fn test_blocked_volatile_access_does_not_set_bitmap() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"volatile_data_accessed should be empty after blocked access, got: {:?}",
tracker.get_volatile_data_accessed()
);
assert!(
tracker.get_compute_gas_limit().is_none(),
"compute_gas_limit should not be set after blocked access, got: {:?}",
tracker.get_compute_gas_limit()
);
}
#[test]
fn test_blocked_beneficiary_balance_does_not_set_bitmap() {
let beneficiary = Address::ZERO;
let child_code = BytecodeBuilder::default()
.push_address(beneficiary)
.append(BALANCE)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"volatile_data_accessed should be empty after blocked beneficiary BALANCE, got: {:?}",
tracker.get_volatile_data_accessed()
);
assert!(
tracker.get_compute_gas_limit().is_none(),
"compute_gas_limit should not be set after blocked beneficiary BALANCE, got: {:?}",
tracker.get_compute_gas_limit()
);
}
#[test]
fn test_blocked_oracle_sload_does_not_set_bitmap() {
let oracle_code = BytecodeBuilder::default()
.push_number(0_u64) .append(SLOAD)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, ORACLE_CONTRACT_ADDRESS, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(ORACLE_CONTRACT_ADDRESS, oracle_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"volatile_data_accessed should be empty after blocked oracle SLOAD, got: {:?}",
tracker.get_volatile_data_accessed()
);
assert!(
tracker.get_compute_gas_limit().is_none(),
"compute_gas_limit should not be set after blocked oracle SLOAD, got: {:?}",
tracker.get_compute_gas_limit()
);
}
fn append_callcode(builder: BytecodeBuilder, target: Address, gas: u64) -> BytecodeBuilder {
builder
.push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_number(0_u64) .push_address(target)
.push_number(gas)
.append(CALLCODE)
}
fn transact_with_oracle(
db: &mut MemoryDatabase,
tx: TxEnv,
) -> Result<ResultAndState<MegaHaltReason>, EVMError<Infallible, MegaTransactionError>> {
let external_envs = TestExternalEnvs::<Infallible>::new()
.with_oracle_storage(U256::from(0), U256::from(0x1234));
let mut context =
MegaContext::new(db, MegaSpecId::REX4).with_external_envs((&external_envs).into());
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 mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
alloy_evm::Evm::transact_raw(&mut evm, tx)
}
#[test]
fn test_delegatecall_to_access_control_not_intercepted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = BytecodeBuilder::default().mstore(0x0, DISABLE_VOLATILE_DATA_ACCESS_SELECTOR);
let parent_code = parent_code
.push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64) .append(DELEGATECALL)
.append(POP); let parent_code = append_call(parent_code, CHILD, 50_000_000)
.push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code)
.account_code(ACCESS_CONTROL_ADDRESS, mega_evm::ACCESS_CONTROL_CODE);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let child_success = U256::from_be_slice(output.as_ref());
assert_eq!(
child_success,
U256::from(1),
"Child should succeed because DELEGATECALL to access control was not intercepted"
);
}
#[test]
fn test_callcode_to_access_control_not_intercepted() {
let child_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = BytecodeBuilder::default().mstore(0x0, DISABLE_VOLATILE_DATA_ACCESS_SELECTOR);
let parent_code = parent_code
.push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_number(0_u64) .push_address(ACCESS_CONTROL_ADDRESS)
.push_number(100_000_u64) .append(CALLCODE)
.append(POP); let parent_code = append_call(parent_code, CHILD, 50_000_000)
.push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code)
.account_code(ACCESS_CONTROL_ADDRESS, mega_evm::ACCESS_CONTROL_CODE);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let child_success = U256::from_be_slice(output.as_ref());
assert_eq!(
child_success,
U256::from(1),
"Child should succeed because CALLCODE to access control was not intercepted"
);
}
#[test]
fn test_disable_in_reverted_child_does_not_affect_sibling() {
let child_code = call_disable_volatile_data_access(BytecodeBuilder::default())
.push_number(0_u64) .push_number(0_u64) .append(REVERT)
.build();
let sibling_code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build();
let parent_code = append_call(BytecodeBuilder::default(), CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code);
let parent_code = append_call(parent_code, SIBLING, 50_000_000)
.push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code)
.account_code(SIBLING, sibling_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let output = result.result.output().expect("Should have output");
let sibling_success = U256::from_be_slice(output.as_ref());
assert_eq!(
sibling_success,
U256::from(1),
"SIBLING should succeed: CHILD's disable was cleared when CHILD's frame reverted"
);
}
#[test]
fn test_oracle_sload_reverts_when_volatile_access_disabled() {
let oracle_code = BytecodeBuilder::default()
.push_number(0_u64) .append(SLOAD)
.append(POP)
.stop()
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code =
append_call_and_return_data(parent_code, ORACLE_CONTRACT_ADDRESS, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(ORACLE_CONTRACT_ADDRESS, oracle_code);
let result = transact_with_oracle(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(
&output[..4],
&VOLATILE_DATA_ACCESS_DISABLED_SELECTOR,
"Oracle SLOAD should revert with VolatileDataAccessDisabled error"
);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(decoded.accessType, VolatileDataAccessType::Oracle, "Access type should be Oracle");
}
#[test]
fn test_create_reverts_when_volatile_access_disabled() {
let init_code = BytecodeBuilder::default()
.append(TIMESTAMP)
.append(POP)
.push_number(0_u64) .push_number(0_u64) .append(RETURN)
.build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = parent_code
.mstore(0x40, init_code.clone())
.push_number(init_code.len() as u64) .push_number(0x40_u64) .push_number(0_u64) .append(CREATE)
.push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
let created_address = U256::from_be_slice(output.as_ref());
assert_eq!(
created_address,
U256::ZERO,
"CREATE should fail (return 0) because init code accessed volatile data"
);
}
#[test]
fn test_call_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code = append_call(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"CALL to beneficiary should revert with Beneficiary access type"
);
}
#[test]
fn test_staticcall_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code =
append_staticcall(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"STATICCALL to beneficiary should revert with Beneficiary access type"
);
}
#[test]
fn test_delegatecall_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code =
append_delegatecall(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"DELEGATECALL to beneficiary should revert with Beneficiary access type"
);
}
#[test]
fn test_callcode_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code =
append_callcode(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
let output = result.result.output().expect("Should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"CALLCODE to beneficiary should revert with Beneficiary access type"
);
}
#[test]
fn test_call_non_beneficiary_not_restricted() {
let grandchild_code = BytecodeBuilder::default().stop().build();
let child_code = append_call(BytecodeBuilder::default(), GRANDCHILD, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code)
.account_code(GRANDCHILD, grandchild_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "CALL to non-beneficiary should not be restricted");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_call_beneficiary_not_restricted_without_disable() {
let beneficiary = Address::ZERO;
let child_code = append_call(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = append_call(BytecodeBuilder::default(), CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(
result.result.is_success(),
"CALL to beneficiary should succeed when volatile access is not disabled"
);
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_blocked_call_beneficiary_does_not_pollute_tracker() {
let beneficiary = Address::ZERO;
let child_code = append_call(BytecodeBuilder::default(), beneficiary, 100_000).stop().build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"volatile_data_accessed should be empty after blocked CALL to beneficiary"
);
assert!(
tracker.get_compute_gas_limit().is_none(),
"compute_gas_limit should not be set after blocked CALL to beneficiary"
);
}
#[test]
fn test_selfdestruct_beneficiary_restricted() {
let beneficiary = Address::ZERO;
let child_code =
BytecodeBuilder::default().push_address(beneficiary).append(SELFDESTRUCT).build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call_and_return_data(parent_code, CHILD, 50_000_000).build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
let output = result.result.output().expect("should have output");
assert_eq!(&output[..4], &VOLATILE_DATA_ACCESS_DISABLED_SELECTOR);
let decoded = decode_volatile_data_access_disabled(output);
assert_eq!(
decoded.accessType,
VolatileDataAccessType::Beneficiary,
"SELFDESTRUCT to beneficiary should revert with Beneficiary access type"
);
}
#[test]
fn test_selfdestruct_non_beneficiary_not_restricted() {
let child_code =
BytecodeBuilder::default().push_address(GRANDCHILD).append(SELFDESTRUCT).build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(result.result.is_success(), "SELFDESTRUCT to non-beneficiary should not be restricted");
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_selfdestruct_beneficiary_not_restricted_without_disable() {
let beneficiary = Address::ZERO;
let child_code =
BytecodeBuilder::default().push_address(beneficiary).append(SELFDESTRUCT).build();
let parent_code = append_call(BytecodeBuilder::default(), CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let result = transact(&mut db, default_tx(PARENT)).unwrap();
assert!(
result.result.is_success(),
"SELFDESTRUCT to beneficiary should succeed when volatile access is not disabled"
);
assert_log_call_status(&result, 0, true);
}
#[test]
fn test_blocked_selfdestruct_beneficiary_does_not_pollute_tracker() {
let beneficiary = Address::ZERO;
let child_code =
BytecodeBuilder::default().push_address(beneficiary).append(SELFDESTRUCT).build();
let parent_code = call_disable_volatile_data_access(BytecodeBuilder::default());
let parent_code = append_call(parent_code, CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "Parent tx should succeed");
assert_log_call_status(&result, 0, false);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"volatile_data_accessed should be empty after blocked SELFDESTRUCT to beneficiary"
);
assert!(
tracker.get_compute_gas_limit().is_none(),
"compute_gas_limit should not be set after blocked SELFDESTRUCT to beneficiary"
);
}
#[test]
fn test_selfdestruct_beneficiary_not_restricted_pre_rex4() {
let beneficiary = Address::ZERO;
let child_code =
BytecodeBuilder::default().push_address(beneficiary).append(SELFDESTRUCT).build();
let parent_code = append_call(BytecodeBuilder::default(), CHILD, 50_000_000);
let parent_code = append_log_call_status(parent_code).stop().build();
let mut db = MemoryDatabase::default()
.account_balance(CALLER, U256::from(1_000_000))
.account_code(PARENT, parent_code)
.account_code(CHILD, child_code);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX3);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::from(0));
chain.operator_fee_constant = Some(U256::from(0));
});
let volatile_data_tracker = context.volatile_data_tracker.clone();
let mut evm = MegaEvm::new(context);
let mut tx = MegaTransaction::new(default_tx(PARENT));
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(
result.result.is_success(),
"On Rex3, SELFDESTRUCT to beneficiary should not be restricted"
);
assert_log_call_status(&result, 0, true);
let tracker = volatile_data_tracker.borrow();
assert!(
!tracker.accessed(),
"On Rex3, SELFDESTRUCT to beneficiary should not mark volatile data access"
);
}