From 119eecf2faa628efcb71e13a0471b1935754ab13 Mon Sep 17 00:00:00 2001 From: Omeraydognn Date: Sun, 16 Aug 2026 01:39:56 +0300 Subject: [PATCH 1/2] fix: prevent staticcall bypass and missing value check in CallFrom --- crates/precompiles/src/call_from.rs | 8 +++++++- crates/precompiles/src/subcall.rs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/precompiles/src/call_from.rs b/crates/precompiles/src/call_from.rs index 328c8764..b4e41649 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; + if let CallValue::Transfer(v) = inputs.value { + if v > U256::ZERO { + return Err(SubcallError::UnexpectedValue); + } + } + // init_subcall overhead: fixed base + per-word charge for the dynamic `bytes data`. let overhead = abi_decode_gas(calldata.len()); @@ -129,7 +135,7 @@ fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallEr value: CallValue::Transfer(U256::ZERO), input: CallInput::Bytes(calldata), gas_limit: child_gas_limit, - is_static: false, + is_static: inputs.is_static, caller: sender, return_memory_offset: 0..0, }; diff --git a/crates/precompiles/src/subcall.rs b/crates/precompiles/src/subcall.rs index 65f989e0..c1527f51 100644 --- a/crates/precompiles/src/subcall.rs +++ b/crates/precompiles/src/subcall.rs @@ -143,6 +143,8 @@ pub enum SubcallError { UnexpectedFrameResult, #[error("insufficient gas: {0}")] InsufficientGas(String), + #[error("unexpected value sent to precompile")] + UnexpectedValue, #[error("internal error: {0}")] InternalError(String), } From b9e114dc63439eb0993107bc6dfacb63ef9bf234 Mon Sep 17 00:00:00 2001 From: Omeraydognn Date: Sun, 16 Aug 2026 12:40:45 +0300 Subject: [PATCH 2/2] refactor: apply defense-in-depth and add value transfer test Addresses reviewer (@osr21) feedback by applying a "defense-in-depth" approach for subcall precompiles. Changes made: - Replaced the `debug_assert!(!inputs.is_static)` with a hard runtime error (`Err(SubcallError::StaticCallNotAllowed)`) to ensure symmetry with the value transfer check, both acting as a secondary defense layer alongside the EVM gate (`ArcEvm::frame_init`). - Retained the value transfer check but aligned it with EVM layer logic (`inputs.transfers_value()`), explicitly documenting it as a belt-and-suspenders assertion. - Added `decode_child_call_rejects_value_transfer` and `decode_child_call_rejects_static_context` unit tests to prove that the precompile explicitly rejects these inputs. --- crates/precompiles/src/call_from.rs | 81 +++++++++++++++++++++++++++-- crates/precompiles/src/subcall.rs | 2 + 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/crates/precompiles/src/call_from.rs b/crates/precompiles/src/call_from.rs index b4e41649..9a626813 100644 --- a/crates/precompiles/src/call_from.rs +++ b/crates/precompiles/src/call_from.rs @@ -106,10 +106,10 @@ fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallEr let target = decoded.target; let calldata = decoded.data; - if let CallValue::Transfer(v) = inputs.value { - if v > U256::ZERO { - return Err(SubcallError::UnexpectedValue); - } + // 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`. @@ -127,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, @@ -135,7 +142,7 @@ fn decode_child_call(inputs: &CallInputs) -> Result<(CallInputs, u64), SubcallEr value: CallValue::Transfer(U256::ZERO), input: CallInput::Bytes(calldata), gas_limit: child_gas_limit, - is_static: inputs.is_static, + is_static: false, caller: sender, return_memory_offset: 0..0, }; @@ -288,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 c1527f51..e0e9eae7 100644 --- a/crates/precompiles/src/subcall.rs +++ b/crates/precompiles/src/subcall.rs @@ -145,6 +145,8 @@ pub enum SubcallError { 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), }