use alloy_evm::Database;
use alloy_primitives::Bytes;
use alloy_sol_types::{SolCall, SolError};
use revm::{
context::{ContextTr, LocalContextTr},
handler::FrameResult,
interpreter::{CallInput, CallInputs, CallOutcome, Gas, InstructionResult, InterpreterResult},
};
use crate::{
sandbox::execute_keyless_deploy_call, ExternalEnvTypes, IKeylessDeploy, IMegaAccessControl,
IMegaLimitControl, IOracle, MegaContext, MegaSpecId, OracleEnv, ACCESS_CONTROL_ADDRESS,
DISABLED_BY_PARENT_REVERT_DATA, KEYLESS_DEPLOY_ADDRESS, LIMIT_CONTROL_ADDRESS,
ORACLE_CONTRACT_ADDRESS,
};
pub type InterceptResult = Option<FrameResult>;
alloy_sol_types::sol! {
error NonZeroTransfer();
}
#[inline]
fn peek_selector<CTX: ContextTr>(input: &CallInput, ctx: &CTX) -> Option<[u8; 4]> {
let mut out = [0u8; 4];
match input {
CallInput::Bytes(bytes) => {
if bytes.len() < 4 {
return None;
}
out.copy_from_slice(&bytes[..4]);
Some(out)
}
CallInput::SharedBuffer(range) => {
if range.len() < 4 {
return None;
}
let head = range.start..range.start + 4;
let slice = ctx.local().shared_memory_buffer_slice(head)?;
out.copy_from_slice(&slice);
Some(out)
}
}
}
pub trait SystemContractInterceptor<DB: Database, ExtEnvs: ExternalEnvTypes> {
fn intercept(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
depth: usize,
) -> InterceptResult;
}
pub fn dispatch_system_contract_interceptors<DB: Database, ExtEnvs: ExternalEnvTypes>(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
depth: usize,
) -> InterceptResult {
let spec = ctx.spec;
if spec.is_enabled(OracleHintInterceptor::ACTIVATION_SPEC) {
let hint_result = OracleHintInterceptor::intercept(ctx, call_inputs, depth);
debug_assert!(
hint_result.is_none(),
"OracleHintInterceptor must be side-effect only and never short-circuit",
);
}
if spec.is_enabled(KeylessDeployInterceptor::ACTIVATION_SPEC) {
if let Some(result) = KeylessDeployInterceptor::intercept(ctx, call_inputs, depth) {
return Some(result);
}
}
if spec.is_enabled(AccessControlInterceptor::ACTIVATION_SPEC) {
if let Some(result) = AccessControlInterceptor::intercept(ctx, call_inputs, depth) {
return Some(result);
}
}
if spec.is_enabled(LimitControlInterceptor::ACTIVATION_SPEC) {
if let Some(result) = LimitControlInterceptor::intercept(ctx, call_inputs, depth) {
return Some(result);
}
}
None
}
fn reject_non_zero_transfer(call_inputs: &CallInputs) -> InterceptResult {
if call_inputs.transfer_value().is_some_and(|value| !value.is_zero()) {
return Some(FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Revert,
Bytes::copy_from_slice(&NonZeroTransfer::SELECTOR),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
)));
}
None
}
#[derive(Debug)]
pub struct OracleHintInterceptor;
impl OracleHintInterceptor {
pub const ACTIVATION_SPEC: MegaSpecId = MegaSpecId::REX2;
}
impl<DB: Database, ExtEnvs: ExternalEnvTypes> SystemContractInterceptor<DB, ExtEnvs>
for OracleHintInterceptor
{
fn intercept(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
_depth: usize,
) -> InterceptResult {
if call_inputs.target_address != ORACLE_CONTRACT_ADDRESS {
return None;
}
let is_rex5_enabled = ctx.spec.is_enabled(MegaSpecId::REX5);
if is_rex5_enabled && call_inputs.gas_limit == 0 {
return None;
}
if is_rex5_enabled {
let selector = peek_selector(&call_inputs.input, ctx)?;
if selector != IOracle::sendHintCall::SELECTOR {
return None;
}
let input_bytes = call_inputs.input.bytes(ctx);
let within = ctx
.additional_limit
.borrow_mut()
.record_oracle_hint_bytes(input_bytes.len() as u64);
if !within {
return None;
}
let Ok(call) = IOracle::sendHintCall::abi_decode(&input_bytes) else {
return None;
};
ctx.oracle_env.borrow().on_hint(call_inputs.caller, call.topic, call.data);
return None;
}
let input_bytes = call_inputs.input.bytes(ctx);
if let Ok(call) = IOracle::sendHintCall::abi_decode(&input_bytes) {
ctx.oracle_env.borrow().on_hint(call_inputs.caller, call.topic, call.data);
}
None
}
}
#[derive(Debug)]
pub struct KeylessDeployInterceptor;
impl KeylessDeployInterceptor {
pub const ACTIVATION_SPEC: MegaSpecId = MegaSpecId::REX2;
}
impl<DB: Database, ExtEnvs: ExternalEnvTypes> SystemContractInterceptor<DB, ExtEnvs>
for KeylessDeployInterceptor
{
fn intercept(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
depth: usize,
) -> InterceptResult {
if ctx.is_inside_sandbox() || depth != 0 {
return None;
}
if call_inputs.target_address != KEYLESS_DEPLOY_ADDRESS {
return None;
}
let selector = peek_selector(&call_inputs.input, ctx)?;
if selector != IKeylessDeploy::keylessDeployCall::SELECTOR {
return None;
}
let input_bytes = call_inputs.input.bytes(ctx);
let call = IKeylessDeploy::keylessDeployCall::abi_decode(&input_bytes).ok()?;
Some(execute_keyless_deploy_call(
ctx,
call_inputs,
&call.keylessDeploymentTransaction,
call.gasLimitOverride,
))
}
}
#[derive(Debug)]
pub struct AccessControlInterceptor;
impl AccessControlInterceptor {
pub const ACTIVATION_SPEC: MegaSpecId = MegaSpecId::REX4;
}
impl AccessControlInterceptor {
fn handle_disable<DB: Database, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
caller_journal_depth: usize,
) -> InterceptResult {
if let Some(result) = reject_non_zero_transfer(call_inputs) {
return Some(result);
}
ctx.volatile_data_tracker.borrow_mut().disable_access(caller_journal_depth);
Some(FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return,
Bytes::new(),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
)))
}
fn handle_enable<DB: Database, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
caller_journal_depth: usize,
) -> InterceptResult {
if let Some(result) = reject_non_zero_transfer(call_inputs) {
return Some(result);
}
let success = ctx.volatile_data_tracker.borrow_mut().enable_access(caller_journal_depth);
let result = if success {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return,
Bytes::new(),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
))
} else {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Revert,
Bytes::copy_from_slice(&DISABLED_BY_PARENT_REVERT_DATA),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
))
};
Some(result)
}
fn handle_is_disabled<DB: Database, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
caller_journal_depth: usize,
) -> InterceptResult {
if let Some(result) = reject_non_zero_transfer(call_inputs) {
return Some(result);
}
let disabled =
ctx.volatile_data_tracker.borrow().volatile_access_disabled(caller_journal_depth);
let output =
IMegaAccessControl::isVolatileDataAccessDisabledCall::abi_encode_returns(&disabled);
Some(FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return,
Bytes::from(output),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
)))
}
}
impl<DB: Database, ExtEnvs: ExternalEnvTypes> SystemContractInterceptor<DB, ExtEnvs>
for AccessControlInterceptor
{
fn intercept(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
depth: usize,
) -> InterceptResult {
if call_inputs.target_address != ACCESS_CONTROL_ADDRESS {
return None;
}
let caller_journal_depth = depth;
let selector = peek_selector(&call_inputs.input, ctx)?;
if selector == IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR {
return Self::handle_disable(ctx, call_inputs, caller_journal_depth);
}
if selector == IMegaAccessControl::enableVolatileDataAccessCall::SELECTOR {
return Self::handle_enable(ctx, call_inputs, caller_journal_depth);
}
if selector == IMegaAccessControl::isVolatileDataAccessDisabledCall::SELECTOR {
return Self::handle_is_disabled(ctx, call_inputs, caller_journal_depth);
}
None
}
}
#[derive(Debug)]
pub struct LimitControlInterceptor;
impl LimitControlInterceptor {
pub const ACTIVATION_SPEC: MegaSpecId = MegaSpecId::REX4;
}
impl LimitControlInterceptor {
fn handle_remaining_compute_gas<DB: Database, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
) -> InterceptResult {
if let Some(result) = reject_non_zero_transfer(call_inputs) {
return Some(result);
}
let remaining = ctx.additional_limit.borrow().current_call_remaining_compute_gas();
let output = IMegaLimitControl::remainingComputeGasCall::abi_encode_returns(&remaining);
Some(FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return,
Bytes::from(output),
Gas::new(call_inputs.gas_limit),
),
call_inputs.return_memory_offset.clone(),
)))
}
}
impl<DB: Database, ExtEnvs: ExternalEnvTypes> SystemContractInterceptor<DB, ExtEnvs>
for LimitControlInterceptor
{
fn intercept(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &CallInputs,
_depth: usize,
) -> InterceptResult {
if call_inputs.target_address != LIMIT_CONTROL_ADDRESS {
return None;
}
let selector = peek_selector(&call_inputs.input, ctx)?;
if selector == IMegaLimitControl::remainingComputeGasCall::SELECTOR {
return Self::handle_remaining_compute_gas(ctx, call_inputs);
}
None
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{Bytes, U256};
use alloy_sol_types::SolCall;
use revm::{
bytecode::opcode::{CALLCODE, MSTORE, RETURN},
context::tx::TxEnvBuilder,
};
use crate::{
test_utils::{BytecodeBuilder, MemoryDatabase},
IMegaAccessControl, IMegaLimitControl, MegaContext, MegaEvm, MegaSpecId, MegaTransaction,
LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE,
};
const REMAINING_COMPUTE_GAS_SELECTOR: [u8; 4] =
IMegaLimitControl::remainingComputeGasCall::SELECTOR;
#[test]
fn test_alloy_abi_decode_accepts_selector_plus_trailing_bytes_on_zero_arg_calls() {
let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR;
let exact = selector;
let with_junk: Vec<u8> = selector.iter().copied().chain(core::iter::once(0xff)).collect();
let with_pad: Vec<u8> = selector.iter().copied().chain([0u8; 32]).collect();
assert!(
IMegaAccessControl::disableVolatileDataAccessCall::abi_decode(&exact).is_ok(),
"exact 4-byte selector must decode",
);
assert!(
IMegaAccessControl::disableVolatileDataAccessCall::abi_decode(&with_junk).is_ok(),
"current alloy-sol-types accepts selector+1B junk for empty-tuple decode; if this \
fails the pre-REX5 dispatch path's admission rule has changed — re-spec the \
selector probe before landing",
);
assert!(
IMegaAccessControl::disableVolatileDataAccessCall::abi_decode(&with_pad).is_ok(),
"current alloy-sol-types accepts selector+32B padding for empty-tuple decode; if \
this fails the pre-REX5 dispatch path's admission rule has changed — re-spec the \
selector probe before landing",
);
}
#[test]
fn test_callcode_scheme_guard_skips_interception() {
let code = BytecodeBuilder::default()
.mstore(0x0, REMAINING_COMPUTE_GAS_SELECTOR)
.push_number(0_u64) .push_number(0_u64) .push_number(4_u64) .push_number(0_u64) .push_number(0_u64) .push_address(LIMIT_CONTROL_ADDRESS)
.push_number(100_000_u64) .append(CALLCODE) .push_number(0_u64)
.append(MSTORE)
.push_number(32_u64)
.push_number(0_u64)
.append(RETURN)
.build();
let caller = alloy_primitives::address!("0000000000000000000000000000000000300000");
let contract = alloy_primitives::address!("0000000000000000000000000000000000300001");
let mut db = MemoryDatabase::default()
.account_balance(caller, U256::from(1_000_000))
.account_code(contract, code)
.account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE);
let mut context = MegaContext::new(&mut db, MegaSpecId::REX4);
context.modify_chain(|chain| {
chain.operator_fee_scalar = Some(U256::ZERO);
chain.operator_fee_constant = Some(U256::ZERO);
});
let mut evm = MegaEvm::new(context);
let tx = TxEnvBuilder::default()
.caller(caller)
.call(contract)
.gas_limit(100_000_000)
.build_fill();
let mut tx = MegaTransaction::new(tx);
tx.enveloped_tx = Some(Bytes::new());
let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap();
assert!(result.result.is_success(), "outer tx should succeed");
let output = result.result.output().expect("should have output");
let success_flag = U256::from_be_slice(output);
assert_eq!(
success_flag,
U256::ZERO,
"CALLCODE to system contract must not be intercepted — scheme guard must reject it"
);
}
}