diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 494fc163f7..1a76984fcb 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -85,11 +85,6 @@ impl Pallet { let tao_to_swap_with: TaoBalance = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - // Inject tao and alpha into protocol liquidity. In theorry, it may not always - // be a success (returned values are 0s) in case of high liquidity disbalance - let (actual_injected_tao, actual_injected_alpha) = - T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); - // Clear per-block pool-side emission counters up front so a subnet // disabled this block does not display stale values from an earlier block. SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); @@ -130,38 +125,60 @@ impl Pallet { } } - // Inject Alpha in. - SubnetAlphaInEmission::::insert(*netuid_i, actual_injected_alpha); - - // Mint alpha and resolve to alpha reserve - Self::resolve_to_alpha_in(Self::mint_alpha(*netuid_i, actual_injected_alpha)); - - // Inject TAO in. - if !actual_injected_tao.is_zero() { - match Self::spend_tao(&subnet_account_id, remaining_credit, actual_injected_tao) - { + // Materialize this block's TAO before updating balancer reservoir + // state. If spending fails, do not let the swap pallet consume + // reservoir state as if this block's TAO arrived. + let materialized_tao_delta = if tao_in_i.is_zero() { + TaoBalance::ZERO + } else { + match Self::spend_tao(&subnet_account_id, remaining_credit, tao_in_i) { Ok(remainder) => { remaining_credit = remainder; - - SubnetTaoInEmission::::insert(*netuid_i, actual_injected_tao); - SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(actual_injected_tao); - }); - TotalStake::::mutate(|total| { - *total = total.saturating_add(actual_injected_tao); - }); - - // Record emission injection as protocol inflow. - Self::record_protocol_inflow(*netuid_i, actual_injected_tao); + tao_in_i } Err(remainder) => { remaining_credit = remainder; let remaining_balance = remaining_credit.peek(); log::error!( - "Failed to spend credit: injected_tao = {actual_injected_tao:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" + "Failed to spend credit: tao_to_materialize = {tao_in_i:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" ); + TaoBalance::ZERO } } + }; + + // Decide which current/reservoir liquidity can become price-active + // without pushing balancer weights out of range. Only already + // materialized current TAO is offered to the swap pallet. + let ( + price_active_tao, + _tao_to_materialize, + price_active_alpha, + alpha_to_materialize, + ) = T::SwapInterface::adjust_protocol_liquidity( + *netuid_i, + materialized_tao_delta, + alpha_in_i, + ); + + // Materialize this block's alpha emission, then add only the + // price-active portion to the pool reserve. The price-active + // portion may include alpha that was materialized in an earlier + // block and held in the reservoir. + let _ = Self::mint_alpha(*netuid_i, alpha_to_materialize); + SubnetAlphaInEmission::::insert(*netuid_i, price_active_alpha); + Self::increase_provided_alpha_reserve(*netuid_i, price_active_alpha); + + // Add only the price-active TAO to the pool reserve. This may + // include TAO materialized in an earlier block and held in the + // reservoir. + if !price_active_tao.is_zero() { + SubnetTaoInEmission::::insert(*netuid_i, price_active_tao); + Self::increase_provided_tao_reserve(*netuid_i, price_active_tao); + TotalStake::::mutate(|total| { + *total = total.saturating_add(price_active_tao); + }); + Self::record_protocol_inflow(*netuid_i, price_active_tao); } } } diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 20944a03d0..abbe18e08d 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -3678,6 +3678,42 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { }); } +#[test] +fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let initial_reserve = TaoBalance::from(1_000_000_u64); + let reservoir_tao = TaoBalance::from(100_u64); + let current_tao = TaoBalance::from(200_u64); + let current_alpha = AlphaBalance::from(100_u64); + + mock::setup_reserves(netuid, initial_reserve, AlphaBalance::from(1_000_000_u64)); + Swap::maybe_initialize_palswap(netuid, None); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(netuid, reservoir_tao); + + let tao_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); + let alpha_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_alpha))]); + let excess_tao = BTreeMap::new(); + let credit = SubtensorModule::mint_tao(TaoBalance::ZERO); + + SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!( + SubnetTAO::::get(netuid), + initial_reserve.saturating_add(reservoir_tao) + ); + assert_eq!(SubnetTaoInEmission::::get(netuid), reservoir_tao); + assert_eq!( + SubnetProtocolFlow::::get(netuid), + reservoir_tao.to_u64() as i64 + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), + TaoBalance::ZERO + ); + }); +} + #[test] fn test_coinbase_drain_pending_increments_blockssincelaststep() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4696507e2e..c49677bddf 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -370,6 +370,8 @@ fn dissolve_clears_all_per_subnet_storages() { SubnetAlphaOutEmission::::insert(net, AlphaBalance::from(1)); SubnetTaoInEmission::::insert(net, TaoBalance::from(1)); SubnetVolume::::insert(net, 1u128); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(net, TaoBalance::from(1)); + pallet_subtensor_swap::BalancerAlphaReservoir::::insert(net, AlphaBalance::from(1)); // Items now REMOVED (not zeroed) by dissolution SubnetAlphaIn::::insert(net, AlphaBalance::from(2)); @@ -521,6 +523,8 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!SubnetAlphaOutEmission::::contains_key(net)); assert!(!SubnetTaoInEmission::::contains_key(net)); assert!(!SubnetVolume::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); // TAO Flow assert!(!SubnetTaoFlow::::contains_key(net)); diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c3e0b2f1d3..2afa9569b9 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -79,41 +79,112 @@ impl Pallet { Ok(()) } - /// Returns actually added Tao and Alpha, which may be zero in case - /// of a high disbalance + /// Adjusts balancer weights with minted TAO and alpha liquidity to + /// maintain price. + /// + /// If weights cannot be adjusted (get pushed out of range), the excess TAO + /// and/or Alpha are added to reservoirs and an attempt to use them will be made + /// later. + /// + /// Returns: + /// 1. price-active TAO delta to add to `SubnetTAO` + /// 2. TAO delta to materialize by the caller + /// 3. price-active Alpha delta to add to `SubnetAlphaIn` + /// 4. Alpha delta to materialize by the caller + /// + /// Amounts that would push weights out of range are materialized but left in + /// per-subnet reservoirs for a later balancer update. + /// + /// Reservoir amounts may be included in the balancer update, but they are not + /// returned for materialization because they were already materialized when + /// first stored. pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, - ) -> (TaoBalance, AlphaBalance) { + ) -> (TaoBalance, TaoBalance, AlphaBalance, AlphaBalance) { // Get reserves let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); let tao_reserve = T::TaoReserve::reserve(netuid.into()); - let mut balancer = SwapBalancer::::get(netuid); + let balancer = SwapBalancer::::get(netuid); + + let pending_tao = BalancerTaoReservoir::::get(netuid).saturating_add(tao_delta); + let pending_alpha = BalancerAlphaReservoir::::get(netuid).saturating_add(alpha_delta); + + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + pending_tao, + pending_alpha, + ) { + BalancerTaoReservoir::::insert(netuid, TaoBalance::ZERO); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::ZERO); + SwapBalancer::::insert(netuid, new_balancer); + return (pending_tao, tao_delta, pending_alpha, alpha_delta); + } - // Update weights and log errors if they go out of range - if balancer - .update_weights_for_added_liquidity( - u64::from(tao_reserve), - u64::from(alpha_reserve), - u64::from(tao_delta), - u64::from(alpha_delta), - ) - .is_err() - { + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + TaoBalance::ZERO, + pending_alpha, + ) { + BalancerTaoReservoir::::insert(netuid, pending_tao); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::ZERO); + SwapBalancer::::insert(netuid, new_balancer); + return (TaoBalance::ZERO, tao_delta, pending_alpha, alpha_delta); + } + + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + pending_tao, + AlphaBalance::ZERO, + ) { + BalancerTaoReservoir::::insert(netuid, TaoBalance::ZERO); + BalancerAlphaReservoir::::insert(netuid, pending_alpha); + SwapBalancer::::insert(netuid, new_balancer); + return (pending_tao, tao_delta, AlphaBalance::ZERO, alpha_delta); + } + + BalancerTaoReservoir::::insert(netuid, pending_tao); + BalancerAlphaReservoir::::insert(netuid, pending_alpha); + if pending_tao > TaoBalance::ZERO || pending_alpha > AlphaBalance::ZERO { log::warn!( - "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}", + "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}, tao_reservoir = {}, alpha_reservoir = {}", netuid, tao_reserve, alpha_reserve, tao_delta, - alpha_delta + alpha_delta, + pending_tao, + pending_alpha ); - (TaoBalance::ZERO, AlphaBalance::ZERO) - } else { - SwapBalancer::::insert(netuid, balancer); - (tao_delta, alpha_delta) } + + (TaoBalance::ZERO, tao_delta, AlphaBalance::ZERO, alpha_delta) + } + + fn try_update_balancer( + balancer: &Balancer, + tao_reserve: TaoBalance, + alpha_reserve: AlphaBalance, + tao_delta: TaoBalance, + alpha_delta: AlphaBalance, + ) -> Option { + let mut new_balancer = balancer.clone(); + new_balancer + .update_weights_for_added_liquidity( + u64::from(tao_reserve), + u64::from(alpha_reserve), + u64::from(tao_delta), + u64::from(alpha_delta), + ) + .ok()?; + Some(new_balancer) } /// Executes a token swap on the specified subnet. @@ -276,6 +347,8 @@ impl Pallet { FeeRate::::remove(netuid); SwapBalancer::::remove(netuid); + BalancerTaoReservoir::::remove(netuid); + BalancerAlphaReservoir::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" @@ -390,7 +463,7 @@ impl SwapHandler for Pallet { netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, - ) -> (TaoBalance, AlphaBalance) { + ) -> (TaoBalance, TaoBalance, AlphaBalance, AlphaBalance) { Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 1d2fd07c59..630e51c49f 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -113,6 +113,15 @@ mod pallet { #[pallet::storage] pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + /// TAO protocol liquidity that could not be injected without exceeding balancer weight bounds. + #[pallet::storage] + pub type BalancerTaoReservoir = StorageMap<_, Twox64Concat, NetUid, TaoBalance, ValueQuery>; + + /// Alpha protocol liquidity that could not be injected without exceeding balancer weight bounds. + #[pallet::storage] + pub type BalancerAlphaReservoir = + StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; + /// --- Storage for migration run status #[pallet::storage] pub type HasMigrationRun = diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index b1071294d3..689e48e3d4 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -155,6 +155,138 @@ mod dispatchables { }); } + #[test] + fn test_adjust_protocol_liquidity_materializes_tao_when_reservoiring_tao() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, tao_materialized, price_active_alpha, alpha_materialized) = + Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(tao_materialized, TaoBalance::from(200_000_u64)); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + assert_eq!(alpha_materialized, AlphaBalance::from(1_000_u64)); + assert_eq!( + BalancerTaoReservoir::::get(netuid), + TaoBalance::from(200_000_u64) + ); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_materializes_alpha_when_reservoiring_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, tao_materialized, price_active_alpha, alpha_materialized) = + Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::from(1_000_u64)); + assert_eq!(tao_materialized, TaoBalance::from(1_000_u64)); + assert_eq!(price_active_alpha, AlphaBalance::ZERO); + assert_eq!(alpha_materialized, AlphaBalance::from(200_000_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::from(200_000_u64) + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_retries_reservoir_with_new_injection() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let mut tao = TaoBalance::from(1_000_u64); + let mut alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, tao_materialized, price_active_alpha, alpha_materialized) = + Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(tao_materialized, TaoBalance::from(200_000_u64)); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + assert_eq!(alpha_materialized, AlphaBalance::from(1_000_u64)); + tao += price_active_tao; + alpha += price_active_alpha; + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, tao_materialized, price_active_alpha, alpha_materialized) = + Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert!(price_active_tao >= tao_materialized); + assert!(price_active_alpha >= alpha_materialized); + assert_eq!(tao_materialized, TaoBalance::from(1_000_u64)); + assert_eq!(alpha_materialized, AlphaBalance::from(200_000_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_does_not_materialize_reservoir_amounts() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_u64)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_u64)); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(10_000_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(20_000_u64)); + + let tao_delta = TaoBalance::from(300_u64); + let alpha_delta = AlphaBalance::from(400_u64); + let (price_active_tao, tao_materialized, price_active_alpha, alpha_materialized) = + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + + assert_eq!(price_active_tao, TaoBalance::from(10_300_u64)); + assert_eq!(tao_materialized, tao_delta); + assert_eq!(price_active_alpha, AlphaBalance::from(20_400_u64)); + assert_eq!(alpha_materialized, alpha_delta); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + /// This test case verifies that small gradual injections (like emissions in every block) /// in the worst case /// - Do not cause price to change @@ -749,6 +881,8 @@ fn test_liquidate_pal_simple_ok_and_clears() { // Insert map values FeeRate::::insert(netuid, 1_000); PalSwapInitialized::::insert(netuid, true); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(12_345_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(67_890_u64)); let w_quote_pt = Perquintill::from_rational(1u128, 2u128); let bal = Balancer::new(w_quote_pt).unwrap(); SwapBalancer::::insert(netuid, bal); @@ -763,6 +897,8 @@ fn test_liquidate_pal_simple_ok_and_clears() { assert!(!FeeRate::::contains_key(netuid)); assert!(!PalSwapInitialized::::contains_key(netuid)); assert!(!SwapBalancer::::contains_key(netuid)); + assert!(!BalancerTaoReservoir::::contains_key(netuid)); + assert!(!BalancerAlphaReservoir::::contains_key(netuid)); }); } diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs index 9980604707..85e8cacc7a 100644 --- a/primitives/swap-interface/src/lib.rs +++ b/primitives/swap-interface/src/lib.rs @@ -46,7 +46,7 @@ pub trait SwapHandler { netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, - ) -> (TaoBalance, AlphaBalance); + ) -> (TaoBalance, TaoBalance, AlphaBalance, AlphaBalance); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; fn init_swap(netuid: NetUid, maybe_price: Option); fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5cf4d7aadb..94e25a46fb 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 419, + spec_version: 420, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,