code/pallets/swap/src/pallet/impls.rs

impls.rs

540 lines · 19,418 bytes · 19a6485969RawGitHub
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<T: Config> Pallet<T> {
    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::<T>::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<U64F64>,
    ) -> Result<(), Error<T>> {
        if PalSwapInitialized::<T>::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::<u64>() 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::<u128>();
            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::<T>::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::<T>::ReservesOutOfBalance,
        })?;
        SwapBalancer::<T>::insert(netuid, balancer.clone());

        PalSwapInitialized::<T>::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::<T>::get(netuid);

        let pending_tao = BalancerTaoReservoir::<T>::get(netuid).saturating_add(tao_delta);
        let pending_alpha = BalancerAlphaReservoir::<T>::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::<T>::insert(netuid, TaoBalance::ZERO);
            BalancerAlphaReservoir::<T>::insert(netuid, AlphaBalance::ZERO);
            SwapBalancer::<T>::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::<T>::insert(netuid, pending_tao);
            BalancerAlphaReservoir::<T>::insert(netuid, AlphaBalance::ZERO);
            SwapBalancer::<T>::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::<T>::insert(netuid, TaoBalance::ZERO);
            BalancerAlphaReservoir::<T>::insert(netuid, pending_alpha);
            SwapBalancer::<T>::insert(netuid, new_balancer);
            return (pending_tao, AlphaBalance::ZERO);
        }

        BalancerTaoReservoir::<T>::insert(netuid, pending_tao);
        BalancerAlphaReservoir::<T>::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<Balancer> {
        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<Order>(
        netuid: NetUid,
        order: Order,
        limit_price: U64F64,
        drop_fees: bool,
        simulate: bool,
    ) -> Result<SwapResult<Order::PaidIn, Order::PaidOut>, DispatchError>
    where
        Order: OrderT,
        BasicSwapStep<T, Order::PaidIn, Order::PaidOut>: SwapStep<T, Order::PaidIn, Order::PaidOut>,
    {
        transactional::with_transaction(|| {
            let reserve = Order::ReserveOut::reserve(netuid.into());

            let result = Self::ensure_swap_input_within_reserve_limit::<Order>(
                netuid,
                order.amount(),
                drop_fees,
            )
            .and_then(|_| Self::swap_inner::<Order>(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::<T>::InsufficientLiquidity.into()
                    ));
                }

                TransactionOutcome::Commit(result)
            }
        })
    }

    fn ensure_swap_input_within_reserve_limit<Order>(
        netuid: NetUid,
        amount: Order::PaidIn,
        drop_fees: bool,
    ) -> Result<(), Error<T>>
    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::<T>::SwapInputTooLarge);
        Ok(())
    }

    fn swap_inner<Order>(
        netuid: NetUid,
        order: Order,
        limit_price: U64F64,
        drop_fees: bool,
    ) -> Result<SwapResult<Order::PaidIn, Order::PaidOut>, Error<T>>
    where
        Order: OrderT,
        BasicSwapStep<T, Order::PaidIn, Order::PaidOut>: SwapStep<T, Order::PaidIn, Order::PaidOut>,
    {
        ensure!(
            Order::ReserveOut::reserve(netuid).to_u64() >= T::MinimumReserve::get().get(),
            Error::<T>::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::<T>::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::<T, Order::PaidIn, Order::PaidOut>::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<C: Token>(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::<T>::get(netuid))
                    .safe_div(U64F64::saturating_from_num(u16::MAX));
                U64F64::saturating_from_num(amount)
                    .saturating_mul(fee_rate)
                    .saturating_to_num::<u64>()
                    .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: Token>() -> C {
        u64::from(1_000_u64).into()
    }

    pub(crate) fn max_price_inner<C: Token>() -> 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::<T>::take(netuid);
        let reservoir_alpha = BalancerAlphaReservoir::<T>::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::<T>::remove(netuid);

        FeeRate::<T>::remove(netuid);
        SwapBalancer::<T>::remove(netuid);

        log::debug!(
            "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared"
        );

        true
    }
}

impl<T: Config> DefaultPriceLimit<TaoBalance, AlphaBalance> for Pallet<T> {
    fn default_price_limit<C: Token>() -> C {
        Self::max_price_inner::<C>()
    }
}

impl<T: Config> DefaultPriceLimit<AlphaBalance, TaoBalance> for Pallet<T> {
    fn default_price_limit<C: Token>() -> C {
        Self::min_price_inner::<C>()
    }
}

impl<T, O> SwapEngine<O> for Pallet<T>
where
    T: Config,
    O: OrderT,
    BasicSwapStep<T, O::PaidIn, O::PaidOut>: SwapStep<T, O::PaidIn, O::PaidOut>,
    Self: DefaultPriceLimit<O::PaidIn, O::PaidOut>,
{
    fn swap(
        netuid: NetUid,
        order: O,
        price_limit: TaoBalance,
        drop_fees: bool,
        should_rollback: bool,
    ) -> Result<SwapResult<O::PaidIn, O::PaidOut>, 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::<O>(
            NetUid::from(netuid),
            order,
            limit_price,
            drop_fees,
            should_rollback,
        )
        .map_err(Into::into)
    }
}

impl<T: Config> SwapHandler for Pallet<T> {
    fn swap<O>(
        netuid: NetUid,
        order: O,
        price_limit: TaoBalance,
        drop_fees: bool,
        should_rollback: bool,
    ) -> Result<SwapResult<O::PaidIn, O::PaidOut>, DispatchError>
    where
        O: OrderT,
        Self: SwapEngine<O>,
    {
        <Self as SwapEngine<O>>::swap(netuid, order, price_limit, drop_fees, should_rollback)
    }

    fn sim_swap<O>(
        netuid: NetUid,
        order: O,
    ) -> Result<SwapResult<O::PaidIn, O::PaidOut>, DispatchError>
    where
        O: OrderT,
        Self: SwapEngine<O>,
    {
        match T::SubnetInfo::mechanism(netuid) {
            1 => {
                let price_limit = Self::default_price_limit::<TaoBalance>();

                <Self as SwapEngine<O>>::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<C: Token>(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: Token>() -> C {
        Self::min_price_inner()
    }

    fn max_price<C: Token>() -> 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::<T>::get(netuid)
    }

    fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance {
        BalancerTaoReservoir::<T>::get(netuid)
    }

    fn clear_protocol_liquidity_reservoirs(netuid: NetUid) {
        BalancerTaoReservoir::<T>::remove(netuid);
        BalancerAlphaReservoir::<T>::remove(netuid);
    }

    fn init_swap(netuid: NetUid, maybe_price: Option<U64F64>) {
        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::<u64>(),
                )
            }

            // Static subnet, alpha == tao
            _ => u64::from(tao_amount).into(),
        }
    }
}