Skip to content

Commit 3506ffa

Browse files
authored
Merge pull request #1663 from opentensor/hotfix/add-call-filter-for-nested-batches
Hotfix/add call filter for nested batches
2 parents 6b86ebf + 0c3c6a0 commit 3506ffa

File tree

6 files changed

+236
-6
lines changed

6 files changed

+236
-6
lines changed

pallets/subtensor/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,20 @@ where
20452045
Self::get_priority_staking(who, hotkey, *amount_unstaked),
20462046
)
20472047
}
2048+
Some(Call::unstake_all { hotkey }) => {
2049+
// Fully validate the user input
2050+
Self::result_to_validity(
2051+
Pallet::<T>::validate_unstake_all(who, hotkey, false),
2052+
Self::get_priority_vanilla(),
2053+
)
2054+
}
2055+
Some(Call::unstake_all_alpha { hotkey }) => {
2056+
// Fully validate the user input
2057+
Self::result_to_validity(
2058+
Pallet::<T>::validate_unstake_all(who, hotkey, true),
2059+
Self::get_priority_vanilla(),
2060+
)
2061+
}
20482062
Some(Call::move_stake {
20492063
origin_hotkey,
20502064
destination_hotkey,

pallets/subtensor/src/staking/stake_utils.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,42 @@ impl<T: Config> Pallet<T> {
987987
Ok(())
988988
}
989989

990+
/// Validate if unstake_all can be executed
991+
///
992+
pub fn validate_unstake_all(
993+
coldkey: &T::AccountId,
994+
hotkey: &T::AccountId,
995+
only_alpha: bool,
996+
) -> Result<(), Error<T>> {
997+
// Get all netuids (filter out root)
998+
let subnets: Vec<u16> = Self::get_all_subnet_netuids();
999+
1000+
// Ensure that the hotkey account exists this is only possible through registration.
1001+
ensure!(
1002+
Self::hotkey_account_exists(hotkey),
1003+
Error::<T>::HotKeyAccountNotExists
1004+
);
1005+
1006+
let mut unstaking_any = false;
1007+
for netuid in subnets.iter() {
1008+
if only_alpha && (*netuid == Self::get_root_netuid()) {
1009+
continue;
1010+
}
1011+
1012+
// Get user's stake in this subnet
1013+
let alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, *netuid);
1014+
1015+
if Self::validate_remove_stake(coldkey, hotkey, *netuid, alpha, alpha, false).is_ok() {
1016+
unstaking_any = true;
1017+
}
1018+
}
1019+
1020+
// If no unstaking happens, return error
1021+
ensure!(unstaking_any, Error::<T>::AmountTooLow);
1022+
1023+
Ok(())
1024+
}
1025+
9901026
/// Validate stake transition user input
9911027
/// That works for move_stake, transfer_stake, and swap_stake
9921028
///

pallets/subtensor/src/tests/batch_tx.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use super::mock::*;
2-
use frame_support::{assert_ok, traits::Currency};
2+
use frame_support::{
3+
assert_ok,
4+
traits::{Contains, Currency},
5+
};
36
use frame_system::Config;
47
use sp_core::U256;
58

@@ -33,3 +36,84 @@ fn test_batch_txs() {
3336
assert_eq!(Balances::total_balance(&charlie), 2_000_000_000);
3437
});
3538
}
39+
40+
#[test]
41+
fn test_cant_nest_batch_txs() {
42+
let bob = U256::from(1);
43+
let charlie = U256::from(2);
44+
45+
new_test_ext(1).execute_with(|| {
46+
let call = RuntimeCall::Utility(pallet_utility::Call::batch {
47+
calls: vec![
48+
RuntimeCall::Balances(BalanceCall::transfer_allow_death {
49+
dest: bob,
50+
value: 1_000_000_000,
51+
}),
52+
RuntimeCall::Utility(pallet_utility::Call::batch {
53+
calls: vec![RuntimeCall::Balances(BalanceCall::transfer_allow_death {
54+
dest: charlie,
55+
value: 1_000_000_000,
56+
})],
57+
}),
58+
],
59+
});
60+
61+
assert!(!<Test as Config>::BaseCallFilter::contains(&call));
62+
});
63+
}
64+
65+
#[test]
66+
fn test_can_batch_txs() {
67+
let bob = U256::from(1);
68+
69+
new_test_ext(1).execute_with(|| {
70+
let call = RuntimeCall::Utility(pallet_utility::Call::batch {
71+
calls: vec![RuntimeCall::Balances(BalanceCall::transfer_allow_death {
72+
dest: bob,
73+
value: 1_000_000_000,
74+
})],
75+
});
76+
77+
assert!(<Test as Config>::BaseCallFilter::contains(&call));
78+
});
79+
}
80+
81+
#[test]
82+
fn test_cant_nest_batch_diff_batch_txs() {
83+
let charlie = U256::from(2);
84+
85+
new_test_ext(1).execute_with(|| {
86+
let call = RuntimeCall::Utility(pallet_utility::Call::batch {
87+
calls: vec![RuntimeCall::Utility(pallet_utility::Call::force_batch {
88+
calls: vec![RuntimeCall::Balances(BalanceCall::transfer_allow_death {
89+
dest: charlie,
90+
value: 1_000_000_000,
91+
})],
92+
})],
93+
});
94+
95+
assert!(!<Test as Config>::BaseCallFilter::contains(&call));
96+
97+
let call2 = RuntimeCall::Utility(pallet_utility::Call::batch_all {
98+
calls: vec![RuntimeCall::Utility(pallet_utility::Call::batch {
99+
calls: vec![RuntimeCall::Balances(BalanceCall::transfer_allow_death {
100+
dest: charlie,
101+
value: 1_000_000_000,
102+
})],
103+
})],
104+
});
105+
106+
assert!(!<Test as Config>::BaseCallFilter::contains(&call2));
107+
108+
let call3 = RuntimeCall::Utility(pallet_utility::Call::force_batch {
109+
calls: vec![RuntimeCall::Utility(pallet_utility::Call::batch_all {
110+
calls: vec![RuntimeCall::Balances(BalanceCall::transfer_allow_death {
111+
dest: charlie,
112+
value: 1_000_000_000,
113+
})],
114+
})],
115+
});
116+
117+
assert!(!<Test as Config>::BaseCallFilter::contains(&call3));
118+
});
119+
}

pallets/subtensor/src/tests/mock.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
use crate::utils::rate_limiting::TransactionType;
33
use frame_support::derive_impl;
44
use frame_support::dispatch::DispatchResultWithPostInfo;
5+
use frame_support::traits::{Contains, Everything, InsideBoth};
56
use frame_support::weights::Weight;
67
use frame_support::weights::constants::RocksDbWeight;
78
use frame_support::{
89
assert_ok, parameter_types,
9-
traits::{Everything, Hooks, PrivilegeCmp},
10+
traits::{Hooks, PrivilegeCmp},
1011
};
1112
use frame_system as system;
1213
use frame_system::{EnsureNever, EnsureRoot, RawOrigin, limits};
@@ -88,9 +89,31 @@ impl pallet_balances::Config for Test {
8889
type MaxFreezes = ();
8990
}
9091

92+
pub struct NoNestingCallFilter;
93+
94+
impl Contains<RuntimeCall> for NoNestingCallFilter {
95+
fn contains(call: &RuntimeCall) -> bool {
96+
match call {
97+
RuntimeCall::Utility(inner) => {
98+
let calls = match inner {
99+
pallet_utility::Call::force_batch { calls } => calls,
100+
pallet_utility::Call::batch { calls } => calls,
101+
pallet_utility::Call::batch_all { calls } => calls,
102+
_ => &Vec::new(),
103+
};
104+
105+
!calls.iter().any(|call| {
106+
matches!(call, RuntimeCall::Utility(inner) if matches!(inner, pallet_utility::Call::force_batch { .. } | pallet_utility::Call::batch_all { .. } | pallet_utility::Call::batch { .. }))
107+
})
108+
}
109+
_ => true,
110+
}
111+
}
112+
}
113+
91114
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
92115
impl system::Config for Test {
93-
type BaseCallFilter = Everything;
116+
type BaseCallFilter = InsideBoth<Everything, NoNestingCallFilter>;
94117
type BlockWeights = BlockWeights;
95118
type BlockLength = ();
96119
type DbWeight = RocksDbWeight;

pallets/subtensor/src/tests/staking.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2836,6 +2836,57 @@ fn test_unstake_low_liquidity_validate() {
28362836
});
28372837
}
28382838

2839+
#[test]
2840+
fn test_unstake_all_validate() {
2841+
// Testing the signed extension validate function
2842+
// correctly filters the `unstake_all` transaction.
2843+
2844+
new_test_ext(0).execute_with(|| {
2845+
let subnet_owner_coldkey = U256::from(1001);
2846+
let subnet_owner_hotkey = U256::from(1002);
2847+
let hotkey = U256::from(2);
2848+
let coldkey = U256::from(3);
2849+
let amount_staked = DefaultMinStake::<Test>::get() * 10 + DefaultStakingFee::<Test>::get();
2850+
2851+
let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey);
2852+
SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey);
2853+
SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount_staked);
2854+
2855+
// Simulate stake for hotkey
2856+
SubnetTAO::<Test>::insert(netuid, u64::MAX / 1000);
2857+
SubnetAlphaIn::<Test>::insert(netuid, u64::MAX / 1000);
2858+
SubtensorModule::stake_into_subnet(&hotkey, &coldkey, netuid, amount_staked, 0);
2859+
2860+
// Set the liquidity at lowest possible value so that all staking requests fail
2861+
SubnetTAO::<Test>::insert(
2862+
netuid,
2863+
DefaultMinimumPoolLiquidity::<Test>::get().to_num::<u64>(),
2864+
);
2865+
SubnetAlphaIn::<Test>::insert(
2866+
netuid,
2867+
DefaultMinimumPoolLiquidity::<Test>::get().to_num::<u64>(),
2868+
);
2869+
2870+
// unstake_all call
2871+
let call = RuntimeCall::SubtensorModule(SubtensorCall::unstake_all { hotkey });
2872+
2873+
let info: DispatchInfo =
2874+
DispatchInfoOf::<<Test as frame_system::Config>::RuntimeCall>::default();
2875+
2876+
let extension = SubtensorSignedExtension::<Test>::new();
2877+
// Submit to the signed extension validate function
2878+
let result_no_stake = extension.validate(&coldkey, &call.clone(), &info, 10);
2879+
2880+
// Should fail due to insufficient stake
2881+
assert_err!(
2882+
result_no_stake,
2883+
TransactionValidityError::Invalid(InvalidTransaction::Custom(
2884+
CustomTransactionError::StakeAmountTooLow.into()
2885+
))
2886+
);
2887+
});
2888+
}
2889+
28392890
#[test]
28402891
fn test_max_amount_add_root() {
28412892
new_test_ext(0).execute_with(|| {

runtime/src/lib.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub mod check_nonce;
1212
mod migrations;
1313

1414
use codec::{Compact, Decode, Encode};
15-
use frame_support::traits::Imbalance;
15+
use frame_support::traits::{Imbalance, InsideBoth};
1616
use frame_support::{
1717
dispatch::DispatchResultWithPostInfo,
1818
genesis_builder_helper::{build_state, get_preset},
@@ -207,7 +207,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
207207
// `spec_version`, and `authoring_version` are the same between Wasm and native.
208208
// This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use
209209
// the compatible custom types.
210-
spec_version: 261,
210+
spec_version: 263,
211211
impl_version: 1,
212212
apis: RUNTIME_API_VERSIONS,
213213
transaction_version: 1,
@@ -242,11 +242,33 @@ parameter_types! {
242242
pub const SS58Prefix: u8 = 42;
243243
}
244244

245+
pub struct NoNestingCallFilter;
246+
247+
impl Contains<RuntimeCall> for NoNestingCallFilter {
248+
fn contains(call: &RuntimeCall) -> bool {
249+
match call {
250+
RuntimeCall::Utility(inner) => {
251+
let calls = match inner {
252+
pallet_utility::Call::force_batch { calls } => calls,
253+
pallet_utility::Call::batch { calls } => calls,
254+
pallet_utility::Call::batch_all { calls } => calls,
255+
_ => &Vec::new(),
256+
};
257+
258+
!calls.iter().any(|call| {
259+
matches!(call, RuntimeCall::Utility(inner) if matches!(inner, pallet_utility::Call::force_batch { .. } | pallet_utility::Call::batch_all { .. } | pallet_utility::Call::batch { .. }))
260+
})
261+
}
262+
_ => true,
263+
}
264+
}
265+
}
266+
245267
// Configure FRAME pallets to include in runtime.
246268

247269
impl frame_system::Config for Runtime {
248270
// The basic call filter to use in dispatchable.
249-
type BaseCallFilter = SafeMode;
271+
type BaseCallFilter = InsideBoth<SafeMode, NoNestingCallFilter>;
250272
// Block & extrinsics weights: base values and limits.
251273
type BlockWeights = BlockWeights;
252274
// The maximum length of a block (in bytes).

0 commit comments

Comments
 (0)