diff --git a/pallets/commitments/src/weights.rs b/pallets/commitments/src/weights.rs index bd36d96e2b..a7818b54b9 100644 --- a/pallets/commitments/src/weights.rs +++ b/pallets/commitments/src/weights.rs @@ -1,28 +1,35 @@ //! Autogenerated weights for `pallet_commitments` //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-12-08, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-07-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `rustys-mbp.lan`, CPU: `` -//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("local")`, DB CACHE: `1024` +//! HOSTNAME: `runnervmkkn4f`, CPU: `AMD EPYC 9V74 80-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: -// ./target/release/node-subtensor +// /home/runner/work/subtensor/subtensor/target/production/node-subtensor // benchmark // pallet -// --chain=local -// --execution=wasm +// --runtime=/home/runner/work/subtensor/subtensor/target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm +// --genesis-builder=runtime +// --genesis-builder-preset=benchmark // --wasm-execution=compiled // --pallet=pallet_commitments // --extrinsic=* -// --output=pallets/commitments/src/weights.rs -// --template=./.maintain/frame-weight-template.hbs +// --steps=50 +// --repeat=20 +// --no-storage-info +// --no-min-squares +// --no-median-slopes +// --output=/tmp/tmp.V8xURZVxTO +// --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] #![allow(missing_docs)] +#![allow(dead_code)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use core::marker::PhantomData; @@ -36,26 +43,70 @@ pub trait WeightInfo { /// Weights for `pallet_commitments` using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { + /// Storage: `Commitments::UsedSpaceOf` (r:1 w:1) + /// Proof: `Commitments::UsedSpaceOf` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetEpochIndex` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetEpochIndex` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::MaxSpace` (r:1 w:0) + /// Proof: `Commitments::MaxSpace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::CommitmentOf` (r:1 w:1) + /// Proof: `Commitments::CommitmentOf` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::TimelockedIndex` (r:1 w:1) + /// Proof: `Commitments::TimelockedIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::LastCommitment` (r:0 w:1) + /// Proof: `Commitments::LastCommitment` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_commitment() -> Weight { - Weight::from_parts(33_480_000, 0) + // Proof Size summary in bytes: + // Measured: `504` + // Estimated: `3969` + // Minimum execution time: 19_540_000 picoseconds. + Weight::from_parts(20_150_000, 3969) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } + /// Storage: `Commitments::MaxSpace` (r:0 w:1) + /// Proof: `Commitments::MaxSpace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_max_space() -> Weight { - Weight::from_parts(2_856_000, 0) + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_372_000 picoseconds. + Weight::from_parts(1_692_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } } // For backwards compatibility and tests. impl WeightInfo for () { + /// Storage: `Commitments::UsedSpaceOf` (r:1 w:1) + /// Proof: `Commitments::UsedSpaceOf` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetEpochIndex` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetEpochIndex` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::MaxSpace` (r:1 w:0) + /// Proof: `Commitments::MaxSpace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::CommitmentOf` (r:1 w:1) + /// Proof: `Commitments::CommitmentOf` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::TimelockedIndex` (r:1 w:1) + /// Proof: `Commitments::TimelockedIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Commitments::LastCommitment` (r:0 w:1) + /// Proof: `Commitments::LastCommitment` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_commitment() -> Weight { - Weight::from_parts(33_480_000, 0) + // Proof Size summary in bytes: + // Measured: `504` + // Estimated: `3969` + // Minimum execution time: 19_540_000 picoseconds. + Weight::from_parts(20_150_000, 3969) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } + /// Storage: `Commitments::MaxSpace` (r:0 w:1) + /// Proof: `Commitments::MaxSpace` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_max_space() -> Weight { - Weight::from_parts(2_856_000, 0) + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_372_000 picoseconds. + Weight::from_parts(1_692_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } } diff --git a/pallets/crowdloan/src/weights.rs b/pallets/crowdloan/src/weights.rs index 6ec23b2c23..026e0acb22 100644 --- a/pallets/crowdloan/src/weights.rs +++ b/pallets/crowdloan/src/weights.rs @@ -2,9 +2,9 @@ //! Autogenerated weights for `pallet_crowdloan` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-06-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-07-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runnervmmklqx`, CPU: `AMD EPYC 9V74 80-Core Processor` +//! HOSTNAME: `runnervmkkn4f`, CPU: `AMD EPYC 9V74 80-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.CWtPZcdN9i +// --output=/tmp/tmp.HXC5x2AL6y // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -63,8 +63,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `119` // Estimated: `6148` - // Minimum execution time: 59_888_000 picoseconds. - Weight::from_parts(61_110_000, 6148) + // Minimum execution time: 62_814_000 picoseconds. + Weight::from_parts(65_689_000, 6148) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -80,8 +80,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `448` // Estimated: `6148` - // Minimum execution time: 65_807_000 picoseconds. - Weight::from_parts(66_819_000, 6148) + // Minimum execution time: 68_563_000 picoseconds. + Weight::from_parts(71_076_000, 6148) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -95,8 +95,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `408` // Estimated: `6148` - // Minimum execution time: 59_447_000 picoseconds. - Weight::from_parts(61_220_000, 6148) + // Minimum execution time: 61_532_000 picoseconds. + Weight::from_parts(64_286_000, 6148) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -110,13 +110,17 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn finalize() -> Weight { // Proof Size summary in bytes: - // Measured: `1181` + // Measured: `1216` // Estimated: `4197809` - // Minimum execution time: 32_548_000 picoseconds. - Weight::from_parts(34_110_000, 4197809) - .saturating_add(T::DbWeight::get().reads(5_u64)) + // Minimum execution time: 31_887_000 picoseconds. + Weight::from_parts(33_050_000, 4197809) + .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) @@ -130,10 +134,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `324 + k * (46 ±0)` // Estimated: `3747 + k * (2579 ±0)` - // Minimum execution time: 109_320_000 picoseconds. - Weight::from_parts(110_392_000, 3747) - // Standard Error: 94_672 - .saturating_add(Weight::from_parts(39_822_919, 0).saturating_mul(k.into())) + // Minimum execution time: 109_925_000 picoseconds. + Weight::from_parts(115_633_000, 3747) + // Standard Error: 101_663 + .saturating_add(Weight::from_parts(43_172_223, 0).saturating_mul(k.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(k.into()))) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(k.into()))) @@ -151,8 +155,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `370` // Estimated: `6148` - // Minimum execution time: 69_442_000 picoseconds. - Weight::from_parts(70_724_000, 6148) + // Minimum execution time: 69_645_000 picoseconds. + Weight::from_parts(71_247_000, 6148) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -164,8 +168,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 13_049_000 picoseconds. - Weight::from_parts(13_510_000, 3747) + // Minimum execution time: 11_467_000 picoseconds. + Weight::from_parts(11_828_000, 3747) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -175,8 +179,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 11_557_000 picoseconds. - Weight::from_parts(12_318_000, 3747) + // Minimum execution time: 10_125_000 picoseconds. + Weight::from_parts(10_586_000, 3747) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -186,8 +190,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 11_386_000 picoseconds. - Weight::from_parts(11_797_000, 3747) + // Minimum execution time: 9_905_000 picoseconds. + Weight::from_parts(10_165_000, 3747) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -201,8 +205,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `293` // Estimated: `3747` - // Minimum execution time: 15_924_000 picoseconds. - Weight::from_parts(16_805_000, 3747) + // Minimum execution time: 13_941_000 picoseconds. + Weight::from_parts(14_311_000, 3747) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -222,8 +226,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `119` // Estimated: `6148` - // Minimum execution time: 59_888_000 picoseconds. - Weight::from_parts(61_110_000, 6148) + // Minimum execution time: 62_814_000 picoseconds. + Weight::from_parts(65_689_000, 6148) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -239,8 +243,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `448` // Estimated: `6148` - // Minimum execution time: 65_807_000 picoseconds. - Weight::from_parts(66_819_000, 6148) + // Minimum execution time: 68_563_000 picoseconds. + Weight::from_parts(71_076_000, 6148) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -254,8 +258,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `408` // Estimated: `6148` - // Minimum execution time: 59_447_000 picoseconds. - Weight::from_parts(61_220_000, 6148) + // Minimum execution time: 61_532_000 picoseconds. + Weight::from_parts(64_286_000, 6148) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -269,13 +273,17 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn finalize() -> Weight { // Proof Size summary in bytes: - // Measured: `1181` + // Measured: `1216` // Estimated: `4197809` - // Minimum execution time: 32_548_000 picoseconds. - Weight::from_parts(34_110_000, 4197809) - .saturating_add(RocksDbWeight::get().reads(5_u64)) + // Minimum execution time: 31_887_000 picoseconds. + Weight::from_parts(33_050_000, 4197809) + .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) @@ -289,10 +297,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `324 + k * (46 ±0)` // Estimated: `3747 + k * (2579 ±0)` - // Minimum execution time: 109_320_000 picoseconds. - Weight::from_parts(110_392_000, 3747) - // Standard Error: 94_672 - .saturating_add(Weight::from_parts(39_822_919, 0).saturating_mul(k.into())) + // Minimum execution time: 109_925_000 picoseconds. + Weight::from_parts(115_633_000, 3747) + // Standard Error: 101_663 + .saturating_add(Weight::from_parts(43_172_223, 0).saturating_mul(k.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(k.into()))) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(k.into()))) @@ -310,8 +318,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `370` // Estimated: `6148` - // Minimum execution time: 69_442_000 picoseconds. - Weight::from_parts(70_724_000, 6148) + // Minimum execution time: 69_645_000 picoseconds. + Weight::from_parts(71_247_000, 6148) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -323,8 +331,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 13_049_000 picoseconds. - Weight::from_parts(13_510_000, 3747) + // Minimum execution time: 11_467_000 picoseconds. + Weight::from_parts(11_828_000, 3747) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -334,8 +342,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 11_557_000 picoseconds. - Weight::from_parts(12_318_000, 3747) + // Minimum execution time: 10_125_000 picoseconds. + Weight::from_parts(10_586_000, 3747) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -345,8 +353,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `229` // Estimated: `3747` - // Minimum execution time: 11_386_000 picoseconds. - Weight::from_parts(11_797_000, 3747) + // Minimum execution time: 9_905_000 picoseconds. + Weight::from_parts(10_165_000, 3747) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -360,8 +368,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `293` // Estimated: `3747` - // Minimum execution time: 15_924_000 picoseconds. - Weight::from_parts(16_805_000, 3747) + // Minimum execution time: 13_941_000 picoseconds. + Weight::from_parts(14_311_000, 3747) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } diff --git a/pallets/proxy/src/weights.rs b/pallets/proxy/src/weights.rs index 5f6db1d84d..273578f1cd 100644 --- a/pallets/proxy/src/weights.rs +++ b/pallets/proxy/src/weights.rs @@ -59,12 +59,16 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `Proxy::LastCallResult` (r:0 w:1) /// Proof: `Proxy::LastCallResult` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) /// The range of component `p` is `[1, 19]`. fn proxy(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `637 + p * (37 ±0)` + // Measured: `672 + p * (37 ±0)` // Estimated: `4254 + p * (37 ±0)` // Minimum execution time: 22_934_000 picoseconds. Weight::from_parts(24_359_160, 4254) @@ -84,13 +88,17 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `Proxy::LastCallResult` (r:0 w:1) /// Proof: `Proxy::LastCallResult` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) /// The range of component `a` is `[0, 74]`. /// The range of component `p` is `[1, 19]`. fn proxy_announced(a: u32, p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `894 + a * (68 ±0) + p * (37 ±0)` + // Measured: `929 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615 + a * (68 ±0) + p * (37 ±0)` // Minimum execution time: 48_171_000 picoseconds. Weight::from_parts(49_373_534, 8615) @@ -275,12 +283,16 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `Proxy::LastCallResult` (r:0 w:1) /// Proof: `Proxy::LastCallResult` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) /// The range of component `p` is `[1, 19]`. fn proxy(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `637 + p * (37 ±0)` + // Measured: `672 + p * (37 ±0)` // Estimated: `4254 + p * (37 ±0)` // Minimum execution time: 22_934_000 picoseconds. Weight::from_parts(24_359_160, 4254) @@ -300,13 +312,17 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `Proxy::LastCallResult` (r:0 w:1) /// Proof: `Proxy::LastCallResult` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) /// The range of component `a` is `[0, 74]`. /// The range of component `p` is `[1, 19]`. fn proxy_announced(a: u32, p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `894 + a * (68 ±0) + p * (37 ±0)` + // Measured: `929 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615 + a * (68 ±0) + p * (37 ±0)` // Minimum execution time: 48_171_000 picoseconds. Weight::from_parts(49_373_534, 8615) diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index d0c7c46b52..1e1192a053 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -175,6 +175,24 @@ mod pallet_benchmarks { ecdsa::Signature::from_raw(signature) } + fn setup_subnet_sale_offer() + -> (NetUid, T::AccountId, T::AccountId, T::AccountId, TaoBalance) { + let netuid = NetUid::from(1); + let seller: T::AccountId = whitelisted_caller(); + let owner_hotkey: T::AccountId = account("owner_hotkey", 0, 0); + let authorized_buyer: T::AccountId = account("authorized_buyer", 0, 0); + let price = TaoBalance::from(1_000_000_000_u64); + + Subtensor::::init_new_network(netuid, 1); + SubnetOwner::::insert(netuid, seller.clone()); + assert_ok!(Subtensor::::set_subnet_owner_hotkey( + netuid, + &owner_hotkey + )); + + (netuid, seller, owner_hotkey, authorized_buyer, price) + } + #[benchmark] fn register() { let netuid = NetUid::from(1); @@ -1852,6 +1870,55 @@ mod pallet_benchmarks { assert!(!AccumulatedLeaseDividends::::contains_key(lease_id)); } + #[benchmark] + fn create_sale_offer() { + let (netuid, seller, owner_hotkey, authorized_buyer, price) = + setup_subnet_sale_offer::(); + + #[extrinsic_call] + _( + RawOrigin::Signed(seller.clone()), + netuid, + price, + Some(authorized_buyer.clone()), + ); + + let offer = SubnetSaleOffers::::get(netuid).unwrap(); + assert_eq!(offer.id, 0); + assert_eq!(offer.netuid, netuid); + assert_eq!(offer.seller_coldkey, seller); + assert_eq!(offer.seller_hotkey, owner_hotkey); + assert_eq!(offer.authorized_buyer, Some(authorized_buyer)); + assert_eq!(offer.price, price.into()); + assert_eq!(offer.created_at, frame_system::Pallet::::block_number()); + assert!(SubnetSaleFrozenColdkeys::::contains_key( + &offer.seller_coldkey + )); + assert!(SubnetSaleFrozenHotkeys::::contains_key( + &offer.seller_hotkey + )); + } + + #[benchmark] + fn cancel_sale_offer() { + let (netuid, seller, owner_hotkey, authorized_buyer, price) = + setup_subnet_sale_offer::(); + + assert_ok!(Subtensor::::create_sale_offer( + RawOrigin::Signed(seller.clone()).into(), + netuid, + price, + Some(authorized_buyer), + )); + + #[extrinsic_call] + _(RawOrigin::Signed(seller.clone()), netuid); + + assert!(!SubnetSaleOffers::::contains_key(netuid)); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + } + #[benchmark] fn update_symbol() { let coldkey: T::AccountId = whitelisted_caller(); @@ -2371,6 +2438,26 @@ mod pallet_benchmarks { } } + #[benchmark] + fn check_subnet_sale_extension() { + let who: T::AccountId = account("who", 0, 1); + let hotkey: T::AccountId = account("hotkey", 0, 1); + let call = runtime_call::(Call::::register_network { hotkey }); + + // Freeze `who` as an owner hotkey only. This exercises the worst case: the coldkey + // lookup misses (so the check does not return early) and the hotkey lookup hits, so + // both storage reads are performed. + SubnetSaleFrozenHotkeys::::insert(&who, ()); + + #[block] + { + assert_eq!( + CheckSubnetSale::::check(&who, &call), + Err(Error::::HotkeyLockedDuringSale) + ); + } + } + #[benchmark] fn check_weights_extension() { let netuid = NetUid::from(1); diff --git a/pallets/subtensor/src/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index 7899ed855e..4a2ab4e97c 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -1,6 +1,6 @@ use crate::{ Call, CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits, - CheckServingEndpoints, CheckWeights, Config, Error, guards::applicable_call, + CheckServingEndpoints, CheckSubnetSale, CheckWeights, Config, Error, guards::applicable_call, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::{ @@ -88,6 +88,7 @@ impl SubtensorTransactionExtension { }; CheckColdkeySwap::::check(who, call)?; + CheckSubnetSale::::check(who, call)?; if let Some(call) = applicable_call(call, CheckWeights::::applies_to) { CheckWeights::::check(who, call)?; @@ -126,6 +127,7 @@ where fn weight(&self, call: &CallOf) -> Weight { use DispatchExtension as DE; as DE>>::weight(call) + .saturating_add( as DE>>::weight(call)) .saturating_add( as DE>>::weight(call)) .saturating_add( as DE>>::weight(call)) .saturating_add( as DE>>::weight(call)) @@ -157,8 +159,8 @@ mod tests { use super::SubtensorTransactionExtension; use crate::{ CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits, - CheckServingEndpoints, CheckWeights, ColdkeySwapAnnouncements, ColdkeySwapDisputes, - tests::mock::*, + CheckServingEndpoints, CheckSubnetSale, CheckWeights, ColdkeySwapAnnouncements, + ColdkeySwapDisputes, SubnetSaleFrozenColdkeys, tests::mock::*, }; use frame_support::{ assert_ok, @@ -197,6 +199,7 @@ mod tests { fn expected_transaction_extension_weight(call: &RuntimeCall) -> frame_support::weights::Weight { use DispatchExtension as DE; as DE>::weight(call) + .saturating_add( as DE>::weight(call)) .saturating_add( as DE>::weight(call)) .saturating_add( as DE>::weight(call)) .saturating_add( as DE>::weight(call)) @@ -278,6 +281,19 @@ mod tests { }); } + #[test] + fn validate_rejects_sale_frozen_coldkey_call() { + new_test_ext(1).execute_with(|| { + let seller = U256::from(1); + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + + SubnetSaleFrozenColdkeys::::insert(seller, ()); + + let err = validate_signed(seller, &call).unwrap_err(); + assert_eq!(err, CustomTransactionError::BadRequest.into()); + }); + } + #[test] fn weight_matches_top_level_dispatch_extension_checks() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_coldkey_swap.rs b/pallets/subtensor/src/guards/check_coldkey_swap.rs index 907fed1d3b..683bc43025 100644 --- a/pallets/subtensor/src/guards/check_coldkey_swap.rs +++ b/pallets/subtensor/src/guards/check_coldkey_swap.rs @@ -134,6 +134,11 @@ mod tests { RuntimeCall::SubtensorModule(crate::Call::register_network { hotkey: U256::from(1), }), + RuntimeCall::SubtensorModule(crate::Call::create_sale_offer { + netuid: 1u16.into(), + price: TaoBalance::from(1_000_u64), + authorized_buyer: None, + }), ] } diff --git a/pallets/subtensor/src/guards/check_subnet_sale.rs b/pallets/subtensor/src/guards/check_subnet_sale.rs new file mode 100644 index 0000000000..f31dde7362 --- /dev/null +++ b/pallets/subtensor/src/guards/check_subnet_sale.rs @@ -0,0 +1,302 @@ +use super::{CallOf, DispatchableOriginOf}; +use crate::weights::WeightInfo; +use crate::{Call, Config, Error, SubnetSaleFrozenColdkeys, SubnetSaleFrozenHotkeys}; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchExtension, DispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + traits::{IsSubType, OriginTrait}, +}; +use sp_runtime::traits::Dispatchable; +use sp_std::marker::PhantomData; + +/// Dispatch extension that blocks seller coldkey and owner hotkey calls during a subnet sale. +/// +/// When a subnet sale offer is active: +/// - The frozen seller coldkey can only cancel the sale offer. +/// - The frozen owner hotkey is fully locked and can submit no calls. +/// +/// Root origin bypasses this extension entirely. +/// Non-signed origins pass through. +/// +/// Because this is a `DispatchExtension` (not a `TransactionExtension`), it fires at every +/// `call.dispatch(origin)` site, including inside proxy dispatch with the resolved origin. +/// Any indirectly dispatched call that resolves to a frozen signer is therefore re-checked +/// here, so the freeze cannot be bypassed by wrapping a call in another dispatch layer. +pub struct CheckSubnetSale(PhantomData); + +impl CheckSubnetSale +where + T: Config, + CallOf: IsSubType>, +{ + pub fn check(who: &T::AccountId, call: &CallOf) -> Result<(), Error> { + // A frozen seller coldkey may only cancel the sale offer. + if SubnetSaleFrozenColdkeys::::contains_key(who) + && !matches!(call.is_sub_type(), Some(Call::cancel_sale_offer { .. })) + { + return Err(Error::::ColdkeyLockedDuringSale); + } + + // A frozen seller hotkey is fully locked while the sale is active. Cancellation is a + // coldkey action, so the seller cancels through the (also frozen) seller coldkey, + // which is why a same-account seller hotkey is never frozen here (see do_create_sale_offer). + if SubnetSaleFrozenHotkeys::::contains_key(who) { + return Err(Error::::HotkeyLockedDuringSale); + } + + Ok(()) + } +} + +impl DispatchExtension<::RuntimeCall> for CheckSubnetSale +where + T: Config, + ::RuntimeCall: + Dispatchable + IsSubType>, + DispatchableOriginOf: OriginTrait, +{ + type Pre = (); + + fn weight(_call: &CallOf) -> Weight { + ::WeightInfo::check_subnet_sale_extension() + } + + fn pre_dispatch( + origin: &DispatchableOriginOf, + call: &CallOf, + ) -> Result { + let Some(who) = origin.as_signer() else { + return Ok(()); + }; + + Self::check(who, call).map_err(Into::into) + } +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +mod tests { + use crate::{ + Error, SubnetSaleFrozenColdkeys, SubnetSaleFrozenHotkeys, tests::mock::*, + weights::WeightInfo as _, + }; + use frame_support::{ + assert_noop, assert_ok, + dispatch::{DispatchErrorWithPostInfo, DispatchExtension}, + }; + use frame_system::Call as SystemCall; + use pallet_subtensor_proxy::Call as ProxyCall; + use sp_core::U256; + use sp_runtime::traits::Dispatchable; + use subtensor_runtime_common::{NetUid, ProxyType, TaoBalance}; + + type SaleGuard = super::CheckSubnetSale; + + fn pre_dispatch( + origin: RuntimeOrigin, + call: &RuntimeCall, + ) -> Result<(), DispatchErrorWithPostInfo> { + >::pre_dispatch(&origin, call) + } + + fn sale_netuid() -> NetUid { + NetUid::from(1) + } + + fn freeze_coldkey(who: U256) { + SubnetSaleFrozenColdkeys::::insert(who, ()); + } + + fn freeze_owner_hotkey(who: U256) { + SubnetSaleFrozenHotkeys::::insert(who, ()); + } + + fn remark_call() -> RuntimeCall { + RuntimeCall::System(SystemCall::remark { remark: vec![] }) + } + + fn cancel_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(crate::Call::cancel_sale_offer { + netuid: sale_netuid(), + }) + } + + fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { + let credit = SubtensorModule::mint_tao(tao); + let _ = SubtensorModule::spend_tao(coldkey, credit, tao).unwrap(); + } + + #[test] + fn no_sale_freeze_allows_signed_calls() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + + assert_ok!(pre_dispatch(RuntimeOrigin::signed(who), &remark_call())); + }); + } + + #[test] + fn none_and_root_bypass_sale_freezes() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + freeze_coldkey(who); + freeze_owner_hotkey(who); + + assert_ok!(pre_dispatch(RuntimeOrigin::none(), &remark_call())); + assert_ok!(pre_dispatch(RuntimeOrigin::root(), &remark_call())); + }); + } + + #[test] + fn freeze_coldkey_blocks_regular_signed_calls() { + new_test_ext(1).execute_with(|| { + let seller = U256::from(1); + freeze_coldkey(seller); + + assert_noop!( + pre_dispatch(RuntimeOrigin::signed(seller), &remark_call()), + Error::::ColdkeyLockedDuringSale + ); + }); + } + + #[test] + fn freeze_owner_hotkey_blocks_regular_signed_calls() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(2); + freeze_owner_hotkey(owner_hotkey); + + assert_noop!( + pre_dispatch(RuntimeOrigin::signed(owner_hotkey), &remark_call()), + Error::::HotkeyLockedDuringSale + ); + }); + } + + #[test] + fn freeze_coldkey_allows_sale_cancellation() { + new_test_ext(1).execute_with(|| { + let seller = U256::from(1); + freeze_coldkey(seller); + + assert_ok!(pre_dispatch(RuntimeOrigin::signed(seller), &cancel_call())); + }); + } + + #[test] + fn freeze_owner_hotkey_does_not_allow_sale_cancellation() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(2); + freeze_owner_hotkey(owner_hotkey); + + assert_noop!( + pre_dispatch(RuntimeOrigin::signed(owner_hotkey), &cancel_call()), + Error::::HotkeyLockedDuringSale + ); + }); + } + + #[test] + fn frozen_owner_hotkey_rejects_sale_cancellation_even_if_coldkey() { + new_test_ext(1).execute_with(|| { + let seller_and_owner_hotkey = U256::from(1); + freeze_coldkey(seller_and_owner_hotkey); + freeze_owner_hotkey(seller_and_owner_hotkey); + + assert_noop!( + pre_dispatch( + RuntimeOrigin::signed(seller_and_owner_hotkey), + &cancel_call() + ), + Error::::HotkeyLockedDuringSale + ); + }); + } + + #[test] + fn weight_is_constant_across_calls_because_freeze_can_block_any_signed_call() { + let expected = ::WeightInfo::check_subnet_sale_extension(); + + for call in [remark_call(), cancel_call()] { + assert_eq!( + >::weight(&call), + expected + ); + } + } + + #[test] + fn proxied_call_from_sale_frozen_coldkey_is_blocked() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate = U256::from(2); + freeze_coldkey(real); + + add_balance_to_coldkey_account(&real, 1_000_000_000.into()); + add_balance_to_coldkey_account(&delegate, 1_000_000_000.into()); + + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(real), + delegate, + ProxyType::Any, + 0 + )); + + let proxy_call = RuntimeCall::Proxy(ProxyCall::proxy { + real, + force_proxy_type: None, + call: Box::new(remark_call()), + }); + + assert_ok!(proxy_call.dispatch(RuntimeOrigin::signed(delegate))); + assert_eq!( + pallet_subtensor_proxy::LastCallResult::::get(real), + Some(Err(Error::::ColdkeyLockedDuringSale.into())) + ); + }); + } + + #[test] + fn nested_proxied_call_from_sale_frozen_owner_hotkey_is_blocked() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate1 = U256::from(2); + let delegate2 = U256::from(3); + freeze_owner_hotkey(real); + + add_balance_to_coldkey_account(&real, 1_000_000_000.into()); + add_balance_to_coldkey_account(&delegate1, 1_000_000_000.into()); + add_balance_to_coldkey_account(&delegate2, 1_000_000_000.into()); + + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(real), + delegate1, + ProxyType::Any, + 0 + )); + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(delegate1), + delegate2, + ProxyType::Any, + 0 + )); + + let inner_proxy = RuntimeCall::Proxy(ProxyCall::proxy { + real, + force_proxy_type: None, + call: Box::new(remark_call()), + }); + let outer_proxy = RuntimeCall::Proxy(ProxyCall::proxy { + real: delegate1, + force_proxy_type: None, + call: Box::new(inner_proxy), + }); + + assert_ok!(outer_proxy.dispatch(RuntimeOrigin::signed(delegate2))); + assert_eq!( + pallet_subtensor_proxy::LastCallResult::::get(real), + Some(Err(Error::::HotkeyLockedDuringSale.into())) + ); + }); + } +} diff --git a/pallets/subtensor/src/guards/mod.rs b/pallets/subtensor/src/guards/mod.rs index 3865352858..2e4fc1c5eb 100644 --- a/pallets/subtensor/src/guards/mod.rs +++ b/pallets/subtensor/src/guards/mod.rs @@ -3,6 +3,7 @@ mod check_delegate_take; mod check_evm_key_association; mod check_rate_limits; mod check_serving_endpoints; +mod check_subnet_sale; mod check_weights; use crate::{Call, Config}; @@ -14,6 +15,7 @@ pub use check_delegate_take::*; pub use check_evm_key_association::*; pub use check_rate_limits::*; pub use check_serving_endpoints::*; +pub use check_subnet_sale::*; pub use check_weights::*; pub(crate) type CallOf = ::RuntimeCall; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f19b8086a3..4683a93d6f 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -93,9 +93,12 @@ pub const ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA: u128 = 1u128 << 0; pub mod pallet { use crate::migrations; use crate::staking::lock::LockState; - use crate::subnets::dissolution::DissolveCleanupStatus; - use crate::subnets::leasing::{LeaseId, SubnetLeaseOf}; - use crate::subnets::subnet::NetworkRegistrationInfo; + use crate::subnets::{ + leasing::{LeaseId, SubnetLeaseOf}, + sale_offer::{SaleOfferId, SubnetSaleOfferOf}, + dissolution::DissolveCleanupStatus, + subnet::NetworkRegistrationInfo, + }; use crate::weights::WeightInfo; use crate::{MAX_ASSOCIATED_UIDS_PER_EVM_ADDRESS, RateLimitKey}; use frame_support::Twox64Concat; @@ -112,7 +115,7 @@ pub mod pallet { use pallet_drand::types::RoundNumber; use runtime_common::prod_or_fast; use share_pool::SafeFloat; - use sp_core::{ConstU32, H160, H256}; + use sp_core::{ConstU32, ConstU64, H160, H256}; use sp_runtime::traits::{Dispatchable, TrailingZeroInput}; use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; @@ -2664,6 +2667,29 @@ pub mod pallet { pub type AccumulatedLeaseDividends = StorageMap<_, Twox64Concat, LeaseId, AlphaBalance, ValueQuery, DefaultZeroAlpha>; + /// =========================== + /// ==== Subnet Sale Offers ==== + /// =========================== + /// --- ITEM ( next_sale_offer_id ) | The next subnet sale offer id. + #[pallet::storage] + pub type NextSubnetSaleOfferId = + StorageValue<_, SaleOfferId, ValueQuery, ConstU64<0>>; + + /// --- MAP ( netuid ) --> subnet sale offer | Active sale offer for a subnet. + #[pallet::storage] + pub type SubnetSaleOffers = + StorageMap<_, Twox64Concat, NetUid, SubnetSaleOfferOf, OptionQuery>; + + /// --- MAP ( coldkey ) --> () | Owner coldkeys frozen by an active subnet sale offer. + #[pallet::storage] + pub type SubnetSaleFrozenColdkeys = + StorageMap<_, Identity, T::AccountId, (), OptionQuery>; + + /// --- MAP ( hotkey ) --> () | Owner hotkeys frozen by an active subnet sale offer. + #[pallet::storage] + pub type SubnetSaleFrozenHotkeys = + StorageMap<_, Identity, T::AccountId, (), OptionQuery>; + /// --- ITEM ( CommitRevealWeightsVersion ) #[pallet::storage] pub type CommitRevealWeightsVersion = diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index f06e42201e..ef8c9cb5b6 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2643,5 +2643,31 @@ mod dispatches { Self::deposit_event(Event::RejectLockedAlphaUpdated { coldkey, enabled }); Ok(()) } + + /// Create a sale offer for an owned subnet. + /// + /// While the offer exists, the seller coldkey, subnet, and owner hotkey are locked. + /// The seller can optionally restrict the future buyer to a specific coldkey. + #[pallet::call_index(143)] + #[pallet::weight(::WeightInfo::create_sale_offer())] + pub fn create_sale_offer( + origin: OriginFor, + netuid: NetUid, + price: TaoBalance, + authorized_buyer: Option, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_create_sale_offer(who, netuid, price, authorized_buyer) + } + + /// Cancel an active subnet sale offer. + /// + /// The seller that created the offer or root can cancel it. + #[pallet::call_index(144)] + #[pallet::weight(::WeightInfo::cancel_sale_offer())] + pub fn cancel_sale_offer(origin: OriginFor, netuid: NetUid) -> DispatchResult { + let maybe_who = crate::system::ensure_signed_or_root(origin)?; + Self::do_cancel_sale_offer(maybe_who, netuid) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 434951856f..573c5b4df8 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -229,6 +229,16 @@ mod errors { LeaseHasNoEndBlock, /// Lease has not ended. LeaseHasNotEnded, + /// Active subnet sale offer already exists for this subnet. + SaleOfferAlreadyExists, + /// No active subnet sale offer exists for this subnet. + SaleOfferNotFound, + /// The subnet is under an active lease and cannot be listed for sale. + SubnetIsLeased, + /// The seller coldkey is locked by an active subnet sale offer. + ColdkeyLockedDuringSale, + /// The subnet owner hotkey is locked by an active subnet sale offer. + HotkeyLockedDuringSale, /// An overflow occurred. Overflow, /// Beneficiary does not own hotkey. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 698cc4bc16..71da47b2c7 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -396,6 +396,30 @@ mod events { netuid: NetUid, }, + /// A subnet sale offer has been created and the seller coldkey has been locked. + SubnetSaleOfferCreated { + /// The unique id of the sale offer. + id: SaleOfferId, + /// The subnet owner coldkey selling the subnet. + seller_coldkey: T::AccountId, + /// The subnet ID. + netuid: NetUid, + /// The sale price. + price: TaoBalance, + /// Optional buyer coldkey authorized to consume the offer. + authorized_buyer: Option, + }, + + /// A subnet sale offer has been cancelled and its locks have been released. + SubnetSaleOfferCancelled { + /// The unique id of the sale offer. + id: SaleOfferId, + /// The subnet owner coldkey that cancelled the offer. + seller_coldkey: T::AccountId, + /// The subnet ID. + netuid: NetUid, + }, + /// The symbol for a subnet has been updated. SymbolUpdated { /// The subnet ID diff --git a/pallets/subtensor/src/subnets/mod.rs b/pallets/subtensor/src/subnets/mod.rs index c04db2027b..5320f83723 100644 --- a/pallets/subtensor/src/subnets/mod.rs +++ b/pallets/subtensor/src/subnets/mod.rs @@ -3,6 +3,7 @@ pub mod dissolution; pub mod leasing; pub mod mechanism; pub mod registration; +pub mod sale_offer; pub mod serving; pub mod subnet; pub mod symbols; diff --git a/pallets/subtensor/src/subnets/sale_offer.rs b/pallets/subtensor/src/subnets/sale_offer.rs new file mode 100644 index 0000000000..18909518f0 --- /dev/null +++ b/pallets/subtensor/src/subnets/sale_offer.rs @@ -0,0 +1,152 @@ +//! Subnet sale offers and sale-time freezes. +//! +//! This module intentionally only owns the seller-side primitive: listing a subnet +//! for sale freezes the seller coldkey and seller hotkey until the offer is cancelled +//! or later consumed by a sale finalization path. + +use super::*; +use frame_support::traits::fungible; +use frame_system::pallet_prelude::BlockNumberFor; +use subtensor_runtime_common::{NetUid, TaoBalance}; + +pub type CurrencyOf = ::Currency; + +pub type BalanceOf = + as fungible::Inspect<::AccountId>>::Balance; + +/// Monotonically increasing identifier minted for every subnet sale offer. +/// +/// A fresh id is assigned on each creation so that a consumer that binds to an +/// offer (e.g. a crowdloan raising funds to buy the subnet) can detect a +/// cancel-and-recreate: the recreated offer carries a new id and no longer +/// matches the one that was bound, so the stale binding fails instead of being +/// silently satisfied by a different offer with modified terms. +pub type SaleOfferId = u64; + +#[freeze_struct("6adb80684dface3a")] +#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct SubnetSaleOffer { + /// Unique identifier for this offer. + pub id: SaleOfferId, + /// The subnet being sold. + pub netuid: NetUid, + /// The subnet owner coldkey that created the offer. + pub seller_coldkey: AccountId, + /// The subnet owner hotkey frozen by this offer. + /// + /// Kept here (rather than re-read from `SubnetOwnerHotkey` at unfreeze time) so + /// cancellation and subnet dissolution unfreeze exactly the hotkey that was frozen, + /// even if the subnet owner hotkey changed while the offer was active. + pub seller_hotkey: AccountId, + /// Optional coldkey that is allowed to consume this offer. + pub authorized_buyer: Option, + /// Sale price expected by the seller. + pub price: Balance, + /// Block at which the sale offer was created. + pub created_at: BlockNumber, +} + +pub type SubnetSaleOfferOf = SubnetSaleOffer, BalanceOf, BlockNumberFor>; + +impl Pallet { + pub fn do_create_sale_offer( + seller_coldkey: T::AccountId, + netuid: NetUid, + price: TaoBalance, + authorized_buyer: Option, + ) -> DispatchResult { + ensure!(price > TaoBalance::from(0_u64), Error::::AmountTooLow); + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + ensure!( + SubnetOwner::::get(netuid) == seller_coldkey, + Error::::NotSubnetOwner + ); + ensure!( + !SubnetUidToLeaseId::::contains_key(netuid), + Error::::SubnetIsLeased + ); + ensure!( + !SubnetSaleOffers::::contains_key(netuid), + Error::::SaleOfferAlreadyExists + ); + ensure!( + !SubnetSaleFrozenColdkeys::::contains_key(&seller_coldkey), + Error::::ColdkeyLockedDuringSale + ); + let seller_hotkey = SubnetOwnerHotkey::::try_get(netuid) + .map_err(|_| Error::::HotKeyAccountNotExists)?; + ensure!( + !SubnetSaleFrozenHotkeys::::contains_key(&seller_hotkey), + Error::::HotkeyLockedDuringSale + ); + + let id = Self::get_next_sale_offer_id()?; + + SubnetSaleOffers::::insert( + netuid, + SubnetSaleOffer { + id, + netuid, + seller_coldkey: seller_coldkey.clone(), + seller_hotkey: seller_hotkey.clone(), + authorized_buyer: authorized_buyer.clone(), + price: price.into(), + created_at: frame_system::Pallet::::block_number(), + }, + ); + SubnetSaleFrozenColdkeys::::insert(&seller_coldkey, ()); + // When the seller coldkey and seller hotkey are the same account, the coldkey freeze + // already locks it (and still permits cancellation). Freezing it in the hotkey map as + // well would block cancellation, permanently locking the seller out of their own + // offer, so we skip it in that case. + if seller_hotkey != seller_coldkey { + SubnetSaleFrozenHotkeys::::insert(&seller_hotkey, ()); + } + + Self::deposit_event(Event::SubnetSaleOfferCreated { + id, + seller_coldkey, + netuid, + price, + authorized_buyer, + }); + + Ok(()) + } + + /// Mint the next unique sale offer id, incrementing the on-chain counter. + fn get_next_sale_offer_id() -> Result> { + let id = NextSubnetSaleOfferId::::get(); + let next_id = id.checked_add(1).ok_or(Error::::Overflow)?; + NextSubnetSaleOfferId::::put(next_id); + Ok(id) + } + + pub fn do_cancel_sale_offer( + maybe_seller_coldkey: Option, + netuid: NetUid, + ) -> DispatchResult { + let offer = SubnetSaleOffers::::get(netuid).ok_or(Error::::SaleOfferNotFound)?; + + // If the caller is not the seller, they are root. + if let Some(seller_coldkey) = maybe_seller_coldkey { + ensure!( + seller_coldkey == offer.seller_coldkey, + Error::::NotSubnetOwner + ); + } + + let seller_coldkey = offer.seller_coldkey.clone(); + SubnetSaleOffers::::remove(offer.netuid); + SubnetSaleFrozenColdkeys::::remove(&offer.seller_coldkey); + SubnetSaleFrozenHotkeys::::remove(&offer.seller_hotkey); + + Self::deposit_event(Event::SubnetSaleOfferCancelled { + id: offer.id, + seller_coldkey, + netuid, + }); + + Ok(()) + } +} diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index ca55340554..2386373c9b 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -63,6 +63,14 @@ impl Pallet { // 5. Ensure the new hotkey is different from the old one ensure!(old_hotkey != new_hotkey, Error::::NewHotKeyIsSameWithOld); + // 5.1 Ensure the old hotkey is not frozen by an active subnet sale offer. + // Swapping it would rewrite SubnetOwnerHotkey while the sale freeze maps still + // reference the old hotkey, bypassing the freeze and leaving it frozen forever. + ensure!( + !SubnetSaleFrozenHotkeys::::contains_key(old_hotkey), + Error::::HotkeyLockedDuringSale + ); + // 6. Get the current block number let block: u64 = Self::get_current_block_as_u64(); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 4af03c19fe..6c3a5a58d9 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -197,6 +197,7 @@ impl system::Config for Test { type Block = Block; type DispatchExtension = ( crate::CheckColdkeySwap, + crate::CheckSubnetSale, crate::CheckWeights, crate::CheckRateLimits, crate::CheckDelegateTake, diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index fec3b580d0..1d5a97ceaa 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -25,6 +25,7 @@ mod networks; mod neuron_info; mod recycle_alpha; mod registration; +mod sale_offer; mod remove_data_tests; mod serving; mod staking; diff --git a/pallets/subtensor/src/tests/sale_offer.rs b/pallets/subtensor/src/tests/sale_offer.rs new file mode 100644 index 0000000000..52e36f9587 --- /dev/null +++ b/pallets/subtensor/src/tests/sale_offer.rs @@ -0,0 +1,421 @@ +#![allow(clippy::unwrap_used)] + +use super::mock::*; +use crate::*; +use frame_support::{assert_err, assert_noop, assert_ok}; +use sp_core::U256; +use subtensor_runtime_common::{NetUid, TaoBalance}; + +const SELLER: u64 = 1; +const OWNER_HOTKEY: u64 = 2; +const BUYER: u64 = 3; + +fn sale_fixture() -> (NetUid, U256, U256, U256) { + let seller = U256::from(SELLER); + let owner_hotkey = U256::from(OWNER_HOTKEY); + let buyer = U256::from(BUYER); + let netuid = add_dynamic_network(&owner_hotkey, &seller); + + (netuid, seller, owner_hotkey, buyer) +} + +fn create_offer(netuid: NetUid, seller: U256, buyer: Option) { + assert_ok!(SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + buyer, + )); +} + +#[test] +fn create_sale_offer_stores_offer_and_freezes_keys() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + let price = TaoBalance::from(1_000_000_000_u64); + + assert_ok!(SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + price, + Some(buyer), + )); + + let offer = SubnetSaleOffers::::get(netuid).unwrap(); + assert_eq!(offer.id, 0); + assert_eq!(offer.netuid, netuid); + assert_eq!(offer.seller_coldkey, seller); + assert_eq!(offer.seller_hotkey, owner_hotkey); + assert_eq!(offer.authorized_buyer, Some(buyer)); + assert_eq!(offer.price, price); + assert_eq!(offer.created_at, System::block_number()); + assert!(SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + assert_eq!( + last_event(), + RuntimeEvent::SubtensorModule(Event::SubnetSaleOfferCreated { + id: 0, + seller_coldkey: seller, + netuid, + price, + authorized_buyer: Some(buyer), + }) + ); + }); +} + +#[test] +fn create_sale_offer_assigns_incrementing_ids() { + new_test_ext(1).execute_with(|| { + let (first_netuid, seller, _first_owner_hotkey, buyer) = sale_fixture(); + let second_owner_hotkey = U256::from(20); + let second_seller = U256::from(21); + let second_netuid = add_dynamic_network(&second_owner_hotkey, &second_seller); + + create_offer(first_netuid, seller, Some(buyer)); + create_offer(second_netuid, second_seller, None); + + assert_eq!(SubnetSaleOffers::::get(first_netuid).unwrap().id, 0); + assert_eq!(SubnetSaleOffers::::get(second_netuid).unwrap().id, 1); + // The counter keeps advancing; ids are never reused. + assert_eq!(NextSubnetSaleOfferId::::get(), 2); + }); +} + +#[test] +fn create_sale_offer_allows_open_offer() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, _buyer) = sale_fixture(); + + assert_ok!(SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + None, + )); + + let offer = SubnetSaleOffers::::get(netuid).unwrap(); + assert_eq!(offer.authorized_buyer, None); + }); +} + +#[test] +fn create_sale_offer_rejects_zero_price() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(0_u64), + Some(buyer), + ), + Error::::AmountTooLow, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_missing_subnet() { + new_test_ext(1).execute_with(|| { + let seller = U256::from(SELLER); + let missing_netuid = NetUid::from(99); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + missing_netuid, + TaoBalance::from(1_000_000_000_u64), + None, + ), + Error::::SubnetNotExists, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_non_owner() { + new_test_ext(1).execute_with(|| { + let (netuid, _seller, _owner_hotkey, buyer) = sale_fixture(); + let not_owner = U256::from(99); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(not_owner), + netuid, + TaoBalance::from(1_000_000_000_u64), + Some(buyer), + ), + Error::::NotSubnetOwner, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_existing_offer() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + create_offer(netuid, seller, Some(buyer)); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(2_000_000_000_u64), + None, + ), + Error::::SaleOfferAlreadyExists, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_frozen_seller() { + new_test_ext(1).execute_with(|| { + let (first_netuid, seller, _first_owner_hotkey, buyer) = sale_fixture(); + let second_owner_hotkey = U256::from(20); + let second_netuid = add_dynamic_network(&second_owner_hotkey, &seller); + create_offer(first_netuid, seller, Some(buyer)); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + second_netuid, + TaoBalance::from(2_000_000_000_u64), + None, + ), + Error::::ColdkeyLockedDuringSale, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_missing_owner_hotkey() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + SubnetOwnerHotkey::::remove(netuid); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + Some(buyer), + ), + Error::::HotKeyAccountNotExists, + ); + }); +} + +#[test] +fn create_sale_offer_allows_owner_hotkey_same_as_seller() { + new_test_ext(1).execute_with(|| { + let seller = U256::from(SELLER); + let buyer = U256::from(BUYER); + // Subnet whose owner coldkey and owner hotkey are the same account. + let netuid = add_dynamic_network(&seller, &seller); + + assert_ok!(SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + Some(buyer), + )); + + // Only the coldkey is frozen: freezing the same account in the hotkey map as well + // would block the seller from ever cancelling their own offer. + assert!(SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key(seller)); + + // The seller can still cancel and fully release the lock. + assert_ok!(SubtensorModule::cancel_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + )); + assert!(!SubnetSaleOffers::::contains_key(netuid)); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + }); +} + +#[test] +fn create_sale_offer_rejects_leased_subnet() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + SubnetUidToLeaseId::::insert(netuid, 0); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + Some(buyer), + ), + Error::::SubnetIsLeased, + ); + }); +} + +#[test] +fn create_sale_offer_rejects_frozen_owner_hotkey() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + SubnetSaleFrozenHotkeys::::insert(owner_hotkey, ()); + + assert_noop!( + SubtensorModule::create_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + TaoBalance::from(1_000_000_000_u64), + Some(buyer), + ), + Error::::HotkeyLockedDuringSale, + ); + }); +} + +#[test] +fn cancel_sale_offer_unfreezes_keys() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + create_offer(netuid, seller, Some(buyer)); + + assert_ok!(SubtensorModule::cancel_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + )); + + assert!(!SubnetSaleOffers::::contains_key(netuid)); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + assert_eq!( + last_event(), + RuntimeEvent::SubtensorModule(Event::SubnetSaleOfferCancelled { + id: 0, + seller_coldkey: seller, + netuid + }) + ); + }); +} + +#[test] +fn cancel_sale_offer_root_unfreezes_keys() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + create_offer(netuid, seller, Some(buyer)); + + assert_ok!(SubtensorModule::cancel_sale_offer( + RuntimeOrigin::root(), + netuid, + )); + + assert!(!SubnetSaleOffers::::contains_key(netuid)); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + assert_eq!( + last_event(), + RuntimeEvent::SubtensorModule(Event::SubnetSaleOfferCancelled { + id: 0, + seller_coldkey: seller, + netuid + }) + ); + }); +} + +#[test] +fn cancel_sale_offer_rejects_missing_offer() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, _buyer) = sale_fixture(); + + assert_noop!( + SubtensorModule::cancel_sale_offer(RuntimeOrigin::signed(seller), netuid), + Error::::SaleOfferNotFound, + ); + }); +} + +#[test] +fn cancel_sale_offer_rejects_non_seller() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + let not_seller = U256::from(99); + create_offer(netuid, seller, Some(buyer)); + + assert_noop!( + SubtensorModule::cancel_sale_offer(RuntimeOrigin::signed(not_seller), netuid), + Error::::NotSubnetOwner, + ); + }); +} + +#[test] +fn cancel_sale_offer_unfreezes_original_hotkey_after_owner_hotkey_change() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + create_offer(netuid, seller, Some(buyer)); + + // Simulate the subnet owner hotkey changing mid-sale (only root can do this + // while the seller is frozen). + let new_owner_hotkey = U256::from(99); + SubnetOwnerHotkey::::insert(netuid, new_owner_hotkey); + + assert_ok!(SubtensorModule::cancel_sale_offer( + RuntimeOrigin::signed(seller), + netuid, + )); + + // The hotkey frozen at offer creation is unfrozen, not the current one. + assert!(!SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key( + new_owner_hotkey + )); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + }); +} + +#[test] +fn swap_hotkey_rejects_sale_frozen_owner_hotkey() { + new_test_ext(1).execute_with(|| { + // The subnet owner hotkey is owned by a coldkey other than the seller, so the + // sale freeze on the seller coldkey alone would not stop the owner from + // swapping the hotkey out from under the offer. + let (netuid, seller, _owner_hotkey, buyer) = sale_fixture(); + let other_coldkey = U256::from(50); + let other_hotkey = U256::from(51); + let new_hotkey = U256::from(52); + register_ok_neuron(netuid, other_hotkey, other_coldkey, 0); + SubnetOwnerHotkey::::insert(netuid, other_hotkey); + create_offer(netuid, seller, Some(buyer)); + assert!(SubnetSaleFrozenHotkeys::::contains_key(other_hotkey)); + + assert_err!( + SubtensorModule::do_swap_hotkey( + RuntimeOrigin::signed(other_coldkey), + &other_hotkey, + &new_hotkey, + None, + false, + ), + Error::::HotkeyLockedDuringSale + ); + }); +} + +#[test] +fn remove_network_cleans_sale_offer() { + new_test_ext(1).execute_with(|| { + let (netuid, seller, owner_hotkey, buyer) = sale_fixture(); + create_offer(netuid, seller, Some(buyer)); + + assert_ok!(SubtensorModule::root_dissolve_network( + RuntimeOrigin::root(), + netuid, + )); + + assert!(!SubnetSaleOffers::::contains_key(netuid)); + assert!(!SubnetSaleFrozenColdkeys::::contains_key(seller)); + assert!(!SubnetSaleFrozenHotkeys::::contains_key(owner_hotkey)); + }); +} diff --git a/pallets/subtensor/src/weights.rs b/pallets/subtensor/src/weights.rs index 244a11c46c..c6298625a6 100644 --- a/pallets/subtensor/src/weights.rs +++ b/pallets/subtensor/src/weights.rs @@ -80,6 +80,8 @@ pub trait WeightInfo { fn remove_stake_full_limit() -> Weight; fn register_leased_network(k: u32, ) -> Weight; fn terminate_lease(k: u32, ) -> Weight; + fn create_sale_offer() -> Weight; + fn cancel_sale_offer() -> Weight; fn update_symbol() -> Weight; fn commit_timelocked_weights() -> Weight; fn set_coldkey_auto_stake_hotkey() -> Weight; @@ -98,6 +100,7 @@ pub trait WeightInfo { fn set_activity_cutoff_factor() -> Weight; fn trigger_epoch() -> Weight; fn check_coldkey_swap_extension() -> Weight; + fn check_subnet_sale_extension() -> Weight; fn check_weights_extension() -> Weight; fn check_rate_limits_extension() -> Weight; fn check_delegate_take_extension() -> Weight; diff --git a/pallets/utility/src/weights.rs b/pallets/utility/src/weights.rs index 30d2417a6b..58bada20df 100644 --- a/pallets/utility/src/weights.rs +++ b/pallets/utility/src/weights.rs @@ -52,6 +52,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn batch(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -67,6 +71,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn as_derivative() -> Weight { // Proof Size summary in bytes: // Measured: `518` @@ -79,6 +87,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn batch_all(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -101,6 +113,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn force_batch(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -123,6 +139,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn if_else() -> Weight { // Proof Size summary in bytes: // Measured: `518` @@ -139,6 +159,10 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn batch(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -154,6 +178,10 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn as_derivative() -> Weight { // Proof Size summary in bytes: // Measured: `518` @@ -166,6 +194,10 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn batch_all(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -188,6 +220,10 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// The range of component `c` is `[0, 1000]`. fn force_batch(c: u32, ) -> Weight { // Proof Size summary in bytes: @@ -210,6 +246,10 @@ impl WeightInfo for () { /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenColdkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetSaleFrozenHotkeys` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetSaleFrozenHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn if_else() -> Weight { // Proof Size summary in bytes: // Measured: `518` diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f5f5ddfcc7..cabf5fc023 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -350,6 +350,7 @@ impl frame_system::Config for Runtime { type ExtensionsWeightInfo = frame_system::SubstrateExtensionsWeight; type DispatchExtension = ( pallet_subtensor::CheckColdkeySwap, + pallet_subtensor::CheckSubnetSale, pallet_subtensor::CheckWeights, pallet_subtensor::CheckRateLimits, pallet_subtensor::CheckDelegateTake, diff --git a/runtime/src/proxy_filters/call_groups.rs b/runtime/src/proxy_filters/call_groups.rs index 6099b2ad65..3989fd2819 100644 --- a/runtime/src/proxy_filters/call_groups.rs +++ b/runtime/src/proxy_filters/call_groups.rs @@ -430,7 +430,7 @@ call_filter_group!( // Residual pallet-subtensor calls that no proxy needs to grant on their own: // weights, serving, delegate-take, alpha lock/burn/preferences, network // registration, childkey admin, account association, tempo control, voting -// power, root-claim admin, and lease teardown. +// power, root-claim admin, lease teardown, and sale-offer cancellation. call_filter_group!( SubtensorCommonCalls, [ @@ -471,6 +471,7 @@ call_filter_group!( RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_min_childkey_take), RuntimeCall::SubtensorModule(SubtensorCall::sudo_set_max_childkey_take), RuntimeCall::SubtensorModule(SubtensorCall::terminate_lease), + RuntimeCall::SubtensorModule(SubtensorCall::cancel_sale_offer), RuntimeCall::SubtensorModule(SubtensorCall::set_tempo), RuntimeCall::SubtensorModule(SubtensorCall::set_activity_cutoff_factor), RuntimeCall::SubtensorModule(SubtensorCall::trigger_epoch), @@ -482,6 +483,16 @@ call_filter_group!( ] ); +// Listing an owned subnet for sale: an ownership/economic disposal (the offer's +// price/buyer become a pre-authorized sale once finalization lands), so it is +// granted by no proxy allowlist — only `Any` or the coldkey directly. +call_filter_group!( + SubnetSaleCalls, + [RuntimeCall::SubtensorModule( + SubtensorCall::create_sale_offer + ),] +); + // Subnet parameters a subnet owner may set directly (the admin-utils calls // guarded by `ensure_sn_owner_or_root`). These are the genuine owner/lease // management surface, as opposed to the root-only `RootConfigCalls`. @@ -678,6 +689,7 @@ type SubtensorSplitCalls = ( SubnetIdentityCalls, SubnetActivationCalls, SubtensorCommonCalls, + SubnetSaleCalls, ); // admin-utils, split for the Owner and SubnetLeaseBeneficiary proxies. diff --git a/runtime/src/proxy_filters/mod.rs b/runtime/src/proxy_filters/mod.rs index df1d936066..d3368e01fe 100644 --- a/runtime/src/proxy_filters/mod.rs +++ b/runtime/src/proxy_filters/mod.rs @@ -309,6 +309,7 @@ mod tests { let denied = &(&group_calls::() | &group_calls::()) | &(&group_calls::() | &group_calls::()); + let denied = &denied | &group_calls::(); assert_eq!( allowed_calls(ProxyType::NonTransfer), &all_runtime_calls() - &denied @@ -323,6 +324,7 @@ mod tests { | &(&(&group_calls::() | &group_calls::()) | &(&group_calls::() | &group_calls::())); + let denied = &denied | &group_calls::(); assert_eq!( allowed_calls(ProxyType::NonFungible), &all_runtime_calls() - &denied @@ -334,6 +336,7 @@ mod tests { let denied = &(&(&group_calls::() | &group_calls::()) | &(&group_calls::() | &group_calls::())) | &group_calls::(); + let denied = &denied | &group_calls::(); assert_eq!( allowed_calls(ProxyType::NonCritical), &all_runtime_calls() - &denied @@ -531,6 +534,36 @@ mod tests { ); } + // Creating a sale offer is a disposal of the subnet, so no proxy but `Any` + // may do it; cancellation stays broadly available. + #[test] + fn sale_offer_creation_is_any_only() { + let create = "SubtensorModule::create_sale_offer"; + let cancel = "SubtensorModule::cancel_sale_offer"; + + for proxy_type in all_proxy_types() { + let allowed = allowed_calls(proxy_type); + if proxy_type == ProxyType::Any { + assert!(allowed.contains(create)); + } else { + assert!( + !allowed.contains(create), + "{:?} must not be able to create a sale offer", + proxy_type + ); + } + } + + // The broad proxies can still cancel an offer. + for proxy_type in [ + ProxyType::NonTransfer, + ProxyType::NonFungible, + ProxyType::NonCritical, + ] { + assert!(allowed_calls(proxy_type).contains(cancel)); + } + } + // The SmallTransfer / SudoUncheckedSetCode metadata must carry their // amount / nested-call constraints. #[test]