Skip to content
Merged
Changes from 3 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
117 changes: 115 additions & 2 deletions precompiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ where
Self(Default::default())
}

/// Precompiles that sign pallet calls as `context.caller` and can move
/// native balance or stake. Direct CALL only.
fn requires_direct_call(address: H160) -> bool {
address == hash(6)
|| address == hash(BalanceTransferPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompileV2::<R>::INDEX)
}

pub fn used_addresses() -> [H160; 33] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Protect every signed-dispatch precompile

This list omits Subnet, Neuron, Alpha, Crowdloan, Leasing, VotingPower, Proxy, and Balance, although each can dispatch a runtime call using an origin derived from context.caller. A contract reached by an EOA can therefore use a foreign frame to invoke those operations as that EOA—the same confused-deputy path this check is intended to close. Restrict every precompile that dispatches as the caller; leave foreign-frame execution only for genuinely read-only or cryptographic precompiles.

Suggested change
fn requires_direct_call(address: H160) -> bool {
address == hash(6)
|| address == hash(BalanceTransferPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompileV2::<R>::INDEX)
}
pub fn used_addresses() -> [H160; 33] {
/// Precompiles that dispatch pallet calls as `context.caller`.
/// Direct CALL only.
fn requires_direct_call(address: H160) -> bool {
address == hash(6)
|| address == hash(BalanceTransferPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompile::<R>::INDEX)
|| address == hash(StakingPrecompileV2::<R>::INDEX)
|| address == hash(SubnetPrecompile::<R>::INDEX)
|| address == hash(NeuronPrecompile::<R>::INDEX)
|| address == hash(AlphaPrecompile::<R>::INDEX)
|| address == hash(CrowdloanPrecompile::<R>::INDEX)
|| address == hash(LeasingPrecompile::<R>::INDEX)
|| address == hash(VotingPowerPrecompile::<R>::INDEX)
|| address == hash(ProxyPrecompile::<R>::INDEX)
|| address == hash(BalancePrecompile::<R>::INDEX)
}

[
hash(1),
Expand Down Expand Up @@ -247,7 +256,19 @@ where
<<R as frame_system::Config>::Lookup as StaticLookup>::Source: From<R::AccountId>,
{
fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
match handle.code_address() {
let code_address = handle.code_address();
if !Self::used_addresses().contains(&code_address) {
return None;
}
if Self::requires_direct_call(code_address) && code_address != handle.context().address {
return Some(Err(PrecompileFailure::Error {
exit_status: ExitError::Other(
"Cannot be called with DELEGATECALL or CALLCODE".into(),
),
}));
}

match code_address {
// Ethereum precompiles :
a if a == hash(1) => Some(ECRecover::execute(handle)),
a if a == hash(2) => Some(Sha256::execute(handle)),
Expand Down Expand Up @@ -381,9 +402,101 @@ fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileF
#[cfg(test)]
mod address_and_selector_tests {
use super::*;
use crate::mock::{Runtime, selector_u32};
use crate::mock::{Runtime, execute_precompile, new_test_ext, selector_u32};
use alloc::collections::BTreeSet;
use codec::Encode;
use fp_evm::Context;
use precompile_utils::testing::MockHandle;

#[test]
fn precompile_set_rejects_mismatched_frame() {
new_test_ext().execute_with(|| {
let code_address = hash(6);
let caller = H160::from_low_u64_be(0xBEEF);
let frame_address = H160::from_low_u64_be(0xDEAD);
let mut handle = MockHandle::new(
code_address,
Context {
address: frame_address,
caller,
apparent_value: U256::zero(),
},
);

assert_eq!(
Precompiles::<Runtime>::new().execute(&mut handle),
Some(Err(PrecompileFailure::Error {
exit_status: ExitError::Other(
"Cannot be called with DELEGATECALL or CALLCODE".into(),
),
}))
);
});
}

#[test]
fn precompile_set_accepts_matching_frame() {
new_test_ext().execute_with(|| {
let result = execute_precompile(
&Precompiles::<Runtime>::new(),
hash(6),
H160::from_low_u64_be(0xBEEF),
alloc::vec::Vec::new(),
U256::zero(),
);
assert_ne!(
result,
Some(Err(PrecompileFailure::Error {
exit_status: ExitError::Other(
"Cannot be called with DELEGATECALL or CALLCODE".into(),
),
}))
);
});
}

#[test]
fn precompile_set_allows_foreign_frame_for_view_precompile() {
new_test_ext().execute_with(|| {
let code_address = hash(TimestampPrecompile::<Runtime>::INDEX);
let mut handle = MockHandle::new(
code_address,
Context {
address: H160::from_low_u64_be(0xDEAD),
caller: H160::from_low_u64_be(0xBEEF),
apparent_value: U256::zero(),
},
);
let result = Precompiles::<Runtime>::new().execute(&mut handle);
assert_ne!(
result,
Some(Err(PrecompileFailure::Error {
exit_status: ExitError::Other(
"Cannot be called with DELEGATECALL or CALLCODE".into(),
),
}))
);
assert!(result.is_some());
});
}

#[test]
fn precompile_set_returns_none_for_unknown_address() {
new_test_ext().execute_with(|| {
let unknown = H160::from_low_u64_be(0x1111);
let frame_address = H160::from_low_u64_be(0xDEAD);
let mut handle = MockHandle::new(
unknown,
Context {
address: frame_address,
caller: H160::from_low_u64_be(0xBEEF),
apparent_value: U256::zero(),
},
);

assert_eq!(Precompiles::<Runtime>::new().execute(&mut handle), None);
});
}

#[test]
fn precompile_addresses_are_unique_and_new_addresses_are_locked() {
Expand Down
Loading