Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions crates/precompiles/src/call_from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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,
Expand Down Expand Up @@ -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)));
}
}
4 changes: 4 additions & 0 deletions crates/precompiles/src/subcall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}