From 04aef0c24d014c2f77421ead91d3804e2909f976 Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Sun, 20 Apr 2025 01:13:59 -0400 Subject: [PATCH 1/9] new ema --- pallets/subtensor/src/staking/stake_utils.rs | 81 ++++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index b7010185f..8c5319201 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -60,36 +60,67 @@ impl Pallet { } } + pub fn get_l_max() -> U96F32 { + U96F32::saturating_from_num(2000) + } + + pub fn compute_alpha_for_ema(l: U96F32, l_max: U96F32) -> U96F32 { + // comment this + let i_l_max = I96F32::saturating_from_num(l_max); + let i_l = I96F32::saturating_from_num(l); + let neg_one = I96F32::from_num(-1); + let two = I96F32::from_num(2); + let a = I96F32::from_num(7).saturating_div(two); + let b = neg_one; + let c = I96F32::from_num(3).saturating_div(two); + let d = neg_one.saturating_mul(I96F32::from_num(4)); + let x = (two.saturating_mul(i_l).saturating_div(i_l_max)).saturating_add(neg_one); + + let x_cubed = x.saturating_mul(x).saturating_mul(x); + let f_x = ((a.saturating_mul(x_cubed).saturating_add(b)) + .saturating_mul(x_cubed) + .saturating_add(c)) + .saturating_mul(x) + .saturating_add(d); + + let abs_f_x = f_x.saturating_neg(); + let exp = abs_f_x.ceil(); + + let exp_int = exp.to_num::(); + let mut alpha = I96F32::from_num(1); + let ten = I96F32::from_num(10); + + for _ in 0..exp_int { + alpha = alpha.saturating_div(ten); + } + + U96F32::saturating_from_num(alpha) + } + pub fn update_moving_price(netuid: u16) { - let blocks_since_start_call = U96F32::saturating_from_num({ - // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when - // `start_call` was called (first block before FirstEmissionBlockNumber). - let start_call_block = FirstEmissionBlockNumber::::get(netuid) - .unwrap_or_default() - .saturating_sub(1); - - Self::get_current_block_as_u64().saturating_sub(start_call_block) - }); + let tao_reserves = U96F32::saturating_from_num(SubnetTAO::::get(netuid)); + let alpha_reserves = U96F32::saturating_from_num(SubnetAlphaIn::::get(netuid)); + + let k = tao_reserves.saturating_mul(alpha_reserves); + let epsilon: U96F32 = U96F32::from_num(0.0000001); // TODO: how accurate to make this baby + let l = checked_sqrt(k, epsilon).unwrap_or(U96F32::from_num(0)); + let l_max = Self::get_l_max(); + let alpha = Self::compute_alpha_for_ema(l, l_max); - // Use halving time hyperparameter. The meaning of this parameter can be best explained under - // the assumption of a constant price and SubnetMovingAlpha == 0.5: It is how many blocks it - // will take in order for the distance between current EMA of price and current price to shorten - // by half. - let halving_time = EMAPriceHalvingBlocks::::get(netuid); - let current_ma_unsigned = U96F32::saturating_from_num(SubnetMovingAlpha::::get()); - let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( - blocks_since_start_call.saturating_add(U96F32::saturating_from_num(halving_time)), - )); // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. // We can use unsigned type here: U96F32 let one_minus_alpha: U96F32 = U96F32::saturating_from_num(1.0).saturating_sub(alpha); - let current_price: U96F32 = alpha - .saturating_mul(Self::get_alpha_price(netuid).min(U96F32::saturating_from_num(1.0))); - let current_moving: U96F32 = - one_minus_alpha.saturating_mul(Self::get_moving_alpha_price(netuid)); - // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` - let new_moving: I96F32 = - I96F32::saturating_from_num(current_price.saturating_add(current_moving)); + let moving_price = Self::get_moving_alpha_price(netuid); + let current_price = Self::get_alpha_price(netuid); + let weighted_current_price: U96F32 = alpha.saturating_mul(current_price); + let weighted_current_moving: U96F32 = one_minus_alpha.saturating_mul(moving_price); + + // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now + let mut new_moving: I96F32 = I96F32::saturating_from_num( + weighted_current_price.saturating_add(weighted_current_moving), + ); + + new_moving = new_moving.min(I96F32::from_num(current_price)); SubnetMovingPrice::::insert(netuid, new_moving); } From 554249bf79ec6b23e9859668f162e694137507ad Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Mon, 21 Apr 2025 14:36:01 -0400 Subject: [PATCH 2/9] checkin --- pallets/subtensor/src/staking/stake_utils.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 8c5319201..892fed73e 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -65,7 +65,10 @@ impl Pallet { } pub fn compute_alpha_for_ema(l: U96F32, l_max: U96F32) -> U96F32 { - // comment this + if l >= l_max { + return U96F32::saturating_from_num(1); + } + let i_l_max = I96F32::saturating_from_num(l_max); let i_l = I96F32::saturating_from_num(l); let neg_one = I96F32::from_num(-1); From 44779feb9206e5a580869a1ef917e9a39e103529 Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Tue, 22 Apr 2025 01:36:31 -0400 Subject: [PATCH 3/9] fix: test alpha --- pallets/admin-utils/src/tests/mock.rs | 2 + pallets/subtensor/src/lib.rs | 9 ++++ pallets/subtensor/src/macros/config.rs | 3 ++ pallets/subtensor/src/staking/stake_utils.rs | 24 +++++----- pallets/subtensor/src/tests/coinbase.rs | 48 +++++++++++++++++++- pallets/subtensor/src/tests/mock.rs | 2 + runtime/src/lib.rs | 2 + 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index f8b3e6a9b..ca806dde3 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -139,6 +139,7 @@ parameter_types! { pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // 5 days pub const InitialTaoWeight: u64 = u64::MAX/10; // 10% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks + pub const LiquidityScaleMax: u64 = 2000; pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // 7 days } @@ -208,6 +209,7 @@ impl pallet_subtensor::Config for Test { type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; + type LiquidityScaleMax = LiquidityScaleMax; type DurationOfStartCall = DurationOfStartCall; } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 197cd5f8f..b5a5bfdcf 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -476,6 +476,11 @@ pub mod pallet { T::InitialEmaPriceHalvingPeriod::get() } #[pallet::type_value] + /// Default Liquidity Scale Max factor for which to retain ema + pub fn DefaultLiquidityScaleMax() -> u64 { + T::LiquidityScaleMax::get() + } + #[pallet::type_value] /// Default registrations this block. pub fn DefaultBurn() -> u64 { T::InitialBurn::get() @@ -1449,6 +1454,10 @@ pub mod pallet { pub type EMAPriceHalvingBlocks = StorageMap<_, Identity, u16, u64, ValueQuery, DefaultEMAPriceMovingBlocks>; #[pallet::storage] + /// --- MAP ( netuid) ) --> Liquidity level at which to remove EMA + pub type LiquidityScaleMax = + StorageMap<_, Identity, u16, u64, ValueQuery, DefaultLiquidityScaleMax>; + #[pallet::storage] /// --- MAP ( netuid ) --> global_RAO_recycled_for_registration pub type RAORecycledForRegistration = StorageMap<_, Identity, u16, u64, ValueQuery, DefaultRAORecycledForRegistration>; diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 4377d9f01..e715cb26a 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -221,6 +221,9 @@ mod config { /// Initial EMA price halving period #[pallet::constant] type InitialEmaPriceHalvingPeriod: Get; + /// Maximum liquidity scale after which to drop the EMA + #[pallet::constant] + type LiquidityScaleMax: Get; /// Block number after a new subnet accept the start call extrinsic. #[pallet::constant] type DurationOfStartCall: Get; diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 892fed73e..7d2c57356 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -60,16 +60,12 @@ impl Pallet { } } - pub fn get_l_max() -> U96F32 { - U96F32::saturating_from_num(2000) - } - - pub fn compute_alpha_for_ema(l: U96F32, l_max: U96F32) -> U96F32 { - if l >= l_max { + pub fn compute_alpha_for_ema(l: U96F32, liquidity_scale_max: U96F32) -> U96F32 { + if l >= liquidity_scale_max { return U96F32::saturating_from_num(1); } - let i_l_max = I96F32::saturating_from_num(l_max); + let i_l_max = I96F32::saturating_from_num(liquidity_scale_max); let i_l = I96F32::saturating_from_num(l); let neg_one = I96F32::from_num(-1); let two = I96F32::from_num(2); @@ -101,17 +97,19 @@ impl Pallet { } pub fn update_moving_price(netuid: u16) { - let tao_reserves = U96F32::saturating_from_num(SubnetTAO::::get(netuid)); - let alpha_reserves = U96F32::saturating_from_num(SubnetAlphaIn::::get(netuid)); + let tao_reserves_rao = U96F32::saturating_from_num(SubnetTAO::::get(netuid)); + let alpha_reserves_rao = U96F32::saturating_from_num(SubnetAlphaIn::::get(netuid)); + let tao_reserves = + tao_reserves_rao.saturating_div(U96F32::saturating_from_num(1_000_000_000)); + let alpha_reserves = + alpha_reserves_rao.saturating_div(U96F32::saturating_from_num(1_000_000_000)); let k = tao_reserves.saturating_mul(alpha_reserves); let epsilon: U96F32 = U96F32::from_num(0.0000001); // TODO: how accurate to make this baby let l = checked_sqrt(k, epsilon).unwrap_or(U96F32::from_num(0)); - let l_max = Self::get_l_max(); - let alpha = Self::compute_alpha_for_ema(l, l_max); + let liquidity_scale_max = U96F32::saturating_from_num(LiquidityScaleMax::::get(netuid)); + let alpha = Self::compute_alpha_for_ema(l, liquidity_scale_max); - // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. - // We can use unsigned type here: U96F32 let one_minus_alpha: U96F32 = U96F32::saturating_from_num(1.0).saturating_sub(alpha); let moving_price = Self::get_moving_alpha_price(netuid); let current_price = Self::get_alpha_price(netuid); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 1345f36b7..bc486d25f 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -174,18 +174,49 @@ fn test_coinbase_tao_issuance_different_prices() { }); } +// Test dynamic alpha curve computation +// This test verifies that: +// - The dynamic alpha curve is computed correctly for various liquidity levels +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_moving_prices --exact --show-output --nocapture +#[test] +fn test_ema_alpha_calculation() { + new_test_ext(1).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + let ls: [u64; 6] = [200, 600, 1400, 1700, 1900, 2500]; + let ema_alphas: [f64; 6] = [0.0000001, 0.00001, 0.0001, 0.001, 0.01, 1.0]; + + let liquidity_scale_max = crate::LiquidityScaleMax::::get(netuid); + + for i in 0..6 { + let l = U96F32::from_num(ls[i]); + let lsm = U96F32::from_num(liquidity_scale_max); + let alpha = SubtensorModule::compute_alpha_for_ema(l, lsm); + let expected_alpha = I96F32::from_num(ema_alphas[i]); + + assert_abs_diff_eq!( + alpha.to_num::(), + expected_alpha.to_num::(), + epsilon = 0.000000001, + ); + } + }); +} + // Test moving price updates with different alpha values. // This test verifies that: // - Moving price stays constant when alpha is 1.0 // - Moving price converges to real price at expected rate with alpha 0.1 // - Moving price updates correctly over multiple iterations // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_moving_prices --exact --show-output --nocapture + #[test] fn test_coinbase_moving_prices() { new_test_ext(1).execute_with(|| { let netuid: u16 = 1; add_network(netuid, 1, 0); - // Set price to 1.0 + SubnetTAO::::insert(netuid, 1_000_000); SubnetAlphaIn::::insert(netuid, 1_000_000); SubnetMechanism::::insert(netuid, 1); @@ -197,14 +228,26 @@ fn test_coinbase_moving_prices() { SubtensorModule::get_moving_alpha_price(netuid), I96F32::from_num(1) ); + // Skip some blocks so that EMA price is not slowed down + println!("TAO Reserves before: {:?}", SubnetTAO::::get(netuid)); + println!( + "ALPHA Reserves before: {:?}", + SubnetAlphaIn::::get(netuid) + ); System::set_block_number(7_200_000); + println!("TAO Reserves before: {:?}", SubnetTAO::::get(netuid)); + println!( + "ALPHA Reserves before: {:?}", + SubnetAlphaIn::::get(netuid) + ); SubtensorModule::update_moving_price(netuid); assert_eq!( SubtensorModule::get_moving_alpha_price(netuid), I96F32::from_num(1) ); + // Check alpha of 1. // Set price to zero. SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); @@ -729,7 +772,8 @@ fn test_drain_base_with_subnet_with_two_stakers_registered_and_root_different_am assert_abs_diff_eq!(expected_root1.to_num::(), root_after1, epsilon = 10); // Registered gets 2/3 tao emission let expected_root2 = I96F32::from_num(stake_before) + I96F32::from_num(pending_tao) * I96F32::from_num(1.0 / 3.0); - assert_abs_diff_eq!(expected_root2.to_num::(), root_after2, epsilon = 10); // Registered gets 1/3 tao emission + assert_abs_diff_eq!(expected_root2.to_num::(), root_after2, epsilon = 10); + // Registered gets 1/3 tao emission }); } diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 221d802cc..3296fca8e 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -189,6 +189,7 @@ parameter_types! { pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days pub const InitialTaoWeight: u64 = 0; // 100% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks + pub const LiquidityScaleMax: u64 = 2000; // TODO: figure out what this value should be pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // Default as 7 days } @@ -417,6 +418,7 @@ impl crate::Config for Test { type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialTaoWeight = InitialTaoWeight; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; + type LiquidityScaleMax = LiquidityScaleMax; type DurationOfStartCall = DurationOfStartCall; } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 95b032f9e..11d78590c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1091,6 +1091,7 @@ parameter_types! { pub const InitialDissolveNetworkScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days pub const SubtensorInitialTaoWeight: u64 = 971_718_665_099_567_868; // 0.05267697438728329% tao weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks + pub const LiquidityScaleMax: u64 = 2000; // TODO: figure out what this should be pub const DurationOfStartCall: u64 = if cfg!(feature = "fast-blocks") { 10 // Only 10 blocks for fast blocks } else { @@ -1164,6 +1165,7 @@ impl pallet_subtensor::Config for Runtime { type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; + type LiquidityScaleMax = LiquidityScaleMax; type DurationOfStartCall = DurationOfStartCall; } From 261a2aee1dbb885108faebc23aedca8d41188d2d Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Wed, 23 Apr 2025 00:02:36 -0400 Subject: [PATCH 4/9] testing in on updating alpha --- pallets/subtensor/src/staking/stake_utils.rs | 41 +++++++- pallets/subtensor/src/tests/coinbase.rs | 100 ++++++++----------- 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 7d2c57356..ce2fa4448 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -47,6 +47,7 @@ impl Pallet { .unwrap_or(U96F32::saturating_from_num(0)) } } + pub fn get_moving_alpha_price(netuid: u16) -> U96F32 { let one = U96F32::saturating_from_num(1.0); if netuid == Self::get_root_netuid() { @@ -60,6 +61,24 @@ impl Pallet { } } + /// Computes the smoothing factor α for the exponential moving average (EMA) + /// based on current pool liquidity. + /// + /// This function implements a custom curve: + /// 1. If `l >= liquidity_scale_max`, returns α = 1. + /// 2. Otherwise: + /// - Normalize `x = 2·l / liquidity_scale_max − 1`. + /// - Compute a cubic polynomial + /// `f(x) = (((7/2·x³ − 1)·x³ + 3/2)·x − 4)`. + /// - Take `|f(x)|`, ceiling it to an integer `exp_int`, and set + /// α = 10^(−exp_int). + /// + /// # Arguments + /// * `l` – Current liquidity measure (√(TAO·α) after scaling). + /// * `liquidity_scale_max` – Liquidity level at which α saturates to 1. + /// + /// # Returns + /// * `U96F32` – The EMA weight α in the range [0, 1]. pub fn compute_alpha_for_ema(l: U96F32, liquidity_scale_max: U96F32) -> U96F32 { if l >= liquidity_scale_max { return U96F32::saturating_from_num(1); @@ -82,7 +101,7 @@ impl Pallet { .saturating_mul(x) .saturating_add(d); - let abs_f_x = f_x.saturating_neg(); + let abs_f_x = f_x.saturating_abs(); let exp = abs_f_x.ceil(); let exp_int = exp.to_num::(); @@ -96,6 +115,23 @@ impl Pallet { U96F32::saturating_from_num(alpha) } + /// Updates the stored “moving” alpha price for a subnet using a dynamic EMA. + /// + /// Steps performed: + /// 1. Load raw TAO and α reserves (`SubnetTAO`, `SubnetAlphaIn`) and down-scale by 1e9 (to TAO units) + /// 2. Compute the constant-product k = TAO_reserves·α_reserves, then + /// l = √k (with minimal epsilon). + /// 3. Call `compute_alpha_for_ema(l, liquidity_scale_max)` to obtain α. + /// 4. Blend current price (`get_alpha_price`) and previous moving price + /// (`get_moving_alpha_price`) as + /// `α·current + (1−α)·moving`. + /// 5. Clamp the result to ≤ current price and write into `SubnetMovingPrice`. + /// + /// # Arguments + /// * `netuid` – The subnet identifier whose price to update. + /// + /// # Effects + /// * Writes a new `I96F32` into storage map `SubnetMovingPrice::::insert(netuid, …)`. pub fn update_moving_price(netuid: u16) { let tao_reserves_rao = U96F32::saturating_from_num(SubnetTAO::::get(netuid)); let alpha_reserves_rao = U96F32::saturating_from_num(SubnetAlphaIn::::get(netuid)); @@ -105,7 +141,7 @@ impl Pallet { alpha_reserves_rao.saturating_div(U96F32::saturating_from_num(1_000_000_000)); let k = tao_reserves.saturating_mul(alpha_reserves); - let epsilon: U96F32 = U96F32::from_num(0.0000001); // TODO: how accurate to make this baby + let epsilon: U96F32 = U96F32::from_num(0.0000001); let l = checked_sqrt(k, epsilon).unwrap_or(U96F32::from_num(0)); let liquidity_scale_max = U96F32::saturating_from_num(LiquidityScaleMax::::get(netuid)); let alpha = Self::compute_alpha_for_ema(l, liquidity_scale_max); @@ -116,7 +152,6 @@ impl Pallet { let weighted_current_price: U96F32 = alpha.saturating_mul(current_price); let weighted_current_moving: U96F32 = one_minus_alpha.saturating_mul(moving_price); - // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now let mut new_moving: I96F32 = I96F32::saturating_from_num( weighted_current_price.saturating_add(weighted_current_moving), ); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index bc486d25f..ca6e55710 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -206,106 +206,84 @@ fn test_ema_alpha_calculation() { // Test moving price updates with different alpha values. // This test verifies that: -// - Moving price stays constant when alpha is 1.0 -// - Moving price converges to real price at expected rate with alpha 0.1 -// - Moving price updates correctly over multiple iterations +// - Moving price is correct at low liquidity (with low alpha) +// - Moving price is correct at medium liquidity (with medium alpha) +// - Moving price is correct at high liquidity (with alpha = 1.0, beyond EMA falloff) +// - Moving price takes minimum of current price and moving price // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_moving_prices --exact --show-output --nocapture - #[test] fn test_coinbase_moving_prices() { new_test_ext(1).execute_with(|| { let netuid: u16 = 1; add_network(netuid, 1, 0); + // check 1: moving alpha price close to zero when liquidity is extremely low SubnetTAO::::insert(netuid, 1_000_000); SubnetAlphaIn::::insert(netuid, 1_000_000); SubnetMechanism::::insert(netuid, 1); SubnetMovingPrice::::insert(netuid, I96F32::from_num(1)); FirstEmissionBlockNumber::::insert(netuid, 1); - // Updating the moving price keeps it the same. assert_eq!( SubtensorModule::get_moving_alpha_price(netuid), I96F32::from_num(1) ); - // Skip some blocks so that EMA price is not slowed down - println!("TAO Reserves before: {:?}", SubnetTAO::::get(netuid)); - println!( - "ALPHA Reserves before: {:?}", - SubnetAlphaIn::::get(netuid) - ); - System::set_block_number(7_200_000); - println!("TAO Reserves before: {:?}", SubnetTAO::::get(netuid)); - println!( - "ALPHA Reserves before: {:?}", - SubnetAlphaIn::::get(netuid) - ); - SubtensorModule::update_moving_price(netuid); assert_eq!( SubtensorModule::get_moving_alpha_price(netuid), I96F32::from_num(1) ); - // Check alpha of 1. - // Set price to zero. + // check 2: medium liquidity (relative to falloff) SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - SubnetMovingAlpha::::set(I96F32::from_num(1.0)); - // Run moving 1 times. SubtensorModule::update_moving_price(netuid); - // Assert price is ~ 100% of the real price. - assert!(U96F32::from_num(1.0) - SubtensorModule::get_moving_alpha_price(netuid) < 0.05); - // Set price to zero. - SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - SubnetMovingAlpha::::set(I96F32::from_num(0.1)); + assert_eq!(SubtensorModule::get_moving_alpha_price(netuid), 0.0); - // EMA price 28 days after registration - System::set_block_number(7_200 * 28); + SubnetTAO::::insert(netuid, 1_500_000_000_000); + SubnetAlphaIn::::insert(netuid, 1_500_000_000_000); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - // Run moving 14 times. - for _ in 0..14 { - SubtensorModule::update_moving_price(netuid); - } + SubtensorModule::update_moving_price(netuid); - // Assert price is > 50% of the real price. assert_abs_diff_eq!( - 0.512325, - SubtensorModule::get_moving_alpha_price(netuid).to_num::(), - epsilon = 0.001 + SubnetMovingPrice::::get(netuid).to_num::(), + 0.0001, + epsilon = 0.00000001 ); - }); -} -// Test moving price updates slow down at the beginning. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_update_moving_price_initial --exact --show-output --nocapture -#[test] -fn test_update_moving_price_initial() { - new_test_ext(1).execute_with(|| { - let netuid: u16 = 1; - add_network(netuid, 1, 0); - // Set current price to 1.0 - SubnetTAO::::insert(netuid, 1_000_000); - SubnetAlphaIn::::insert(netuid, 1_000_000); - SubnetMechanism::::insert(netuid, 1); - SubnetMovingAlpha::::set(I96F32::from_num(0.5)); + // check 3: high liquidity, check no EMA applied + SubnetTAO::::insert(netuid, 2_500_000_000_000); + SubnetAlphaIn::::insert(netuid, 2_500_000_000_000); SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - // Registered recently - System::set_block_number(510); - FirstEmissionBlockNumber::::insert(netuid, 500); + SubtensorModule::update_moving_price(netuid); + + assert_abs_diff_eq!( + SubnetMovingPrice::::get(netuid).to_num::(), + 1.0, + epsilon = 0.00000001 + ); + // check 4: minimum case, i.e. current_price of 1.0 and moving_price > current_price + SubnetTAO::::insert(netuid, 2_500_000_000_000); + SubnetTAO::::insert(netuid, 2_500_000_000_000); + SubnetAlphaIn::::insert(netuid, 2_500_000_000_000); + SubnetMovingPrice::::insert(netuid, I96F32::from_num(1.5)); SubtensorModule::update_moving_price(netuid); - let new_price = SubnetMovingPrice::::get(netuid); - assert!(new_price.to_num::() < 0.001); + assert_abs_diff_eq!( + SubnetMovingPrice::::get(netuid).to_num::(), + 1.0, + epsilon = 0.00000001 + ); }); } // Test moving price updates slow down at the beginning. -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_update_moving_price_after_time --exact --show-output --nocapture +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_update_moving_price_initial --exact --show-output --nocapture #[test] -fn test_update_moving_price_after_time() { +fn test_update_moving_price_initial() { new_test_ext(1).execute_with(|| { let netuid: u16 = 1; add_network(netuid, 1, 0); @@ -316,14 +294,14 @@ fn test_update_moving_price_after_time() { SubnetMovingAlpha::::set(I96F32::from_num(0.5)); SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); - // Registered long time ago - System::set_block_number(144_000_500); + // Registered recently + System::set_block_number(510); FirstEmissionBlockNumber::::insert(netuid, 500); SubtensorModule::update_moving_price(netuid); let new_price = SubnetMovingPrice::::get(netuid); - assert!((new_price.to_num::() - 0.5).abs() < 0.001); + assert!(new_price.to_num::() < 0.001); }); } From 9306e67f3f5138ccd71f3e8ba9a233ae0eea05a6 Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Wed, 23 Apr 2025 00:13:41 -0400 Subject: [PATCH 5/9] cleanup and add sudo set for liquidity scale max --- pallets/admin-utils/src/lib.rs | 2 +- pallets/admin-utils/src/tests/mod.rs | 52 ++++++++++++++++++++++++++++ pallets/subtensor/src/tests/mock.rs | 2 +- runtime/src/lib.rs | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 2b4153981..a7fcff96c 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1516,7 +1516,7 @@ pub mod pallet { Ok(()) } - /// + /// Change liquidity scale max for a given subnet. /// /// # Arguments /// * `origin` - The origin of the call, which must be the root account. diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index bb813ce11..00fb9a259 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -1780,3 +1780,55 @@ fn test_set_sn_owner_hotkey_root() { assert_eq!(actual_hotkey, hotkey); }); } + +#[test] +fn test_sudo_set_liquidity_scale_max() { + new_test_ext().execute_with(|| { + let netuid: u16 = 1; + let to_be_set: u64 = 1_234; + // install a subnet with owner = 10 + add_network(netuid, 10); + + // read the default + let before: u64 = pallet_subtensor::LiquidityScaleMax::::get(netuid); + + // 1) wrong signed account → BadOrigin, no change + assert_eq!( + AdminUtils::sudo_set_liquidity_scale_max( + <::RuntimeOrigin>::signed(U256::from(1)), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + pallet_subtensor::LiquidityScaleMax::::get(netuid), + before + ); + + // 2) subnet owner but not root → still BadOrigin + let owner = U256::from(10); + pallet_subtensor::SubnetOwner::::insert(netuid, owner); + assert_eq!( + AdminUtils::sudo_set_liquidity_scale_max( + <::RuntimeOrigin>::signed(owner), + netuid, + to_be_set + ), + Err(DispatchError::BadOrigin) + ); + assert_eq!( + pallet_subtensor::LiquidityScaleMax::::get(netuid), + before + ); + + // 3) root origin → Ok, value updated + assert_ok!(AdminUtils::sudo_set_liquidity_scale_max( + <::RuntimeOrigin>::root(), + netuid, + to_be_set + )); + let after: u64 = pallet_subtensor::LiquidityScaleMax::::get(netuid); + assert_eq!(after, to_be_set); + }); +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 3296fca8e..e2533252a 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -189,7 +189,7 @@ parameter_types! { pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days pub const InitialTaoWeight: u64 = 0; // 100% global weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks - pub const LiquidityScaleMax: u64 = 2000; // TODO: figure out what this value should be + pub const LiquidityScaleMax: u64 = 2000; pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // Default as 7 days } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 11d78590c..2314babbe 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1091,7 +1091,7 @@ parameter_types! { pub const InitialDissolveNetworkScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days pub const SubtensorInitialTaoWeight: u64 = 971_718_665_099_567_868; // 0.05267697438728329% tao weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks - pub const LiquidityScaleMax: u64 = 2000; // TODO: figure out what this should be + pub const LiquidityScaleMax: u64 = 2000; pub const DurationOfStartCall: u64 = if cfg!(feature = "fast-blocks") { 10 // Only 10 blocks for fast blocks } else { From 7743cc1b39713e21ece4a6a150b416e69cc5cfd8 Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Wed, 23 Apr 2025 00:20:56 -0400 Subject: [PATCH 6/9] fmt --- pallets/subtensor/src/staking/stake_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index ce2fa4448..3d7a5cfd8 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -61,7 +61,7 @@ impl Pallet { } } - /// Computes the smoothing factor α for the exponential moving average (EMA) + /// Computes the smoothing factor α for the exponential moving average (EMA) /// based on current pool liquidity. /// /// This function implements a custom curve: @@ -115,7 +115,7 @@ impl Pallet { U96F32::saturating_from_num(alpha) } - /// Updates the stored “moving” alpha price for a subnet using a dynamic EMA. + /// Updates the stored “moving” alpha price for a subnet using a dynamic EMA. /// /// Steps performed: /// 1. Load raw TAO and α reserves (`SubnetTAO`, `SubnetAlphaIn`) and down-scale by 1e9 (to TAO units) From 1ce80d3b6b348cabd43f2e52eed3a5ddac83263a Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Thu, 24 Apr 2025 16:24:10 -0400 Subject: [PATCH 7/9] safe divs and saturating fixes --- pallets/admin-utils/src/lib.rs | 30 ++++++++++++++++++++ pallets/subtensor/src/staking/stake_utils.rs | 17 ++++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index a7fcff96c..552c8805c 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1671,6 +1671,36 @@ pub mod pallet { ); Ok(()) } + + /// Change liquidity scale max for a given subnet. + /// + /// # Arguments + /// * `origin` - The origin of the call, which must be the root account. + /// * `netuid` - The unique identifier for the subnet. + /// * `liquidity_scale_max` - The new maximum liquidity scale value. + /// + /// # Errors + /// * `BadOrigin` - If the caller is not the root account. + /// + /// # Weight + /// Weight is handled by the `#[pallet::weight]` attribute. + #[pallet::call_index(67)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_liquidity_scale_max( + origin: OriginFor, + netuid: u16, + liquidity_scale_max: u64, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::LiquidityScaleMax::::set(netuid, liquidity_scale_max); + + log::debug!( + "LiquidityScaleMax( netuid: {:?}, liquidity_scale_max: {:?} )", + netuid, + liquidity_scale_max + ); + Ok(()) + } } } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 3d7a5cfd8..c367313e6 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -88,11 +88,11 @@ impl Pallet { let i_l = I96F32::saturating_from_num(l); let neg_one = I96F32::from_num(-1); let two = I96F32::from_num(2); - let a = I96F32::from_num(7).saturating_div(two); + let a = I96F32::from_num(7).safe_div(two); let b = neg_one; - let c = I96F32::from_num(3).saturating_div(two); + let c = I96F32::from_num(3).safe_div(two); let d = neg_one.saturating_mul(I96F32::from_num(4)); - let x = (two.saturating_mul(i_l).saturating_div(i_l_max)).saturating_add(neg_one); + let x = (two.saturating_mul(i_l).safe_div(i_l_max)).saturating_add(neg_one); let x_cubed = x.saturating_mul(x).saturating_mul(x); let f_x = ((a.saturating_mul(x_cubed).saturating_add(b)) @@ -105,11 +105,11 @@ impl Pallet { let exp = abs_f_x.ceil(); let exp_int = exp.to_num::(); - let mut alpha = I96F32::from_num(1); - let ten = I96F32::from_num(10); + let mut alpha = I96F32::saturating_to_num(1); + let ten = I96F32::saturating_to_num(10); for _ in 0..exp_int { - alpha = alpha.saturating_div(ten); + alpha = alpha.safe_div(ten); } U96F32::saturating_from_num(alpha) @@ -135,10 +135,9 @@ impl Pallet { pub fn update_moving_price(netuid: u16) { let tao_reserves_rao = U96F32::saturating_from_num(SubnetTAO::::get(netuid)); let alpha_reserves_rao = U96F32::saturating_from_num(SubnetAlphaIn::::get(netuid)); - let tao_reserves = - tao_reserves_rao.saturating_div(U96F32::saturating_from_num(1_000_000_000)); + let tao_reserves = tao_reserves_rao.safe_div(U96F32::saturating_from_num(1_000_000_000)); let alpha_reserves = - alpha_reserves_rao.saturating_div(U96F32::saturating_from_num(1_000_000_000)); + alpha_reserves_rao.safe_div(U96F32::saturating_from_num(1_000_000_000)); let k = tao_reserves.saturating_mul(alpha_reserves); let epsilon: U96F32 = U96F32::from_num(0.0000001); From de2002b3e522dfb4909138d60d8718123e26fd44 Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Thu, 24 Apr 2025 16:27:22 -0400 Subject: [PATCH 8/9] move all from_num to saturating_from_num --- pallets/subtensor/src/staking/stake_utils.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index c367313e6..7c1c1fd1f 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -86,12 +86,12 @@ impl Pallet { let i_l_max = I96F32::saturating_from_num(liquidity_scale_max); let i_l = I96F32::saturating_from_num(l); - let neg_one = I96F32::from_num(-1); - let two = I96F32::from_num(2); - let a = I96F32::from_num(7).safe_div(two); + let neg_one = I96F32::saturating_from_num(-1); + let two = I96F32::saturating_from_num(2); + let a = I96F32::saturating_from_num(7).safe_div(two); let b = neg_one; - let c = I96F32::from_num(3).safe_div(two); - let d = neg_one.saturating_mul(I96F32::from_num(4)); + let c = I96F32::saturating_from_num(3).safe_div(two); + let d = neg_one.saturating_mul(I96F32::saturating_from_num(4)); let x = (two.saturating_mul(i_l).safe_div(i_l_max)).saturating_add(neg_one); let x_cubed = x.saturating_mul(x).saturating_mul(x); @@ -105,8 +105,8 @@ impl Pallet { let exp = abs_f_x.ceil(); let exp_int = exp.to_num::(); - let mut alpha = I96F32::saturating_to_num(1); - let ten = I96F32::saturating_to_num(10); + let mut alpha = I96F32::saturating_from_num(1); + let ten = I96F32::saturating_from_num(10); for _ in 0..exp_int { alpha = alpha.safe_div(ten); @@ -140,8 +140,8 @@ impl Pallet { alpha_reserves_rao.safe_div(U96F32::saturating_from_num(1_000_000_000)); let k = tao_reserves.saturating_mul(alpha_reserves); - let epsilon: U96F32 = U96F32::from_num(0.0000001); - let l = checked_sqrt(k, epsilon).unwrap_or(U96F32::from_num(0)); + let epsilon: U96F32 = U96F32::saturating_from_num(0.0000001); + let l = checked_sqrt(k, epsilon).unwrap_or(U96F32::saturating_from_num(0)); let liquidity_scale_max = U96F32::saturating_from_num(LiquidityScaleMax::::get(netuid)); let alpha = Self::compute_alpha_for_ema(l, liquidity_scale_max); @@ -155,7 +155,7 @@ impl Pallet { weighted_current_price.saturating_add(weighted_current_moving), ); - new_moving = new_moving.min(I96F32::from_num(current_price)); + new_moving = new_moving.min(I96F32::saturating_from_num(current_price)); SubnetMovingPrice::::insert(netuid, new_moving); } From ea7beb7df24caff15314c78595ddc8b13b059b6b Mon Sep 17 00:00:00 2001 From: 0xcacti <0xcacti@gmail.com> Date: Mon, 26 May 2025 21:40:42 -0400 Subject: [PATCH 9/9] update call index --- pallets/admin-utils/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 552c8805c..307e0ae2d 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1684,7 +1684,7 @@ pub mod pallet { /// /// # Weight /// Weight is handled by the `#[pallet::weight]` attribute. - #[pallet::call_index(67)] + #[pallet::call_index(70)] #[pallet::weight((0, DispatchClass::Operational, Pays::No))] pub fn sudo_set_liquidity_scale_max( origin: OriginFor,