Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions chain-extensions/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = 0; // 0% TAO weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 7 * 24 * 60 * 60 / 12; // Default as 7 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(10_000_000);
pub const HotkeySwapOnSubnetInterval: u64 = 15; // 15 block, should be bigger than subnet number, then trigger clean up for all subnets
pub const MaxContributorsPerLeaseToRemove: u32 = 3;
Expand Down Expand Up @@ -428,6 +429,7 @@ impl pallet_subtensor::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = pallet_subtensor_swap::Pallet<Self>;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
2 changes: 2 additions & 0 deletions eco-tests/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = 0; // 0% TAO weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 0; // 0 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: u64 = 10_000_000;
pub const HotkeySwapOnSubnetInterval: u64 = 15; // 15 block, should be bigger than subnet number, then trigger clean up for all subnets
pub const MaxContributorsPerLeaseToRemove: u32 = 3;
Expand Down Expand Up @@ -322,6 +323,7 @@ impl pallet_subtensor::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = pallet_subtensor_swap::Pallet<Self>;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
11 changes: 11 additions & 0 deletions pallets/admin-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,17 @@ pub mod pallet {
Ok(())
}

/// Sets the number of blocks after subnet-start during which staking is blocked (#2844).
#[pallet::call_index(98)]
#[pallet::weight(Weight::from_parts(14_000_000, 0)
.saturating_add(<T as frame_system::Config>::DbWeight::get().writes(1)))]
pub fn sudo_set_min_trade_delay(origin: OriginFor<T>, delay: u64) -> DispatchResult {
ensure_root(origin)?;
pallet_subtensor::Pallet::<T>::set_min_trade_delay(delay);
log::debug!("MinTradeDelay( delay: {delay:?} ) ");
Ok(())
}

/// Sets the announcement delay for coldkey swap.
#[pallet::call_index(86)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::sudo_set_coldkey_swap_announcement_delay())]
Expand Down
2 changes: 2 additions & 0 deletions pallets/admin-utils/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 0; // 0 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(10_000_000);
pub const HotkeySwapOnSubnetInterval: u64 = 7 * 24 * 60 * 60 / 12; // 7 days
pub const LeaseDividendsDistributionInterval: u32 = 100; // 100 blocks
Expand Down Expand Up @@ -237,6 +238,7 @@ impl pallet_subtensor::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = Swap;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
30 changes: 30 additions & 0 deletions pallets/admin-utils/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3383,3 +3383,33 @@ fn test_sudo_set_start_call_delay_permissions_and_zero_delay() {
);
});
}

// #2844 — sudo can set the post-start trading delay; non-root cannot.
#[test]
fn test_sudo_set_min_trade_delay() {
new_test_ext().execute_with(|| {
let non_root = U256::from(1);

// Mock default is 0.
assert_eq!(pallet_subtensor::MinTradeDelay::<Test>::get(), 0);

// Non-root is rejected.
assert_noop!(
AdminUtils::sudo_set_min_trade_delay(
<<Test as Config>::RuntimeOrigin>::signed(non_root),
360
),
DispatchError::BadOrigin
);

// Root sets it, storage updates, event is emitted.
assert_ok!(AdminUtils::sudo_set_min_trade_delay(
<<Test as Config>::RuntimeOrigin>::root(),
360
));
assert_eq!(pallet_subtensor::MinTradeDelay::<Test>::get(), 360);
frame_system::Pallet::<Test>::assert_last_event(RuntimeEvent::SubtensorModule(
pallet_subtensor::Event::MinTradeDelaySet(360),
));
});
}
7 changes: 7 additions & 0 deletions pallets/subtensor/src/coinbase/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ impl<T: Config> Pallet<T> {
StartCallDelay::<T>::set(delay);
Self::deposit_event(Event::StartCallDelaySet(delay));
}
pub fn get_min_trade_delay() -> u64 {
MinTradeDelay::<T>::get()
}
pub fn set_min_trade_delay(delay: u64) {
MinTradeDelay::<T>::set(delay);
Self::deposit_event(Event::MinTradeDelaySet(delay));
}
pub fn set_network_min_lock(net_min_lock: TaoBalance) {
NetworkMinLockCost::<T>::set(net_min_lock);
Self::deposit_event(Event::NetworkMinLockCostSet(net_min_lock));
Expand Down
24 changes: 23 additions & 1 deletion pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,13 @@ pub mod pallet {
#[pallet::storage]
pub type StartCallDelay<T: Config> = StorageValue<_, u64, ValueQuery, T::InitialStartCallDelay>;

/// ITEM( min_trade_delay )
/// Blocks after subnet-start (`start_call`) during which staking in/out is blocked, so a
/// subnet owner cannot bundle `add_stake` into the `start_call` block and self-snipe the
/// launch. See opentensor/subtensor#2844.
#[pallet::storage]
pub type MinTradeDelay<T: Config> = StorageValue<_, u64, ValueQuery, T::InitialMinTradeDelay>;

/// ITEM( min_network_lock_cost )
#[pallet::storage]
pub type NetworkMinLockCost<T> =
Expand Down Expand Up @@ -2822,12 +2829,27 @@ pub mod pallet {
true
}

/// Ensure subtoken enalbed
/// Ensure the subtoken is enabled AND the post-start trading delay has elapsed.
///
/// Staking (in or out) is blocked until `MinTradeDelay` blocks after the subnet-start
/// block, so a subnet owner cannot bundle `add_stake` into the same block as `start_call`
/// and self-snipe the launch (opentensor/subtensor#2844). The subnet-start block is
/// `FirstEmissionBlockNumber - 1` (both set atomically by `do_start_call`). Subnets whose
/// subtoken was enabled without a start block (e.g. root / genesis paths where
/// `FirstEmissionBlockNumber` is unset) are unaffected.
pub fn ensure_subtoken_enabled(subnet: NetUid) -> Result<(), Error<T>> {
ensure!(
SubtokenEnabled::<T>::get(subnet),
Error::<T>::SubtokenDisabled
);
if let Some(first_emission_block) = FirstEmissionBlockNumber::<T>::get(subnet) {
let start_call_block = first_emission_block.saturating_sub(1);
let trading_open_block = start_call_block.saturating_add(MinTradeDelay::<T>::get());
ensure!(
Self::get_current_block_as_u64() >= trading_open_block,
Error::<T>::TradingNotOpenYet
);
}
Ok(())
}
}
Expand Down
3 changes: 3 additions & 0 deletions pallets/subtensor/src/macros/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ mod config {
/// Delay after which a new subnet can dispatch start call extrinsic.
#[pallet::constant]
type InitialStartCallDelay: Get<u64>;
/// Initial blocks after subnet-start during which staking is blocked (#2844).
#[pallet::constant]
type InitialMinTradeDelay: Get<u64>;
/// Cost of swapping a hotkey in a subnet.
#[pallet::constant]
type KeySwapOnSubnetCost: Get<TaoBalance>;
Expand Down
2 changes: 2 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,5 +332,7 @@ mod errors {
StartCallNotReady,
/// The caller does not have enough Alpha stake for the operation.
InsufficientAlphaBalance,
/// Trading is not open yet: the post-start trading delay (MinTradeDelay) has not elapsed.
TradingNotOpenYet,
}
}
2 changes: 2 additions & 0 deletions pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ mod events {
NetworkImmunityPeriodSet(u64),
/// the start call delay is set.
StartCallDelaySet(u64),
/// the minimum post-start trading delay is set.
MinTradeDelaySet(u64),
/// the network minimum locking cost is set.
NetworkMinLockCostSet(TaoBalance),
/// the maximum number of subnets is set
Expand Down
15 changes: 6 additions & 9 deletions pallets/subtensor/src/staking/stake_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,15 +1273,12 @@ impl<T: Config> Pallet<T> {
);
}

ensure!(
SubtokenEnabled::<T>::get(origin_netuid),
Error::<T>::SubtokenDisabled
);

ensure!(
SubtokenEnabled::<T>::get(destination_netuid),
Error::<T>::SubtokenDisabled
);
// Route through ensure_subtoken_enabled so the post-start trading delay (#2844) also
// gates swap/move/transfer. Otherwise an owner could pre-stake into an always-open
// subnet (e.g. root) and bundle batch_all[start_call, swap_stake(root -> new)] to
// acquire alpha in the new subnet at launch, bypassing the add_stake gate.
Self::ensure_subtoken_enabled(origin_netuid)?;
Self::ensure_subtoken_enabled(destination_netuid)?;

// Ensure that the origin hotkey account exists
ensure!(
Expand Down
2 changes: 2 additions & 0 deletions pallets/subtensor/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = 0; // 0% TAO weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 0; // 0 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: u64 = 10_000_000;
pub const HotkeySwapOnSubnetInterval: u64 = 15; // 15 block, should be bigger than subnet number, then trigger clean up for all subnets
pub const MaxContributorsPerLeaseToRemove: u32 = 3;
Expand Down Expand Up @@ -373,6 +374,7 @@ impl crate::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = pallet_subtensor_swap::Pallet<Self>;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
2 changes: 2 additions & 0 deletions pallets/subtensor/src/tests/mock_high_ed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = 0; // 0% TAO weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 0; // 0 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: u64 = 10_000_000;
pub const HotkeySwapOnSubnetInterval: u64 = 15; // 15 block, should be bigger than subnet number, then trigger clean up for all subnets
pub const MaxContributorsPerLeaseToRemove: u32 = 3;
Expand Down Expand Up @@ -295,6 +296,7 @@ impl crate::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = pallet_subtensor_swap::Pallet<Self>;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
78 changes: 78 additions & 0 deletions pallets/subtensor/src/tests/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,81 @@ fn test_register_network_gives_owner_no_initial_alpha_distribution() {
);
});
}

/***************************
#2844 — post-start trading delay (MinTradeDelay)
*****************************/

// A non-zero MinTradeDelay must block staking (including an owner's same-block bundle) until
// `start_call_block + MinTradeDelay`, then open it. This is the fix for the SN99 self-snipe.
#[test]
fn test_min_trade_delay_blocks_stake_until_window_elapses() {
new_test_ext(0).execute_with(|| {
let netuid = NetUid::from(1);
let tempo: u16 = 13;
let coldkey = U256::from(0); // default subnet owner
let hotkey = U256::from(1);
let delay: u64 = 10;

add_network_without_emission_block(netuid, tempo, 0);
mock::setup_reserves(netuid, 1_000_000_000.into(), 1_000_000_000.into());
register_ok_neuron(netuid, hotkey, coldkey, 0);
add_balance_to_coldkey_account(&coldkey, 10_000.into());

SubtensorModule::set_min_trade_delay(delay);
assert_eq!(MinTradeDelay::<Test>::get(), delay);

// Owner starts the subnet at block B: FirstEmissionBlockNumber = B+1, start block = B.
let start_block = System::block_number();
assert_ok!(SubtensorModule::start_call(
RuntimeOrigin::signed(coldkey),
netuid
));
assert!(SubtokenEnabled::<Test>::get(netuid));

// Same block as start: the owner's bundled add_stake is rejected.
assert_noop!(
SubtensorModule::add_stake(
RuntimeOrigin::signed(coldkey),
hotkey,
netuid,
1_000_000.into()
),
Error::<Test>::TradingNotOpenYet
);

// The gate stays closed for the whole window and opens exactly at start + delay.
assert!(matches!(
SubtensorModule::ensure_subtoken_enabled(netuid),
Err(Error::<Test>::TradingNotOpenYet)
));
System::set_block_number(start_block + delay - 1);
assert!(matches!(
SubtensorModule::ensure_subtoken_enabled(netuid),
Err(Error::<Test>::TradingNotOpenYet)
));
System::set_block_number(start_block + delay);
assert!(SubtensorModule::ensure_subtoken_enabled(netuid).is_ok());
});
}

// A zero MinTradeDelay preserves today's behaviour: trading is open in the start block.
#[test]
fn test_min_trade_delay_zero_preserves_immediate_trading() {
new_test_ext(0).execute_with(|| {
let netuid = NetUid::from(1);
let tempo: u16 = 13;
let coldkey = U256::from(0);

add_network_without_emission_block(netuid, tempo, 0);
mock::setup_reserves(netuid, 1_000_000_000.into(), 1_000_000_000.into());

SubtensorModule::set_min_trade_delay(0);
assert_ok!(SubtensorModule::start_call(
RuntimeOrigin::signed(coldkey),
netuid
));
// delay = 0 -> trading is open in the start block, exactly as today.
assert!(SubtensorModule::ensure_subtoken_enabled(netuid).is_ok());
});
}
2 changes: 2 additions & 0 deletions pallets/transaction-fee/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight.
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
pub const InitialStartCallDelay: u64 = 0; // 0 days
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(10_000_000);
pub const HotkeySwapOnSubnetInterval: u64 = 7 * 24 * 60 * 60 / 12; // 7 days
pub const LeaseDividendsDistributionInterval: u32 = 100; // 100 blocks
Expand Down Expand Up @@ -307,6 +308,7 @@ impl pallet_subtensor::Config for Test {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = Swap;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
2 changes: 2 additions & 0 deletions precompiles/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ parameter_types! {
pub const InitialTaoWeight: u64 = u64::MAX / 10;
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600;
pub const InitialStartCallDelay: u64 = 0;
pub const InitialMinTradeDelay: u64 = 0; // #2844: 0 in mocks
pub const InitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(10_000_000);
pub const HotkeySwapOnSubnetInterval: u64 = 50_400;
pub const LeaseDividendsDistributionInterval: u32 = 100;
Expand Down Expand Up @@ -484,6 +485,7 @@ impl pallet_subtensor::Config for Runtime {
type InitialTaoWeight = InitialTaoWeight;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = Swap;
type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
6 changes: 5 additions & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// `spec_version`, and `authoring_version` are the same between Wasm and native.
// This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use
// the compatible custom types.
spec_version: 431,
spec_version: 432,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
Expand Down Expand Up @@ -848,6 +848,9 @@ parameter_types! {
pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks
// 0 days
pub const InitialStartCallDelay: u64 = 0;
// #2844: block staking for a predictable window after subnet-start (prod ~72 min; 0 on
// fast-blocks). The value is the policy knob — sudo-tunable via sudo_set_min_trade_delay.
pub const InitialMinTradeDelay: u64 = prod_or_fast!(360, 0);
pub const SubtensorInitialKeySwapOnSubnetCost: TaoBalance = TaoBalance::new(1_000_000); // 0.001 TAO
pub const HotkeySwapOnSubnetInterval : BlockNumber = prod_or_fast!(24 * 60 * 60 / 12, 1); // 1 day
pub const LeaseDividendsDistributionInterval: BlockNumber = 100; // 100 blocks
Expand Down Expand Up @@ -926,6 +929,7 @@ impl pallet_subtensor::Config for Runtime {
type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration;
type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod;
type InitialStartCallDelay = InitialStartCallDelay;
type InitialMinTradeDelay = InitialMinTradeDelay;
type SwapInterface = Swap;
type KeySwapOnSubnetCost = SubtensorInitialKeySwapOnSubnetCost;
type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval;
Expand Down
1 change: 1 addition & 0 deletions runtime/src/proxy_filters/call_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ call_filter_group!(
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_net_tao_flow_enabled),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_max_mechanism_count),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_start_call_delay),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_min_trade_delay),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_coldkey_swap_announcement_delay),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_coldkey_swap_reannouncement_delay),
RuntimeCall::AdminUtils(AdminUtilsCall::sudo_set_subnet_emission_enabled),
Expand Down