diff --git a/crates/precompiles/src/call_from.rs b/crates/precompiles/src/call_from.rs index 328c8764..9a626813 100644 --- a/crates/precompiles/src/call_from.rs +++ b/crates/precompiles/src/call_from.rs @@ -106,6 +106,12 @@ fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallEr let target = decoded.target; let calldata = decoded.data; + // Defense-in-depth: the EVM gate (`ArcEvm::frame_init`) already rejects value transfers + // to subcall precompiles. We check it here to document the invariant. + if inputs.transfers_value() { + return Err(SubcallError::UnexpectedValue); + } + // init_subcall overhead: fixed base + per-word charge for the dynamic `bytes data`. let overhead = abi_decode_gas(calldata.len()); @@ -121,6 +127,13 @@ fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallEr #[allow(clippy::arithmetic_side_effects)] let child_gas_limit = available - (available / 64); + // Defense-in-depth: the EVM gate (`ArcEvm::frame_init`) already rejects static calls + // to subcall precompiles. We enforce the invariant here as well for symmetry with + // the value transfer check. + if inputs.is_static { + return Err(SubcallError::StaticCallNotAllowed); + } + let child_inputs = CallInputs { scheme: CallScheme::Call, target_address: target, @@ -282,4 +295,68 @@ mod tests { "is_static mismatch" ); } + + #[test] + fn decode_child_call_rejects_value_transfer() { + use revm::interpreter::interpreter_action::{CallInput, CallScheme, CallValue}; + use alloy_primitives::{address, U256}; + use alloy_sol_types::SolCall; + + let sender = address!("e000000000000000000000000000000000000001"); + let target = address!("c000000000000000000000000000000000000002"); + let calldata = ICallFrom::callFromCall { + sender, + target, + data: vec![].into(), + } + .abi_encode(); + + let inputs = CallInputs { + scheme: CallScheme::Call, + target_address: CALL_FROM_ADDRESS, + bytecode_address: CALL_FROM_ADDRESS, + known_bytecode: None, + value: CallValue::Transfer(U256::from(1)), // Value > 0 + input: CallInput::Bytes(calldata.into()), + gas_limit: 100_000, + is_static: false, + caller: address!("c000000000000000000000000000000000000001"), + return_memory_offset: 0..0, + }; + + let result = decode_child_call(&inputs); + assert!(matches!(result, Err(SubcallError::UnexpectedValue))); + } + + #[test] + fn decode_child_call_rejects_static_context() { + use revm::interpreter::interpreter_action::{CallInput, CallScheme, CallValue}; + use alloy_primitives::{address, U256}; + use alloy_sol_types::SolCall; + + let sender = address!("e000000000000000000000000000000000000001"); + let target = address!("c000000000000000000000000000000000000002"); + let calldata = ICallFrom::callFromCall { + sender, + target, + data: vec![].into(), + } + .abi_encode(); + + let inputs = CallInputs { + scheme: CallScheme::Call, + target_address: CALL_FROM_ADDRESS, + bytecode_address: CALL_FROM_ADDRESS, + known_bytecode: None, + value: CallValue::Transfer(U256::ZERO), + input: CallInput::Bytes(calldata.into()), + gas_limit: 100_000, + is_static: true, // Static context + caller: address!("c000000000000000000000000000000000000001"), + return_memory_offset: 0..0, + }; + + let result = decode_child_call(&inputs); + assert!(matches!(result, Err(SubcallError::StaticCallNotAllowed))); + } } diff --git a/crates/precompiles/src/subcall.rs b/crates/precompiles/src/subcall.rs index 65f989e0..e0e9eae7 100644 --- a/crates/precompiles/src/subcall.rs +++ b/crates/precompiles/src/subcall.rs @@ -143,6 +143,10 @@ pub enum SubcallError { UnexpectedFrameResult, #[error("insufficient gas: {0}")] InsufficientGas(String), + #[error("unexpected value sent to precompile")] + UnexpectedValue, + #[error("subcall precompiles cannot be invoked in static context")] + StaticCallNotAllowed, #[error("internal error: {0}")] InternalError(String), }