use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ ensure, pallet_prelude::{DispatchError, Zero}, traits::Get, weights::WeightMeter, }; use safe_math::*; use sp_arithmetic::Perquintill; use sp_runtime::traits::AccountIdConversion; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve}; use subtensor_swap_interface::{ DefaultPriceLimit, Order as OrderT, SwapEngine, SwapHandler, SwapResult, }; use super::pallet::*; use super::swap_step::{BasicSwapStep, MAX_SWAP_INPUT_RESERVE_MULTIPLIER, SwapStep}; use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { pub fn current_price(netuid: NetUid) -> U64F64 { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); if !alpha_reserve.is_zero() { let tao_reserve = T::TaoReserve::reserve(netuid.into()); let balancer = SwapBalancer::::get(netuid); balancer.calculate_price(alpha_reserve.into(), tao_reserve.into()) } else { U64F64::saturating_from_num(0) } } _ => U64F64::saturating_from_num(1), } } // initializes pal-swap (balancer) for a subnet if needed pub fn maybe_initialize_palswap( netuid: NetUid, maybe_price: Option, ) -> Result<(), Error> { if PalSwapInitialized::::get(netuid) { return Ok(()); } // Query reserves let tao_reserve = T::TaoReserve::reserve(netuid.into()); let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); // Create balancer based on price let balancer = Balancer::new(if let Some(price) = maybe_price { // Price is given, calculate weights: // w_quote = y / (px + y) let px_high = (price.saturating_to_num::() as u128) .saturating_mul(u64::from(alpha_reserve) as u128); let px_low = U64F64::saturating_from_num(alpha_reserve) .saturating_mul(price.frac()) .saturating_to_num::(); let px_plus_y = px_high .saturating_add(px_low) .saturating_add(u64::from(tao_reserve) as u128); // If price is given and both reserves are zero, the swap doesn't initialize if px_plus_y == 0u128 { return Err(Error::::ReservesOutOfBalance); } Perquintill::from_rational(u64::from(tao_reserve) as u128, px_plus_y) } else { // No price = insert 0.5 into SwapBalancer Perquintill::from_rational(1_u64, 2_u64) }) .map_err(|err| match err { BalancerError::InvalidValue => Error::::ReservesOutOfBalance, })?; SwapBalancer::::insert(netuid, balancer.clone()); PalSwapInitialized::::insert(netuid, true); Ok(()) } /// 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. price-active Alpha delta to add to `SubnetAlphaIn` /// /// Amounts that would push weights out of range are materialized but left in /// per-subnet reservoirs for a later balancer update. /// /// The caller is responsible for materializing the current `tao_delta` and /// `alpha_delta`; reservoir amounts were materialized when first stored. pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, ) -> (TaoBalance, AlphaBalance) { // Get reserves let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); let tao_reserve = T::TaoReserve::reserve(netuid.into()); 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, pending_alpha); } 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, pending_alpha); } 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, AlphaBalance::ZERO); } 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 = {}, tao_reservoir = {}, alpha_reservoir = {}", netuid, tao_reserve, alpha_reserve, tao_delta, alpha_delta, pending_tao, pending_alpha ); } (TaoBalance::ZERO, AlphaBalance::ZERO) } 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. /// /// # Parameters /// - `netuid`: The identifier of the subnet on which the swap is performed. /// - `order_type`: The type of the swap (e.g., Buy or Sell). /// - `amount`: The amount of tokens to swap. /// - `limit_sqrt_price`: A price limit (expressed as a square root) to bound the swap. /// - `simulate`: If `true`, the function runs in simulation mode and does not persist any /// changes. /// /// # Returns /// Returns a [`Result`] with a [`SwapResult`] on success, or a [`DispatchError`] on failure. /// /// The [`SwapResult`] contains: /// - `amount_paid_out`: The amount of tokens received from the swap. /// - `refund`: Any unswapped portion of the input amount, refunded to the caller. /// /// # Simulation Mode /// When `simulate` is set to `true`, the function: /// 1. Executes all logic without persisting any state changes (i.e., performs a dry run). /// 2. Skips reserve checks — it may return an `amount_paid_out` greater than the available /// reserve. /// /// Use simulation mode to preview the outcome of a swap without modifying the blockchain state. pub(crate) fn do_swap( netuid: NetUid, order: Order, limit_price: U64F64, drop_fees: bool, simulate: bool, ) -> Result, DispatchError> where Order: OrderT, BasicSwapStep: SwapStep, { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); let result = Self::ensure_swap_input_within_reserve_limit::( netuid, order.amount(), drop_fees, ) .and_then(|_| Self::swap_inner::(netuid, order, limit_price, drop_fees)) .map_err(Into::into); if simulate || result.is_err() { // Simulation only TransactionOutcome::Rollback(result) } else { // Should persist changes // Check if reserves are overused if let Ok(ref swap_result) = result && reserve < swap_result.amount_paid_out { return TransactionOutcome::Commit(Err( Error::::InsufficientLiquidity.into() )); } TransactionOutcome::Commit(result) } }) } fn ensure_swap_input_within_reserve_limit( netuid: NetUid, amount: Order::PaidIn, drop_fees: bool, ) -> Result<(), Error> where Order: OrderT, { let fee = Self::calculate_fee_amount(netuid, amount, drop_fees); let net_amount = amount.saturating_sub(fee); let input_reserve = Order::ReserveIn::reserve(netuid); let max_amount = input_reserve.saturating_mul(MAX_SWAP_INPUT_RESERVE_MULTIPLIER.into()); ensure!(net_amount <= max_amount, Error::::SwapInputTooLarge); Ok(()) } fn swap_inner( netuid: NetUid, order: Order, limit_price: U64F64, drop_fees: bool, ) -> Result, Error> where Order: OrderT, BasicSwapStep: SwapStep, { ensure!( Order::ReserveOut::reserve(netuid).to_u64() >= T::MinimumReserve::get().get(), Error::::ReservesTooLow ); Self::maybe_initialize_palswap(netuid, None)?; // Because user specifies the limit price, check that it is in fact beoynd the current one ensure!( order.is_beyond_price_limit(Self::current_price(netuid), limit_price), Error::::PriceLimitExceeded ); log::trace!("======== Start Swap ========"); let amount_to_swap = order.amount(); log::trace!("Amount to swap: {amount_to_swap}"); // Create and execute a swap step let mut swap_step = BasicSwapStep::::new( netuid, amount_to_swap, limit_price, drop_fees, ); let swap_result = swap_step.execute()?; log::trace!("Delta out: {}", swap_result.delta_out); log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); Ok(SwapResult { amount_paid_in: swap_result.delta_in, amount_paid_out: swap_result.delta_out, fee_paid: swap_result.fee_paid, fee_to_block_author: swap_result.fee_to_block_author, }) } /// Calculate fee amount /// /// Fee is provided by state ops as u16-normalized value. pub(crate) fn calculate_fee_amount(netuid: NetUid, amount: C, drop_fees: bool) -> C { if drop_fees { return C::ZERO; } match T::SubnetInfo::mechanism(netuid) { 1 => { let fee_rate = U64F64::saturating_from_num(FeeRate::::get(netuid)) .safe_div(U64F64::saturating_from_num(u16::MAX)); U64F64::saturating_from_num(amount) .saturating_mul(fee_rate) .saturating_to_num::() .into() } _ => C::ZERO, } } /// Returns the protocol account ID /// /// # Returns /// The account ID of the protocol account pub fn protocol_account_id() -> T::AccountId { T::ProtocolId::get().into_account_truncating() } pub(crate) fn min_price_inner() -> C { u64::from(1_000_u64).into() } pub(crate) fn max_price_inner() -> C { u64::from(1_000_000_000_000_000_u64).into() } /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. pub fn do_clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { let clear_weight = T::DbWeight::get().reads_writes(6, 7); if !weight_meter.can_consume(clear_weight) { return false; } weight_meter.consume(clear_weight); // / 1) Force-close protocol liquidity, burning proceeds. // Reservoir balances are materialized protocol liquidity that never became // price-active. Fold them into the reserve abstraction first so cleanup // clears them through the same path as active protocol liquidity. let reservoir_tao = BalancerTaoReservoir::::take(netuid); let reservoir_alpha = BalancerAlphaReservoir::::take(netuid); T::TaoReserve::increase_provided(netuid.into(), reservoir_tao); T::AlphaReserve::increase_provided(netuid.into(), reservoir_alpha); let burned_tao = T::TaoReserve::reserve(netuid.into()); let burned_alpha = T::AlphaReserve::reserve(netuid.into()); T::TaoReserve::decrease_provided(netuid.into(), burned_tao); T::AlphaReserve::decrease_provided(netuid.into(), burned_alpha); PalSwapInitialized::::remove(netuid); FeeRate::::remove(netuid); SwapBalancer::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" ); true } } impl DefaultPriceLimit for Pallet { fn default_price_limit() -> C { Self::max_price_inner::() } } impl DefaultPriceLimit for Pallet { fn default_price_limit() -> C { Self::min_price_inner::() } } impl SwapEngine for Pallet where T: Config, O: OrderT, BasicSwapStep: SwapStep, Self: DefaultPriceLimit, { fn swap( netuid: NetUid, order: O, price_limit: TaoBalance, drop_fees: bool, should_rollback: bool, ) -> Result, DispatchError> { let limit_price = U64F64::saturating_from_num(price_limit.to_u64()) .safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); Self::do_swap::( NetUid::from(netuid), order, limit_price, drop_fees, should_rollback, ) .map_err(Into::into) } } impl SwapHandler for Pallet { fn swap( netuid: NetUid, order: O, price_limit: TaoBalance, drop_fees: bool, should_rollback: bool, ) -> Result, DispatchError> where O: OrderT, Self: SwapEngine, { >::swap(netuid, order, price_limit, drop_fees, should_rollback) } fn sim_swap( netuid: NetUid, order: O, ) -> Result, DispatchError> where O: OrderT, Self: SwapEngine, { match T::SubnetInfo::mechanism(netuid) { 1 => { let price_limit = Self::default_price_limit::(); >::swap(netuid, order, price_limit, false, true) } _ => { let actual_amount = if T::SubnetInfo::exists(netuid) { order.amount() } else { O::PaidIn::ZERO }; Ok(SwapResult { amount_paid_in: actual_amount, amount_paid_out: actual_amount.to_u64().into(), fee_paid: 0.into(), fee_to_block_author: 0.into(), }) } } } fn approx_fee_amount(netuid: NetUid, amount: C) -> C { Self::calculate_fee_amount(netuid, amount, false) } fn current_alpha_price(netuid: NetUid) -> U64F64 { Self::current_price(netuid.into()) } fn min_price() -> C { Self::min_price_inner() } fn max_price() -> C { Self::max_price_inner() } fn clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { Self::do_clear_protocol_liquidity(netuid, weight_meter) } fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, ) -> (TaoBalance, AlphaBalance) { Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } fn protocol_alpha_reservoir(netuid: NetUid) -> AlphaBalance { BalancerAlphaReservoir::::get(netuid) } fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance { BalancerTaoReservoir::::get(netuid) } fn clear_protocol_liquidity_reservoirs(netuid: NetUid) { BalancerTaoReservoir::::remove(netuid); BalancerAlphaReservoir::::remove(netuid); } fn init_swap(netuid: NetUid, maybe_price: Option) { Self::maybe_initialize_palswap(netuid, maybe_price).unwrap_or_default(); } /// Get the amount of Alpha that needs to be sold to get a given amount of Tao fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { // For uniswap v3: Use no-slippage method. Amount is supposed to be small, // hence we can neglect slippage and return slightly lower amount. let alpha_price = Self::current_price(netuid.into()); AlphaBalance::from( U64F64::from(u64::from(tao_amount)) .safe_div(alpha_price) .saturating_to_num::(), ) } // Static subnet, alpha == tao _ => u64::from(tao_amount).into(), } } }