//! 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, 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::::new().execute(h).unwrap() } fn price() -> u64 { ::config().gas_transaction_non_zero_data } fn proxy_input(call: Vec) -> Vec { encode_with_selector( selector_u32(PROXY_CALL), (H256::repeat_byte(0x33), Vec::::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::::insert(contract, code); let result = ::Runner::call( caller, contract, transfer().encode(), U256::zero(), 1_000_000, None, None, None, vec![], vec![], false, false, None, None, ::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::::insert( contract, hex::decode("36600060003760006000366000600060065af160005260206000f3").unwrap(), ); let result = ::Runner::call( caller, contract, transfer().encode(), U256::zero(), 1_000_000, None, None, None, vec![], vec![], false, false, None, None, ::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 = ::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::( 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::::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::::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::::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::::insert(contract, code); let result = ::Runner::call( caller, contract, input, U256::zero(), 5_000_000, None, None, None, vec![], vec![], false, false, None, None, ::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 = ::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::::get(&owner).0; assert_eq!(proxies.len(), 1); assert_eq!(proxies[0].delegate, delegate); let expected = ::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); }); }