diff --git a/Cargo.lock b/Cargo.lock index cfb96c21d6..fbfcf0fb7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9513,6 +9513,30 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-derivatives" +version = "0.1.0" +dependencies = [ + "approx", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-subtensor-swap", + "parity-scale-codec", + "safe-math", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "substrate-fixed", + "subtensor-macros", + "subtensor-runtime-common", + "subtensor-swap-interface", +] + [[package]] name = "pallet-dev-mode" version = "23.0.0" diff --git a/pallets/derivatives/Cargo.toml b/pallets/derivatives/Cargo.toml new file mode 100644 index 0000000000..ecb2caf5a9 --- /dev/null +++ b/pallets/derivatives/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "pallet-derivatives" +version = "0.1.0" +edition.workspace = true +authors = ["Bittensor Nucleus Team"] +license = "Apache-2.0" +homepage = "https://bittensor.com" +description = "Derivatives based on TAO and Alpha" +publish = false +repository = "https://github.com/opentensor/subtensor" + +[lints] +workspace = true + +[dependencies] +safe-math.workspace = true +subtensor-macros.workspace = true +scale-info = { workspace = true, features = ["derive"] } +codec = { workspace = true, features = ["max-encoded-len"] } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +pallet-balances.workspace = true +pallet-subtensor-swap.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +substrate-fixed.workspace = true +subtensor-runtime-common.workspace = true +subtensor-swap-interface.workspace = true +log.workspace = true + +[dev-dependencies] +approx.workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "log/std", + "safe-math/std", + "sp-core/std", + "substrate-fixed/std", + "subtensor-runtime-common/std", + "subtensor-swap-interface/std", + "pallet-balances/std", + "pallet-subtensor-swap/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", + "pallet-balances/try-runtime", +] diff --git a/pallets/derivatives/README.md b/pallets/derivatives/README.md new file mode 100644 index 0000000000..2e1372a875 --- /dev/null +++ b/pallets/derivatives/README.md @@ -0,0 +1,7 @@ +# Pallet derivatives + +Implements derivatives based on TAO and Alpha. + +## Shorts + +TBD \ No newline at end of file diff --git a/pallets/derivatives/src/benchmarking.rs b/pallets/derivatives/src/benchmarking.rs new file mode 100644 index 0000000000..95b6c5c857 --- /dev/null +++ b/pallets/derivatives/src/benchmarking.rs @@ -0,0 +1,24 @@ +//! Benchmarks for Derivatives Pallet +#![cfg(feature = "runtime-benchmarks")] +#![allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used +)] + +extern crate alloc; + +// #[benchmarks] +// mod benchmarks { +// use super::*; + +// #[benchmark] +// fn open_short() { +// } + +// #[benchmark] +// fn close_short() { +// } + +// impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +// } diff --git a/pallets/derivatives/src/lib.rs b/pallets/derivatives/src/lib.rs new file mode 100644 index 0000000000..1acc780a1b --- /dev/null +++ b/pallets/derivatives/src/lib.rs @@ -0,0 +1,402 @@ +//! # Derivatives Pallet +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec; +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::GetDispatchInfo, + pallet_prelude::*, + sp_runtime::{RuntimeDebug, traits::Dispatchable}, + traits::{Get, IsSubType}, +}; +use frame_system::pallet_prelude::*; +use safe_math::*; +use scale_info::TypeInfo; +use weights::WeightInfo; + +pub use pallet::*; +use substrate_fixed::types::U96F32; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{AlphaCurrency, BalanceOps, Currency, NetUid, TaoCurrency}; + +mod benchmarking; +mod mock; +mod tests; +pub mod weights; + +#[derive( + Encode, + Clone, + Decode, + DecodeWithMemTracking, + Default, + Eq, + PartialEq, + Ord, + PartialOrd, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, +)] +pub enum PositionType { + #[default] + Short, +} + +/// Derivative position +#[freeze_struct("8a67a79bd2ec3369")] +#[derive(Encode, Decode, Default, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct DerivativePosition { + /// The hotkey against which the Alpha in this position is accounted + pub hotkey: AccountId, + /// Type of the position + pub pos_type: PositionType, + /// Liquidation price + pub liquidation_price: U96F32, + /// The position collateral + pub tao_collateral: TaoCurrency, + /// The tao received for selling alpha + pub tao_proceeds: TaoCurrency, + /// The position size in Alpha + pub size: AlphaCurrency, +} + +/// Trait for integration with the swap +pub trait DerivativeSwapInterface { + /// Buy alpha with a given tao amount + fn buy(netuid: NetUid, tao: TaoCurrency) -> Result; + /// Buy tao with a given alpha amount + fn sell(netuid: NetUid, alpha: AlphaCurrency) -> Result; + /// Get the amount of tao needed to buy the given amount of alpha + fn get_tao_for_alpha_amount(netuid: NetUid, alpha: AlphaCurrency) -> TaoCurrency; + /// Get the amount of alpha needed to buy the given amount of tao + fn get_alpha_for_tao_amount(netuid: NetUid, tao: TaoCurrency) -> AlphaCurrency; + /// Mint alpha + fn mint_alpha(netuid: NetUid, alpha: AlphaCurrency); + /// Burn alpha + fn burn_alpha(netuid: NetUid, alpha: AlphaCurrency); + /// Get alpha EMA price + fn get_alpha_ema_price(netuid: NetUid) -> U96F32; + /// Remove alpha from reserve and update price accordingly + fn decrease_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) -> DispatchResult; +} + +pub type PositionInfoOf = DerivativePosition<::AccountId>; + +#[frame_support::pallet] +#[allow(clippy::expect_used)] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + /// Configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + IsSubType> + + IsType<::RuntimeCall>; + + /// Operations with balances and stakes + type BalanceOps: BalanceOps; + + // /// The currency mechanism. + // type Currency: fungible::Balanced + // + fungible::Mutate; + + /// The weight information for the pallet. + type WeightInfo: WeightInfo; + + /// The mechanism to swap, mint, and burn + type SwapInterface: DerivativeSwapInterface; + + /// Collateral ratio per billion + type CollateralRatio: Get; + + /// Minimum position size in TAO + type MinPositionSize: Get; + } + + /// A map of open positions + #[pallet::storage] + pub type Positions = StorageNMap< + _, + ( + NMapKey, // cold + NMapKey, // subnet + ), + PositionInfoOf, + OptionQuery, + >; + + /// TODO: Structure that allows efficient search of positions by liquidation price + // #[pallet::storage] + // pub type PositionIndex = + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A position was opened + Opened { + netuid: NetUid, + coldkey: T::AccountId, + hotkey: T::AccountId, + pos_type: PositionType, + collateral: TaoCurrency, + size: AlphaCurrency, + open_price: U96F32, + }, + /// A position was closed + Closed { + netuid: NetUid, + coldkey: T::AccountId, + hotkey: T::AccountId, + pos_type: PositionType, + size: AlphaCurrency, + close_price: U96F32, // Average close price + liquidation: bool, // Whether position was liquidated or closed voluntarily + partial: bool, // Partial or full close + }, + } + + #[pallet::error] + pub enum Error { + /// No open position exists + NoOpenPosition, + /// Trying to close for greater size than open position + InsufficientPositionSize, + /// Position size is too low + AmountTooLow, + /// Insufficient TAO balance to open position + InsufficientBalance, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let weight = frame_support::weights::Weight::from_parts(0, 0); + + // weight = weight + // .saturating_add(migrations::migrate_...::()); + + weight + } + + fn on_initialize(_block_number: BlockNumberFor) -> Weight { + // Execute liquidations here + todo!(); + } + } + + #[pallet::call] + impl Pallet { + #![deny(clippy::expect_used)] + + /// Open a short position at the specified subnet and hotkey + /// + /// - Withdraw a collateral from the calling coldkey balance + /// - Mint and sell new alpha (tao_amount / ema_price), record alpha_amount + /// - Record received tao in tao_proceeds + /// + /// Parameters: + /// - `hotkey`: The hotkey at which alpha is recorded + /// - `netuid`: Subnet ID + /// - `tao_amount`: Amount of TAO to spend on opening position + #[pallet::call_index(0)] + #[pallet::weight(( + Weight::from_parts(100_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn open_short( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + tao_amount: TaoCurrency, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + + // Make sure position size is above the limit + ensure!(tao_amount >= T::MinPositionSize::get(), Error::::AmountTooLow); + + // Withdraw collateral + let tao_collateral = T::BalanceOps::decrease_balance(&coldkey, tao_amount) + .map_err(|_| Error::::InsufficientBalance)?; + + // Set off the collateral ratio + let collateral_ratio = Self::get_collateral_ratio(); + let position_tao: TaoCurrency = U96F32::saturating_from_num(tao_collateral) + .safe_div(collateral_ratio) + .saturating_to_num::() + .into(); + + // Get current alpha price and mint alpha_amount = (tao_amount / ema_price) + let ema_price = T::SwapInterface::get_alpha_ema_price(netuid); + let tao_amount_fixed = U96F32::saturating_from_num(position_tao); + let alpha_amount_fixed = tao_amount_fixed.safe_div(ema_price); + let alpha_amount = AlphaCurrency::from(alpha_amount_fixed.saturating_to_num::()); + T::SwapInterface::mint_alpha(netuid, alpha_amount); + + // Sell minted alpha + let tao_proceeds = T::SwapInterface::sell(netuid, alpha_amount)?; + + // Create/update position + Self::upsert_short_position_add( + coldkey.clone(), + hotkey.clone(), + netuid, + tao_collateral, + tao_proceeds, + alpha_amount, + ); + + // Emit event + Self::deposit_event(Event::Opened { + netuid, + coldkey, + hotkey, + pos_type: PositionType::Short, + collateral: tao_collateral, + size: alpha_amount, + open_price: ema_price, + }); + + Ok(()) + } + + /// Close a short position at the specified subnet and hotkey. + /// + /// - Buy position alpha amount from pool with collateral + proceeds, burn it + /// - If total tao is not enough, remove the alpha remainder from + /// the alpha reserve + /// - If any total tao left, credit the remainder to the coldkey + /// balance + /// - Update position accordingly + /// + /// Parameters: + /// - `netuid`: Subnet ID + #[pallet::call_index(1)] + #[pallet::weight(( + Weight::from_parts(100_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn close_short( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + + // Get the current open short position + let maybe_position = Positions::::get((coldkey.clone(), netuid)); + let position = maybe_position.ok_or(Error::::NoOpenPosition)?; + + // Get the position size + let alpha_amount = position.size; + + // Calculate how much tao we need to buy the minted alpha back + let tao_required_to_close = T::SwapInterface::get_tao_for_alpha_amount(netuid, alpha_amount); + + // Buy minted alpha back + let alpha_amount_actual = T::SwapInterface::buy(netuid, tao_required_to_close)?; + + // Calculate the position tao remainder and act accordingly + let total_tao = position.tao_collateral.saturating_add(position.tao_proceeds); + if total_tao > tao_required_to_close { + // Deposit remaining tao + let tao_remainder = total_tao.saturating_sub(tao_required_to_close); + T::BalanceOps::increase_balance(&coldkey, tao_remainder); + } + + // Calculate alpha needed to cover the loss if any and remove from alpha reserve + if alpha_amount > alpha_amount_actual { + let alpha_remainder = alpha_amount.saturating_sub(alpha_amount_actual); + T::SwapInterface::decrease_alpha_reserve(netuid, alpha_remainder)?; + } + T::SwapInterface::burn_alpha(netuid, alpha_amount); + + // Calculate the average close price + let close_price = U96F32::saturating_from_num(u64::from(tao_required_to_close)).safe_div(U96F32::saturating_from_num(alpha_amount)); + + // Delete position + Positions::::remove((coldkey.clone(), netuid)); + + // Emit event + Self::deposit_event(Event::Closed { + netuid, + coldkey, + hotkey, + pos_type: PositionType::Short, + size: alpha_amount, + close_price, + liquidation: false, + partial: false, + }); + + Ok(()) + } + } +} + +impl Pallet { + pub fn get_collateral_ratio() -> U96F32 { + U96F32::saturating_from_num(T::CollateralRatio::get()) + .safe_div(U96F32::saturating_from_num(1_000_000_000)) + } + + pub fn upsert_short_position_add( + coldkey: T::AccountId, + hotkey: T::AccountId, + netuid: NetUid, + tao_collateral: TaoCurrency, + tao_proceeds: TaoCurrency, + size: AlphaCurrency, + ) { + let liquidation_price; + let new_position = if let Some(position) = Positions::::get((coldkey.clone(), netuid)) { + // Update liquidation price + // TBD + liquidation_price = U96F32::saturating_from_num(1000.); + + let new_collateral = u64::from(tao_collateral).saturating_add(u64::from(position.tao_collateral)); + let new_proceeds = u64::from(tao_proceeds).saturating_add(u64::from(position.tao_proceeds)); + let new_size = u64::from(size).saturating_add(u64::from(position.size)); + + DerivativePosition { + hotkey, + pos_type: PositionType::Short, + liquidation_price, + tao_collateral: new_collateral.into(), + tao_proceeds: new_proceeds.into(), + size: new_size.into(), + } + } else { + // Calculate liquidation price + // TBD + liquidation_price = U96F32::saturating_from_num(1000.); + + DerivativePosition { + hotkey, + pos_type: PositionType::Short, + liquidation_price, + tao_collateral, + tao_proceeds, + size, + } + }; + + Positions::::insert((coldkey, netuid), new_position); + } +} diff --git a/pallets/derivatives/src/mock.rs b/pallets/derivatives/src/mock.rs new file mode 100644 index 0000000000..9129c0838d --- /dev/null +++ b/pallets/derivatives/src/mock.rs @@ -0,0 +1,386 @@ +#![cfg(test)] +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::unwrap_used +)] +use crate::{DerivativeSwapInterface, DispatchError}; +use core::num::NonZeroU64; +use frame_support::{ + ensure, PalletId, derive_impl, parameter_types, + pallet_prelude::DispatchResult, + traits::{OnFinalize, OnInitialize}, +}; +use sp_runtime::{BuildStorage, traits::IdentityLookup}; +use std::{cell::RefCell, collections::HashMap}; +use substrate_fixed::types::U96F32; +use subtensor_runtime_common::{ + AlphaCurrency, BalanceOps, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, +}; +use subtensor_swap_interface::{Order, SwapHandler}; + +use crate::pallet as pallet_derivatives; + +type Block = frame_system::mocking::MockBlock; +pub type AccountId = u32; +pub const COLDKEY1: AccountId = 1; +pub const HOTKEY1: AccountId = 1001; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system = 1, + Balances: pallet_balances = 2, + Derivatives: pallet_derivatives = 3, + Swap: pallet_subtensor_swap = 4, + } +); + +#[allow(unused)] +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Expected to not panic"); + pallet_balances::GenesisConfig:: { + balances: vec![ + (1_u32, 10), + (2_u32, 10), + (3_u32, 10), + (4_u32, 10), + (5_u32, 3), + ], + dev_accounts: None, + } + .assimilate_storage(&mut t) + .expect("Expected to not panic"); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = u32; + type AccountData = pallet_balances::AccountData; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +pub struct MockSwap; + +pub type GetAlphaForTao = subtensor_swap_interface::GetAlphaForTao; +pub type GetTaoForAlpha = subtensor_swap_interface::GetTaoForAlpha; + +impl MockSwap { + pub fn get_alpha_out(netuid: NetUid) -> AlphaCurrency { + if let Some(val) = MOCK_ALPHA_OUT.with(|m| m.borrow().get(&netuid).cloned()) { + val + } else { + 0.into() + } + } + + pub fn get_current_price(netuid: NetUid) -> U96F32 { + as SwapHandler>::current_alpha_price(netuid) + } +} + +impl DerivativeSwapInterface for MockSwap { + fn buy(netuid: NetUid, tao: TaoCurrency) -> Result { + let order = GetAlphaForTao::with_amount(tao); + let max_price = as SwapHandler>::max_price(); + let swap_result = as SwapHandler>::swap( + netuid.into(), + order, + max_price, + true, + false, + )?; + // Update reserves (swap pallet doesn't do that) + let alpha_in_old = AlphaReserve::reserve(netuid); + let tao_in_old = TaoReserve::reserve(netuid); + AlphaReserve::set_mock_reserve(netuid, alpha_in_old - swap_result.amount_paid_out); + TaoReserve::set_mock_reserve(netuid, tao_in_old + swap_result.amount_paid_in); + Ok(swap_result.amount_paid_out) + } + fn sell(netuid: NetUid, alpha: AlphaCurrency) -> Result { + let order = GetTaoForAlpha::with_amount(alpha); + let min_price = as SwapHandler>::min_price(); + let swap_result = as SwapHandler>::swap( + netuid.into(), + order, + min_price, + true, + false, + )?; + // Update reserves (swap pallet doesn't do that) + let alpha_in_old = AlphaReserve::reserve(netuid); + let tao_in_old = TaoReserve::reserve(netuid); + AlphaReserve::set_mock_reserve(netuid, alpha_in_old + swap_result.amount_paid_in); + TaoReserve::set_mock_reserve(netuid, tao_in_old - swap_result.amount_paid_out); + Ok(swap_result.amount_paid_out) + } + fn get_tao_for_alpha_amount(netuid: NetUid, alpha: AlphaCurrency) -> TaoCurrency { + as SwapHandler>::get_tao_amount_for_alpha( + netuid, alpha, + ) + } + fn get_alpha_for_tao_amount(netuid: NetUid, tao: TaoCurrency) -> AlphaCurrency { + as SwapHandler>::get_alpha_amount_for_tao( + netuid, tao, + ) + } + fn mint_alpha(netuid: NetUid, alpha: AlphaCurrency) { + let old = Self::get_alpha_out(netuid); + MOCK_ALPHA_OUT.with(|m| { + m.borrow_mut().insert(netuid, old + alpha); + }); + } + fn burn_alpha(netuid: NetUid, alpha: AlphaCurrency) { + let old = Self::get_alpha_out(netuid); + MOCK_ALPHA_OUT.with(|m| { + m.borrow_mut().insert(netuid, old - alpha); + }); + } + fn get_alpha_ema_price(_netuid: NetUid) -> U96F32 { + U96F32::from_num(0.001) + } + fn decrease_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) -> DispatchResult { + // TODO: Implement the removal on the swap level (so that it updates the price) + + let alpha_in = AlphaReserve::reserve(netuid); + ensure!(alpha <= alpha_in, DispatchError::Other("Alpha out of bounds")); + AlphaReserve::set_mock_reserve(netuid, alpha_in - alpha); + Ok(()) + } +} + +parameter_types! { + pub const CollateralRatio: u64 = 2_000_000_000; + pub const MinPositionSize: TaoCurrency = TaoCurrency::new(1_000_000_000); +} + +impl pallet_derivatives::Config for Test { + type BalanceOps = MockBalanceOps; + type RuntimeCall = RuntimeCall; + type WeightInfo = (); + type SwapInterface = MockSwap; + type CollateralRatio = CollateralRatio; + type MinPositionSize = MinPositionSize; +} + +#[allow(dead_code)] +pub(crate) fn run_to_block(n: u64) { + while System::block_number() < n { + System::on_finalize(System::block_number()); + Balances::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + Balances::on_initialize(System::block_number()); + System::on_initialize(System::block_number()); + } +} + +parameter_types! { + pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); + pub const SwapMaxFeeRate: u16 = 10000; // 15.26% + pub const SwapMaxPositions: u32 = 100; + pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumReserve: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(1_000_000) }; +} + +impl pallet_subtensor_swap::Config for Test { + type SubnetInfo = MockLiquidityProvider; + type BalanceOps = MockBalanceOps; + type ProtocolId = SwapProtocolId; + type TaoReserve = TaoReserve; + type AlphaReserve = AlphaReserve; + type MaxFeeRate = SwapMaxFeeRate; + type MaxPositions = SwapMaxPositions; + type MinimumLiquidity = SwapMinimumLiquidity; + type MinimumReserve = SwapMinimumReserve; + type WeightInfo = pallet_subtensor_swap::weights::DefaultWeight; +} + +// Mock implementor of SubnetInfo trait +pub struct MockLiquidityProvider; + +impl SubnetInfo for MockLiquidityProvider { + fn exists(_netuid: NetUid) -> bool { + true + } + + fn mechanism(netuid: NetUid) -> u16 { + if netuid == NetUid::from(0) { 0 } else { 1 } + } + + fn is_owner(_account_id: &AccountId, _netuid: NetUid) -> bool { + true + } + + // Only disable one subnet for testing + fn is_subtoken_enabled(_netuid: NetUid) -> bool { + true + } + + fn get_validator_trust(_netuid: NetUid) -> Vec { + vec![1000, 800, 600, 400] + } + + fn get_validator_permit(_netuid: NetUid) -> Vec { + vec![true, true, true, true] + } + + fn hotkey_of_uid(_netuid: NetUid, uid: u16) -> Option { + Some(uid as AccountId) + } +} + +pub struct MockBalanceOps; + +thread_local! { + // maps AccountId -> mocked tao balance + static MOCK_TAO_BALANCES: RefCell> = + RefCell::new(HashMap::new()); + // maps AccountId -> mocked alpha balance + static MOCK_ALPHA_BALANCES: RefCell> = + RefCell::new(HashMap::new()); + // maps netuid -> mocked tao reserve + static MOCK_TAO_RESERVES: RefCell> = + RefCell::new(HashMap::new()); + // maps netuid -> mocked alpha reserve + static MOCK_ALPHA_RESERVES: RefCell> = + RefCell::new(HashMap::new()); + // maps netuid -> mocked alpha outstanding + static MOCK_ALPHA_OUT: RefCell> = + RefCell::new(HashMap::new()); +} + +impl BalanceOps for MockBalanceOps { + fn tao_balance(account_id: &AccountId) -> TaoCurrency { + if let Some(val) = MOCK_TAO_BALANCES.with(|m| m.borrow().get(&account_id).cloned()) { + val + } else { + 0.into() + } + } + + fn alpha_balance( + _: NetUid, + coldkey_account_id: &AccountId, + _hotkey_account_id: &AccountId, + ) -> AlphaCurrency { + if let Some(val) = + MOCK_ALPHA_BALANCES.with(|m| m.borrow().get(&coldkey_account_id).cloned()) + { + val + } else { + 0.into() + } + } + + fn increase_balance(coldkey: &AccountId, tao: TaoCurrency) { + let old = Self::tao_balance(coldkey); + MOCK_TAO_BALANCES.with(|m| { + m.borrow_mut().insert(*coldkey, old + tao); + }); + } + + fn decrease_balance( + coldkey: &AccountId, + tao: TaoCurrency, + ) -> Result { + let old = Self::tao_balance(coldkey); + if old < tao { + return Err(DispatchError::Other("Insufficient balance")); + } + MOCK_TAO_BALANCES.with(|m| { + m.borrow_mut().insert(*coldkey, old - tao); + }); + Ok(tao) + } + + fn increase_stake( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + alpha: AlphaCurrency, + ) -> Result<(), DispatchError> { + let old = Self::alpha_balance(netuid, coldkey, hotkey); + MOCK_ALPHA_BALANCES.with(|m| { + m.borrow_mut().insert(*coldkey, old + alpha); + }); + Ok(()) + } + + fn decrease_stake( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + alpha: AlphaCurrency, + ) -> Result { + let old = Self::alpha_balance(netuid, coldkey, hotkey); + if old < alpha { + return Err(DispatchError::Other("Insufficient stake")); + } + MOCK_ALPHA_BALANCES.with(|m| { + m.borrow_mut().insert(*coldkey, old - alpha); + }); + Ok(alpha) + } +} + +#[derive(Clone)] +pub struct TaoReserve; + +impl TaoReserve { + pub fn set_mock_reserve(netuid: NetUid, value: TaoCurrency) { + MOCK_TAO_RESERVES.with(|m| { + m.borrow_mut().insert(netuid, value); + }); + } +} + +impl CurrencyReserve for TaoReserve { + fn reserve(netuid: NetUid) -> TaoCurrency { + // If test has set an override, use it + if let Some(val) = MOCK_TAO_RESERVES.with(|m| m.borrow().get(&netuid).cloned()) { + val + } else { + 0.into() + } + } + + fn increase_provided(_: NetUid, _: TaoCurrency) {} + fn decrease_provided(_: NetUid, _: TaoCurrency) {} +} + +#[derive(Clone)] +pub struct AlphaReserve; + +impl AlphaReserve { + pub fn set_mock_reserve(netuid: NetUid, value: AlphaCurrency) { + MOCK_ALPHA_RESERVES.with(|m| { + m.borrow_mut().insert(netuid, value); + }); + } +} + +impl CurrencyReserve for AlphaReserve { + fn reserve(netuid: NetUid) -> AlphaCurrency { + // If test has set an override, use it + if let Some(val) = MOCK_ALPHA_RESERVES.with(|m| m.borrow().get(&netuid).cloned()) { + val + } else { + 0.into() + } + } + + fn increase_provided(_: NetUid, _: AlphaCurrency) {} + fn decrease_provided(_: NetUid, _: AlphaCurrency) {} +} diff --git a/pallets/derivatives/src/tests.rs b/pallets/derivatives/src/tests.rs new file mode 100644 index 0000000000..751f314a3e --- /dev/null +++ b/pallets/derivatives/src/tests.rs @@ -0,0 +1,504 @@ +#![cfg(test)] +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use crate::mock::*; +use crate::*; +use approx::assert_abs_diff_eq; +use frame_support::{assert_noop, assert_ok}; +use subtensor_runtime_common::{BalanceOps, Currency, CurrencyReserve, NetUid, TaoCurrency}; +use subtensor_swap_interface::{Order, SwapHandler}; + +// Run all tests here: +// cargo test --package pallet-derivatives --lib -- tests --nocapture + +// Test plan: +// - Open +// - Open normally +// - Open with insufficient balance +// - Open with amount below min threshold +// - Open when a position is already open (adds) +// - Close +// - Close normally +// - Open - buy - liquidate before we run out of alpha +// - Close when there's no open position + +#[test] +fn test_open_short_ok() { + new_test_ext().execute_with(|| { + // Setup network and balances (both ema price and price are 0.001) + let netuid = NetUid::from(1); + let balance_before = TaoCurrency::from(10_000_000_000); + let position_tao = TaoCurrency::from(1_000_000_000); + TaoReserve::set_mock_reserve(netuid, 1_000_000_000.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000.into()); + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let ema_price = MockSwap::get_alpha_ema_price(netuid).to_num::(); + let price_before = MockSwap::get_current_price(netuid).to_num::(); + + // Expected alpha to mint + let collateral_ratio = Derivatives::get_collateral_ratio().to_num::(); + let expected_minted_alpha = + (u64::from(position_tao) as f64 / (collateral_ratio * ema_price)) as u64; + + // Simulate swap to estimate tao proceeds + let order = GetTaoForAlpha::with_amount(expected_minted_alpha); + let expected_tao_proceeds = + as SwapHandler>::sim_swap(netuid, order) + .map(|r| r.amount_paid_out.to_u64()) + .unwrap_or_default(); + + // Open short + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + position_tao + )); + + // Check that coldkey balance decreased + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_after, balance_before - position_tao); + + // Check that correct amount of alpha was minted (AlphaOut increased in mock) + let alpha_out_after = MockSwap::get_alpha_out(netuid); + assert_eq!( + alpha_out_after, + alpha_out_before + expected_minted_alpha.into() + ); + assert!(alpha_out_after > alpha_out_before); + + // Check that minted alpha was sold (drives price down) + let price_after = MockSwap::get_current_price(netuid).to_num::(); + assert!(price_before > price_after); + + // Position was created + let position = Positions::::get((COLDKEY1, netuid)).unwrap(); + assert_eq!(position.hotkey, HOTKEY1); + assert_eq!(position.pos_type, PositionType::Short); + // assert_eq!(position.liquidation_price, ??); + assert_eq!(position.tao_collateral, position_tao); + assert_abs_diff_eq!( + position.tao_proceeds, + expected_tao_proceeds.into(), + epsilon = 200000.into(), + ); + assert_eq!(position.size, expected_minted_alpha.into()); + + // Make sure open event gets emitted + assert!(System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Opened { .. }) + ) + })); + }); +} + +#[test] +fn test_open_short_fails_with_insufficient_balance() { + new_test_ext().execute_with(|| { + // Setup network and reserves + let netuid = NetUid::from(1); + let balance_before = TaoCurrency::from(500_000_000); + let position_tao = TaoCurrency::from(1_000_000_000); + + TaoReserve::set_mock_reserve(netuid, 1_000_000_000.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000.into()); + + // Give the caller less balance than required + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let price_before = MockSwap::get_current_price(netuid); + + // Should fail because collateral exceeds available balance + assert_noop!( + Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + position_tao + ), + Error::::InsufficientBalance + ); + + // Balance unchanged + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_after, balance_before); + + // No position created + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // No swap side effects happened + let alpha_out_after = MockSwap::get_alpha_out(netuid); + let price_after = MockSwap::get_current_price(netuid); + assert_eq!(alpha_out_after, alpha_out_before); + assert_eq!(price_after, price_before); + + // No "Opened" event emitted + assert!(!System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Opened { .. }) + ) + })); + }); +} + +#[test] +fn test_open_short_fails_with_amount_too_low() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Setup reserves so pricing logic is valid and failure is specifically min size + TaoReserve::set_mock_reserve(netuid, 1_000_000_000.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000.into()); + + // Give enough balance so we do not fail with InsufficientBalance + let balance_before = TaoCurrency::from(10_000_000_000); + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + + let min_position_size = ::MinPositionSize::get(); + let tao_amount = min_position_size - TaoCurrency::from(1); + + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let price_before = MockSwap::get_current_price(netuid); + + assert_noop!( + Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + tao_amount + ), + Error::::AmountTooLow + ); + + // Balance unchanged + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_after, balance_before); + + // No position created + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // No swap side effects + let alpha_out_after = MockSwap::get_alpha_out(netuid); + let price_after = MockSwap::get_current_price(netuid); + assert_eq!(alpha_out_after, alpha_out_before); + assert_eq!(price_after, price_before); + + // No Opened event emitted + assert!(!System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Opened { .. }) + ) + })); + }); +} + +#[test] +fn test_open_short_twice_increases_existing_position() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Setup network and balances + TaoReserve::set_mock_reserve(netuid, 1_000_000_000.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000.into()); + + let balance_before = TaoCurrency::from(20_000_000_000); + let first_tao = TaoCurrency::from(1_000_000_000); + let second_tao = TaoCurrency::from(2_000_000_000); + + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + + let alpha_out_before = MockSwap::get_alpha_out(netuid); + + // First open + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + first_tao + )); + + let position_after_first = Positions::::get((COLDKEY1, netuid)).unwrap(); + let balance_after_first = MockBalanceOps::tao_balance(&COLDKEY1); + let alpha_out_after_first = MockSwap::get_alpha_out(netuid); + + assert_eq!(position_after_first.hotkey, HOTKEY1); + assert_eq!(position_after_first.pos_type, PositionType::Short); + assert_eq!(position_after_first.tao_collateral, first_tao); + assert!(position_after_first.size > 0.into()); + assert!(position_after_first.tao_proceeds > 0.into()); + assert_eq!(balance_after_first, balance_before - first_tao); + assert!(alpha_out_after_first > alpha_out_before); + + // Second open on the same position + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + second_tao + )); + + let position_after_second = Positions::::get((COLDKEY1, netuid)).unwrap(); + let balance_after_second = MockBalanceOps::tao_balance(&COLDKEY1); + let alpha_out_after_second = MockSwap::get_alpha_out(netuid); + + // Same position key still exists, but values increased + assert_eq!(position_after_second.hotkey, HOTKEY1); + assert_eq!(position_after_second.pos_type, PositionType::Short); + + // Collateral should increase by the second deposit + assert_eq!( + position_after_second.tao_collateral, + position_after_first.tao_collateral + second_tao + ); + + // Position size should increase + assert!(position_after_second.size > position_after_first.size); + + // Proceeds should increase + assert!(position_after_second.tao_proceeds > position_after_first.tao_proceeds); + + // User balance should decrease by both opens + assert_eq!( + balance_after_second, + balance_before - first_tao - second_tao + ); + + // More alpha should have been minted/sold on second open + assert!(alpha_out_after_second > alpha_out_after_first); + + // Optional sanity check: position was updated, not reset + assert!(position_after_second.tao_collateral > position_after_first.tao_collateral); + assert!(position_after_second.size > position_after_first.size); + assert!(position_after_second.tao_proceeds > position_after_first.tao_proceeds); + + // Two Opened events should be emitted + let opened_events = System::events() + .iter() + .filter(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Opened { .. }) + ) + }) + .count(); + + assert_eq!(opened_events, 2); + }); +} + +#[test] +fn test_close_short_ok() { + new_test_ext().execute_with(|| { + // Setup network and balances (both ema price and price are 0.001) + let netuid = NetUid::from(1); + let balance_before = TaoCurrency::from(10_000_000_000_u64); + let position_tao = TaoCurrency::from(1_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, 1_000_000_000_u64.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000_u64.into()); + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let price_before = MockSwap::get_current_price(netuid).to_num::(); + + // Open short + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + position_tao + )); + + // Close the position + assert_ok!(Pallet::::close_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + )); + + // Coldkey balance is back to initial (because there was no price change) + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_before, balance_after); + + // All minted alpha got burned + let alpha_out_after = MockSwap::get_alpha_out(netuid); + assert_eq!(alpha_out_before, alpha_out_after); + + // Final price is back to where it was + let price_after = MockSwap::get_current_price(netuid).to_num::(); + assert_eq!(price_before, price_after); + + // Position is removed + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // Make sure close event gets emitted + assert!(System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Closed { .. }) + ) + })); + }); +} + +#[test] +fn test_close_short_profit() { + new_test_ext().execute_with(|| { + // Setup network and balances (both ema price and price are 0.001) + let netuid = NetUid::from(1); + let balance_before = TaoCurrency::from(10_000_000_000_u64); + let position_tao = TaoCurrency::from(1_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, 10_000_000_000_u64.into()); + AlphaReserve::set_mock_reserve(netuid, 10_000_000_000_000_u64.into()); + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + let alpha_out_before = MockSwap::get_alpha_out(netuid); + + // Open short + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + position_tao + )); + + // Mock-sell (move price down) + let _ = MockSwap::sell(netuid, AlphaCurrency::from(1_000_000_000_000)); + + // Close the position + assert_ok!(Pallet::::close_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + )); + + // Coldkey balance increased + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert!(balance_before < balance_after); + + // All minted alpha got burned + let alpha_out_after = MockSwap::get_alpha_out(netuid); + assert_eq!(alpha_out_before, alpha_out_after); + + // Position is removed + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // Make sure close event gets emitted + assert!(System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Closed { .. }) + ) + })); + }); +} + +#[test] +fn test_close_short_loss_alpha() { + new_test_ext().execute_with(|| { + // Setup network and balances (both ema price and price are 0.001) + let netuid = NetUid::from(1); + let balance_initial = TaoCurrency::from(10_000_000_000_u64); + let position_tao = TaoCurrency::from(1_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, 10_000_000_000_u64.into()); + AlphaReserve::set_mock_reserve(netuid, 10_000_000_000_000_u64.into()); + MockBalanceOps::increase_balance(&COLDKEY1, balance_initial); + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let alpha_in_before = AlphaReserve::reserve(netuid); + + // Open short + assert_ok!(Pallet::::open_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + position_tao + )); + + // Mock-buy (move price up so that position loses all tao and gets some alpha from pool to close) + let buy_swap_result = MockSwap::buy(netuid, TaoCurrency::from(100_000_000_000)).unwrap(); + + // Close the position + let balance_before = MockBalanceOps::tao_balance(&COLDKEY1); + assert_ok!(Pallet::::close_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + )); + + // Coldkey balance did not increase (total loss of collateral) + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_before, balance_after); + + // All minted alpha got burned + let alpha_out_after = MockSwap::get_alpha_out(netuid); + assert_eq!(alpha_out_before, alpha_out_after); + + // Alpha reserve is decreased by exactly buy_swap_result + // (no extra alpha remains in the pool and no missing alpha) + let alpha_in_after = AlphaReserve::reserve(netuid); + assert_eq!(alpha_in_after + buy_swap_result, alpha_in_before); + + // Position is removed + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // Close event gets emitted + assert!(System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Closed { .. }) + ) + })); + }); +} + +#[test] +fn test_close_short_fails_with_no_open_position() { + new_test_ext().execute_with(|| { + // Setup network and balances + let netuid = NetUid::from(1); + let balance_before = TaoCurrency::from(10_000_000_000_u64); + + TaoReserve::set_mock_reserve(netuid, 1_000_000_000_u64.into()); + AlphaReserve::set_mock_reserve(netuid, 1_000_000_000_000_u64.into()); + MockBalanceOps::increase_balance(&COLDKEY1, balance_before); + + let alpha_out_before = MockSwap::get_alpha_out(netuid); + let price_before = MockSwap::get_current_price(netuid).to_num::(); + + // No position was opened, so close should fail + assert_noop!( + Pallet::::close_short( + RuntimeOrigin::signed(COLDKEY1), + HOTKEY1, + netuid, + ), + Error::::NoOpenPosition + ); + + // Balance unchanged + let balance_after = MockBalanceOps::tao_balance(&COLDKEY1); + assert_eq!(balance_before, balance_after); + + // No alpha burned / no swap side effects + let alpha_out_after = MockSwap::get_alpha_out(netuid); + assert_eq!(alpha_out_before, alpha_out_after); + + // Price unchanged + let price_after = MockSwap::get_current_price(netuid).to_num::(); + assert_eq!(price_before, price_after); + + // Still no position + assert!(Positions::::get((COLDKEY1, netuid)).is_none()); + + // No Closed event emitted + assert!(!System::events().iter().any(|event_record| { + matches!( + &event_record.event, + RuntimeEvent::Derivatives(Event::::Closed { .. }) + ) + })); + }); +} diff --git a/pallets/derivatives/src/weights.rs b/pallets/derivatives/src/weights.rs new file mode 100644 index 0000000000..927e078d34 --- /dev/null +++ b/pallets/derivatives/src/weights.rs @@ -0,0 +1,316 @@ + +//! Autogenerated weights for `pallet_crowdloan` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 +//! DATE: 2025-05-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Ubuntu-2404-noble-amd64-base`, CPU: `AMD Ryzen 9 7950X3D 16-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("local")`, DB CACHE: `1024` + +// Executed Command: +// ./target/production/node-subtensor +// benchmark +// pallet +// --chain=local +// --wasm-execution=compiled +// --pallet=pallet-crowdloan +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=pallets/crowdloan/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs +// --allow-missing-host-functions + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_crowdloan`. +pub trait WeightInfo { + fn create() -> Weight; + fn contribute() -> Weight; + fn withdraw() -> Weight; + fn finalize() -> Weight; + fn refund(k: u32, ) -> Weight; + fn dissolve() -> Weight; + fn update_min_contribution() -> Weight; + fn update_end() -> Weight; + fn update_cap() -> Weight; +} + +/// Weights for `pallet_crowdloan` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::NextCrowdloanId` (r:1 w:1) + /// Proof: `Crowdloan::NextCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:0 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Crowdloans` (r:0 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `156` + // Estimated: `6148` + // Minimum execution time: 42_128_000 picoseconds. + Weight::from_parts(42_930_000, 6148) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn contribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `6148` + // Minimum execution time: 43_161_000 picoseconds. + Weight::from_parts(44_192_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn withdraw() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `6148` + // Minimum execution time: 40_235_000 picoseconds. + Weight::from_parts(40_907_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::CurrentCrowdloanId` (r:0 w:1) + /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `376` + // Estimated: `6148` + // Minimum execution time: 40_986_000 picoseconds. + Weight::from_parts(41_858_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:51 w:49) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:50 w:50) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// The range of component `k` is `[3, 50]`. + fn refund(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `372 + k * (49 ±0)` + // Estimated: `3743 + k * (2579 ±0)` + // Minimum execution time: 78_938_000 picoseconds. + Weight::from_parts(2_729_302, 3743) + // Standard Error: 351_422 + .saturating_add(Weight::from_parts(31_033_274, 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()))) + .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:0) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn dissolve() -> Weight { + // Proof Size summary in bytes: + // Measured: `450` + // Estimated: `6148` + // Minimum execution time: 43_341_000 picoseconds. + Weight::from_parts(44_402_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_min_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_876_000 picoseconds. + Weight::from_parts(9_137_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 9_117_000 picoseconds. + Weight::from_parts(9_438_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_cap() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_766_000 picoseconds. + Weight::from_parts(9_087_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::NextCrowdloanId` (r:1 w:1) + /// Proof: `Crowdloan::NextCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:0 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Crowdloans` (r:0 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `156` + // Estimated: `6148` + // Minimum execution time: 42_128_000 picoseconds. + Weight::from_parts(42_930_000, 6148) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn contribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `6148` + // Minimum execution time: 43_161_000 picoseconds. + Weight::from_parts(44_192_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn withdraw() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `6148` + // Minimum execution time: 40_235_000 picoseconds. + Weight::from_parts(40_907_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::CurrentCrowdloanId` (r:0 w:1) + /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `376` + // Estimated: `6148` + // Minimum execution time: 40_986_000 picoseconds. + Weight::from_parts(41_858_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:51 w:49) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:50 w:50) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// The range of component `k` is `[3, 50]`. + fn refund(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `372 + k * (49 ±0)` + // Estimated: `3743 + k * (2579 ±0)` + // Minimum execution time: 78_938_000 picoseconds. + Weight::from_parts(2_729_302, 3743) + // Standard Error: 351_422 + .saturating_add(Weight::from_parts(31_033_274, 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()))) + .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:0) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn dissolve() -> Weight { + // Proof Size summary in bytes: + // Measured: `450` + // Estimated: `6148` + // Minimum execution time: 43_341_000 picoseconds. + Weight::from_parts(44_402_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_min_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_876_000 picoseconds. + Weight::from_parts(9_137_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 9_117_000 picoseconds. + Weight::from_parts(9_438_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_cap() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_766_000 picoseconds. + Weight::from_parts(9_087_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 76e149b4b7..66e80038e3 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -51,6 +51,8 @@ pub trait SwapHandler { fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; fn toggle_user_liquidity(netuid: NetUid, enabled: bool); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoCurrency) -> AlphaCurrency; + fn get_tao_amount_for_alpha(netuid: NetUid, alpha_amount: AlphaCurrency) -> TaoCurrency; } pub trait DefaultPriceLimit diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 38830ec688..3548e79b86 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1165,4 +1165,49 @@ impl SwapHandler for Pallet { fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + + /// 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: TaoCurrency) -> AlphaCurrency { + match T::SubnetInfo::mechanism(netuid.into()) { + 1 => { + // For uniswap v3: Estimate the alpha amount using the v2 contant product + // because we don't use user liquidity and this is accurate. + let tao_reserve = u64::from(T::TaoReserve::reserve(netuid.into())) as u128; + let alpha_reserve = u64::from(T::AlphaReserve::reserve(netuid.into())) as u128; + let alpha_dt = alpha_reserve.saturating_mul(u64::from(tao_amount) as u128); + let new_tao_reserve = tao_reserve.saturating_add(u64::from(tao_amount) as u128); + let mut alpha_amount_u128 = alpha_dt.safe_div(new_tao_reserve); + if alpha_amount_u128 > u64::MAX as u128 { + alpha_amount_u128 = u64::MAX as u128; + } + AlphaCurrency::from(alpha_amount_u128 as u64) + } + + // Static subnet, alpha == tao + _ => u64::from(tao_amount).into(), + } + } + + /// Get the amount of Tao that needs to be sold to get a given amount of Alpha + fn get_tao_amount_for_alpha(netuid: NetUid, alpha_amount: AlphaCurrency) -> TaoCurrency { + match T::SubnetInfo::mechanism(netuid.into()) { + 1 => { + // For uniswap v3: Estimate the alpha amount using the v2 contant product + // because we don't use user liquidity and this is accurate. + let tao_reserve = u64::from(T::TaoReserve::reserve(netuid.into())) as u128; + let alpha_reserve = u64::from(T::AlphaReserve::reserve(netuid.into())) as u128; + let tao_da = tao_reserve.saturating_mul(u64::from(alpha_amount) as u128); + let new_alpha_reserve = + alpha_reserve.saturating_sub(u64::from(alpha_amount) as u128); + let mut tao_amount_u128 = tao_da.safe_div(new_alpha_reserve); + if tao_amount_u128 > u64::MAX as u128 { + tao_amount_u128 = u64::MAX as u128; + } + TaoCurrency::from(tao_amount_u128 as u64) + } + + // Static subnet, alpha == tao + _ => u64::from(alpha_amount).into(), + } + } }