code/precompiles/src/dispatch_security.rs
//! Regression coverage for Pashov T1 M-01 and M-02 using the routed precompiles.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
use crate::{PrecompileHandleExt, Precompiles, mock::*};
use codec::Encode;
use fp_evm::{Context, ExitError, PrecompileFailure, PrecompileResult};
use frame_support::dispatch::{DispatchClass, GetDispatchInfo};
use pallet_evm::{GasWeightMapping, PrecompileHandle, PrecompileSet, Runner};
use precompile_utils::{solidity::encode_with_selector, testing::MockHandle};
use sp_core::{H256, U256};
use subtensor_runtime_common::{ProxyType, TaoBalance};
const PROXY_CALL: &str = "proxyCall(bytes32,uint8[],uint8[])";
fn handle(address: u64, input: Vec<u8>, gas: u64) -> MockHandle {
let address = addr_from_index(address);
let mut h = MockHandle::new(
address,
Context {
address,
caller: addr_from_index(0x9911),
apparent_value: U256::zero(),
},
);
h.input = input;
h.gas_limit = gas;
h
}
fn execute(h: &mut MockHandle) -> PrecompileResult {
Precompiles::<Runtime>::new().execute(h).unwrap()
}
fn price() -> u64 {
<Runtime as pallet_evm::Config>::config().gas_transaction_non_zero_data
}
fn proxy_input(call: Vec<u8>) -> Vec<u8> {
encode_with_selector(
selector_u32(PROXY_CALL),
(H256::repeat_byte(0x33), Vec::<u8>::new(), call),
)
}
fn remark() -> RuntimeCall {
RuntimeCall::System(frame_system::Call::remark { remark: vec![] })
}
fn transfer() -> RuntimeCall {
RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: AccountId::from([0x44; 32]),
value: TaoBalance::new(100),
})
}
fn oog(result: PrecompileResult) {
assert_eq!(
result,
Err(PrecompileFailure::Error {
exit_status: ExitError::OutOfGas
})
);
}
fn short_limits() -> usize {
BlockLength::set(frame_system::limits::BlockLength::max_with_normal_ratio(
1024,
sp_runtime::Perbill::from_percent(75),
));
*BlockLength::get().max.get(DispatchClass::Normal) as usize
}
#[test]
fn static_dispatch_rejects_before_transfer_but_normal_dispatch_works() {
new_test_ext().execute_with(|| {
let mut h = handle(6, transfer().encode(), u64::MAX);
let account = mapped_account(h.context.caller);
fund_account(&account, 1000);
h.is_static = true;
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("cannot dispatch in static frame".into())
})
);
assert_eq!(Balances::free_balance(&account), TaoBalance::new(1000));
h.is_static = false;
assert!(execute(&mut h).is_ok());
assert_eq!(Balances::free_balance(&account), TaoBalance::new(900));
assert_eq!(
Balances::free_balance(AccountId::from([0x44; 32])),
TaoBalance::new(100)
);
});
}
#[test]
fn real_contract_staticcall_cannot_dispatch_native_transfer() {
new_test_ext().execute_with(|| {
let caller = addr_from_index(0x9920);
let contract = addr_from_index(0x9921);
fund_account(&mapped_account(caller), 1000);
fund_account(&mapped_account(contract), 1000);
// Executable EVM contract: copy calldata, STATICCALL address 6, return its success word.
// CALLDATASIZE PUSH1 0 PUSH1 0 CALLDATACOPY; push output/input, target, GAS; STATICCALL.
let code = hex::decode("3660006000376000600036600060065afa60005260206000f3").unwrap();
pallet_evm::AccountCodes::<Runtime>::insert(contract, code);
let result = <Runtime as pallet_evm::Config>::Runner::call(
caller,
contract,
transfer().encode(),
U256::zero(),
1_000_000,
None,
None,
None,
vec![],
vec![],
false,
false,
None,
None,
<Runtime as pallet_evm::Config>::config(),
)
.unwrap();
assert!(result.exit_reason.is_succeed(), "{:?}", result.exit_reason);
assert_eq!(result.value, vec![0; 32]);
assert_eq!(
Balances::free_balance(mapped_account(contract)),
TaoBalance::new(1000)
);
assert_eq!(
Balances::free_balance(AccountId::from([0x44; 32])),
TaoBalance::new(0)
);
// The same contract with CALL instead of STATICCALL can perform the transfer.
pallet_evm::AccountCodes::<Runtime>::insert(
contract,
hex::decode("36600060003760006000366000600060065af160005260206000f3").unwrap(),
);
let result = <Runtime as pallet_evm::Config>::Runner::call(
caller,
contract,
transfer().encode(),
U256::zero(),
1_000_000,
None,
None,
None,
vec![],
vec![],
false,
false,
None,
None,
<Runtime as pallet_evm::Config>::config(),
)
.unwrap();
assert!(result.exit_reason.is_succeed());
assert_eq!(U256::from_big_endian(&result.value), U256::one());
assert_eq!(
Balances::free_balance(mapped_account(contract)),
TaoBalance::new(900)
);
assert_eq!(
Balances::free_balance(AccountId::from([0x44; 32])),
TaoBalance::new(100)
);
});
}
#[test]
fn malformed_scale_is_charged_linearly_in_both_entry_points() {
new_test_ext().execute_with(|| {
for len in [1, 32, 513] {
let mut invalid = vec![0; len];
invalid[0] = 255;
let mut h = handle(6, invalid.clone(), u64::MAX);
assert!(execute(&mut h).is_err());
assert_eq!(h.gas_used, len as u64 * price());
let input = proxy_input(invalid);
let expected = (input.len().div_ceil(32) + len) as u64 * price();
let mut h = handle(0x80b, input, u64::MAX);
assert!(execute(&mut h).is_err());
assert_eq!(h.gas_used, expected);
}
});
}
#[test]
fn malformed_proxy_abi_is_charged_before_argument_decoding() {
new_test_ext().execute_with(|| {
for len in [4, 33, 129, 1025] {
let mut input = vec![255; len];
input[..4].copy_from_slice(&selector_u32(PROXY_CALL).to_be_bytes());
let mut h = handle(0x80b, input, u64::MAX);
assert!(execute(&mut h).is_err());
assert_eq!(h.gas_used, len.div_ceil(32) as u64 * price());
}
});
}
#[test]
fn one_gas_rejects_before_scale_or_abi_decoding() {
new_test_ext().execute_with(|| {
for input in [vec![255; 10000], remark().encode()] {
oog(execute(&mut handle(6, input.clone(), 1)));
oog(execute(&mut handle(0x80b, proxy_input(input), 1)));
}
let input = selector_u32(PROXY_CALL).to_be_bytes().to_vec();
oog(execute(&mut handle(0x80b, input, 1)));
});
}
#[test]
fn dispatch_weight_uses_remaining_gas_and_consumes_only_current_frame() {
new_test_ext().execute_with(|| {
let call = remark();
let dispatch_gas = <Runtime as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
call.get_dispatch_info().total_weight(),
)
.saturating_add(1);
let mut h = handle(6, call.encode(), dispatch_gas);
oog(execute(&mut h));
assert_eq!(h.gas_used, h.gas_limit);
// Also exercise the common helper after an earlier precompile charge.
let mut h = handle(0x80b, vec![], dispatch_gas);
h.record_cost(1).unwrap();
assert_eq!(
h.try_dispatch_runtime_call::<Runtime, _>(
call,
frame_system::RawOrigin::Signed(mapped_account(h.context.caller))
),
Err(PrecompileFailure::Error {
exit_status: ExitError::OutOfGas
})
);
assert_eq!(h.gas_used, h.gas_limit);
});
}
#[test]
fn scale_and_proxy_abi_limits_are_checked_before_decoding() {
new_test_ext().execute_with(|| {
let limit = short_limits();
let mut allowed = remark().encode();
allowed.resize(limit, 0);
assert!(execute(&mut handle(6, allowed, u64::MAX)).is_ok());
let mut h = handle(6, vec![255; limit + 1], u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("call input exceeds normal block length".into())
})
);
assert_eq!(h.gas_used, 0);
let input = proxy_input(vec![255; limit]);
let mut h = handle(0x80b, input, u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("The raw call data not correctly encoded".into())
})
);
// Legacy ABI permits both dynamic arrays to reference the same tail. With
// this compact layout, an oversized SCALE call still fits the ABI bound.
let mut input = selector_u32(PROXY_CALL).to_be_bytes().to_vec();
input.extend_from_slice(H256::repeat_byte(0x33).as_bytes());
input.extend(abi_word(U256::from(96)));
input.extend(abi_word(U256::from(96)));
input.extend(abi_word(U256::from(limit + 1)));
for _ in 0..=limit {
input.extend(abi_word(U256::from(255)));
}
let mut h = handle(0x80b, input, u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("call input exceeds normal block length".into())
})
);
let abi_limit = limit * 32 + 164;
let mut input = proxy_input(remark().encode());
input.resize(abi_limit, 0);
// Accepted by the ABI bound: reaches the unchanged proxy authorization check.
let mut h = handle(0x80b, input, u64::MAX);
assert!(format!("{:?}", execute(&mut h)).contains("NotProxy"));
let mut input = vec![255; abi_limit + 1];
input[..4].copy_from_slice(&selector_u32(PROXY_CALL).to_be_bytes());
let mut h = handle(0x80b, input, u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("proxy ABI input exceeds limit".into())
})
);
assert_eq!(h.gas_used, 0);
// A valid, authorized legacy call succeeds exactly at both byte ceilings.
let mut allowed = remark().encode();
allowed.resize(limit, 0);
let input = proxy_input(allowed);
assert_eq!(input.len(), abi_limit);
let mut h = handle(0x80b, input, u64::MAX);
let real = AccountId::from([0x33; 32]);
fund_account(&real, 1000);
assert!(
pallet_subtensor_proxy::Pallet::<Runtime>::add_proxy(
RuntimeOrigin::signed(real),
mapped_account(h.context.caller),
ProxyType::Any,
0
)
.is_ok()
);
assert!(execute(&mut h).is_ok());
});
}
#[test]
fn legacy_proxy_authorization_and_other_selectors_are_preserved() {
new_test_ext().execute_with(|| {
let mut h = handle(0x80b, proxy_input(transfer().encode()), u64::MAX);
let real = AccountId::from([0x33; 32]);
let delegate = mapped_account(h.context.caller);
fund_account(&real, 1000);
assert!(format!("{:?}", execute(&mut h)).contains("NotProxy"));
assert_eq!(Balances::free_balance(&real), TaoBalance::new(1000));
assert!(
pallet_subtensor_proxy::Pallet::<Runtime>::add_proxy(
RuntimeOrigin::signed(real.clone()),
delegate.clone(),
ProxyType::Any,
0
)
.is_ok()
);
let free_before = Balances::free_balance(&real);
assert!(execute(&mut h).is_ok());
assert_eq!(
Balances::free_balance(&real),
free_before - TaoBalance::new(100)
);
assert_eq!(
Balances::free_balance(AccountId::from([0x44; 32])),
TaoBalance::new(100)
);
let input = encode_with_selector(
selector_u32("getProxyDeposit(bytes32)"),
(H256::repeat_byte(0x33),),
);
let mut h = handle(0x80b, input, u64::MAX);
h.is_static = true;
assert!(execute(&mut h).is_ok());
assert_eq!(
h.gas_used,
precompile_utils::prelude::RuntimeHelper::<Runtime>::db_read_gas_cost()
);
});
}
#[test]
fn real_contract_can_handle_one_gas_failures_at_both_entry_points() {
new_test_ext().execute_with(|| {
let caller = addr_from_index(0x9940);
let contract = addr_from_index(0x9941);
fund_account(&mapped_account(caller), 1000);
for (target, input) in [
(6, vec![255; 10000]),
(0x80b, proxy_input(vec![255; 10000])),
] {
// Copy input once, CALL with exactly one forwarded gas, return success flag.
let code = hex::decode(format!(
"36600060003760006000366000600061{target:04x}6001f160005260206000f3"
))
.unwrap();
pallet_evm::AccountCodes::<Runtime>::insert(contract, code);
let result = <Runtime as pallet_evm::Config>::Runner::call(
caller,
contract,
input,
U256::zero(),
5_000_000,
None,
None,
None,
vec![],
vec![],
false,
false,
None,
None,
<Runtime as pallet_evm::Config>::config(),
)
.unwrap();
assert!(result.exit_reason.is_succeed(), "{:?}", result.exit_reason);
assert_eq!(result.value, vec![0; 32]);
}
});
}
#[test]
fn proxy_dispatch_weight_rejection_spends_the_current_frames_remaining_gas() {
new_test_ext().execute_with(|| {
let call = remark();
let proxy = RuntimeCall::Proxy(pallet_subtensor_proxy::Call::proxy {
real: AccountId::from([0x33; 32]),
force_proxy_type: None,
call: Box::new(call.clone()),
});
let gas = <Runtime as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
proxy.get_dispatch_info().total_weight(),
)
.saturating_add(1);
let mut h = handle(0x80b, proxy_input(call.encode()), gas);
oog(execute(&mut h));
assert_eq!(h.gas_used, h.gas_limit);
});
}
#[test]
fn depth_limits_still_reject_nested_scale_after_charging_input() {
new_test_ext().execute_with(|| {
let mut call = remark();
for _ in 0..9 {
call = RuntimeCall::Proxy(pallet_subtensor_proxy::Call::proxy {
real: AccountId::from([0x33; 32]),
force_proxy_type: None,
call: Box::new(call),
});
}
let encoded = call.encode();
let mut h = handle(6, encoded.clone(), u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("decode failed".into())
})
);
assert_eq!(h.gas_used, encoded.len() as u64 * price());
let input = proxy_input(encoded.clone());
let expected = (input.len().div_ceil(32) + encoded.len()) as u64 * price();
let mut h = handle(0x80b, input, u64::MAX);
assert_eq!(
execute(&mut h),
Err(PrecompileFailure::Error {
exit_status: ExitError::Other("The raw call data not correctly encoded".into())
})
);
assert_eq!(h.gas_used, expected);
});
}
#[test]
fn other_proxy_mutators_keep_their_dispatch_gas_and_mapped_origin() {
new_test_ext().execute_with(|| {
let delegate = AccountId::from([0x55; 32]);
let input = encode_with_selector(
selector_u32("addProxy(bytes32,uint8,uint32)"),
(H256::repeat_byte(0x55), 0_u8, 0_u32),
);
let mut h = handle(0x80b, input, u64::MAX);
let owner = mapped_account(h.context.caller);
fund_account(&owner, 1000);
assert!(execute(&mut h).is_ok());
let proxies = pallet_subtensor_proxy::Proxies::<Runtime>::get(&owner).0;
assert_eq!(proxies.len(), 1);
assert_eq!(proxies[0].delegate, delegate);
let expected = <Runtime as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
RuntimeCall::Proxy(pallet_subtensor_proxy::Call::add_proxy {
delegate,
proxy_type: ProxyType::Any,
delay: 0,
})
.get_dispatch_info()
.total_weight(),
);
assert_eq!(h.gas_used, expected);
});
}