diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index eaaee70d27..943f534d4b 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -8,15 +8,23 @@ mod tests; pub mod types; use crate::types::{FunctionId, Output}; -use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::{DebugNoBound, traits::Get}; +use codec::{Decode, DecodeLimit, Encode, MaxEncodedLen}; +use frame_support::{ + BoundedVec, DebugNoBound, + dispatch::GetDispatchInfo, + traits::{ConstU32, Get}, +}; use frame_system::RawOrigin; use pallet_contracts::chain_extension::{ BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, }; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_proxy::WeightInfo; -use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; +use sp_runtime::{ + DispatchError, Weight, + traits::{Dispatchable, StaticLookup}, +}; +use sp_std::boxed::Box; use sp_std::marker::PhantomData; use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaCurrency, NetUid, ProxyType, TaoCurrency}; @@ -530,6 +538,69 @@ where Ok(RetVal::Converging(Output::Success as u32)) } + FunctionId::ProxyCall => { + // 1. Read params: (real_coldkey, force_proxy_type as Option, call_data) + let (real_coldkey, force_proxy_type_raw, call_data): ( + T::AccountId, + Option, + BoundedVec>, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + // 2. Parse proxy type from raw u8 + let force_proxy_type = force_proxy_type_raw + .map(ProxyType::try_from) + .transpose() + .map_err(|_| DispatchError::Other("Invalid proxy type"))?; + + // 3. Decode the inner RuntimeCall (depth-limited for safety) + let call = <::RuntimeCall>::decode_with_depth_limit( + 8, + &mut &call_data[..], + ) + .map_err(|_| DispatchError::Other("Failed to decode call"))?; + + // 4. Dynamic weight: inner call weight + proxy overhead + let call_weight = call.get_dispatch_info().call_weight; + let proxy_overhead = ::WeightInfo::proxy( + ::MaxProxies::get(), + ); + env.charge_weight(call_weight.saturating_add(proxy_overhead))?; + + // 5. Execute + let caller = env.caller(); + if real_coldkey == caller { + // Direct dispatch — no proxy needed + let result = call.dispatch(RawOrigin::Signed(caller).into()); + match result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => Ok(RetVal::Converging(Output::from(e.error) as u32)), + } + } else { + // Proxy dispatch — pallet handles all permission checking + let real_lookup = <::Lookup as StaticLookup>::Source::from( + real_coldkey.clone(), + ); + let result = pallet_proxy::Pallet::::proxy( + RawOrigin::Signed(caller).into(), + real_lookup, + force_proxy_type, + Box::new(call), + ); + match result { + Ok(()) => { + // Inner call result is in LastCallResult storage + match pallet_proxy::LastCallResult::::get(&real_coldkey) { + Some(Ok(())) => Ok(RetVal::Converging(Output::Success as u32)), + Some(Err(e)) => Ok(RetVal::Converging(Output::from(e) as u32)), + None => Ok(RetVal::Converging(Output::RuntimeError as u32)), + } + } + Err(e) => Ok(RetVal::Converging(Output::from(e) as u32)), + } + } + } } } } diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 27ac5bc06e..69154ceb29 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -156,7 +156,13 @@ impl frame_support::traits::InstanceFilter for subtensor_runtime_co | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake_limit { .. }) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { .. }) - | RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::set_coldkey_auto_stake_hotkey { .. } + ) + ), + subtensor_runtime_common::ProxyType::Transfer => matches!( + c, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) ), _ => false, } diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index bd6f46c8ab..edf991d0f0 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -966,6 +966,514 @@ fn unstake_all_success_unstakes_balance() { }); } +// ============================================================ +// ProxyCall tests (proxy-aware generic call dispatcher) +// ============================================================ + +/// Helper: encode an inner RuntimeCall into ProxyCall input bytes. +fn encode_proxy_call_input( + real_coldkey: AccountId, + force_proxy_type: Option, + inner_call: mock::RuntimeCall, +) -> Vec { + use frame_support::{BoundedVec, traits::ConstU32}; + let call_data: BoundedVec> = inner_call.encode().try_into().unwrap(); + (real_coldkey, force_proxy_type, call_data).encode() +} + +#[test] +fn proxy_call_self_call_add_stake_succeeds() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(101); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, amount_raw, + ); + + assert!( + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() + ); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: amount, + } + .into(); + let input = encode_proxy_call_input(coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, coldkey, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_success(ret); + assert!(env.charged_weight().is_some()); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoCurrency::ZERO); + }); +} + +#[test] +fn proxy_call_with_staking_proxy_add_stake_succeeds() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let real_coldkey = U256::from(101); + let proxy_contract = U256::from(102); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, real_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &real_coldkey, + amount_raw + 1_000_000_000, + ); + + // Add proxy relationship: real_coldkey grants Staking proxy to proxy_contract + assert_ok!(pallet_subtensor_proxy::Pallet::::add_proxy( + RawOrigin::Signed(real_coldkey).into(), + proxy_contract, + subtensor_runtime_common::ProxyType::Staking, + 0u64, + )); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: amount, + } + .into(); + let input = encode_proxy_call_input(real_coldkey, None, inner_call); + + // proxy_contract calls ProxyCall on behalf of real_coldkey + let mut env = MockEnv::new(FunctionId::ProxyCall, proxy_contract, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_success(ret); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoCurrency::ZERO); + }); +} + +#[test] +fn proxy_call_with_any_proxy_add_stake_succeeds() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let real_coldkey = U256::from(101); + let proxy_contract = U256::from(102); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, real_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &real_coldkey, + amount_raw + 1_000_000_000, + ); + + // Add proxy relationship with ProxyType::Any (was broken in V2!) + assert_ok!(pallet_subtensor_proxy::Pallet::::add_proxy( + RawOrigin::Signed(real_coldkey).into(), + proxy_contract, + subtensor_runtime_common::ProxyType::Any, + 0u64, + )); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: amount, + } + .into(); + let input = encode_proxy_call_input(real_coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, proxy_contract, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + // Any proxy should allow staking calls — this was the primary bug in V2 + assert_success(ret); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoCurrency::ZERO); + }); +} + +#[test] +fn proxy_call_without_proxy_fails() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let real_coldkey = U256::from(101); + let unauthorized_caller = U256::from(102); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, real_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &real_coldkey, + amount_raw + 1_000_000_000, + ); + + // No proxy relationship established + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: amount, + } + .into(); + let input = encode_proxy_call_input(real_coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, unauthorized_caller, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + // Should return NotAuthorizedProxy error code (not a hard DispatchError) + match ret { + RetVal::Converging(code) => { + assert_eq!( + code, + Output::NotAuthorizedProxy as u32, + "expected NotAuthorizedProxy error" + ); + } + _ => panic!("unexpected return value"), + } + }); +} + +#[test] +fn proxy_call_with_wrong_proxy_type_fails() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let real_coldkey = U256::from(101); + let proxy_contract = U256::from(102); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, real_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &real_coldkey, + amount_raw + 1_000_000_000, + ); + + // Add Senate proxy — can't do staking (hits _ => false in InstanceFilter) + assert_ok!(pallet_subtensor_proxy::Pallet::::add_proxy( + RawOrigin::Signed(real_coldkey).into(), + proxy_contract, + subtensor_runtime_common::ProxyType::Senate, + 0u64, + )); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: amount, + } + .into(); + let input = encode_proxy_call_input(real_coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, proxy_contract, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + // Senate proxy cannot do staking calls + match ret { + RetVal::Converging(code) => { + assert_ne!(code, Output::Success as u32, "should not succeed"); + } + _ => panic!("unexpected return value"), + } + }); +} + +#[test] +fn proxy_call_with_transfer_proxy_transfer_stake_succeeds() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4301); + let owner_coldkey = U256::from(4302); + let origin_coldkey = U256::from(5301); + let destination_coldkey = U256::from(5302); + let proxy_contract = U256::from(5304); + let hotkey = U256::from(5303); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(250); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &origin_coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(origin_coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &origin_coldkey, netuid); + + // Add Transfer proxy + assert_ok!(pallet_subtensor_proxy::Pallet::::add_proxy( + RawOrigin::Signed(origin_coldkey).into(), + proxy_contract, + subtensor_runtime_common::ProxyType::Transfer, + 0u64, + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let alpha_to_transfer: AlphaCurrency = (alpha_before.to_u64() / 3).into(); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::transfer_stake { + destination_coldkey, + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: alpha_to_transfer, + } + .into(); + let input = encode_proxy_call_input(origin_coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, proxy_contract, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); + assert_eq!(destination_alpha_after, alpha_to_transfer); + }); +} + +#[test] +fn proxy_call_with_staking_proxy_transfer_stake_fails() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4301); + let owner_coldkey = U256::from(4302); + let origin_coldkey = U256::from(5301); + let destination_coldkey = U256::from(5302); + let proxy_contract = U256::from(5304); + let hotkey = U256::from(5303); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(250); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &origin_coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(origin_coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &origin_coldkey, netuid); + + // Add Staking proxy — cannot do transfer_stake + assert_ok!(pallet_subtensor_proxy::Pallet::::add_proxy( + RawOrigin::Signed(origin_coldkey).into(), + proxy_contract, + subtensor_runtime_common::ProxyType::Staking, + 0u64, + )); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let alpha_to_transfer: AlphaCurrency = (alpha_before.to_u64() / 3).into(); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::transfer_stake { + destination_coldkey, + hotkey, + origin_netuid: netuid, + destination_netuid: netuid, + alpha_amount: alpha_to_transfer, + } + .into(); + let input = encode_proxy_call_input(origin_coldkey, None, inner_call); + + let mut env = MockEnv::new(FunctionId::ProxyCall, proxy_contract, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + // Staking proxy cannot perform transfer_stake (requires Transfer proxy) + match ret { + RetVal::Converging(code) => { + assert_ne!(code, Output::Success as u32, "should not succeed"); + } + _ => panic!("unexpected return value"), + } + + // Verify stake was NOT transferred + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + assert_eq!( + origin_alpha_after, alpha_before, + "stake should be unchanged" + ); + }); +} + +#[test] +fn proxy_call_with_invalid_call_data_fails() { + mock::new_test_ext(1).execute_with(|| { + let coldkey = U256::from(101); + + // Use garbage bytes that won't decode as a RuntimeCall + let garbage_data: frame_support::BoundedVec> = + vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB].try_into().unwrap(); + let input = (coldkey, None::, garbage_data).encode(); + + let mut env = MockEnv::new(FunctionId::ProxyCall, coldkey, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env); + + // Should fail with decode error + assert!(matches!( + ret, + Err(DispatchError::Other("Failed to decode call")) + )); + }); +} + +#[test] +fn proxy_call_with_invalid_proxy_type_byte_fails() { + mock::new_test_ext(1).execute_with(|| { + let coldkey = U256::from(101); + let hotkey = U256::from(202); + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let inner_call: mock::RuntimeCall = pallet_subtensor::Call::::add_stake { + hotkey, + netuid, + amount_staked: 1_000u64.into(), + } + .into(); + let call_data: frame_support::BoundedVec> = + inner_call.encode().try_into().unwrap(); + + // Use an invalid proxy type byte (255 is not a valid ProxyType) + let input = (coldkey, Some(255u8), call_data).encode(); + + let mut env = MockEnv::new(FunctionId::ProxyCall, coldkey, input); + + let ret = SubtensorChainExtension::::dispatch(&mut env); + + // Should fail with invalid proxy type error + assert!(matches!( + ret, + Err(DispatchError::Other("Invalid proxy type")) + )); + }); +} + #[test] fn get_alpha_price_returns_encoded_price() { mock::new_test_ext(1).execute_with(|| { diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index ee6298ad5b..83384d2744 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -21,6 +21,8 @@ pub enum FunctionId { AddProxyV1 = 13, RemoveProxyV1 = 14, GetAlphaPriceV1 = 15, + // Proxy-aware generic call dispatcher: wraps any RuntimeCall through pallet_proxy + ProxyCall = 16, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] @@ -66,6 +68,8 @@ pub enum Output { ProxyNoSelfProxy = 18, /// Proxy relationship not found ProxyNotFound = 19, + /// Caller is not an authorized proxy for the specified account + NotAuthorizedProxy = 20, } impl From for Output { @@ -93,6 +97,7 @@ impl From for Output { Some("Duplicate") => Output::ProxyDuplicate, Some("NoSelfProxy") => Output::ProxyNoSelfProxy, Some("NotFound") => Output::ProxyNotFound, + Some("NotProxy") => Output::NotAuthorizedProxy, _ => Output::RuntimeError, } } diff --git a/docs/wasm-contracts.md b/docs/wasm-contracts.md index ed6e9ecdd3..c9370ab14b 100644 --- a/docs/wasm-contracts.md +++ b/docs/wasm-contracts.md @@ -43,6 +43,8 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 12 | `set_coldkey_auto_stake_hotkey` | Configure automatic stake destination | `(NetUid, AccountId)` | Error code | | 13 | `add_proxy` | Add a staking proxy for the caller | `(AccountId)` | Error code | | 14 | `remove_proxy` | Remove a staking proxy for the caller | `(AccountId)` | Error code | +| 15 | `get_alpha_price` | Get the current alpha price for a subnet | `(NetUid)` | `u64` (price × 10⁹) | +| 16 | `proxy_call` | Dispatch any RuntimeCall through pallet_proxy | `(AccountId, Option, BoundedVec)` | Error code | Example usage in your ink! contract: ```rust @@ -85,6 +87,19 @@ Chain extension functions that modify state return error codes as `u32` values. | 17 | `ProxyDuplicate` | Proxy already exists | | 18 | `ProxyNoSelfProxy` | Cannot add self as proxy | | 19 | `ProxyNotFound` | Proxy relationship not found | +| 20 | `NotAuthorizedProxy` | Caller is not an authorized proxy for the account | + +#### ProxyCall (ID 16) — Proxy-Aware Generic Dispatcher + +Instead of per-function proxy variants, a single `proxy_call` extension dispatches any SCALE-encoded `RuntimeCall` through `pallet_proxy`. Parameters: + +- `real_coldkey: AccountId` — the account to act on behalf of +- `force_proxy_type: Option` — optional proxy type filter (e.g., `5` for Staking), or `None` to match any +- `call_data: BoundedVec` — SCALE-encoded `RuntimeCall` + +Behavior: +- If `real_coldkey == caller`: dispatches the call directly (no proxy needed) +- If `real_coldkey != caller`: routes through `pallet_proxy::proxy()`, which checks proxy permissions via `InstanceFilter` — correctly supporting `Any`, `Staking`, `Transfer`, and all other proxy types ### Call Filter