diff --git a/Cargo.lock b/Cargo.lock index 980b8dede8..fc4b28688c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,17 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -497,7 +508,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-poly 0.5.0", "ark-serialize 0.5.0", @@ -732,7 +743,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", @@ -1669,6 +1680,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bounded-collections" version = "0.1.9" @@ -1944,6 +1978,28 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.24.0" @@ -5691,6 +5747,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -5698,7 +5757,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -5707,7 +5766,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", "serde", ] @@ -8364,6 +8423,7 @@ dependencies = [ "polkadot-runtime-common", "precompile-utils", "rand_chacha 0.3.1", + "safe-math", "scale-info", "serde_json", "sha2 0.10.9", @@ -8582,7 +8642,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn 2.0.106", @@ -10841,6 +10901,9 @@ dependencies = [ "log", "pallet-subtensor-swap-runtime-api", "parity-scale-codec", + "rand 0.8.5", + "rayon", + "safe-bigmath", "safe-math", "scale-info", "serde", @@ -10877,6 +10940,7 @@ dependencies = [ "frame-support", "parity-scale-codec", "scale-info", + "serde", "sp-api", "sp-std", "subtensor-macros", @@ -13476,6 +13540,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -13584,6 +13668,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d9da82a5dc3ff2fb2eee43d2b434fb197a9bf6a2a243850505b61584f888d2" +dependencies = [ + "quoth-macros", + "regex", + "rust_decimal", + "safe-string", +] + +[[package]] +name = "quoth-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58547202bec9896e773db7ef04b4d47c444f9c97bc4386f36e55718c347db440" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -13862,6 +13969,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.5" @@ -13916,6 +14032,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlp" version = "0.5.2" @@ -14151,6 +14296,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -14406,6 +14567,17 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe-bigmath" +version = "0.3.0" +source = "git+https://github.com/sam0x17/safe-bigmath#1da0a09c5bcf143fa7c464b431bfaeff5476b080" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "quoth", +] + [[package]] name = "safe-math" version = "0.1.0" @@ -14425,6 +14597,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "safe-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fc51f1e562058dee569383bfdb5a58752bfeb7fa7f0823f5c07c4c45381b5a" + [[package]] name = "safe_arch" version = "0.7.4" @@ -14846,7 +15024,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15149,7 +15327,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15862,7 +16040,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -15926,6 +16104,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -16357,6 +16541,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple-dns" version = "0.9.3" @@ -17437,7 +17627,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=df2f9b531e05ab2fa58a25113627c02d6fe96aaa#df2f9b531e05ab2fa58a25113627c02d6fe96aaa" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18085,7 +18275,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -19680,7 +19870,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c128c039340ffd50d4195c3f8ce31aac357f06804cfc494c8b9508d4b30dca4" dependencies = [ - "ahash", + "ahash 0.8.12", "hashbrown 0.14.5", "string-interner", ] diff --git a/Cargo.toml b/Cargo.toml index 475ef831e5..db48efdb9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } +safe-bigmath = { package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath" } share-pool = { path = "primitives/share-pool", default-features = false } subtensor-macros = { path = "support/macros", default-features = false } subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc" } diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index eaaee70d27..2e2eb807fa 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -18,7 +18,7 @@ use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_proxy::WeightInfo; use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; use sp_std::marker::PhantomData; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, NetUid, ProxyType, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -520,7 +520,7 @@ where netuid.into(), ); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: u64 = price.saturating_to_num(); let encoded_result = price.encode(); diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 27ac5bc06e..69fb9de089 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -417,7 +417,6 @@ impl pallet_subtensor::Config for Test { 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 = NonZeroU64::new(100).unwrap(); } @@ -429,7 +428,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoCurrencyReserve; type AlphaReserve = AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index bd6f46c8ab..f3abfa2c91 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -10,7 +10,7 @@ use pallet_subtensor::DefaultMinStake; use sp_core::Get; use sp_core::U256; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyTrait, NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -985,7 +985,7 @@ fn get_alpha_price_returns_encoded_price() { as SwapHandler>::current_alpha_price( netuid.into(), ); - let expected_price_scaled = expected_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let expected_price_scaled = expected_price.saturating_mul(U64F64::from_num(1_000_000_000)); let expected_price_u64: u64 = expected_price_scaled.saturating_to_num(); let mut env = MockEnv::new(FunctionId::GetAlphaPriceV1, caller, netuid.encode()); diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 7bca2bd40a..c28755802f 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -322,7 +322,6 @@ impl pallet_balances::Config for Test { 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 = NonZeroU64::new(1_000_000).unwrap(); } @@ -334,7 +333,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index b370b6dbe1..a43a468bef 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -16,7 +16,9 @@ use sp_runtime::{ }; use sp_std::collections::btree_set::BTreeSet; use sp_std::vec; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; #[benchmarks( where @@ -66,6 +68,8 @@ mod pallet_benchmarks { Subtensor::::set_max_registrations_per_block(netuid, 4096); Subtensor::::set_target_registrations_per_interval(netuid, 4096); Subtensor::::set_commit_reveal_weights_enabled(netuid, false); + SubnetTAO::::insert(netuid, TaoCurrency::from(1_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1_000_000_000_000_000_u64)); let mut seed: u32 = 1; let mut dests = Vec::new(); @@ -729,13 +733,12 @@ mod pallet_benchmarks { let coldkey: T::AccountId = account("Test", 0, seed); let hotkey: T::AccountId = account("Alice", 0, seed); - let amount = 900_000_000_000; - let limit = TaoCurrency::from(6_000_000_000); - let amount_to_be_staked = TaoCurrency::from(44_000_000_000); - Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amount); + let initial_balance = 900_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), initial_balance); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + // Price = 0.01 + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -745,14 +748,23 @@ mod pallet_benchmarks { hotkey.clone() )); + // Read current price and set limit price 0.1% higher, which is certainly getting hit + // by swapping 100 TAO + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(1_001_000_000)) + .saturating_to_num::(); + let amount_to_be_staked = TaoCurrency::from(100_000_000_000); + + // Allow partial (worst case) #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), hotkey, netuid, amount_to_be_staked, - limit, - false, + limit.into(), + true, ); } @@ -829,9 +841,9 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - let limit = TaoCurrency::from(1_000_000_000); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + // Price = 0.01 + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -854,7 +866,13 @@ mod pallet_benchmarks { u64_staked_amt.into() )); - let amount_unstaked = AlphaCurrency::from(30_000_000_000); + // Read current price and set limit price 0.01% lower, which is certainly getting hit + // by swapping 100 Alpha + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(999_900_000)) + .saturating_to_num::(); + let amount_unstaked = AlphaCurrency::from(100_000_000_000); // Remove stake limit for benchmark StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); @@ -865,8 +883,8 @@ mod pallet_benchmarks { hotkey.clone(), netuid, amount_unstaked, - limit, - false, + limit.into(), + true, ); } @@ -1320,8 +1338,9 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - SubnetTAO::::insert(netuid, TaoCurrency::from(150_000_000_000)); - SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(100_000_000_000)); + // Price = 0.01 + SubnetTAO::::insert(netuid, TaoCurrency::from(1_000_000_000_000)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(100_000_000_000_000)); Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), 1000000u32.into()); @@ -1368,9 +1387,8 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, 1.into()); - let limit = TaoCurrency::from(1_000_000_000); - let tao_reserve = TaoCurrency::from(150_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + let tao_reserve = TaoCurrency::from(1_000_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000_000); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -1383,7 +1401,13 @@ mod pallet_benchmarks { hotkey.clone() )); - let u64_staked_amt = 100_000_000_000; + // Read current price and set limit price 50% lower, which is not getting hit + // by swapping 1 TAO + let current_price = T::SwapInterface::current_alpha_price(netuid); + let limit = current_price + .saturating_mul(U64F64::saturating_from_num(500_000_000)) + .saturating_to_num::(); + let u64_staked_amt = 1_000_000_000; Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), u64_staked_amt); assert_ok!(Subtensor::::add_stake( @@ -1400,7 +1424,7 @@ mod pallet_benchmarks { RawOrigin::Signed(coldkey.clone()), hotkey.clone(), netuid, - Some(limit), + Some(limit.into()), ); } @@ -1730,7 +1754,7 @@ mod pallet_benchmarks { Subtensor::::init_new_network(netuid, tempo); SubtokenEnabled::::insert(netuid, true); - Subtensor::::set_burn(netuid, 1.into()); + Subtensor::::set_burn(netuid, 1000.into()); Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_max_allowed_uids(netuid, 4096); diff --git a/pallets/subtensor/src/coinbase/block_emission.rs b/pallets/subtensor/src/coinbase/block_emission.rs index 3aefef927f..c499fe3117 100644 --- a/pallets/subtensor/src/coinbase/block_emission.rs +++ b/pallets/subtensor/src/coinbase/block_emission.rs @@ -1,10 +1,90 @@ use super::*; use frame_support::traits::Get; use safe_math::*; -use substrate_fixed::{transcendental::log2, types::I96F32}; -use subtensor_runtime_common::TaoCurrency; +use substrate_fixed::{ + transcendental::log2, + types::{I96F32, U64F64}, +}; +use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; impl Pallet { + /// Calculates the dynamic TAO emission for a given subnet. + /// + /// This function determines the three terms tao_in, alpha_in, alpha_out + /// which are consecutively, 1) the amount of tao injected into the pool + /// 2) the amount of alpha injected into the pool, and 3) the amount of alpha + /// left to be distributed towards miners/validators/owners per block. + /// + /// # Arguments + /// * `netuid` - The unique identifier of the subnet. + /// * `tao_emission` - The amount of tao to distribute for this subnet. + /// * `alpha_block_emission` - The maximum alpha emission allowed for the block. + /// + /// # Returns + /// * `(u64, u64, u64)` - A tuple containing: + /// - `tao_in_emission`: The adjusted TAO emission always lower or equal to tao_emission + /// - `alpha_in_emission`: The adjusted alpha emission amount to be added into the pool. + /// - `alpha_out_emission`: The remaining alpha emission after adjustments to be distributed to miners/validators. + /// + /// The algorithm ensures that the pool injection of tao_in_emission, alpha_in_emission does not effect the pool price + /// It also ensures that the total amount of alpha_in_emission + alpha_out_emission sum to 2 * alpha_block_emission + /// It also ensures that 1 < alpha_out_emission < 2 * alpha_block_emission and 0 < alpha_in_emission < alpha_block_emission. + pub fn get_dynamic_tao_emission( + netuid: NetUid, + tao_emission: u64, + alpha_block_emission: u64, + ) -> (u64, u64, u64) { + // Init terms. + let mut tao_in_emission: U64F64 = U64F64::saturating_from_num(tao_emission); + let float_alpha_block_emission: U64F64 = U64F64::saturating_from_num(alpha_block_emission); + + // Get alpha price for subnet. + let alpha_price = T::SwapInterface::current_alpha_price(netuid.into()); + log::debug!("{netuid:?} - alpha_price: {alpha_price:?}"); + + // Get initial alpha_in + let mut alpha_in_emission: U64F64 = U64F64::saturating_from_num(tao_emission) + .checked_div(alpha_price) + .unwrap_or(float_alpha_block_emission); + + // Check if we are emitting too much alpha_in + if alpha_in_emission >= float_alpha_block_emission { + log::debug!( + "{netuid:?} - alpha_in_emission: {alpha_in_emission:?} > alpha_block_emission: {float_alpha_block_emission:?}" + ); + + // Scale down tao_in + // tao_in_emission = alpha_price.saturating_mul(float_alpha_block_emission); + + // Set to max alpha_block_emission + alpha_in_emission = float_alpha_block_emission; + } + + // Avoid rounding errors. + let zero = U64F64::saturating_from_num(0); + let one = U64F64::saturating_from_num(1); + if tao_in_emission < one || alpha_in_emission < one { + alpha_in_emission = zero; + tao_in_emission = zero; + } + + // Set Alpha in emission. + let alpha_out_emission = float_alpha_block_emission; + + // Log results. + log::debug!("{netuid:?} - tao_in_emission: {tao_in_emission:?}"); + log::debug!("{netuid:?} - alpha_in_emission: {alpha_in_emission:?}"); + log::debug!("{netuid:?} - alpha_out_emission: {alpha_out_emission:?}"); + + // Return result. + ( + tao_in_emission.saturating_to_num::(), + alpha_in_emission.saturating_to_num::(), + alpha_out_emission.saturating_to_num::(), + ) + } + /// Calculates the block emission based on the total issuance. /// /// This function computes the block emission by applying a logarithmic function diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 83567b6f57..ba7d54ed3c 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -213,7 +213,6 @@ impl Pallet { Self::finalize_all_subnet_root_dividends(netuid); // --- Perform the cleanup before removing the network. - T::SwapInterface::dissolve_all_liquidity_providers(netuid)?; Self::destroy_alpha_in_out_stakes(netuid)?; T::SwapInterface::clear_protocol_liquidity(netuid)?; T::CommitmentsInterface::purge_netuid(netuid); @@ -300,7 +299,6 @@ impl Pallet { SubnetMovingPrice::::remove(netuid); SubnetTaoFlow::::remove(netuid); SubnetEmaTaoFlow::::remove(netuid); - SubnetTaoProvided::::remove(netuid); // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..f40d4e704b 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -67,7 +67,8 @@ impl Pallet { let tao_to_swap_with: TaoCurrency = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); + let (actual_injected_tao, actual_injected_alpha) = + T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); if tao_to_swap_with > TaoCurrency::ZERO { let buy_swap_result = Self::swap_tao_for_alpha( @@ -87,7 +88,8 @@ impl Pallet { AlphaCurrency::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0)))); SubnetAlphaInEmission::::insert(*netuid_i, alpha_in_i); SubnetAlphaIn::::mutate(*netuid_i, |total| { - *total = total.saturating_add(alpha_in_i); + // Reserves also received fees in addition to alpha_in_i + *total = total.saturating_add(actual_injected_alpha); }); // Inject TAO in. @@ -95,7 +97,8 @@ impl Pallet { tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); SubnetTaoInEmission::::insert(*netuid_i, injected_tao); SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(injected_tao); + // Reserves also received fees in addition to injected_tao + *total = total.saturating_add(actual_injected_tao); }); TotalStake::::mutate(|total| { *total = total.saturating_add(injected_tao); @@ -140,7 +143,8 @@ impl Pallet { log::debug!("alpha_emission_i: {alpha_emission_i:?}"); // Get subnet price. - let price_i: U96F32 = T::SwapInterface::current_alpha_price(netuid_i.into()); + let price_i: U96F32 = + U96F32::saturating_from_num(T::SwapInterface::current_alpha_price(netuid_i.into())); log::debug!("price_i: {price_i:?}"); let mut tao_in_i: U96F32 = tao_emission_i; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 69645c0419..c137b92371 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1289,11 +1289,6 @@ pub mod pallet { pub type SubnetTAO = StorageMap<_, Identity, NetUid, TaoCurrency, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. - #[pallet::storage] - pub type SubnetTaoProvided = - StorageMap<_, Identity, NetUid, TaoCurrency, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> alpha_in_emission | Returns the amount of alph in emission into the pool per block. #[pallet::storage] pub type SubnetAlphaInEmission = @@ -1314,11 +1309,6 @@ pub mod pallet { pub type SubnetAlphaIn = StorageMap<_, Identity, NetUid, AlphaCurrency, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. - #[pallet::storage] - pub type SubnetAlphaInProvided = - StorageMap<_, Identity, NetUid, AlphaCurrency, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_in_subnet | Returns the amount of alpha in the subnet. #[pallet::storage] pub type SubnetAlphaOut = @@ -2562,7 +2552,7 @@ pub struct TaoCurrencyReserve(PhantomData); impl CurrencyReserve for TaoCurrencyReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> TaoCurrency { - SubnetTAO::::get(netuid).saturating_add(SubnetTaoProvided::::get(netuid)) + SubnetTAO::::get(netuid) } fn increase_provided(netuid: NetUid, tao: TaoCurrency) { @@ -2580,7 +2570,7 @@ pub struct AlphaCurrencyReserve(PhantomData); impl CurrencyReserve for AlphaCurrencyReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> AlphaCurrency { - SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaInProvided::::get(netuid)) + SubnetAlphaIn::::get(netuid) } fn increase_provided(netuid: NetUid, alpha: AlphaCurrency) { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 7cfb224722..60c37de2fe 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -710,9 +710,9 @@ mod dispatches { /// - Errors stemming from transaction pallet. /// #[pallet::call_index(2)] - #[pallet::weight((Weight::from_parts(340_800_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(523_200_000, 0) + .saturating_add(T::DbWeight::get().reads(19_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake( origin: OriginFor, hotkey: T::AccountId, @@ -1040,9 +1040,9 @@ mod dispatches { /// User register a new subnetwork via burning token #[pallet::call_index(7)] - #[pallet::weight((Weight::from_parts(354_200_000, 0) - .saturating_add(T::DbWeight::get().reads(47_u64)) - .saturating_add(T::DbWeight::get().writes(40_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(315_200_000, 0) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().writes(29_u64)), DispatchClass::Normal, Pays::Yes))] pub fn burned_register( origin: OriginFor, netuid: NetUid, @@ -1225,9 +1225,9 @@ mod dispatches { /// User register a new subnetwork #[pallet::call_index(59)] - #[pallet::weight((Weight::from_parts(235_400_000, 0) + #[pallet::weight((Weight::from_parts(238_500_000, 0) .saturating_add(T::DbWeight::get().reads(36_u64)) - .saturating_add(T::DbWeight::get().writes(52_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(50_u64)), DispatchClass::Normal, Pays::Yes))] pub fn register_network(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_register_network(origin, &hotkey, 1, None) } @@ -1434,9 +1434,9 @@ mod dispatches { /// User register a new subnetwork #[pallet::call_index(79)] - #[pallet::weight((Weight::from_parts(396_000_000, 0) + #[pallet::weight((Weight::from_parts(235_700_000, 0) .saturating_add(T::DbWeight::get().reads(35_u64)) - .saturating_add(T::DbWeight::get().writes(51_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(49_u64)), DispatchClass::Normal, Pays::Yes))] pub fn register_network_with_identity( origin: OriginFor, hotkey: T::AccountId, @@ -1504,9 +1504,9 @@ mod dispatches { /// * `TxRateLimitExceeded`: /// - Thrown if key has hit transaction rate limit #[pallet::call_index(84)] - #[pallet::weight((Weight::from_parts(358_500_000, 0) - .saturating_add(T::DbWeight::get().reads(41_u64)) - .saturating_add(T::DbWeight::get().writes(26_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(486_500_000, 0) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().writes(22_u64)), DispatchClass::Normal, Pays::Yes))] pub fn unstake_all_alpha(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all_alpha(origin, hotkey) } @@ -1533,8 +1533,8 @@ mod dispatches { /// - The alpha stake amount to move. /// #[pallet::call_index(85)] - #[pallet::weight((Weight::from_parts(164_300_000, 0) - .saturating_add(T::DbWeight::get().reads(15_u64)) + #[pallet::weight((Weight::from_parts(168_200_000, 0) + .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)), DispatchClass::Normal, Pays::Yes))] pub fn move_stake( origin: T::RuntimeOrigin, @@ -1576,8 +1576,8 @@ mod dispatches { /// # Events /// May emit a `StakeTransferred` event on success. #[pallet::call_index(86)] - #[pallet::weight((Weight::from_parts(160_300_000, 0) - .saturating_add(T::DbWeight::get().reads(13_u64)) + #[pallet::weight((Weight::from_parts(163_400_000, 0) + .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)), DispatchClass::Normal, Pays::Yes))] pub fn transfer_stake( origin: T::RuntimeOrigin, @@ -1618,9 +1618,9 @@ mod dispatches { /// May emit a `StakeSwapped` event on success. #[pallet::call_index(87)] #[pallet::weight(( - Weight::from_parts(351_300_000, 0) - .saturating_add(T::DbWeight::get().reads(37_u64)) - .saturating_add(T::DbWeight::get().writes(24_u64)), + Weight::from_parts(453_800_000, 0) + .saturating_add(T::DbWeight::get().reads(30_u64)) + .saturating_add(T::DbWeight::get().writes(20_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -1683,9 +1683,9 @@ mod dispatches { /// - Errors stemming from transaction pallet. /// #[pallet::call_index(88)] - #[pallet::weight((Weight::from_parts(402_900_000, 0) - .saturating_add(T::DbWeight::get().reads(25_u64)) - .saturating_add(T::DbWeight::get().writes(16_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(713_200_000, 0) + .saturating_add(T::DbWeight::get().reads(19_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake_limit( origin: OriginFor, hotkey: T::AccountId, @@ -1748,9 +1748,9 @@ mod dispatches { /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. /// #[pallet::call_index(89)] - #[pallet::weight((Weight::from_parts(377_400_000, 0) - .saturating_add(T::DbWeight::get().reads(29_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(611_100_000, 0) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_limit( origin: OriginFor, hotkey: T::AccountId, @@ -1792,9 +1792,9 @@ mod dispatches { /// May emit a `StakeSwapped` event on success. #[pallet::call_index(90)] #[pallet::weight(( - Weight::from_parts(411_500_000, 0) - .saturating_add(T::DbWeight::get().reads(37_u64)) - .saturating_add(T::DbWeight::get().writes(24_u64)), + Weight::from_parts(661_800_000, 0) + .saturating_add(T::DbWeight::get().reads(30_u64)) + .saturating_add(T::DbWeight::get().writes(20_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -1970,9 +1970,9 @@ mod dispatches { /// at which or better (higher) the staking should execute. /// Without limit_price it remove all the stake similar to `remove_stake` extrinsic #[pallet::call_index(103)] - #[pallet::weight((Weight::from_parts(395_300_000, 10142) - .saturating_add(T::DbWeight::get().reads(29_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes))] + #[pallet::weight((Weight::from_parts(615_000_000, 10142) + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::Yes))] pub fn remove_stake_full_limit( origin: T::RuntimeOrigin, hotkey: T::AccountId, @@ -2584,9 +2584,9 @@ mod dispatches { /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). #[pallet::call_index(132)] #[pallet::weight(( - Weight::from_parts(368_000_000, 8556) - .saturating_add(T::DbWeight::get().reads(28_u64)) - .saturating_add(T::DbWeight::get().writes(17_u64)), + Weight::from_parts(757_700_000, 8556) + .saturating_add(T::DbWeight::get().reads(22_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::Yes ))] diff --git a/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs new file mode 100644 index 0000000000..13edf21033 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs @@ -0,0 +1,70 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{storage_alias, traits::Get, weights::Weight}; +use scale_info::prelude::string::String; + +pub mod deprecated_swap_maps { + use super::*; + + /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. + #[storage_alias] + pub type SubnetTaoProvided = + StorageMap, Identity, NetUid, TaoCurrency, ValueQuery>; + + /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. + #[storage_alias] + pub type SubnetAlphaInProvided = + StorageMap, Identity, NetUid, AlphaCurrency, ValueQuery>; +} + +pub fn migrate_cleanup_swap_v3() -> Weight { + let migration_name = b"migrate_cleanup_swap_v3".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // ------------------------------ + // Step 1: Move provided to reserves + // ------------------------------ + for (netuid, tao_provided) in deprecated_swap_maps::SubnetTaoProvided::::iter() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao_provided); + }); + } + for (netuid, alpha_provided) in deprecated_swap_maps::SubnetAlphaInProvided::::iter() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha_provided); + }); + } + + // ------------------------------ + // Step 2: Remove Map entries + // ------------------------------ + remove_prefix::("SubtensorModule", "SubnetTaoProvided", &mut weight); + remove_prefix::("SubtensorModule", "SubnetAlphaInProvided", &mut weight); + + // ------------------------------ + // Step 3: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index 23a2899b94..087c787424 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -5,6 +5,7 @@ use sp_io::KillStorageResult; use sp_io::hashing::twox_128; use sp_io::storage::clear_prefix; pub mod migrate_auto_stake_destination; +pub mod migrate_cleanup_swap_v3; pub mod migrate_clear_rank_trust_pruning_maps; pub mod migrate_coldkey_swap_scheduled; pub mod migrate_coldkey_swap_scheduled_to_announcements; diff --git a/pallets/subtensor/src/rpc_info/subnet_info.rs b/pallets/subtensor/src/rpc_info/subnet_info.rs index 6a7966b4fb..7169561d30 100644 --- a/pallets/subtensor/src/rpc_info/subnet_info.rs +++ b/pallets/subtensor/src/rpc_info/subnet_info.rs @@ -365,7 +365,6 @@ impl Pallet { let subnet_token_enabled = Self::get_subtoken_enabled(netuid); let transfers_enabled = Self::get_transfer_toggle(netuid); let bonds_reset = Self::get_bonds_reset(netuid); - let user_liquidity_enabled: bool = Self::is_user_liquidity_enabled(netuid); Some(SubnetHyperparamsV2 { rho: rho.into(), @@ -400,7 +399,7 @@ impl Pallet { subnet_is_active: subnet_token_enabled, transfers_enabled, bonds_reset_enabled: bonds_reset, - user_liquidity_enabled, + user_liquidity_enabled: false, }) } diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 099f8e26b6..998d4f0086 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -6,7 +6,7 @@ use frame_support::traits::{ }, }; use safe_math::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -48,15 +48,13 @@ impl Pallet { Self::get_all_subnet_netuids() .into_iter() .map(|netuid| { - let alpha = U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( + let alpha = U64F64::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( hotkey, netuid, )); - let alpha_price = U96F32::saturating_from_num( - T::SwapInterface::current_alpha_price(netuid.into()), - ); + let alpha_price = T::SwapInterface::current_alpha_price(netuid.into()); alpha.saturating_mul(alpha_price) }) - .sum::() + .sum::() .saturating_to_num::() .into() } @@ -76,7 +74,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -110,7 +108,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -223,7 +221,7 @@ impl Pallet { let alpha_stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid); let min_alpha_stake = - U96F32::saturating_from_num(Self::get_nominator_min_required_stake()) + U64F64::saturating_from_num(Self::get_nominator_min_required_stake()) .safe_div(T::SwapInterface::current_alpha_price(netuid)) .saturating_to_num::(); if alpha_stake > 0.into() && alpha_stake < min_alpha_stake.into() { @@ -352,10 +350,6 @@ impl Pallet { Ok(credit) } - pub fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - T::SwapInterface::is_user_liquidity_enabled(netuid) - } - pub fn recycle_subnet_alpha(netuid: NetUid, amount: AlphaCurrency) { // TODO: record recycled alpha in a tracker SubnetAlphaOut::::mutate(netuid, |total| { diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index a1d9b46d5b..76d9c95d3a 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -418,7 +418,8 @@ impl Pallet { /// /// In the corner case when SubnetTAO(2) == SubnetTAO(1), no slippage is going to occur. /// - /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3. We need an updated one. + /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3 or balancers. + /// We need an updated one. /// pub fn get_max_amount_move( origin_netuid: NetUid, @@ -471,10 +472,8 @@ impl Pallet { } // Corner case: SubnetTAO for any of two subnets is zero - let subnet_tao_1 = SubnetTAO::::get(origin_netuid) - .saturating_add(SubnetTaoProvided::::get(origin_netuid)); - let subnet_tao_2 = SubnetTAO::::get(destination_netuid) - .saturating_add(SubnetTaoProvided::::get(destination_netuid)); + let subnet_tao_1 = SubnetTAO::::get(origin_netuid); + let subnet_tao_2 = SubnetTAO::::get(destination_netuid); if subnet_tao_1.is_zero() || subnet_tao_2.is_zero() { return Err(Error::::ZeroMaxStakeAmount.into()); } @@ -482,10 +481,8 @@ impl Pallet { let subnet_tao_2_float: U64F64 = U64F64::saturating_from_num(subnet_tao_2); // Corner case: SubnetAlphaIn for any of two subnets is zero - let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid) - .saturating_add(SubnetAlphaInProvided::::get(origin_netuid)); - let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid) - .saturating_add(SubnetAlphaInProvided::::get(destination_netuid)); + let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid); + let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid); if alpha_in_1.is_zero() || alpha_in_2.is_zero() { return Err(Error::::ZeroMaxStakeAmount.into()); } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 74a6bf34a6..735ec804df 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -463,7 +463,9 @@ impl Pallet { .saturating_to_num::(); owner_emission_tao = if owner_alpha_u64 > 0 { - let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); + let cur_price: U96F32 = U96F32::saturating_from_num( + T::SwapInterface::current_alpha_price(netuid.into()), + ); let val_u64 = U96F32::from_num(owner_alpha_u64) .saturating_mul(cur_price) .floor() @@ -581,7 +583,6 @@ impl Pallet { } // 7.c) Remove α‑in/α‑out counters (fully destroyed). SubnetAlphaIn::::remove(netuid); - SubnetAlphaInProvided::::remove(netuid); SubnetAlphaOut::::remove(netuid); // Clear the locked balance on the subnet. diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 1aeeacc33c..f7528935b4 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -18,13 +18,7 @@ impl Pallet { /// # Returns /// * `u64` - The total alpha issuance for the specified subnet. pub fn get_alpha_issuance(netuid: NetUid) -> AlphaCurrency { - SubnetAlphaIn::::get(netuid) - .saturating_add(SubnetAlphaInProvided::::get(netuid)) - .saturating_add(SubnetAlphaOut::::get(netuid)) - } - - pub fn get_protocol_tao(netuid: NetUid) -> TaoCurrency { - T::SwapInterface::get_protocol_tao(netuid) + SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaOut::::get(netuid)) } pub fn get_moving_alpha_price(netuid: NetUid) -> U96F32 { @@ -63,10 +57,10 @@ impl Pallet { // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. // We can use unsigned type here: U96F32 let one_minus_alpha: U96F32 = U96F32::saturating_from_num(1.0).saturating_sub(alpha); - let current_price: U96F32 = alpha.saturating_mul( + let current_price: U96F32 = alpha.saturating_mul(U96F32::saturating_from_num( T::SwapInterface::current_alpha_price(netuid.into()) - .min(U96F32::saturating_from_num(1.0)), - ); + .min(U64F64::saturating_from_num(1.0)), + )); let current_moving: U96F32 = one_minus_alpha.saturating_mul(Self::get_moving_alpha_price(netuid)); // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` @@ -890,7 +884,7 @@ impl Pallet { let current_price = ::SwapInterface::current_alpha_price(netuid.into()); let tao_equivalent: TaoCurrency = current_price - .saturating_mul(U96F32::saturating_from_num(actual_alpha_moved)) + .saturating_mul(U64F64::saturating_from_num(actual_alpha_moved)) .saturating_to_num::() .into(); @@ -1219,42 +1213,34 @@ impl Pallet { } pub fn increase_provided_tao_reserve(netuid: NetUid, tao: TaoCurrency) { - SubnetTaoProvided::::mutate(netuid, |total| { - *total = total.saturating_add(tao); - }); + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao); + }); + } } pub fn decrease_provided_tao_reserve(netuid: NetUid, tao: TaoCurrency) { - // First, decrease SubnetTaoProvided, then deduct the rest from SubnetTAO - let subnet_tao = SubnetTAO::::get(netuid); - let subnet_tao_provided = SubnetTaoProvided::::get(netuid); - let remainder = subnet_tao_provided.saturating_sub(tao); - let carry_over = tao.saturating_sub(subnet_tao_provided); - if carry_over.is_zero() { - SubnetTaoProvided::::set(netuid, remainder); - } else { - SubnetTaoProvided::::set(netuid, TaoCurrency::ZERO); - SubnetTAO::::set(netuid, subnet_tao.saturating_sub(carry_over)); + if !tao.is_zero() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_sub(tao); + }); } } pub fn increase_provided_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) { - SubnetAlphaInProvided::::mutate(netuid, |total| { - *total = total.saturating_add(alpha); - }); + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha); + }); + } } pub fn decrease_provided_alpha_reserve(netuid: NetUid, alpha: AlphaCurrency) { - // First, decrease SubnetAlphaInProvided, then deduct the rest from SubnetAlphaIn - let subnet_alpha = SubnetAlphaIn::::get(netuid); - let subnet_alpha_provided = SubnetAlphaInProvided::::get(netuid); - let remainder = subnet_alpha_provided.saturating_sub(alpha); - let carry_over = alpha.saturating_sub(subnet_alpha_provided); - if carry_over.is_zero() { - SubnetAlphaInProvided::::set(netuid, remainder); - } else { - SubnetAlphaInProvided::::set(netuid, AlphaCurrency::ZERO); - SubnetAlphaIn::::set(netuid, subnet_alpha.saturating_sub(carry_over)); + if !alpha.is_zero() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_sub(alpha); + }); } } diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index ecb5ce0452..86462dd06d 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -217,8 +217,6 @@ impl Pallet { SubnetOwner::::insert(netuid_to_register, coldkey.clone()); SubnetOwnerHotkey::::insert(netuid_to_register, hotkey.clone()); SubnetLocked::::insert(netuid_to_register, actual_tao_lock_amount); - SubnetTaoProvided::::insert(netuid_to_register, TaoCurrency::ZERO); - SubnetAlphaInProvided::::insert(netuid_to_register, AlphaCurrency::ZERO); SubnetAlphaOut::::insert(netuid_to_register, AlphaCurrency::ZERO); SubnetVolume::::insert(netuid_to_register, 0u128); RAORecycledForRegistration::::insert( diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 6bcaee2fca..9d08c65e3c 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -19,7 +19,7 @@ use frame_support::{assert_err, assert_noop, assert_ok}; use sp_core::{H256, U256}; use sp_runtime::DispatchError; use std::collections::BTreeSet; -use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use substrate_fixed::types::{I96F32, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -758,6 +758,7 @@ fn test_claim_root_with_drain_emissions_and_swap_claim_type() { }); } +/// cargo test --package pallet-subtensor --lib -- tests::claim_root::test_claim_root_with_run_coinbase --exact --nocapture #[test] fn test_claim_root_with_run_coinbase() { new_test_ext(1).execute_with(|| { @@ -790,10 +791,15 @@ fn test_claim_root_with_run_coinbase() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -901,10 +907,15 @@ fn test_claim_root_with_block_emissions() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -1020,16 +1031,21 @@ fn test_claim_root_coinbase_distribution() { initial_total_hotkey_alpha.into(), ); - let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); - let alpha_emissions: AlphaCurrency = 1_000_000_000u64.into(); - // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(100_000_000_000_u64); + let alpha = AlphaCurrency::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + // let current_price = + // ::SwapInterface::current_alpha_price(netuid.into()) + // .saturating_to_num::(); + // assert_eq!(current_price, 2.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); + + let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); + let alpha_emissions: AlphaCurrency = 1_000_000_000u64.into(); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 093444e955..4e4caf2e9c 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -12,7 +12,6 @@ use crate::*; use alloc::collections::BTreeMap; use approx::assert_abs_diff_eq; use frame_support::assert_ok; -use pallet_subtensor_swap::position::PositionId; use sp_core::U256; use substrate_fixed::{ transcendental::sqrt, @@ -192,20 +191,8 @@ fn test_coinbase_tao_issuance_different_prices() { mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Make subnets dynamic. SubnetMechanism::::insert(netuid1, 1); @@ -268,20 +255,8 @@ fn test_coinbase_tao_issuance_different_prices() { // mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // // Force the swap to initialize -// SubtensorModule::swap_tao_for_alpha( -// netuid1, -// TaoCurrency::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); -// SubtensorModule::swap_tao_for_alpha( -// netuid2, -// TaoCurrency::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); +// ::SwapInterface::init_swap(netuid1); +// ::SwapInterface::init_swap(netuid2); // // Set subnet prices to reversed proportion to ensure they don't affect emissions. // SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); @@ -586,20 +561,8 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { SubnetTaoFlow::::insert(netuid2, 200_000_000_i64); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Get the prices before the run_coinbase let price_1_before = ::SwapInterface::current_alpha_price(netuid1); @@ -2699,54 +2662,6 @@ fn test_run_coinbase_not_started_start_after() { }); } -/// Test that coinbase updates protocol position liquidity -/// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_v3_liquidity_update --exact --show-output -#[test] -fn test_coinbase_v3_liquidity_update() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - - let protocol_account_id = pallet_subtensor_swap::Pallet::::protocol_account_id(); - let position = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_before = position.liquidity; - - // Enable emissions and run coinbase (which will increase position liquidity) - let emission: u64 = 1_234_567; - // Set the TAO flow to non-zero - SubnetTaoFlow::::insert(netuid, 8348383_i64); - FirstEmissionBlockNumber::::insert(netuid, 0); - SubtensorModule::run_coinbase(U96F32::from_num(emission)); - - let position_after = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_after = position_after.liquidity; - - assert!(liquidity_before < liquidity_after); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture #[test] fn test_drain_alpha_childkey_parentkey_with_burn() { @@ -3040,10 +2955,8 @@ fn test_mining_emission_distribution_with_no_root_sell() { // Make root sell NOT happen // set price very low, e.g. a lot of alpha in //SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1_000_000_000_000_000)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(0.01), - ); + let alpha = AlphaCurrency::from(1_000_000_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); // Make sure we ARE NOT root selling, so we do not have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -3235,10 +3148,8 @@ fn test_mining_emission_distribution_with_root_sell() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let alpha = AlphaCurrency::from(100_000_000_000_000); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); @@ -3364,7 +3275,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); // Set netuid0 to have price tao_emission / price > alpha_emission let alpha_emission = U96F32::saturating_from_num( @@ -3375,14 +3286,19 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { ); let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); - let sqrt_price_to_set: U64F64 = sqrt(price_to_set).unwrap(); let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) .saturating_mul(price_to_set_fixed) .saturating_add(U96F32::saturating_from_num(0.01)); // Set the price - pallet_subtensor_swap::AlphaSqrtPrice::::insert(netuid0, sqrt_price_to_set); + let tao = TaoCurrency::from(1_000_000_000_u64); + let alpha = AlphaCurrency::from( + (U64F64::saturating_from_num(u64::from(tao)) / price_to_set).to_num::(), + ); + SubnetTAO::::insert(netuid0, tao); + SubnetAlphaIn::::insert(netuid0, alpha); + // Check the price is set assert_abs_diff_eq!( pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), @@ -3440,7 +3356,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let alpha_emission = U96F32::saturating_from_num( SubtensorModule::get_block_emission_for_issuance( @@ -3450,7 +3366,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { ); let tao_emission = U96F32::saturating_from_num(34566756_u64); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3503,7 +3419,7 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); @@ -3637,7 +3553,7 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3651,7 +3567,7 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -3728,7 +3644,7 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { AlphaCurrency::from(1_000_000_000_000_000), ); // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3742,7 +3658,7 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -3857,10 +3773,10 @@ fn test_pending_emission_start_call_not_done() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoCurrency::from(10_000_000_000_u64); + let alpha = AlphaCurrency::from(1_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index beec7a3cba..04c9ffbaf6 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -2727,9 +2727,6 @@ fn test_migrate_reset_unactive_sn() { RAORecycledForRegistration::::get(netuid), actual_tao_lock_amount_less_pool_tao ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - *netuid - )); assert_eq!(PendingOwnerCut::::get(netuid), AlphaCurrency::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -2800,9 +2797,6 @@ fn test_migrate_reset_unactive_sn() { SubnetAlphaOutEmission::::get(netuid), AlphaCurrency::ZERO ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - *netuid - )); assert_ne!(PendingOwnerCut::::get(netuid), AlphaCurrency::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -2968,6 +2962,54 @@ fn test_migrate_remove_unknown_neuron_axon_cert_prom() { } } +// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_cleanup_swap_v3 --exact --nocapture +#[test] +fn test_migrate_cleanup_swap_v3() { + use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext(1).execute_with(|| { + let migration = crate::migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::; + + const MIGRATION_NAME: &str = "migrate_cleanup_swap_v3"; + + let provided: u64 = 9876; + let reserves: u64 = 1_000_000; + + SubnetTAO::::insert(NetUid::from(1), TaoCurrency::from(reserves)); + SubnetAlphaIn::::insert(NetUid::from(1), AlphaCurrency::from(reserves)); + + // Insert deprecated maps values + deprecated_swap_maps::SubnetTaoProvided::::insert( + NetUid::from(1), + TaoCurrency::from(provided), + ); + deprecated_swap_maps::SubnetAlphaInProvided::::insert( + NetUid::from(1), + AlphaCurrency::from(provided), + ); + + // Run migration + let weight = migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(NetUid::from(1)),); + assert!( + !deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(NetUid::from(1)), + ); + + // Provided got added to reserves + assert_eq!( + u64::from(SubnetTAO::::get(NetUid::from(1))), + reserves + provided + ); + assert_eq!( + u64::from(SubnetAlphaIn::::get(NetUid::from(1))), + reserves + provided + ); + }); +} + #[test] fn test_migrate_coldkey_swap_scheduled_to_announcements() { new_test_ext(1000).execute_with(|| { diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index fca61172dc..62f11ad4d0 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -308,7 +308,6 @@ impl crate::Config for Test { 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 = NonZeroU64::new(100).unwrap(); } @@ -320,7 +319,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoCurrencyReserve; type AlphaReserve = AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index dfd9927da4..77012e6817 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -619,8 +619,9 @@ fn test_do_move_event_emission() { // Move stake and capture events System::reset_events(); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); let tao_equivalent = (current_price * U96F32::from_num(alpha)).to_num::(); // no fee conversion assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4605ac8bef..5176ae05dc 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -6,10 +6,16 @@ use crate::*; use frame_support::{assert_err, assert_ok}; use frame_system::Config; use sp_core::U256; -use sp_std::collections::{btree_map::BTreeMap, vec_deque::VecDeque}; +use sp_std::collections::{ + //btree_map::BTreeMap, + vec_deque::VecDeque, +}; use substrate_fixed::types::{I96F32, U64F64, U96F32}; use subtensor_runtime_common::{MechId, NetUidStorageIndex, TaoCurrency}; -use subtensor_swap_interface::{Order, SwapHandler}; +use subtensor_swap_interface::{ + //Order, + SwapHandler, +}; #[test] fn test_registration_ok() { @@ -247,8 +253,9 @@ fn dissolve_owner_cut_refund_logic() { // Use the current alpha price to estimate the TAO equivalent. let owner_emission_tao = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(net.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(net.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -365,8 +372,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves TokenSymbol::::insert(net, b"XX".to_vec()); SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); - SubnetTaoProvided::::insert(net, TaoCurrency::from(1)); - SubnetAlphaInProvided::::insert(net, AlphaCurrency::from(1)); // TAO Flow SubnetTaoFlow::::insert(net, 0i64); @@ -529,8 +534,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves assert!(!TokenSymbol::::contains_key(net)); assert!(!SubnetMovingPrice::::contains_key(net)); - assert!(!SubnetTaoProvided::::contains_key(net)); - assert!(!SubnetAlphaInProvided::::contains_key(net)); // Subnet locks assert!(!TransferToggle::::contains_key(net)); @@ -906,8 +909,9 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let owner_emission_tao: u64 = { // Fallback matches the pallet's fallback - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -986,8 +990,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { .saturating_to_num::(); let owner_emission_tao_u64 = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -1774,458 +1779,6 @@ fn test_tempo_greater_than_weight_set_rate_limit() { }) } -#[allow(clippy::indexing_slicing)] -#[test] -fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { - new_test_ext(0).execute_with(|| { - // ──────────────────────────────────────────────────────────────────── - // 0) Constants and helpers (distinct hotkeys & coldkeys) - // ──────────────────────────────────────────────────────────────────── - const NUM_NETS: usize = 4; - - // Six LP coldkeys - let cold_lps: [U256; 6] = [ - U256::from(3001), - U256::from(3002), - U256::from(3003), - U256::from(3004), - U256::from(3005), - U256::from(3006), - ]; - - // For each coldkey, define two DISTINCT hotkeys it owns. - let mut cold_to_hots: BTreeMap = BTreeMap::new(); - for &c in cold_lps.iter() { - let h1 = U256::from(c.low_u64().saturating_add(100_000)); - let h2 = U256::from(c.low_u64().saturating_add(200_000)); - cold_to_hots.insert(c, [h1, h2]); - } - - // Distinct τ pot sizes per net. - let pots: [u64; NUM_NETS] = [12_345, 23_456, 34_567, 45_678]; - - let lp_sets_per_net: [&[U256]; NUM_NETS] = [ - &cold_lps[0..4], // net0: A,B,C,D - &cold_lps[2..6], // net1: C,D,E,F - &cold_lps[0..6], // net2: A..F - &cold_lps[1..5], // net3: B,C,D,E - ]; - - // Multiple bands/sizes → many positions per cold across nets, using mixed hotkeys. - // let bands: [i32; 3] = [5, 13, 30]; - // let liqs: [u64; 3] = [400_000, 700_000, 1_100_000]; - - // TODO: Revise when user liquidity is available - // Helper: add a V3 position via a (hot, cold) pair. - // let add_pos = |net: NetUid, hot: U256, cold: U256, band: i32, liq: u64| { - // let ct = pallet_subtensor_swap::CurrentTick::::get(net); - // let lo = ct.saturating_sub(band); - // let hi = ct.saturating_add(band); - // pallet_subtensor_swap::EnabledUserLiquidity::::insert(net, true); - // assert_ok!(pallet_subtensor_swap::Pallet::::add_liquidity( - // RuntimeOrigin::signed(cold), - // hot, - // net, - // lo, - // hi, - // liq - // )); - // }; - - // ──────────────────────────────────────────────────────────────────── - // 1) Create many subnets, enable V3, fix price at tick=0 (sqrt≈1) - // ──────────────────────────────────────────────────────────────────── - let mut nets: Vec = Vec::new(); - for i in 0..NUM_NETS { - let owner_hot = U256::from(10_000 + (i as u64)); - let owner_cold = U256::from(20_000 + (i as u64)); - let net = add_dynamic_network(&owner_hot, &owner_cold); - SubtensorModule::set_max_registrations_per_block(net, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net, 1_000u16); - Emission::::insert(net, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); - - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net, - true - ) - ); - - // Price/tick pinned so LP math stays stable (sqrt(1)). - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1) price"); - pallet_subtensor_swap::CurrentTick::::set(net, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net, sqrt1); - - nets.push(net); - } - - // Map net → index for quick lookups. - let mut net_index: BTreeMap = BTreeMap::new(); - for (i, &n) in nets.iter().enumerate() { - net_index.insert(n, i); - } - - // ──────────────────────────────────────────────────────────────────── - // 2) Pre-create a handful of small (hot, cold) pairs so accounts exist - // ──────────────────────────────────────────────────────────────────── - for id in 0u64..10 { - let cold_acc = U256::from(1_000_000 + id); - let hot_acc = U256::from(2_000_000 + id); - for &net in nets.iter() { - register_ok_neuron(net, hot_acc, cold_acc, 100_000 + id); - } - } - - // ──────────────────────────────────────────────────────────────────── - // 3) LPs per net: register each (hot, cold), massive τ prefund, and stake - // ──────────────────────────────────────────────────────────────────── - for &cold in cold_lps.iter() { - SubtensorModule::add_balance_to_coldkey_account(&cold, u64::MAX); - } - - // τ balances before LP adds (after staking): - let mut tao_before: BTreeMap = BTreeMap::new(); - - // Ordered α snapshot per net at **pair granularity** (pre‑LP): - let mut alpha_pairs_per_net: BTreeMap> = BTreeMap::new(); - - // Register both hotkeys for each participating cold on each net and stake τ→α. - for (ni, &net) in nets.iter().enumerate() { - let participants = lp_sets_per_net[ni]; - for &cold in participants.iter() { - let [hot1, hot2] = cold_to_hots[&cold]; - - // Ensure (hot, cold) neurons exist on this net. - register_ok_neuron( - net, - hot1, - cold, - (ni as u64) * 10_000 + (hot1.low_u64() % 10_000), - ); - register_ok_neuron( - net, - hot2, - cold, - (ni as u64) * 10_000 + (hot2.low_u64() % 10_000) + 1, - ); - - // Stake τ (split across the two hotkeys). - let base: u64 = - 5_000_000 + ((ni as u64) * 1_000_000) + ((cold.low_u64() % 10) * 250_000); - let stake1: u64 = base.saturating_mul(3) / 5; // 60% - let stake2: u64 = base.saturating_sub(stake1); // 40% - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net, - stake1.into() - )); - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot2, - net, - stake2.into() - )); - } - } - - // Record τ balances now (post‑stake, pre‑LP). - for &cold in cold_lps.iter() { - tao_before.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); - } - - // Capture **pair‑level** α snapshot per net (pre‑LP). - for ((hot, cold, net), amt) in Alpha::::iter() { - if let Some(&ni) = net_index.get(&net) - && lp_sets_per_net[ni].contains(&cold) { - let a: u128 = amt.saturating_to_num(); - if a > 0 { - alpha_pairs_per_net - .entry(net) - .or_default() - .push(((hot, cold), a)); - } - } - } - - // ──────────────────────────────────────────────────────────────────── - // 4) Add many V3 positions per cold across nets, alternating hotkeys - // ──────────────────────────────────────────────────────────────────── - // TODO: Revise when user liquidity is available - // for (ni, &net) in nets.iter().enumerate() { - // let participants = lp_sets_per_net[ni]; - // for (pi, &cold) in participants.iter().enumerate() { - // let [hot1, hot2] = cold_to_hots[&cold]; - // let hots = [hot1, hot2]; - // for k in 0..3 { - // let band = bands[(pi + k) % bands.len()]; - // let liq = liqs[(ni + k) % liqs.len()]; - // let hot = hots[k % hots.len()]; - // add_pos(net, hot, cold, band, liq); - // } - // } - // } - - // Snapshot τ balances AFTER LP adds (to measure actual principal debit). - let mut tao_after_adds: BTreeMap = BTreeMap::new(); - for &cold in cold_lps.iter() { - tao_after_adds.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); - } - - // ──────────────────────────────────────────────────────────────────── - // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover - // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. - // ──────────────────────────────────────────────────────────────────── - let mut base_share_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - let mut pair_count_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u32)).collect(); - - let mut leftover_total: u64 = 0; - - for (ni, &net) in nets.iter().enumerate() { - let pot = pots[ni]; - let pairs = alpha_pairs_per_net.get(&net).cloned().unwrap_or_default(); - if pot == 0 || pairs.is_empty() { - continue; - } - let total_alpha: u128 = pairs.iter().map(|(_, a)| *a).sum(); - if total_alpha == 0 { - continue; - } - - let mut base_sum_net: u64 = 0; - for ((_, cold), a) in pairs.iter().copied() { - // quota = a * pot / total_alpha - let prod: u128 = a.saturating_mul(pot as u128); - let base: u64 = (prod / total_alpha) as u64; - base_sum_net = base_sum_net.saturating_add(base); - *base_share_cold.entry(cold).or_default() = - base_share_cold[&cold].saturating_add(base); - *pair_count_cold.entry(cold).or_default() += 1; - } - let leftover_net = pot.saturating_sub(base_sum_net); - leftover_total = leftover_total.saturating_add(leftover_net); - } - - // ──────────────────────────────────────────────────────────────────── - // 6) Seed τ pots and dissolve *all* networks (liquidates LPs + refunds) - // ──────────────────────────────────────────────────────────────────── - for (ni, &net) in nets.iter().enumerate() { - SubnetTAO::::insert(net, TaoCurrency::from(pots[ni])); - } - for &net in nets.iter() { - assert_ok!(SubtensorModule::do_dissolve_network(net)); - } - - // ──────────────────────────────────────────────────────────────────── - // 7) Assertions: τ balances, α gone, nets removed, swap state clean - // (Hamilton invariants enforced at cold-level without relying on tie-break) - // ──────────────────────────────────────────────────────────────────── - // Collect actual pot credits per cold (principal cancels out against adds when comparing before→after). - let mut actual_pot_cold: BTreeMap = - cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - actual_pot_cold.insert(cold, after.saturating_sub(before)); - } - - // (a) Sum of actual pot credits equals total pots. - let total_actual: u64 = actual_pot_cold.values().copied().sum(); - let total_pots: u64 = pots.iter().copied().sum(); - assert_eq!( - total_actual, total_pots, - "total τ pot credited across colds must equal sum of pots" - ); - - // (b) Each cold’s pot is within Hamilton bounds: base ≤ actual ≤ base + #pairs. - let mut extra_accum: u64 = 0; - for &cold in cold_lps.iter() { - let base = *base_share_cold.get(&cold).unwrap_or(&0); - let pairs = *pair_count_cold.get(&cold).unwrap_or(&0) as u64; - let actual = *actual_pot_cold.get(&cold).unwrap_or(&0); - - assert!( - actual >= base, - "cold {cold:?} actual pot {actual} is below base {base}" - ); - assert!( - actual <= base.saturating_add(pairs), - "cold {cold:?} actual pot {actual} exceeds base + pairs ({base} + {pairs})" - ); - - extra_accum = extra_accum.saturating_add(actual.saturating_sub(base)); - } - - // (c) The total “extra beyond base” equals the computed leftover_total across nets. - assert_eq!( - extra_accum, leftover_total, - "sum of extras beyond base must equal total leftover" - ); - - // (d) τ principal was fully refunded (compare after_adds → after). - for &cold in cold_lps.iter() { - let before = tao_before[&cold]; - let mid = tao_after_adds[&cold]; - let after = SubtensorModule::get_coldkey_balance(&cold); - let principal_actual = before.saturating_sub(mid); - let actual_pot = after.saturating_sub(before); - assert_eq!( - after.saturating_sub(mid), - principal_actual.saturating_add(actual_pot), - "cold {cold:?} τ balance incorrect vs 'after_adds'" - ); - } - - // For each dissolved net, check α ledgers gone, network removed, and swap state clean. - for &net in nets.iter() { - assert!( - Alpha::::iter().all(|((_h, _c, n), _)| n != net), - "alpha ledger not fully cleared for net {net:?}" - ); - assert!( - !SubtensorModule::if_subnet_exist(net), - "subnet {net:?} still exists" - ); - assert!( - pallet_subtensor_swap::Ticks::::iter_prefix(net) - .next() - .is_none(), - "ticks not cleared for net {net:?}" - ); - assert!( - !pallet_subtensor_swap::Positions::::iter() - .any(|((n, _owner, _pid), _)| n == net), - "swap positions not fully cleared for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalTao::::get(net).saturating_to_num::(), - 0, - "FeeGlobalTao nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::FeeGlobalAlpha::::get(net).saturating_to_num::(), - 0, - "FeeGlobalAlpha nonzero for net {net:?}" - ); - assert_eq!( - pallet_subtensor_swap::CurrentLiquidity::::get(net), - 0, - "CurrentLiquidity not zero for net {net:?}" - ); - assert!( - !pallet_subtensor_swap::SwapV3Initialized::::get(net), - "SwapV3Initialized still set" - ); - assert!( - !pallet_subtensor_swap::EnabledUserLiquidity::::get(net), - "EnabledUserLiquidity still set" - ); - assert!( - pallet_subtensor_swap::TickIndexBitmapWords::::iter_prefix((net,)) - .next() - .is_none(), - "TickIndexBitmapWords not cleared for net {net:?}" - ); - } - - // ──────────────────────────────────────────────────────────────────── - // 8) Re-register a fresh subnet and re‑stake using the pallet’s min rule - // Assert αΔ equals the sim-swap result for the exact τ staked. - // ──────────────────────────────────────────────────────────────────── - let new_owner_hot = U256::from(99_000); - let new_owner_cold = U256::from(99_001); - let net_new = add_dynamic_network(&new_owner_hot, &new_owner_cold); - SubtensorModule::set_max_registrations_per_block(net_new, 1_000u16); - SubtensorModule::set_target_registrations_per_interval(net_new, 1_000u16); - Emission::::insert(net_new, Vec::::new()); - SubtensorModule::set_subnet_locked_balance(net_new, TaoCurrency::from(0)); - - assert_ok!( - pallet_subtensor_swap::Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - net_new, - true - ) - ); - let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); - let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1)"); - pallet_subtensor_swap::CurrentTick::::set(net_new, ct0); - pallet_subtensor_swap::AlphaSqrtPrice::::set(net_new, sqrt1); - - // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). - let min_stake = DefaultMinStake::::get(); - let order = GetAlphaForTao::::with_amount(min_stake); - let fee_for_min = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.fee_paid) - .unwrap_or_else(|_e| { - as subtensor_swap_interface::SwapHandler>::approx_fee_amount(net_new, min_stake) - }); - let min_amount_required = min_stake.saturating_add(fee_for_min).to_u64(); - - // Re‑stake from three coldkeys; choose a specific DISTINCT hotkey per cold. - for &cold in &cold_lps[0..3] { - let [hot1, _hot2] = cold_to_hots[&cold]; - register_ok_neuron(net_new, hot1, cold, 7777); - - let before_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_prev: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); - - // Expected α for this exact τ, using the same sim path as the pallet. - let order = GetAlphaForTao::::with_amount(min_amount_required); - let expected_alpha_out = pallet_subtensor_swap::Pallet::::sim_swap( - net_new, - order, - ) - .map(|r| r.amount_paid_out) - .expect("sim_swap must succeed for fresh net and min amount"); - - assert_ok!(SubtensorModule::do_add_stake( - RuntimeOrigin::signed(cold), - hot1, - net_new, - min_amount_required.into() - )); - - let after_tao = SubtensorModule::get_coldkey_balance(&cold); - let a_new: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); - let a_delta = a_new.saturating_sub(a_prev); - - // τ decreased by exactly the amount we sent. - assert_eq!( - after_tao, - before_tao.saturating_sub(min_amount_required), - "τ did not decrease by the min required restake amount for cold {cold:?}" - ); - - // α minted equals the simulated swap’s net out for that same τ. - assert_eq!( - a_delta, expected_alpha_out.to_u64(), - "α minted mismatch for cold {cold:?} (hot {hot1:?}) on new net (αΔ {a_delta}, expected {expected_alpha_out})" - ); - } - - // Ensure V3 still functional on new net: add a small position for the first cold using its hot1 - // TODO: Revise when user liquidity is available - // let who_cold = cold_lps[0]; - // let [who_hot, _] = cold_to_hots[&who_cold]; - // add_pos(net_new, who_hot, who_cold, 8, 123_456); - // assert!( - // pallet_subtensor_swap::Positions::::iter() - // .any(|((n, owner, _pid), _)| n == net_new && owner == who_cold), - // "new position not recorded on the re-registered net" - // ); - }); -} - #[test] fn dissolve_clears_all_mechanism_scoped_maps_for_all_mechanisms() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 6c7e18b707..0dfd687959 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -6,9 +6,9 @@ use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays use frame_support::sp_runtime::DispatchError; use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; -use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use sp_core::{Get, H256, U256}; +// use sp_runtime::traits::Dispatchable; use substrate_fixed::traits::FromFixed; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; use subtensor_runtime_common::{ @@ -572,13 +572,7 @@ fn test_add_stake_partial_below_min_stake_fails() { mock::setup_reserves(netuid, (amount * 10).into(), (amount * 10).into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); // Get the current price (should be 1.0) let current_price = @@ -714,8 +708,10 @@ fn test_remove_stake_total_balance_no_change() { ); // Add subnet TAO for the equivalent amount added at price - let amount_tao = U96F32::saturating_from_num(amount) - * ::SwapInterface::current_alpha_price(netuid.into()); + let amount_tao = U96F32::from_num(amount) + * U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); SubnetTAO::::mutate(netuid, |v| { *v += amount_tao.saturating_to_num::().into() }); @@ -800,7 +796,7 @@ fn test_add_stake_insufficient_liquidity_one_side_ok() { SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount_staked); // Set the liquidity at lowest possible value so that all staking requests fail - let reserve_alpha = u64::from(mock::SwapMinimumReserve::get()); + let reserve_alpha = 1_000_000_000_u64; let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); @@ -884,9 +880,9 @@ fn test_remove_stake_insufficient_liquidity() { Error::::InsufficientLiquidity ); - // Mock provided liquidity - remove becomes successful - SubnetTaoProvided::::insert(netuid, TaoCurrency::from(amount_staked + 1)); - SubnetAlphaInProvided::::insert(netuid, AlphaCurrency::from(1)); + // Mock more liquidity - remove becomes successful + SubnetTAO::::insert(netuid, TaoCurrency::from(amount_staked + 1)); + SubnetAlphaIn::::insert(netuid, AlphaCurrency::from(1)); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -2176,8 +2172,9 @@ fn test_get_total_delegated_stake_after_unstaking() { netuid, unstake_amount_alpha.into() )); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); // Calculate the expected delegated stake let unstake_amount = @@ -2855,8 +2852,15 @@ fn test_max_amount_add_dynamic() { pallet_subtensor_swap::Error::::PriceLimitExceeded, )), ), - (150_000_000_000, 100_000_000_000, 1_500_000_000, Ok(5)), - (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(51)), + ( + 150_000_000_000, + 100_000_000_000, + 1_500_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(49)), ( 150_000_000_000, 100_000_000_000, @@ -2879,13 +2883,7 @@ fn test_max_amount_add_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); if !alpha_in.is_zero() { let expected_price = U96F32::from_num(tao_in) / U96F32::from_num(alpha_in); @@ -3021,13 +3019,16 @@ fn test_max_amount_remove_dynamic() { (10_000_000_000, 10_000_000_000, 0, Ok(u64::MAX)), // Low bounds (numbers are empirical, it is only important that result // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(4_308_000_000_000)), - (1_001, 1_001, 0, Ok(4_310_000_000_000)), - (1_001, 1_001, 1, Ok(31_750_000)), - (1_001, 1_001, 2, Ok(22_500_000)), - (1_001, 1_001, 1_001, Ok(1_000_000)), - (1_001, 1_001, 10_000, Ok(316_000)), - (1_001, 1_001, 100_000, Ok(100_000)), + (1_000, 1_000, 0, Ok(u64::MAX)), + (1_001, 1_001, 0, Ok(u64::MAX)), + (1_001, 1_001, 1, Ok(17_472)), + (1_001, 1_001, 2, Ok(17_472)), + (1_001, 1_001, 1_001, Ok(17_472)), + (1_001, 1_001, 10_000, Ok(17_472)), + (1_001, 1_001, 100_000, Ok(17_472)), + (1_001, 1_001, 1_000_000, Ok(17_472)), + (1_001, 1_001, 10_000_000, Ok(9_013)), + (1_001, 1_001, 100_000_000, Ok(2_165)), // Basic math (1_000_000, 1_000_000, 250_000_000, Ok(1_000_000)), (1_000_000, 1_000_000, 62_500_000, Ok(3_000_000)), @@ -3074,7 +3075,7 @@ fn test_max_amount_remove_dynamic() { 21_000_000_000_000_000, 1_000_000, 21_000_000_000_000_000, - Ok(30_700_000), + Ok(17_455_533), ), (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_164)), ( @@ -3112,7 +3113,7 @@ fn test_max_amount_remove_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); if !alpha_in.is_zero() { - let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + let expected_price = U64F64::from_num(tao_in) / U64F64::from_num(alpha_in); assert_eq!( ::SwapInterface::current_alpha_price(netuid.into()), expected_price @@ -3301,7 +3302,7 @@ fn test_max_amount_move_stable_dynamic() { dynamic_netuid, TaoCurrency::from(2_000_000_000) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) ); // 3.0 price => max is 0 @@ -3671,29 +3672,27 @@ fn test_max_amount_move_dynamic_dynamic() { expected_max_swappable, precision, )| { - let alpha_in_1 = AlphaCurrency::from(alpha_in_1); - let alpha_in_2 = AlphaCurrency::from(alpha_in_2); let expected_max_swappable = AlphaCurrency::from(expected_max_swappable); // Forse-set alpha in and tao reserve to achieve relative price of subnets SubnetTAO::::insert(origin_netuid, TaoCurrency::from(tao_in_1)); - SubnetAlphaIn::::insert(origin_netuid, alpha_in_1); + SubnetAlphaIn::::insert(origin_netuid, AlphaCurrency::from(alpha_in_1)); SubnetTAO::::insert(destination_netuid, TaoCurrency::from(tao_in_2)); - SubnetAlphaIn::::insert(destination_netuid, alpha_in_2); + SubnetAlphaIn::::insert(destination_netuid, AlphaCurrency::from(alpha_in_2)); if !alpha_in_1.is_zero() && !alpha_in_2.is_zero() { - let origin_price = - I96F32::from_num(tao_in_1) / I96F32::from_num(u64::from(alpha_in_1)); - let dest_price = - I96F32::from_num(tao_in_2) / I96F32::from_num(u64::from(alpha_in_2)); - if dest_price != 0 { + let origin_price = tao_in_1 as f64 / alpha_in_1 as f64; + let dest_price = tao_in_2 as f64 / alpha_in_2 as f64; + if dest_price != 0. { let expected_price = origin_price / dest_price; - assert_eq!( - ::SwapInterface::current_alpha_price( + assert_abs_diff_eq!( + (::SwapInterface::current_alpha_price( origin_netuid.into() ) / ::SwapInterface::current_alpha_price( destination_netuid.into() - ), - expected_price + )) + .to_num::(), + expected_price, + epsilon = 0.000_000_001 ); } } @@ -3790,7 +3789,7 @@ fn test_add_stake_limit_fill_or_kill() { new_test_ext(1).execute_with(|| { let hotkey_account_id = U256::from(533453); let coldkey_account_id = U256::from(55453); - let amount = 900_000_000_000; // over the maximum + let amount = 300_000_000_000; // over the maximum // add network let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); @@ -3810,9 +3809,7 @@ fn test_add_stake_limit_fill_or_kill() { SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); // Setup limit price so that it doesn't peak above 4x of current price - // The amount that can be executed at this price is 450 TAO only - // Alpha produced will be equal to 25 = 100 - 450*100/(150+450) - let limit_price = TaoCurrency::from(24_000_000_000); + let limit_price = TaoCurrency::from(6_000_000_000); // Add stake with slippage safety and check if it fails assert_noop!( @@ -3828,7 +3825,7 @@ fn test_add_stake_limit_fill_or_kill() { ); // Lower the amount and it should succeed now - let amount_ok = TaoCurrency::from(450_000_000_000); // fits the maximum + let amount_ok = TaoCurrency::from(150_000_000_000); // fits the maximum assert_ok!(SubtensorModule::add_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -4560,13 +4557,15 @@ fn test_stake_into_subnet_low_amount() { false, false, )); - let expected_stake = AlphaCurrency::from(((amount as f64) * 0.997 / current_price) as u64); + let expected_stake = (amount as f64) * 0.997 / current_price; // Check if stake has increased assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + u64::from(SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid + )) as f64, expected_stake, - epsilon = expected_stake / 100.into() + epsilon = expected_stake / 100. ); }); } @@ -4838,34 +4837,9 @@ fn test_unstake_full_amount() { }); } -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: U64F64 = U64F64::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} - /// Test correctness of swap fees: /// 1. TAO is not minted or burned /// 2. Fees match FeeRate -/// #[test] fn test_swap_fees_tao_correctness() { new_test_ext(1).execute_with(|| { @@ -4882,7 +4856,6 @@ fn test_swap_fees_tao_correctness() { SubtensorModule::add_balance_to_coldkey_account(&coldkey, user_balance_before); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); // Forse-set alpha in and tao reserve to make price equal 0.25 let tao_reserve = TaoCurrency::from(100_000_000_000); @@ -4909,18 +4882,6 @@ fn test_swap_fees_tao_correctness() { .to_num::() + 0.0001; let limit_price = current_price + 0.01; - let tick_low = price_to_tick(current_price); - let tick_high = price_to_tick(limit_price); - let liquidity = amount; - - assert_ok!(::SwapInterface::do_add_liquidity( - netuid.into(), - &owner_coldkey, - &owner_hotkey, - tick_low, - tick_high, - liquidity, - )); // Limit-buy and then sell all alpha for user to hit owner liquidity assert_ok!(SubtensorModule::add_stake_limit( @@ -5200,181 +5161,6 @@ fn test_default_min_stake_sufficiency() { }); } -/// Test that modify_position always credits fees -/// -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_update_position_fees --exact --show-output -#[test] -fn test_update_position_fees() { - // Test cases: add or remove liquidity during modification - [false, true].into_iter().for_each(|add| { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - let coldkey = U256::from(4); - let amount = 1_000_000_000; - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - SubtensorModule::add_balance_to_coldkey_account(&owner_coldkey, amount * 10); - SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount * 100); - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); - - // Forse-set alpha in and tao reserve to make price equal 0.25 - let tao_reserve = TaoCurrency::from(100_000_000_000); - let alpha_in = AlphaCurrency::from(400_000_000_000); - mock::setup_reserves(netuid, tao_reserve, alpha_in); - - // Get alpha for owner - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - - // Add owner coldkey Alpha as concentrated liquidity - // between current price current price + 0.01 - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()) - .to_num::() - + 0.0001; - let limit_price = current_price + 0.001; - let tick_low = price_to_tick(current_price); - let tick_high = price_to_tick(limit_price); - let liquidity = amount; - - let (position_id, _, _) = ::SwapInterface::do_add_liquidity( - NetUid::from(netuid), - &owner_coldkey, - &owner_hotkey, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Buy and then sell all alpha for user to hit owner liquidity - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - amount.into(), - )); - - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); - - let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &coldkey, - netuid, - ); - assert_ok!(SubtensorModule::remove_stake( - RuntimeOrigin::signed(coldkey), - owner_hotkey, - netuid, - user_alpha, - )); - - // Modify position - fees should be collected and paid to the owner - let owner_tao_before = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - // Make small modification - let delta = - ::MinimumLiquidity::get() - as i64 - * (if add { 1 } else { -1 }); - assert_ok!(Swap::modify_position( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid.into(), - position_id.into(), - delta, - )); - - // Check ending owner TAO and alpha - let owner_tao_after_add = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_after_add = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - assert!(owner_tao_after_add > owner_tao_before); - assert!(owner_alpha_after_add > owner_alpha_before); // always greater because of claimed fees - - // Make small modification again - should not claim more fees - assert_ok!(Swap::modify_position( - RuntimeOrigin::signed(owner_coldkey), - owner_hotkey, - netuid.into(), - position_id.into(), - delta, - )); - - // Check ending owner TAO and alpha - let owner_tao_after_repeat = SubtensorModule::get_coldkey_balance(&owner_coldkey); - let owner_alpha_after_repeat = - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - ); - - assert!(owner_tao_after_add == owner_tao_after_repeat); - if add { - assert!(owner_alpha_after_add > owner_alpha_after_repeat); - } else { - assert!(owner_alpha_after_add < owner_alpha_after_repeat); - } - }); - }); -} - -// TODO: Revise when user liquidity is available -// fn setup_positions(netuid: NetUid) { -// for (coldkey, hotkey, low_price, high_price, liquidity) in [ -// (2, 12, 0.1, 0.20, 1_000_000_000_000_u64), -// (3, 13, 0.15, 0.25, 200_000_000_000_u64), -// (4, 14, 0.25, 0.5, 3_000_000_000_000_u64), -// (5, 15, 0.3, 0.6, 300_000_000_000_u64), -// (6, 16, 0.4, 0.7, 8_000_000_000_000_u64), -// (7, 17, 0.5, 0.8, 600_000_000_000_u64), -// (8, 18, 0.6, 0.9, 700_000_000_000_u64), -// (9, 19, 0.7, 1.0, 100_000_000_000_u64), -// (10, 20, 0.8, 1.1, 300_000_000_000_u64), -// ] { -// SubtensorModule::create_account_if_non_existent(&U256::from(coldkey), &U256::from(hotkey)); -// SubtensorModule::add_balance_to_coldkey_account( -// &U256::from(coldkey), -// 1_000_000_000_000_000, -// ); -// SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( -// &U256::from(hotkey), -// &U256::from(coldkey), -// netuid.into(), -// 1_000_000_000_000_000.into(), -// ); - -// let tick_low = price_to_tick(low_price); -// let tick_high = price_to_tick(high_price); -// let add_lq_call = SwapCall::::add_liquidity { -// hotkey: U256::from(hotkey), -// netuid: netuid.into(), -// tick_low, -// tick_high, -// liquidity, -// }; -// assert_ok!( -// RuntimeCall::Swap(add_lq_call).dispatch(RuntimeOrigin::signed(U256::from(coldkey))) -// ); -// } -// } - #[test] fn test_large_swap() { new_test_ext(1).execute_with(|| { @@ -5385,19 +5171,13 @@ fn test_large_swap() { // add network let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000); - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); + let tao = TaoCurrency::from(100_000_000u64); + let alpha = AlphaCurrency::from(1_000_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoCurrency::ZERO, - 1_000_000_000_000.into(), - false, - ) - .unwrap(); - - // TODO: Revise when user liquidity is available - // setup_positions(netuid.into()); + ::SwapInterface::init_swap(netuid, None); let swap_amount = TaoCurrency::from(100_000_000_000_000); assert_ok!(SubtensorModule::add_stake( diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index a547b30a14..4eb13fe5fb 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -714,63 +714,6 @@ fn test_subtoken_enable_ok_for_burn_register_before_enable() { }); } -// #[test] -// fn test_user_liquidity_access_control() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(1); -// let owner_coldkey = U256::from(2); -// let not_owner = U256::from(999); // arbitrary non-owner - -// // add network -// let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Not owner, not root: should fail -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(not_owner), netuid, true), -// DispatchError::BadOrigin -// ); - -// // Subnet owner can enable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::signed(owner_coldkey), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can disable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// false -// )); -// assert!(!pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can enable again -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Subnet owner cannot disable (only root can disable) -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(owner_coldkey), netuid, false), -// DispatchError::BadOrigin -// ); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); -// }); -// } - // cargo test --package pallet-subtensor --lib -- tests::subnet::test_no_duplicates_in_symbol_static --exact --show-output #[test] fn test_no_duplicates_in_symbol_static() { diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 19af1303c1..10804bd2e4 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -2,7 +2,7 @@ use core::ops::Neg; use frame_support::pallet_prelude::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; @@ -38,19 +38,16 @@ pub trait SwapHandler { Self: SwapEngine; fn approx_fee_amount(netuid: NetUid, amount: T) -> T; - fn current_alpha_price(netuid: NetUid) -> U96F32; - fn get_protocol_tao(netuid: NetUid) -> TaoCurrency; + fn current_alpha_price(netuid: NetUid) -> U64F64; fn max_price() -> C; fn min_price() -> C; fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ); - fn is_user_liquidity_enabled(netuid: NetUid) -> bool; - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; - fn toggle_user_liquidity(netuid: NetUid, enabled: bool); + ) -> (TaoCurrency, AlphaCurrency); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn init_swap(netuid: NetUid, maybe_price: Option); } pub trait DefaultPriceLimit diff --git a/pallets/swap-interface/src/order.rs b/pallets/swap-interface/src/order.rs index 1576283fd5..fc3f2acefe 100644 --- a/pallets/swap-interface/src/order.rs +++ b/pallets/swap-interface/src/order.rs @@ -11,7 +11,7 @@ pub trait Order: Clone { fn with_amount(amount: impl Into) -> Self; fn amount(&self) -> Self::PaidIn; - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool; + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool; } #[derive(Clone, Default)] @@ -45,8 +45,8 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price < limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price < limit_price } } @@ -81,7 +81,7 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price > limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price > limit_price } } diff --git a/pallets/swap/Cargo.toml b/pallets/swap/Cargo.toml index 7de8e49c1d..45389874a1 100644 --- a/pallets/swap/Cargo.toml +++ b/pallets/swap/Cargo.toml @@ -12,6 +12,7 @@ frame-support.workspace = true frame-system.workspace = true log.workspace = true safe-math.workspace = true +safe-bigmath.workspace = true scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, optional = true } sp-arithmetic.workspace = true @@ -28,6 +29,8 @@ subtensor-swap-interface.workspace = true [dev-dependencies] sp-tracing.workspace = true +rand = { version = "0.8", default-features = false } +rayon = "1.10" [lints] workspace = true @@ -42,7 +45,9 @@ std = [ "frame-system/std", "log/std", "pallet-subtensor-swap-runtime-api/std", + "rand/std", "safe-math/std", + "safe-bigmath/std", "scale-info/std", "serde/std", "sp-arithmetic/std", diff --git a/pallets/swap/rpc/src/lib.rs b/pallets/swap/rpc/src/lib.rs index b83a083ad5..a58334dea8 100644 --- a/pallets/swap/rpc/src/lib.rs +++ b/pallets/swap/rpc/src/lib.rs @@ -13,12 +13,14 @@ use sp_blockchain::HeaderBackend; use sp_runtime::traits::Block as BlockT; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; -pub use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +pub use pallet_subtensor_swap_runtime_api::{SubnetPrice, SwapRuntimeApi}; #[rpc(client, server)] pub trait SwapRpcApi { #[method(name = "swap_currentAlphaPrice")] fn current_alpha_price(&self, netuid: NetUid, at: Option) -> RpcResult; + #[method(name = "swap_currentAlphaPriceAll")] + fn current_alpha_price_all(&self, at: Option) -> RpcResult>; #[method(name = "swap_simSwapTaoForAlpha")] fn sim_swap_tao_for_alpha( &self, @@ -92,6 +94,18 @@ where }) } + fn current_alpha_price_all( + &self, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + api.current_alpha_price_all(at).map_err(|e| { + Error::RuntimeError(format!("Unable to get all current alpha prices: {e:?}")).into() + }) + } + fn sim_swap_tao_for_alpha( &self, netuid: NetUid, diff --git a/pallets/swap/runtime-api/Cargo.toml b/pallets/swap/runtime-api/Cargo.toml index 042875fdd0..50f92d19e2 100644 --- a/pallets/swap/runtime-api/Cargo.toml +++ b/pallets/swap/runtime-api/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] codec = { workspace = true, features = ["derive"] } frame-support.workspace = true +serde.workspace = true scale-info.workspace = true sp-api.workspace = true sp-std.workspace = true @@ -20,6 +21,7 @@ std = [ "codec/std", "frame-support/std", "scale-info/std", + "serde/std", "sp-api/std", "sp-std/std", "subtensor-runtime-common/std", diff --git a/pallets/swap/runtime-api/src/lib.rs b/pallets/swap/runtime-api/src/lib.rs index 01d2ccf23e..ce61a93dc6 100644 --- a/pallets/swap/runtime-api/src/lib.rs +++ b/pallets/swap/runtime-api/src/lib.rs @@ -1,21 +1,33 @@ #![cfg_attr(not(feature = "std"), no_std)] use frame_support::pallet_prelude::*; +use scale_info::prelude::vec::Vec; +use serde::{Deserialize, Serialize}; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; -#[freeze_struct("3a4fd213b5de5eb6")] +#[freeze_struct("ee2ba1ec4ee58ae6")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SimSwapResult { pub tao_amount: TaoCurrency, pub alpha_amount: AlphaCurrency, pub tao_fee: TaoCurrency, pub alpha_fee: AlphaCurrency, + pub tao_slippage: TaoCurrency, + pub alpha_slippage: AlphaCurrency, +} + +#[freeze_struct("d7bbb761fc2b2eac")] +#[derive(Decode, Deserialize, Encode, PartialEq, Eq, Clone, Debug, Serialize, TypeInfo)] +pub struct SubnetPrice { + pub netuid: NetUid, + pub price: u64, } sp_api::decl_runtime_apis! { pub trait SwapRuntimeApi { fn current_alpha_price(netuid: NetUid) -> u64; + fn current_alpha_price_all() -> Vec; fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoCurrency) -> SimSwapResult; fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaCurrency) -> SimSwapResult; } diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index a17ac59141..c4a6afa87e 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -2,22 +2,16 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::multiple_bound_locations)] -use core::marker::PhantomData; - use frame_benchmarking::v2::*; -use frame_support::traits::Get; use frame_system::RawOrigin; -use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; -use crate::{ - pallet::{ - AlphaSqrtPrice, Call, Config, CurrentLiquidity, CurrentTick, Pallet, Positions, - SwapV3Initialized, - }, - position::{Position, PositionId}, - tick::TickIndex, -}; +use crate::pallet::{Call, Config, Pallet}; + +#[allow(dead_code)] +fn init_swap(netuid: NetUid) { + let _ = Pallet::::maybe_initialize_palswap(netuid, None); +} #[benchmarks(where T: Config)] mod benchmarks { @@ -32,117 +26,5 @@ mod benchmarks { set_fee_rate(RawOrigin::Root, netuid, rate); } - // TODO: Revise when user liquidity is available - // #[benchmark] - // fn add_liquidity() { - // let netuid = NetUid::from(1); - - // if !SwapV3Initialized::::get(netuid) { - // SwapV3Initialized::::insert(netuid, true); - // AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - // CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - // CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - // } - - // let caller: T::AccountId = whitelisted_caller(); - // let hotkey: T::AccountId = account("hotkey", 0, 0); - // let tick_low = TickIndex::new_unchecked(-1000); - // let tick_high = TickIndex::new_unchecked(1000); - - // #[extrinsic_call] - // add_liquidity( - // RawOrigin::Signed(caller), - // hotkey, - // netuid, - // tick_low, - // tick_high, - // 1000, - // ); - // } - - #[benchmark] - fn remove_liquidity() { - let netuid = NetUid::from(1); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 1000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - remove_liquidity(RawOrigin::Signed(caller), hotkey, netuid.into(), id.into()); - } - - #[benchmark] - fn modify_position() { - let netuid = NetUid::from(1); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 10000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - modify_position( - RawOrigin::Signed(caller), - hotkey, - netuid.into(), - id.into(), - -5000, - ); - } - - // #[benchmark] - // fn toggle_user_liquidity() { - // let netuid = NetUid::from(101); - - // assert!(!EnabledUserLiquidity::::get(netuid)); - - // #[extrinsic_call] - // toggle_user_liquidity(RawOrigin::Root, netuid.into(), true); - - // assert!(EnabledUserLiquidity::::get(netuid)); - // } - impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/swap/src/lib.rs b/pallets/swap/src/lib.rs index 6257df852b..b51c3351dc 100644 --- a/pallets/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -1,10 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -use substrate_fixed::types::U64F64; - pub mod pallet; -pub mod position; -pub mod tick; pub mod weights; pub use pallet::*; @@ -14,5 +10,3 @@ pub mod benchmarking; #[cfg(test)] pub(crate) mod mock; - -type SqrtPrice = U64F64; diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index aacdf90835..142a59b8df 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -14,14 +14,19 @@ use sp_runtime::{ BuildStorage, Vec, traits::{BlakeTwo256, IdentityLookup}, }; -use substrate_fixed::types::U64F64; +use std::{cell::RefCell, collections::HashMap}; +// use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, + AlphaCurrency, + BalanceOps, + // Currency, + CurrencyReserve, + NetUid, + SubnetInfo, + TaoCurrency, }; use subtensor_swap_interface::Order; -use crate::pallet::{EnabledUserLiquidity, FeeGlobalAlpha, FeeGlobalTao}; - construct_runtime!( pub enum Test { System: frame_system = 0, @@ -82,16 +87,38 @@ impl system::Config for Test { parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const MaxFeeRate: u16 = 10000; // 15.26% - pub const MaxPositions: u32 = 100; pub const MinimumLiquidity: u64 = 1_000; pub const MinimumReserves: NonZeroU64 = NonZeroU64::new(1).unwrap(); } +thread_local! { + // 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()); +} + #[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()) { + return val; + } + + // Otherwise, fall back to our defaults match netuid.into() { 123u16 => 10_000, WRAPPING_FEES_NETUID => 100_000_000_000, @@ -107,8 +134,22 @@ impl CurrencyReserve for TaoReserve { #[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()) { + return val; + } + + // Otherwise, fall back to our defaults match netuid.into() { 123u16 => 10_000.into(), WRAPPING_FEES_NETUID => 400_000_000_000.into(), @@ -123,22 +164,7 @@ impl CurrencyReserve for AlphaReserve { pub type GetAlphaForTao = subtensor_swap_interface::GetAlphaForTao; pub type GetTaoForAlpha = subtensor_swap_interface::GetTaoForAlpha; -pub(crate) trait GlobalFeeInfo: Currency { - fn global_fee(&self, netuid: NetUid) -> U64F64; -} - -impl GlobalFeeInfo for TaoCurrency { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalTao::::get(netuid) - } -} - -impl GlobalFeeInfo for AlphaCurrency { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalAlpha::::get(netuid) - } -} - +#[allow(dead_code)] pub(crate) trait TestExt { fn approx_expected_swap_output( sqrt_current_price: f64, @@ -277,7 +303,6 @@ impl crate::pallet::Config for Test { type BalanceOps = MockBalanceOps; type ProtocolId = SwapProtocolId; type MaxFeeRate = MaxFeeRate; - type MaxPositions = MaxPositions; type MinimumLiquidity = MinimumLiquidity; type MinimumReserve = MinimumReserves; type WeightInfo = (); @@ -292,12 +317,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(storage); ext.execute_with(|| { System::set_block_number(1); - - for netuid in 0u16..=100 { - // enable V3 for this range of netuids - EnabledUserLiquidity::::set(NetUid::from(netuid), true); - } - EnabledUserLiquidity::::set(NetUid::from(WRAPPING_FEES_NETUID), true); }); ext } diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs new file mode 100644 index 0000000000..1e1386bd41 --- /dev/null +++ b/pallets/swap/src/pallet/balancer.rs @@ -0,0 +1,1095 @@ +// Balancer swap +// +// Unlike uniswap v2 or v3, it allows adding liquidity disproportionally to price. This is +// achieved by introducing the weights w1 and w2 so that w1 + w2 = 1. In these formulas x +// means base currency (alpha) and y means quote currency (tao). The w1 weight in the code +// below is referred as weight_base, and w2 as weight_quote. Because of the w1 + w2 = 1 +// constraint, only weight_quote is stored, and weight_base is always calculated. +// +// The formulas used for pool operation are following: +// +// Price: p = (w1*y) / (w2*x) +// +// Reserve deltas / (or -1 * payouts) in swaps are computed by: +// +// if ∆x is given (sell) ∆y = y * ((x / (x+∆x))^(w1/w2) - 1) +// if ∆y is given (buy) ∆x = x * ((y / (y+∆y))^(w2/w1) - 1) +// +// When swaps are executing the orders with slippage control, we need to know what amount +// we can swap before the price reaches the limit value of p': +// +// If p' < p (sell): ∆x = x * ((p / p')^w2 - 1) +// If p' < p (buy): ∆y = y * ((p' / p)^w1 - 1) +// +// In order to initialize weights with existing reserve values and price: +// +// w1 = px / (px + y) +// w2 = y / (px + y) +// +// Weights are adjusted when some amounts are added to the reserves. This prevents price +// from changing. +// +// new_w1 = p * (x + ∆x) / (p * (x + ∆x) + y + ∆y) +// new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y) +// +// Weights are limited to stay within [0.1, 0.9] range to avoid precision issues in exponentiation. +// Practically, these limitations will not be achieved, but if they are, the swap will not allow injection +// that will push the weights out of this interval because we prefer chain and swap stability over success +// of a single injection. Currently, we only allow the protocol to inject disproportionally to price, and +// the amount of disproportion will not cause weigths to get far from 0.5. +// + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::*; +use safe_bigmath::*; +use safe_math::*; +use sp_arithmetic::Perquintill; +use sp_core::U256; +use sp_runtime::Saturating; +use sp_std::ops::Neg; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +/// Balancer implements all high complexity math for swap operations such as: +/// - Swapping x for y, which includes limit orders +/// - Adding and removing liquidity (including unbalanced) +/// +/// Notation used in this file: +/// - x: Base reserve (alplha reserve) +/// - y: Quote reserve (tao reserve) +/// - ∆x: Alpha paid in/out +/// - ∆y: Tao paid in/out +/// - w1: Base weight (a.k.a weight_base) +/// - w2: Quote weight (a.k.a weight_quote) +#[freeze_struct("33a4fb0774da77c7")] +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Balancer { + quote: Perquintill, +} + +/// Accuracy matches to 18 decimal digits used to represent weights +pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64; +/// Lower imit of weights is 0.01 +pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100); +/// 1.0 in Perquintill +pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY); + +#[derive(Debug)] +pub enum BalancerError { + /// The provided weight value is out of range + InvalidValue, +} + +impl Default for Balancer { + /// The default value of weights is 0.5 for pool initialization + fn default() -> Self { + Self { + quote: Perquintill::from_rational(1u128, 2u128), + } + } +} + +impl Balancer { + /// Creates a new instance of balancer with a given quote weight + pub fn new(quote: Perquintill) -> Result { + if Self::check_constraints(quote) { + Ok(Balancer { quote }) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// Constraints limit balancer weights within certain range of values: + /// - Both weights are above minimum + /// - Sum of weights is equal to 1.0 + fn check_constraints(quote: Perquintill) -> bool { + let base = ONE.saturating_sub(quote); + (base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT) + } + + /// We store quote weight as Perquintill + pub fn get_quote_weight(&self) -> Perquintill { + self.quote + } + + /// Base weight is calculated as 1.0 - quote_weight + pub fn get_base_weight(&self) -> Perquintill { + ONE.saturating_sub(self.quote) + } + + /// Sets quote currency weight in the balancer. + /// Because sum of weights is always 1.0, there is no need to + /// store base currency weight + pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> { + if Self::check_constraints(new_value) { + self.quote = new_value; + Ok(()) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote), + /// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base) + /// + /// Here we use SafeInt from bigmath crate for high-precision exponentiation, + /// which exposes the function pow_ratio_scaled. + /// + /// Note: ∆x may be negative + fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 { + let x_plus_dx = if dx >= 0 { + x.saturating_add(dx as u64) + } else { + x.saturating_sub(dx.neg() as u64) + }; + + if x_plus_dx == 0 { + return U64F64::saturating_from_num(0); + } + let w1: u128 = self.get_base_weight().deconstruct() as u128; + let w2: u128 = self.get_quote_weight().deconstruct() as u128; + + let precision = 1024; + let x_safe = SafeInt::from(x); + let w1_safe = SafeInt::from(w1); + let w2_safe = SafeInt::from(w2); + let perquintill_scale = SafeInt::from(ACCURACY as u128); + let denominator = SafeInt::from(x_plus_dx); + log::debug!("x = {:?}", x); + log::debug!("dx = {:?}", dx); + log::debug!("x_safe = {:?}", x_safe); + log::debug!("denominator = {:?}", denominator); + log::debug!("w1_safe = {:?}", w1_safe); + log::debug!("w2_safe = {:?}", w2_safe); + log::debug!("precision = {:?}", precision); + log::debug!("perquintill_scale = {:?}", perquintill_scale); + + let maybe_result_safe_int = if base_quote { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w1_safe, + &w2_safe, + precision, + &perquintill_scale, + ) + } else { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w2_safe, + &w1_safe, + precision, + &perquintill_scale, + ) + }; + + if let Some(result_safe_int) = maybe_result_safe_int + && let Some(result_u64) = result_safe_int.to_u64() + { + return U64F64::saturating_from_num(result_u64) + .safe_div(U64F64::saturating_from_num(ACCURACY)); + } + U64F64::saturating_from_num(0) + } + + /// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote) + /// This method is used in sell swaps + /// (∆x is given by user, ∆y is paid out by the pool) + pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 { + self.exp_scaled(x, dx as i128, true) + } + + /// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base) + /// This method is used in buy swaps + /// (∆y is given by user, ∆x is paid out by the pool) + pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 { + self.exp_scaled(y, dy as i128, false) + } + + /// Calculates price as (w1/w2) * (y/x), where + /// - w1 is base weight + /// - w2 is quote weight + /// - x is base reserve + /// - y is quote reserve + pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 { + let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct()); + let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct()); + let x_fixed = U64F64::saturating_from_num(x); + let y_fixed = U64F64::saturating_from_num(y); + w1_fixed + .safe_div(w2_fixed) + .saturating_mul(y_fixed.safe_div(x_fixed)) + } + + /// Multiply a u128 value by a Perquintill with u128 result rounded to the + /// nearest integer + fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 { + let parts = p.deconstruct() as u128; + let acc = ACCURACY as u128; + + let num = U256::from(value).saturating_mul(U256::from(parts)); + let den = U256::from(acc); + + // Add 0.5 before integer division to achieve rounding to the nearest + // integer + let zero = U256::from(0); + let res = num + .saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero)) + .checked_div(den) + .unwrap_or(zero); + res.min(U256::from(u128::MAX)) + .try_into() + .unwrap_or_default() + } + + /// When liquidity is added to balancer swap, it may be added with arbitrary proportion, + /// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to + /// stay within balancer pool invariant, the weights need to be updated. Invariant: + /// + /// L = x ^ weight_base * y ^ weight_quote + /// + /// Note that weights must remain within the proper range (both be above MIN_WEIGHT), + /// so only reasonably small disproportions of updates are appropriate. + pub fn update_weights_for_added_liquidity( + &mut self, + tao_reserve: u64, + alpha_reserve: u64, + tao_delta: u64, + alpha_delta: u64, + ) -> Result<(), BalancerError> { + // Calculate new to-be reserves (do not update here) + let tao_reserve_u128 = u64::from(tao_reserve) as u128; + let alpha_reserve_u128 = u64::from(alpha_reserve) as u128; + let tao_delta_u128 = u64::from(tao_delta) as u128; + let alpha_delta_u128 = u64::from(alpha_delta) as u128; + let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128); + let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128); + + // Calculate new weights + let quantity_1: u128 = Self::mul_perquintill_round( + self.get_base_weight(), + tao_reserve_u128.saturating_mul(new_alpha_reserve_u128), + ); + let quantity_2: u128 = Self::mul_perquintill_round( + self.get_quote_weight(), + alpha_reserve_u128.saturating_mul(new_tao_reserve_u128), + ); + let q_sum = quantity_1.saturating_add(quantity_2); + + // Calculate new reserve weights + let new_reserve_weight = if q_sum != 0 { + // Both TAO and Alpha are non-zero, normal case + Perquintill::from_rational(quantity_2, q_sum) + } else { + // Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5 + Perquintill::from_rational(1u128, 2u128) + }; + + self.set_quote_weight(new_reserve_weight) + } + + /// Calculates quote delta needed to reach the price up when byuing + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆y = y * ((price_new / price)^weight_base - 1) + /// price_new >= price + pub fn calculate_quote_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = target_price.to_bits(); + let base_denominator: u128 = current_price.to_bits(); + let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w1_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let reserve_fixed = U64F64::saturating_from_num(reserve); + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates base delta needed to reach the price down when selling + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆x = x * ((price / price_new)^weight_quote - 1) + /// price_new <= price + pub fn calculate_base_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = current_price.to_bits(); + let base_denominator: u128 = target_price.to_bits(); + let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w2_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let reserve_fixed = U64F64::saturating_from_num(reserve); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates amount of Alpha that needs to be sold to get a given amount of TAO + pub fn get_base_needed_for_quote( + &self, + tao_reserve: u64, + alpha_reserve: u64, + delta_tao: u64, + ) -> u64 { + let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + // e > 1 in this case + alpha_reserve_fixed + .saturating_mul(e.saturating_sub(one)) + .saturating_to_num::() + } +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +#[cfg(feature = "std")] +mod tests { + use crate::pallet::Balancer; + use crate::pallet::balancer::*; + use approx::assert_abs_diff_eq; + use sp_arithmetic::Perquintill; + + // Helper: convert Perquintill to f64 for comparison + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / ACCURACY as f64 + } + + // Helper: convert U64F64 to f64 for comparison + fn f(v: U64F64) -> f64 { + v.to_num::() + } + + #[test] + fn test_perquintill_power() { + const PRECISION: u32 = 4096; + const PERQUINTILL: u128 = ACCURACY as u128; + + let x = SafeInt::from(21_000_000_000_000_000u64); + let delta = SafeInt::from(7_000_000_000_000_000u64); + let w1 = SafeInt::from(600_000_000_000_000_000u128); + let w2 = SafeInt::from(400_000_000_000_000_000u128); + let denominator = &x + δ + assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL)); + + let perquintill_result = SafeInt::pow_ratio_scaled( + &x, + &denominator, + &w1, + &w2, + PRECISION, + &SafeInt::from(PERQUINTILL), + ) + .expect("perquintill integer result"); + + assert_eq!( + perquintill_result, + SafeInt::from(649_519_052_838_328_985u128) + ); + let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result); + assert_eq!(format!("{}", readable), "0.649519052838328985"); + } + + /// Validate realistic values that can be calculated with f64 precision + #[test] + fn test_exp_base_quote_happy_path() { + // Outer test cases: w_quote + [ + Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational( + 102_337_248_363_782_924_u128, + 1_000_000_000_000_000_000_u128, + ), + ] + .into_iter() + .for_each(|w_quote| { + // Inner test cases: y, x, ∆x + [ + (1_000_u64, 1_000_u64, 0_u64), + (1_000_u64, 1_000_u64, 1_u64), + (1_500_u64, 1_000_u64, 1_u64), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + (100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + (10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + // Extreme values of ∆x for small x + (1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64), + ( + 5_628_038_062_729_553_u64, + 400_775_553_u64, + 14_446_633_907_665_582_u64, + ), + ( + 5_600_000_000_000_000_u64, + 400_000_000_u64, + 14_000_000_000_000_000_u64, + ), + ] + .into_iter() + .for_each(|(y, x, dx)| { + let bal = Balancer::new(w_quote).unwrap(); + let e1 = bal.exp_base_quote(x, dx); + let e2 = bal.exp_quote_base(x, dx); + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - e1); + let dy2 = y_fixed * (one - e2); + + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy1_expected = y as f64 * (1. - e1_expected); + let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1); + let dy2_expected = y as f64 * (1. - e2_expected); + + // Start tolerance with 0.001 rao + let mut eps1 = 0.001; + let mut eps2 = 0.001; + + // If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao + if dy1_expected > 100_000_000_000_000_f64 { + eps1 = 1.0; + } + if dy2_expected > 100_000_000_000_000_f64 { + eps2 = 1.0; + } + assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1); + assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2); + }) + }); + } + + /// This test exercises practical application edge cases of exp_base_quote + /// The practical formula where this function is used: + /// ∆y = y * (exp_base_quote(x, ∆x) - 1) + /// + /// The test validates that two different sets of parameters produce (sensibly) + /// different results + /// + #[test] + fn test_exp_base_quote_dy_precision() { + // Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2 + // Realized dy1 should be greater than dy2 + [ + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 2_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ] + .into_iter() + .for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| { + let bal1 = Balancer::new(w_quote1).unwrap(); + let bal2 = Balancer::new(w_quote2).unwrap(); + + let exp1 = bal1.exp_base_quote(x1, dx1); + let exp2 = bal2.exp_base_quote(x2, dx2); + + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - exp1); + let dy2 = y_fixed * (one - exp2); + + assert!(dy1 > dy2); + + let zero = U64F64::from_num(0); + assert!(dy1 != zero); + assert!(dy2 != zero); + }) + } + + /// Test the broad range of w_quote values, usually should be ignored + #[ignore] + #[test] + fn test_exp_quote_broad_range() { + let y = 1_000_000_000_000_u64; + let x = 100_000_000_000_000_u64; + let dx = 10_000_000_u64; + + let mut prev = U64F64::from_num(1_000_000_000); + let mut last_progress = 0.; + let start = 100_000_000_000_u128; + let stop = 900_000_000_000_u128; + for num in (start..=stop).step_by(1000_usize) { + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + let progress = (num as f64 - start as f64) / (stop as f64 - start as f64); + if progress - last_progress >= 0.0001 { + // Replace with println for real-time progress + log::debug!("progress = {:?}%", progress * 100.); + log::debug!("dy = {:?}", dy); + last_progress = progress; + } + + assert!(dy != U64F64::from_num(0)); + assert!(dy <= prev); + prev = dy; + } + } + + #[ignore] + #[test] + fn test_exp_quote_fuzzy() { + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + const ITERATIONS: usize = 1_000_000_000; + let counter = Arc::new(AtomicUsize::new(0)); + + (0..ITERATIONS) + .into_par_iter() + .for_each(|i| { + // Each iteration gets its own deterministic RNG. + // Seed depends on i, so runs are reproducible. + let mut rng = StdRng::seed_from_u64(42 + i as u64); + let max_supply: u64 = 21_000_000_000_000_000; + let full_range = true; + + let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve + let y: u64 = if full_range { + // TAO reserve (allow huge prices) + rng.gen_range(1_000..=max_supply) + } else { + // TAO reserve (limit prices with 0-1000) + rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply)) + }; + let dx: u64 = if full_range { + // Alhpa sold (allow huge values) + rng.gen_range(1_000..=21_000_000_000_000_000) + } else { + // Alhpa sold (do not sell more than 100% of what's in alpha reserve) + rng.gen_range(1_000..=x) + }; + let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9); + let w_quote = Perquintill::from_rational(w_numerator, ACCURACY); + + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + // Calculate expected in f64 and approx-assert + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy_expected = y as f64 * (1. - e_expected); + + let actual = dy.to_num::(); + let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0); + + assert!( + (actual - dy_expected).abs() <= eps, + "dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n", + actual, dy_expected, eps, x, y, dx, w_numerator, + ); + + // Assert that we aren't giving out more than reserve y + assert!(dy <= y, "dy = {},\ny = {}", dy, y,); + + // Print progress + let done = counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 100_000_000 == 0 { + let progress = done as f64 / ITERATIONS as f64 * 100.0; + // Replace with println for real-time progress + log::debug!("progress = {progress:.4}%"); + } + }); + } + + #[test] + fn test_calculate_quote_delta_in() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + + // ∆y = y•[(p'/p)^w1 - 1] + let dy_expected = tao_reserve as f64 + * ((target_price.to_num::() / current_price.to_num::()).powf(0.75) - 1.0); + + assert_eq!(dy, dy_expected as u64,); + } + + #[test] + fn test_calculate_base_delta_in() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.2); + let target_price: U64F64 = U64F64::from_num(0.1); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + + // ∆x = x•[(p/p')^w2 - 1] + let dx_expected = alpha_reserve as f64 + * ((current_price.to_num::() / target_price.to_num::()).powf(0.25) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } + + #[test] + fn test_calculate_quote_delta_in_impossible() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (lower) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.05); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dy_expected = 0u64; + + assert_eq!(dy, dy_expected); + } + + #[test] + fn test_calculate_base_delta_in_impossible() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (higher) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + let dx_expected = 0u64; + + assert_eq!(dx, dx_expected); + } + + #[test] + fn test_calculate_delta_in_reverse_swap() { + let num = 500_000_000_000_u128; + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + // Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price + let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::()) as u64; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dx = alpha_reserve as f64 + * (1.0 + - (tao_reserve as f64 / (tao_reserve as f64 + dy as f64)) + .powf(num as f64 / (1_000_000_000_000 - num) as f64)); + + // Verify that buying with dy will in fact bring the price to target_price + let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy); + assert_abs_diff_eq!( + actual_price.to_num::(), + target_price.to_num::(), + epsilon = target_price.to_num::() / 1_000_000_000. + ); + } + + #[test] + fn test_mul_round_zero_and_one() { + let v = 1_000_000u128; + + // p = 0 -> always 0 + assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0); + + // p = 1 -> identity + assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v); + } + + #[test] + fn test_mul_round_half_behaviour() { + // p = 1/2 + let p = Perquintill::from_rational(1u128, 2u128); + + // Check rounding around .5 boundaries + // value * 1/2, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0 + assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up) + assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3 + assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512 + assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513 + } + + #[test] + fn test_mul_round_third_behaviour() { + // p = 1/3 + let p = Perquintill::from_rational(1u128, 3u128); + + // value * 1/3, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2 + } + + #[test] + fn test_mul_round_large_values_simple_rational() { + // p = 7/10 (exact in perquintill: 0.7) + let p = Perquintill::from_rational(7u128, 10u128); + let v: u128 = 1_000_000_000_000_000_000; + + let res = Balancer::mul_perquintill_round(p, v); + + // Expected = round(0.7 * v) with pure integer math: + // round(v * 7 / 10) = (v*7 + 10/2) / 10 + let expected = (v.saturating_mul(7) + 10 / 2) / 10; + + assert_eq!(res, expected); + } + + #[test] + fn test_mul_round_max_value_with_one() { + let v = u128::MAX; + let p = ONE; + + // For p = 1, result must be exactly value, and must not overflow + let res = Balancer::mul_perquintill_round(p, v); + assert_eq!(res, v); + } + + #[test] + fn test_price_with_equal_weights_is_y_over_x() { + // quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x + let quote = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 2u64; + let y = 5u64; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + let expected_f = (y as f64) / (x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12); + } + + #[test] + fn test_price_scales_with_weight_ratio_two_to_one() { + // Assume base = 1 - quote. + // quote = 1/3 -> base = 2/3, so w1 / w2 = 2. + // Then price = 2 * (y/x). + let quote = Perquintill::from_rational(1u128, 3u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 4u64; + let y = 10u64; + + let price_f = f(bal.calculate_price(x, y)); + let expected_f = 2.0 * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10); + } + + #[test] + fn test_price_is_zero_when_y_is_zero() { + // If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0). + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x = 10u64; + let y = 0u64; + + let price_f = f(bal.calculate_price(x, y)); + assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0); + } + + #[test] + fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() { + // For equal weights, price(x, y) == price(kx, ky). + let quote = Perquintill::from_rational(1u128, 2u128); // 0.5 + let bal = Balancer::new(quote).unwrap(); + + let x1 = 3u64; + let y1 = 7u64; + let k = 10u64; + let x2 = x1 * k; + let y2 = y1 * k; + + let p1 = f(bal.calculate_price(x1, y1)); + let p2 = f(bal.calculate_price(x2, y2)); + + assert_abs_diff_eq!(p1, p2, epsilon = 1e-12); + } + + #[test] + fn test_price_matches_formula_for_general_quote() { + // General check: price = (w1 / w2) * (y/x), + // where w1 = base_weight, w2 = quote_weight. + // Here we assume get_base_weight = 1 - quote. + let quote = Perquintill::from_rational(2u128, 5u128); // 0.4 + let bal = Balancer::new(quote).unwrap(); + + let x = 9u64; + let y = 25u64; + + let price_f = f(bal.calculate_price(x, y)); + + let base = Perquintill::one() - quote; + let w1 = base.deconstruct() as f64; + let w2 = quote.deconstruct() as f64; + + let expected_f = (w1 / w2) * (y as f64 / x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + #[test] + fn test_price_high_values_non_equal_weights() { + // Non-equal weights, high x and y (up to 21e15) + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x: u64 = 21_000_000_000_000_000; + let y: u64 = 15_000_000_000_000_000; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + // Expected: (w1 / w2) * (y / x), using Balancer's actual weights + let w1 = bal.get_base_weight().deconstruct() as f64; + let w2 = bal.get_quote_weight().deconstruct() as f64; + let expected_f = (w1 / w2) * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture + #[test] + fn test_exp_scaled() { + [ + // base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote + (5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999), + (1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644), + (3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644), + ( + 9_u64, + 10_u64, + 13513642_u64, + 1673_u64, + false, + 0.998886481979889, + ), + ( + 773_u64, + 1000_u64, + 7_000_000_000_u64, + 10_000_u64, + true, + 0.999999580484586, + ), + ] + .into_iter() + .map(|v| { + ( + Perquintill::from_rational(v.0, v.1), + v.2, + v.3, + v.4, + U64F64::from_num(v.5), + ) + }) + .for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| { + let balancer = Balancer::new(quote_weight).unwrap(); + let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote); + assert_abs_diff_eq!( + result.to_num::(), + expected.to_num::(), + epsilon = 0.000000001 + ); + }); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture + #[test] + fn test_base_needed_for_quote() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let tao_reserve: u64 = 1_000_000_000; + let alpha_reserve: u64 = 1_000_000_000; + let tao_delta: u64 = 1_123_432; // typical fee range + + let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta); + + // ∆x = x•[(y/(y+∆y))^(w2/w1) - 1] + let dx_expected = tao_reserve as f64 + * ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } +} diff --git a/pallets/swap/src/pallet/hooks.rs b/pallets/swap/src/pallet/hooks.rs new file mode 100644 index 0000000000..90989d5f52 --- /dev/null +++ b/pallets/swap/src/pallet/hooks.rs @@ -0,0 +1,30 @@ +use frame_support::pallet_macros::pallet_section; + +#[pallet_section] +mod hooks { + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(block_number: BlockNumberFor) -> Weight { + Weight::from_parts(0, 0) + } + + fn on_finalize(_block_number: BlockNumberFor) {} + + fn on_runtime_upgrade() -> Weight { + // --- Migrate storage + let mut weight = Weight::from_parts(0, 0); + + weight = weight + // Cleanup uniswap v3 and migrate to balancer + .saturating_add( + migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::(), + ); + weight + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Ok(()) + } + } +} diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 6ec02879bf..8c3fd68209 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,215 +1,136 @@ -use core::ops::Neg; - use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get}; use safe_math::*; -use sp_arithmetic::helpers_128bit; -use sp_runtime::{DispatchResult, Vec, traits::AccountIdConversion}; -use substrate_fixed::types::{I64F64, U64F64, U96F32}; +use sp_arithmetic::{ + //helpers_128bit, + Perquintill, +}; +use sp_runtime::{DispatchResult, traits::AccountIdConversion}; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, + AlphaCurrency, + // BalanceOps, + Currency, + CurrencyReserve, + NetUid, + SubnetInfo, + TaoCurrency, }; use subtensor_swap_interface::{ DefaultPriceLimit, Order as OrderT, SwapEngine, SwapHandler, SwapResult, }; use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; -use crate::{ - SqrtPrice, - position::{Position, PositionId}, - tick::{ActiveTickIndexManager, Tick, TickIndex}, -}; - -const MAX_SWAP_ITERATIONS: u16 = 1000; - -#[derive(Debug, PartialEq)] -pub struct UpdateLiquidityResult { - pub tao: TaoCurrency, - pub alpha: AlphaCurrency, - pub fee_tao: TaoCurrency, - pub fee_alpha: AlphaCurrency, - pub removed: bool, - pub tick_low: TickIndex, - pub tick_high: TickIndex, -} - -#[derive(Debug, PartialEq)] -pub struct RemoveLiquidityResult { - pub tao: TaoCurrency, - pub alpha: AlphaCurrency, - pub fee_tao: TaoCurrency, - pub fee_alpha: AlphaCurrency, - pub tick_low: TickIndex, - pub tick_high: TickIndex, - pub liquidity: u64, -} +use super::swap_step::{BasicSwapStep, SwapStep}; +use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { - pub fn current_price(netuid: NetUid) -> U96F32 { + pub fn current_price(netuid: NetUid) -> U64F64 { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { - if SwapV3Initialized::::get(netuid) { - let sqrt_price = AlphaSqrtPrice::::get(netuid); - U96F32::saturating_from_num(sqrt_price.saturating_mul(sqrt_price)) - } else { + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + if !alpha_reserve.is_zero() { let tao_reserve = T::TaoReserve::reserve(netuid.into()); - let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - if !alpha_reserve.is_zero() { - U96F32::saturating_from_num(tao_reserve) - .saturating_div(U96F32::saturating_from_num(alpha_reserve)) - } else { - U96F32::saturating_from_num(0) - } + let balancer = SwapBalancer::::get(netuid); + balancer.calculate_price(alpha_reserve.into(), tao_reserve.into()) + } else { + U64F64::saturating_from_num(0) } } - _ => U96F32::saturating_from_num(1), + _ => U64F64::saturating_from_num(1), } } - // initializes V3 swap for a subnet if needed - pub fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { - if SwapV3Initialized::::get(netuid) { + // 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(()); } - // Initialize the v3: - // Reserves are re-purposed, nothing to set, just query values for liquidity and price - // calculation + // Query reserves let tao_reserve = T::TaoReserve::reserve(netuid.into()); let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - // Set price - let price = U64F64::saturating_from_num(tao_reserve) - .safe_div(U64F64::saturating_from_num(alpha_reserve)); - - let epsilon = U64F64::saturating_from_num(0.000000000001); - - let current_sqrt_price = price.checked_sqrt(epsilon).unwrap_or(U64F64::from_num(0)); - AlphaSqrtPrice::::set(netuid, current_sqrt_price); - - // Set current tick - let current_tick = TickIndex::from_sqrt_price_bounded(current_sqrt_price); - CurrentTick::::set(netuid, current_tick); - - // Set initial (protocol owned) liquidity and positions - // Protocol liquidity makes one position from TickIndex::MIN to TickIndex::MAX - // We are using the sp_arithmetic sqrt here, which works for u128 - let liquidity = helpers_128bit::sqrt( - (tao_reserve.to_u64() as u128).saturating_mul(alpha_reserve.to_u64() as u128), - ) as u64; - let protocol_account_id = Self::protocol_account_id(); - - let (position, _, _) = Self::add_liquidity_not_insert( - netuid, - &protocol_account_id, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - )?; + // 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()); - Positions::::insert(&(netuid, protocol_account_id, position.id), position); + PalSwapInitialized::::insert(netuid, true); Ok(()) } - pub(crate) fn get_proportional_alpha_tao_and_remainders( - sqrt_alpha_price: U64F64, - amount_tao: TaoCurrency, - amount_alpha: AlphaCurrency, - ) -> (TaoCurrency, AlphaCurrency, TaoCurrency, AlphaCurrency) { - let price = sqrt_alpha_price.saturating_mul(sqrt_alpha_price); - let tao_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_alpha)) - .saturating_mul(price) - .saturating_to_num(); - let amount_tao_u64 = u64::from(amount_tao); - - if tao_equivalent <= amount_tao_u64 { - // Too much or just enough TAO - ( - tao_equivalent.into(), - amount_alpha, - amount_tao.saturating_sub(TaoCurrency::from(tao_equivalent)), - 0.into(), - ) - } else { - // Too much Alpha - let alpha_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_tao)) - .safe_div(price) - .saturating_to_num(); - ( - amount_tao, - alpha_equivalent.into(), - 0.into(), - u64::from(amount_alpha) - .saturating_sub(alpha_equivalent) - .into(), - ) - } - } - /// Adjusts protocol liquidity with new values of TAO and Alpha reserve + /// Returns actually added Tao and Alpha, which includes fees pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ) { - // Update protocol position with new liquidity - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - // Claim protocol fees and add them to liquidity - let (tao_fees, alpha_fees) = position.collect_fees(); - - // Add fee reservoirs and get proportional amounts - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let tao_reservoir = ScrapReservoirTao::::get(netuid); - let alpha_reservoir = ScrapReservoirAlpha::::get(netuid); - let (corrected_tao_delta, corrected_alpha_delta, tao_scrap, alpha_scrap) = - Self::get_proportional_alpha_tao_and_remainders( - current_sqrt_price, - tao_delta - .saturating_add(TaoCurrency::from(tao_fees)) - .saturating_add(tao_reservoir), - alpha_delta - .saturating_add(AlphaCurrency::from(alpha_fees)) - .saturating_add(alpha_reservoir), - ); - - // Update scrap reservoirs - ScrapReservoirTao::::insert(netuid, tao_scrap); - ScrapReservoirAlpha::::insert(netuid, alpha_scrap); - - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, alpha)) = maybe_token_amounts { - // Get updated reserves, calculate liquidity - let new_tao_reserve = tao.saturating_add(corrected_tao_delta.to_u64()); - let new_alpha_reserve = alpha.saturating_add(corrected_alpha_delta.to_u64()); - let new_liquidity = helpers_128bit::sqrt( - (new_tao_reserve as u128).saturating_mul(new_alpha_reserve as u128), - ) as u64; - let liquidity_delta = new_liquidity.saturating_sub(position.liquidity); - - // Update current liquidity - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - *current_liquidity = current_liquidity.saturating_add(liquidity_delta); - }); - - // Update protocol position - position.liquidity = new_liquidity; - Positions::::insert( - (netuid, protocol_account_id, position.id), - position.clone(), - ); - - // Update position ticks - Self::add_liquidity_at_index(netuid, position.tick_low, liquidity_delta, false); - Self::add_liquidity_at_index(netuid, position.tick_high, liquidity_delta, true); - } + ) -> (TaoCurrency, AlphaCurrency) { + // Collect fees + let tao_fees = FeesTao::::get(netuid); + let alpha_fees = FeesAlpha::::get(netuid); + FeesTao::::insert(netuid, TaoCurrency::ZERO); + FeesAlpha::::insert(netuid, AlphaCurrency::ZERO); + let actual_tao_delta = tao_delta.saturating_add(tao_fees); + let actual_alpha_delta = alpha_delta.saturating_add(alpha_fees); + + // Get reserves + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let mut balancer = SwapBalancer::::get(netuid); + + // Update weights and log errors if they go out of range + if balancer + .update_weights_for_added_liquidity( + u64::from(tao_reserve), + u64::from(alpha_reserve), + u64::from(actual_tao_delta), + u64::from(actual_alpha_delta), + ) + .is_err() + { + log::error!( + "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}", + netuid, + tao_reserve, + alpha_reserve, + actual_tao_delta, + actual_alpha_delta + ); + // Return fees back into fee storage and return zeroes + FeesTao::::insert(netuid, tao_fees); + FeesAlpha::::insert(netuid, alpha_fees); + (TaoCurrency::ZERO, AlphaCurrency::ZERO) + } else { + SwapBalancer::::insert(netuid, balancer); + (actual_tao_delta, actual_alpha_delta) } } @@ -240,7 +161,7 @@ impl Pallet { pub(crate) fn do_swap( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, simulate: bool, ) -> Result, DispatchError> @@ -251,7 +172,7 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = Self::swap_inner::(netuid, order, limit_sqrt_price, drop_fees) + let result = Self::swap_inner::(netuid, order, limit_price, drop_fees) .map_err(Into::into); if simulate || result.is_err() { @@ -277,7 +198,7 @@ impl Pallet { fn swap_inner( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Result, Error> where @@ -289,70 +210,36 @@ impl Pallet { Error::::ReservesTooLow ); - Self::maybe_initialize_v3(netuid)?; + 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(AlphaSqrtPrice::::get(netuid), limit_sqrt_price), + order.is_beyond_price_limit(Self::current_price(netuid), limit_price), Error::::PriceLimitExceeded ); - let mut amount_remaining = order.amount(); - let mut amount_paid_out = Order::PaidOut::ZERO; - let mut iteration_counter: u16 = 0; - let mut in_acc = Order::PaidIn::ZERO; - let mut fee_acc = Order::PaidIn::ZERO; - log::trace!("======== Start Swap ========"); - log::trace!("Amount Remaining: {amount_remaining}"); - - // Swap one tick at a time until we reach one of the stop conditions - while !amount_remaining.is_zero() { - log::trace!("\nIteration: {iteration_counter}"); - log::trace!( - "\tCurrent Liquidity: {}", - CurrentLiquidity::::get(netuid) - ); - - // Create and execute a swap step - let mut swap_step = BasicSwapStep::::new( - netuid, - amount_remaining, - limit_sqrt_price, - drop_fees, - ); - - let swap_result = swap_step.execute()?; - - in_acc = in_acc.saturating_add(swap_result.delta_in); - fee_acc = fee_acc.saturating_add(swap_result.fee_paid); - amount_remaining = amount_remaining.saturating_sub(swap_result.amount_to_take); - amount_paid_out = amount_paid_out.saturating_add(swap_result.delta_out); - - if swap_step.action() == SwapStepAction::Stop { - amount_remaining = Order::PaidIn::ZERO; - } + let amount_to_swap = order.amount(); + log::trace!("Amount to swap: {amount_to_swap}"); - // The swap step didn't exchange anything - if swap_result.amount_to_take.is_zero() { - amount_remaining = Order::PaidIn::ZERO; - } - - iteration_counter = iteration_counter.saturating_add(1); + // Create and execute a swap step + let mut swap_step = BasicSwapStep::::new( + netuid, + amount_to_swap, + limit_price, + drop_fees, + ); - ensure!( - iteration_counter <= MAX_SWAP_ITERATIONS, - Error::::TooManySwapSteps - ); - } + let swap_result = swap_step.execute()?; - log::trace!("\nAmount Paid Out: {amount_paid_out}"); + log::trace!("Delta out: {}", swap_result.delta_out); + log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); Ok(SwapResult { - amount_paid_in: in_acc, - amount_paid_out, - fee_paid: fee_acc, + amount_paid_in: swap_result.delta_in, + amount_paid_out: swap_result.delta_out, + fee_paid: swap_result.fee_paid, }) } @@ -381,425 +268,12 @@ impl Pallet { } } - pub fn find_closest_lower_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - pub fn find_closest_higher_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - /// Here we subtract minimum safe liquidity from current liquidity to stay in the safe range - pub(crate) fn current_liquidity_safe(netuid: NetUid) -> U64F64 { - U64F64::saturating_from_num( - CurrentLiquidity::::get(netuid).saturating_sub(T::MinimumLiquidity::get()), - ) - } - - /// Adds liquidity to the specified price range. - /// - /// This function allows an account to provide liquidity to a given range of price ticks. The - /// amount of liquidity to be added can be determined using - /// [`get_tao_based_liquidity`] and [`get_alpha_based_liquidity`], which compute the required - /// liquidity based on TAO and Alpha balances for the current price tick. - /// - /// ### Behavior: - /// - If the `protocol` flag is **not set** (`false`), the function will attempt to - /// **withdraw balances** from the account using `state_ops.withdraw_balances()`. - /// - If the `protocol` flag is **set** (`true`), the liquidity is added without modifying balances. - /// - If swap V3 was not initialized before, updates the value in storage. - /// - /// ### Parameters: - /// - `coldkey_account_id`: A reference to the account coldkey that is providing liquidity. - /// - `hotkey_account_id`: A reference to the account hotkey that is providing liquidity. - /// - `tick_low`: The lower bound of the price tick range. - /// - `tick_high`: The upper bound of the price tick range. - /// - `liquidity`: The amount of liquidity to be added. - /// - /// ### Returns: - /// - `Ok((u64, u64))`: (tao, alpha) amounts at new position - /// - `Err(SwapError)`: If the operation fails due to insufficient balance, invalid tick range, - /// or other swap-related errors. - /// - /// ### Errors: - /// - [`SwapError::InsufficientBalance`] if the account does not have enough balance. - /// - [`SwapError::InvalidTickRange`] if `tick_low` is greater than or equal to `tick_high`. - /// - Other [`SwapError`] variants as applicable. - pub fn do_add_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(PositionId, u64, u64), Error> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - let (position, tao, alpha) = Self::add_liquidity_not_insert( - netuid, - coldkey_account_id, - tick_low, - tick_high, - liquidity, - )?; - let position_id = position.id; - - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) >= TaoCurrency::from(tao) - && T::BalanceOps::alpha_balance( - netuid.into(), - coldkey_account_id, - hotkey_account_id - ) >= AlphaCurrency::from(alpha), - Error::::InsufficientBalance - ); - - // Small delta is not allowed - ensure!( - liquidity >= T::MinimumLiquidity::get(), - Error::::InvalidLiquidityValue - ); - - Positions::::insert(&(netuid, coldkey_account_id, position.id), position); - - Ok((position_id, tao, alpha)) - } - - // add liquidity without inserting position into storage (used privately for v3 intiialization). - // unlike Self::add_liquidity it also doesn't perform account's balance check. - // - // the public interface is [`Self::add_liquidity`] - fn add_liquidity_not_insert( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(Position, u64, u64), Error> { - ensure!( - Self::count_positions(netuid, coldkey_account_id) < T::MaxPositions::get() as usize, - Error::::MaxPositionsExceeded - ); - - // Ensure that tick_high is actually higher than tick_low - ensure!(tick_high > tick_low, Error::::InvalidTickRange); - - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, tick_low, liquidity, false); - Self::add_liquidity_at_index(netuid, tick_high, liquidity, true); - - // Update current tick liquidity - let current_tick_index = TickIndex::current_bounded::(netuid); - Self::clamp_sqrt_price(netuid, current_tick_index); - - Self::update_liquidity_if_needed(netuid, tick_low, tick_high, liquidity as i128); - - // New position - let position_id = PositionId::new::(); - let position = Position::new(position_id, netuid, tick_low, tick_high, liquidity); - - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price_sqrt)?; - - SwapV3Initialized::::set(netuid, true); - - Ok((position, tao, alpha)) - } - - /// Remove liquidity and credit balances back to (coldkey_account_id, hotkey_account_id) stake. - /// Removing is allowed even when user liquidity is enabled. - /// - /// Account ID and Position ID identify position in the storage map - pub fn do_remove_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - position_id: PositionId, - ) -> Result> { - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Collect fees and get tao and alpha amounts - let (fee_tao, fee_alpha) = position.collect_fees(); - let current_price = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price)?; - - // Update liquidity at position ticks - Self::remove_liquidity_at_index(netuid, position.tick_low, position.liquidity, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, position.liquidity, true); - - // Update current tick liquidity - Self::update_liquidity_if_needed( - netuid, - position.tick_low, - position.tick_high, - (position.liquidity as i128).neg(), - ); - - // Remove user position - Positions::::remove((netuid, coldkey_account_id, position_id)); - - Ok(RemoveLiquidityResult { - tao: tao.into(), - alpha: alpha.into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - tick_low: position.tick_low, - tick_high: position.tick_high, - liquidity: position.liquidity, - }) - } - - pub fn do_modify_position( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - position_id: PositionId, - liquidity_delta: i64, - ) -> Result> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - // Find the position - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Small delta is not allowed - ensure!( - liquidity_delta.abs() >= T::MinimumLiquidity::get() as i64, - Error::::InvalidLiquidityValue - ); - let mut delta_liquidity_abs = liquidity_delta.unsigned_abs(); - - // Determine the effective price for token calculations - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let sqrt_pa: SqrtPrice = position - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_pb: SqrtPrice = position - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_box = if current_price_sqrt < sqrt_pa { - sqrt_pa - } else if current_price_sqrt > sqrt_pb { - sqrt_pb - } else { - // Update current liquidity if price is in range - let new_liquidity_curr = if liquidity_delta > 0 { - CurrentLiquidity::::get(netuid).saturating_add(delta_liquidity_abs) - } else { - CurrentLiquidity::::get(netuid).saturating_sub(delta_liquidity_abs) - }; - CurrentLiquidity::::set(netuid, new_liquidity_curr); - current_price_sqrt - }; - - // Calculate token amounts for the liquidity change - let mul = SqrtPrice::from_num(1) - .safe_div(sqrt_price_box) - .saturating_sub(SqrtPrice::from_num(1).safe_div(sqrt_pb)); - let alpha = SqrtPrice::saturating_from_num(delta_liquidity_abs).saturating_mul(mul); - let tao = SqrtPrice::saturating_from_num(delta_liquidity_abs) - .saturating_mul(sqrt_price_box.saturating_sub(sqrt_pa)); - - // Validate delta - if liquidity_delta > 0 { - // Check that user has enough balances - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) - >= TaoCurrency::from(tao.saturating_to_num::()) - && T::BalanceOps::alpha_balance(netuid, coldkey_account_id, hotkey_account_id) - >= AlphaCurrency::from(alpha.saturating_to_num::()), - Error::::InsufficientBalance - ); - } else { - // Check that position has enough liquidity - ensure!( - position.liquidity >= delta_liquidity_abs, - Error::::InsufficientLiquidity - ); - } - - // Collect fees - let (fee_tao, fee_alpha) = position.collect_fees(); - - // If delta brings the position liquidity below MinimumLiquidity, eliminate position and - // withdraw full amounts - let mut remove = false; - if (liquidity_delta < 0) - && (position.liquidity.saturating_sub(delta_liquidity_abs) < T::MinimumLiquidity::get()) - { - delta_liquidity_abs = position.liquidity; - remove = true; - } - - // Adjust liquidity at the ticks based on the delta sign - if liquidity_delta > 0 { - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::add_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Add liquidity to user position - position.liquidity = position.liquidity.saturating_add(delta_liquidity_abs); - } else { - // Remove liquidity at tick - Self::remove_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Remove liquidity from user position - position.liquidity = position.liquidity.saturating_sub(delta_liquidity_abs); - } - - // Update or, in case if full liquidity is removed, remove the position - if remove { - Positions::::remove((netuid, coldkey_account_id, position_id)); - } else { - Positions::::insert(&(netuid, coldkey_account_id, position.id), position.clone()); - } - - Ok(UpdateLiquidityResult { - tao: tao.saturating_to_num::().into(), - alpha: alpha.saturating_to_num::().into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - removed: remove, - tick_low: position.tick_low, - tick_high: position.tick_high, - }) - } - - /// Adds or updates liquidity at a specific tick index for a subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `tick_index` - The tick index to add liquidity to - /// * `liquidity` - The amount of liquidity to add - fn add_liquidity_at_index(netuid: NetUid, tick_index: TickIndex, liquidity: u64, upper: bool) { - // Convert liquidity to signed value, negating it for upper bounds - let net_liquidity_change = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate(netuid, tick_index, |maybe_tick| match maybe_tick { - Some(tick) => { - tick.liquidity_net = tick.liquidity_net.saturating_add(net_liquidity_change); - tick.liquidity_gross = tick.liquidity_gross.saturating_add(liquidity); - } - None => { - let current_tick = TickIndex::current_bounded::(netuid); - - let (fees_out_tao, fees_out_alpha) = if tick_index > current_tick { - ( - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)), - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)), - ) - } else { - ( - I64F64::saturating_from_num(0), - I64F64::saturating_from_num(0), - ) - }; - *maybe_tick = Some(Tick { - liquidity_net: net_liquidity_change, - liquidity_gross: liquidity, - fees_out_tao, - fees_out_alpha, - }); - } - }); - - // Update active ticks - ActiveTickIndexManager::::insert(netuid, tick_index); - } - - /// Remove liquidity at tick index. - fn remove_liquidity_at_index( - netuid: NetUid, - tick_index: TickIndex, - liquidity: u64, - upper: bool, - ) { - // Calculate net liquidity addition - let net_reduction = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate_exists(netuid, tick_index, |maybe_tick| { - if let Some(tick) = maybe_tick { - tick.liquidity_net = tick.liquidity_net.saturating_sub(net_reduction); - tick.liquidity_gross = tick.liquidity_gross.saturating_sub(liquidity); - - // If no liquidity is left at the tick, remove it - if tick.liquidity_gross == 0 { - *maybe_tick = None; - - // Update active ticks: Final liquidity is zero, remove this tick from active. - ActiveTickIndexManager::::remove(netuid, tick_index); - } - } - }); - } - - /// Updates the current liquidity for a subnet if the current tick index is within the specified - /// range - /// - /// This function handles both increasing and decreasing liquidity based on the sign of the - /// liquidity parameter. It uses i128 to safely handle values up to u64::MAX in both positive - /// and negative directions. - fn update_liquidity_if_needed( - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: i128, - ) { - let current_tick_index = TickIndex::current_bounded::(netuid); - if (tick_low <= current_tick_index) && (current_tick_index < tick_high) { - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - let is_neg = liquidity.is_negative(); - let liquidity = liquidity.abs().min(u64::MAX as i128) as u64; - if is_neg { - *current_liquidity = current_liquidity.saturating_sub(liquidity); - } else { - *current_liquidity = current_liquidity.saturating_add(liquidity); - } - }); - } - } - - /// Clamps the subnet's sqrt price when tick index is outside of valid bounds - fn clamp_sqrt_price(netuid: NetUid, tick_index: TickIndex) { - if tick_index >= TickIndex::MAX || tick_index <= TickIndex::MIN { - let corrected_price = tick_index.as_sqrt_price_bounded(); - AlphaSqrtPrice::::set(netuid, corrected_price); - } + pub(crate) fn min_price_inner() -> C { + u64::from(1_000_u64).into() } - /// Returns the number of positions for an account in a specific subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `account_id` - The account ID - /// - /// # Returns - /// The number of positions that the account has in the specified subnet - pub(super) fn count_positions(netuid: NetUid, account_id: &T::AccountId) -> usize { - Positions::::iter_prefix_values((netuid, account_id.clone())).count() + pub(crate) fn max_price_inner() -> C { + u64::from(1_000_000_000_000_000_u64).into() } /// Returns the protocol account ID @@ -810,207 +284,22 @@ impl Pallet { T::ProtocolId::get().into_account_truncating() } - pub(crate) fn min_price_inner() -> C { - TickIndex::min_sqrt_price() - .saturating_mul(TickIndex::min_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_to_num::() - .into() - } - - pub(crate) fn max_price_inner() -> C { - TickIndex::max_sqrt_price() - .saturating_mul(TickIndex::max_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_round() - .saturating_to_num::() - .into() - } - - /// Dissolve all LPs and clean state. - pub fn do_dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { - if SwapV3Initialized::::get(netuid) { - // 1) Snapshot only *non‑protocol* positions: (owner, position_id). - struct CloseItem { - owner: A, - pos_id: PositionId, - } - let protocol_account = Self::protocol_account_id(); - - let mut to_close: sp_std::vec::Vec> = sp_std::vec::Vec::new(); - for ((owner, pos_id), _pos) in Positions::::iter_prefix((netuid,)) { - if owner != protocol_account { - to_close.push(CloseItem { owner, pos_id }); - } - } - - if to_close.is_empty() { - log::debug!( - "dissolve_all_lp: no user positions; netuid={netuid:?}, protocol liquidity untouched" - ); - return Ok(()); - } - - let mut user_refunded_tao = TaoCurrency::ZERO; - let mut user_staked_alpha = AlphaCurrency::ZERO; - - let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); - let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); - - // Helper: pick target validator uid, only among permitted validators, by highest trust. - let pick_target_uid = |trust: &Vec, permit: &Vec| -> Option { - let mut best_uid: Option = None; - let mut best_trust: u16 = 0; - for (i, (&t, &p)) in trust.iter().zip(permit.iter()).enumerate() { - if p && (best_uid.is_none() || t > best_trust) { - best_uid = Some(i); - best_trust = t; - } - } - best_uid.map(|i| i as u16) - }; - - for CloseItem { owner, pos_id } in to_close.into_iter() { - match Self::do_remove_liquidity(netuid, &owner, pos_id) { - Ok(rm) => { - // α withdrawn from the pool = principal + accrued fees - let alpha_total_from_pool: AlphaCurrency = - rm.alpha.saturating_add(rm.fee_alpha); - - // ---------------- USER: refund τ and convert α → stake ---------------- - - // 1) Refund τ principal directly. - let tao_total_from_pool: TaoCurrency = rm.tao.saturating_add(rm.fee_tao); - if tao_total_from_pool > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&owner, tao_total_from_pool); - user_refunded_tao = - user_refunded_tao.saturating_add(tao_total_from_pool); - T::TaoReserve::decrease_provided(netuid, tao_total_from_pool); - } - - // 2) Stake ALL withdrawn α (principal + fees) to the best permitted validator. - if alpha_total_from_pool > AlphaCurrency::ZERO { - if let Some(target_uid) = pick_target_uid(&trust, &permit) { - let validator_hotkey: T::AccountId = - T::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid).ok_or( - sp_runtime::DispatchError::Other( - "validator_hotkey_missing", - ), - )?; - - // Stake α from LP owner (coldkey) to chosen validator (hotkey). - T::BalanceOps::increase_stake( - &owner, - &validator_hotkey, - netuid, - alpha_total_from_pool, - )?; - - user_staked_alpha = - user_staked_alpha.saturating_add(alpha_total_from_pool); - - log::debug!( - "dissolve_all_lp: user dissolved & staked α: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_staked={alpha_total_from_pool:?}, target_uid={target_uid}" - ); - } else { - // No permitted validators; burn to avoid balance drift. - log::debug!( - "dissolve_all_lp: no permitted validators; α burned: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_total={alpha_total_from_pool:?}" - ); - } - - T::AlphaReserve::decrease_provided(netuid, alpha_total_from_pool); - } - } - Err(e) => { - log::debug!( - "dissolve_all_lp: force-close failed: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; - } - } - } - - log::debug!( - "dissolve_all_liquidity_providers (users-only): netuid={netuid:?}, users_refunded_total_τ={user_refunded_tao:?}, users_staked_total_α={user_staked_alpha:?}; protocol liquidity untouched" - ); - - return Ok(()); - } - - log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, leaving all liquidity/state intact" - ); - - Ok(()) - } - /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - let protocol_account = Self::protocol_account_id(); + // let protocol_account = Self::protocol_account_id(); - // 1) Force-close only protocol positions, burning proceeds. - let mut burned_tao = TaoCurrency::ZERO; - let mut burned_alpha = AlphaCurrency::ZERO; + // 1) Force-close protocol liquidity, burning proceeds. + let burned_tao = T::TaoReserve::reserve(netuid.into()); + let burned_alpha = T::AlphaReserve::reserve(netuid.into()); - // Collect protocol position IDs first to avoid mutating while iterating. - let protocol_pos_ids: sp_std::vec::Vec = Positions::::iter_prefix((netuid,)) - .filter_map(|((owner, pos_id), _)| { - if owner == protocol_account { - Some(pos_id) - } else { - None - } - }) - .collect(); - - for pos_id in protocol_pos_ids { - match Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { - Ok(rm) => { - let alpha_total_from_pool: AlphaCurrency = - rm.alpha.saturating_add(rm.fee_alpha); - let tao_total_from_pool: TaoCurrency = rm.tao.saturating_add(rm.fee_tao); - - if tao_total_from_pool > TaoCurrency::ZERO { - burned_tao = burned_tao.saturating_add(tao_total_from_pool); - } - if alpha_total_from_pool > AlphaCurrency::ZERO { - burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); - } - - log::debug!( - "clear_protocol_liquidity: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao_total_from_pool:?}, α_total={alpha_total_from_pool:?}" - ); - } - Err(e) => { - log::debug!( - "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; - } - } - } + T::TaoReserve::decrease_provided(netuid.into(), burned_tao); + T::AlphaReserve::decrease_provided(netuid.into(), burned_alpha); - // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); - for ti in active_ticks { - ActiveTickIndexManager::::remove(netuid, ti); - } - - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); - - FeeGlobalTao::::remove(netuid); - FeeGlobalAlpha::::remove(netuid); - CurrentLiquidity::::remove(netuid); - CurrentTick::::remove(netuid); - AlphaSqrtPrice::::remove(netuid); - SwapV3Initialized::::remove(netuid); - - let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); + FeesTao::::remove(netuid); + FeesAlpha::::remove(netuid); + PalSwapInitialized::::remove(netuid); FeeRate::::remove(netuid); - EnabledUserLiquidity::::remove(netuid); + SwapBalancer::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" @@ -1046,15 +335,13 @@ where drop_fees: bool, should_rollback: bool, ) -> Result, DispatchError> { - let limit_sqrt_price = SqrtPrice::saturating_from_num(price_limit.to_u64()) - .safe_div(SqrtPrice::saturating_from_num(1_000_000_000)) - .checked_sqrt(SqrtPrice::saturating_from_num(0.0000000001)) - .ok_or(Error::::PriceLimitExceeded)?; + 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_sqrt_price, + limit_price, drop_fees, should_rollback, ) @@ -1110,28 +397,10 @@ impl SwapHandler for Pallet { Self::calculate_fee_amount(netuid, amount, false) } - fn current_alpha_price(netuid: NetUid) -> U96F32 { + fn current_alpha_price(netuid: NetUid) -> U64F64 { Self::current_price(netuid.into()) } - fn get_protocol_tao(netuid: NetUid) -> TaoCurrency { - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, _)) = maybe_token_amounts { - return tao.into(); - } - } - - TaoCurrency::ZERO - } - fn min_price() -> C { Self::min_price_inner() } @@ -1144,20 +413,14 @@ impl SwapHandler for Pallet { netuid: NetUid, tao_delta: TaoCurrency, alpha_delta: AlphaCurrency, - ) { - Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + ) -> (TaoCurrency, AlphaCurrency) { + Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } - fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - EnabledUserLiquidity::::get(netuid) - } - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { - Self::do_dissolve_all_liquidity_providers(netuid) - } - fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { - EnabledUserLiquidity::::insert(netuid, enabled) - } fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + fn init_swap(netuid: NetUid, maybe_price: Option) { + Self::maybe_initialize_palswap(netuid, maybe_price).unwrap_or_default(); + } } diff --git a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs new file mode 100644 index 0000000000..147849e5a6 --- /dev/null +++ b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs @@ -0,0 +1,81 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{storage_alias, traits::Get, weights::Weight}; +use scale_info::prelude::string::String; +use substrate_fixed::types::U64F64; + +pub mod deprecated_swap_maps { + use super::*; + + #[storage_alias] + pub type AlphaSqrtPrice = + StorageMap, Twox64Concat, NetUid, U64F64, ValueQuery>; + + /// TAO reservoir for scraps of protocol claimed fees. + #[storage_alias] + pub type ScrapReservoirTao = + StorageMap, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; + + /// Alpha reservoir for scraps of protocol claimed fees. + #[storage_alias] + pub type ScrapReservoirAlpha = + StorageMap, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; +} + +pub fn migrate_swapv3_to_balancer() -> Weight { + let migration_name = BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // ------------------------------ + // Step 1: Initialize swaps with price before price removal + // ------------------------------ + for (netuid, price_sqrt) in deprecated_swap_maps::AlphaSqrtPrice::::iter() { + let price = price_sqrt.saturating_mul(price_sqrt); + crate::Pallet::::maybe_initialize_palswap(netuid, Some(price)).unwrap_or_default(); + } + + // ------------------------------ + // Step 2: Clear Map entries + // ------------------------------ + remove_prefix::("Swap", "AlphaSqrtPrice", &mut weight); + remove_prefix::("Swap", "CurrentTick", &mut weight); + remove_prefix::("Swap", "EnabledUserLiquidity", &mut weight); + remove_prefix::("Swap", "FeeGlobalTao", &mut weight); + remove_prefix::("Swap", "FeeGlobalAlpha", &mut weight); + remove_prefix::("Swap", "LastPositionId", &mut weight); + // Scrap reservoirs can be just cleaned because they are already included in reserves + remove_prefix::("Swap", "ScrapReservoirTao", &mut weight); + remove_prefix::("Swap", "ScrapReservoirAlpha", &mut weight); + remove_prefix::("Swap", "Ticks", &mut weight); + remove_prefix::("Swap", "TickIndexBitmapWords", &mut weight); + remove_prefix::("Swap", "SwapV3Initialized", &mut weight); + remove_prefix::("Swap", "CurrentLiquidity", &mut weight); + remove_prefix::("Swap", "Positions", &mut weight); + + // ------------------------------ + // Step 3: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/swap/src/pallet/migrations/mod.rs b/pallets/swap/src/pallet/migrations/mod.rs new file mode 100644 index 0000000000..d34626f05e --- /dev/null +++ b/pallets/swap/src/pallet/migrations/mod.rs @@ -0,0 +1,25 @@ +use super::*; +use frame_support::pallet_prelude::Weight; +use sp_io::KillStorageResult; +use sp_io::hashing::twox_128; +use sp_io::storage::clear_prefix; +use sp_std::vec::Vec; + +pub mod migrate_swapv3_to_balancer; + +pub(crate) fn remove_prefix(module: &str, old_map: &str, weight: &mut Weight) { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&twox_128(module.as_bytes())); + prefix.extend_from_slice(&twox_128(old_map.as_bytes())); + + let removal_results = clear_prefix(&prefix, Some(u32::MAX)); + let removed_entries_count = match removal_results { + KillStorageResult::AllRemoved(removed) => removed as u64, + KillStorageResult::SomeRemaining(removed) => { + log::info!("Failed To Remove Some Items During migration"); + removed as u64 + } + }; + + *weight = (*weight).saturating_add(T::DbWeight::get().writes(removed_entries_count)); +} diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index b55df77fee..842ba8697c 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -1,32 +1,33 @@ use core::num::NonZeroU64; -use core::ops::Neg; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; -use substrate_fixed::types::U64F64; +// use safe_math::SafeDiv; use subtensor_runtime_common::{ - AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, -}; - -use crate::{ - position::{Position, PositionId}, - tick::{LayerLevel, Tick, TickIndex}, - weights::WeightInfo, + AlphaCurrency, BalanceOps, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, }; +use crate::{pallet::balancer::Balancer, weights::WeightInfo}; pub use pallet::*; +use subtensor_macros::freeze_struct; +mod balancer; +mod hooks; mod impls; +pub mod migrations; mod swap_step; #[cfg(test)] mod tests; +// Define a maximum length for the migration key +type MigrationKeyMaxLen = ConstU32<128>; + #[allow(clippy::module_inception)] #[frame_support::pallet] #[allow(clippy::expect_used)] mod pallet { use super::*; - use frame_system::{ensure_root, ensure_signed}; + use frame_system::ensure_root; #[pallet::pallet] pub struct Pallet(_); @@ -56,10 +57,6 @@ mod pallet { #[pallet::constant] type MaxFeeRate: Get; - /// The maximum number of positions a user can have - #[pallet::constant] - type MaxPositions: Get; - /// Minimum liquidity that is safe for rounding and integer math. #[pallet::constant] type MinimumLiquidity: Get; @@ -82,164 +79,41 @@ mod pallet { #[pallet::storage] pub type FeeRate = StorageMap<_, Twox64Concat, NetUid, u16, ValueQuery, DefaultFeeRate>; - // Global accrued fees in tao per subnet - #[pallet::storage] - pub type FeeGlobalTao = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - // Global accrued fees in alpha per subnet - #[pallet::storage] - pub type FeeGlobalAlpha = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for all ticks, using subnet ID as the primary key and tick index as the secondary key - #[pallet::storage] - pub type Ticks = StorageDoubleMap<_, Twox64Concat, NetUid, Twox64Concat, TickIndex, Tick>; + //////////////////////////////////////////////////// + // Balancer (PalSwap) maps and variables - /// Storage to determine whether swap V3 was initialized for a specific subnet. - #[pallet::storage] - pub type SwapV3Initialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; - - /// Storage for the square root price of Alpha token for each subnet. - #[pallet::storage] - pub type AlphaSqrtPrice = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for the current price tick. - #[pallet::storage] - pub type CurrentTick = StorageMap<_, Twox64Concat, NetUid, TickIndex, ValueQuery>; - - /// Storage for the current liquidity amount for each subnet. + /// Default reserve weight + #[pallet::type_value] + pub fn DefaultBalancer() -> Balancer { + Balancer::default() + } + /// u64-normalized reserve weight #[pallet::storage] - pub type CurrentLiquidity = StorageMap<_, Twox64Concat, NetUid, u64, ValueQuery>; + pub type SwapBalancer = + StorageMap<_, Twox64Concat, NetUid, Balancer, ValueQuery, DefaultBalancer>; - /// Indicates whether a subnet has been switched to V3 swap from V2. - /// If `true`, the subnet is permanently on V3 swap mode allowing add/remove liquidity - /// operations. Once set to `true` for a subnet, it cannot be changed back to `false`. + /// Storage to determine whether balancer swap was initialized for a specific subnet. #[pallet::storage] - pub type EnabledUserLiquidity = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; - /// Storage for user positions, using subnet ID and account ID as keys - /// The value is a bounded vector of Position structs with details about the liquidity positions - #[pallet::storage] - pub type Positions = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Account ID - NMapKey, // Position ID - ), - Position, - OptionQuery, - >; - - /// Position ID counter. + /// Total fees in TAO per subnet due to be paid to users / protocol #[pallet::storage] - pub type LastPositionId = StorageValue<_, u128, ValueQuery>; + pub type FeesTao = StorageMap<_, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; - /// Tick index bitmap words storage + /// Total fees in Alpha per subnet due to be paid to users / protocol #[pallet::storage] - pub type TickIndexBitmapWords = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Layer level - NMapKey, // word index - ), - u128, - ValueQuery, - >; - - /// TAO reservoir for scraps of protocol claimed fees. - #[pallet::storage] - pub type ScrapReservoirTao = StorageMap<_, Twox64Concat, NetUid, TaoCurrency, ValueQuery>; + pub type FeesAlpha = StorageMap<_, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; - /// Alpha reservoir for scraps of protocol claimed fees. + /// --- Storage for migration run status #[pallet::storage] - pub type ScrapReservoirAlpha = - StorageMap<_, Twox64Concat, NetUid, AlphaCurrency, ValueQuery>; + pub type HasMigrationRun = + StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Event emitted when the fee rate has been updated for a subnet FeeRateSet { netuid: NetUid, rate: u16 }, - - /// Event emitted when user liquidity operations are enabled for a subnet. - /// First enable even indicates a switch from V2 to V3 swap. - UserLiquidityToggled { netuid: NetUid, enable: bool }, - - /// Event emitted when a liquidity position is added to a subnet's liquidity pool. - LiquidityAdded { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to the position - liquidity: u64, - /// The amount of TAO tokens committed to the position - tao: TaoCurrency, - /// The amount of Alpha tokens committed to the position - alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is removed from a subnet's liquidity pool. - LiquidityRemoved { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity removed from the position - liquidity: u64, - /// The amount of TAO tokens returned to the user - tao: TaoCurrency, - /// The amount of Alpha tokens returned to the user - alpha: AlphaCurrency, - /// The amount of TAO fees earned from the position - fee_tao: TaoCurrency, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is modified in a subnet's liquidity pool. - /// Modifying causes the fees to be claimed. - LiquidityModified { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from or goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to or removed from the position - liquidity: i64, - /// The amount of TAO tokens returned to the user - tao: i64, - /// The amount of Alpha tokens returned to the user - alpha: i64, - /// The amount of TAO fees earned from the position - fee_tao: TaoCurrency, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaCurrency, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, } #[pallet::error] @@ -260,18 +134,9 @@ mod pallet { /// The caller does not have enough balance for the operation. InsufficientBalance, - /// Attempted to remove liquidity that does not exist. - LiquidityNotFound, - /// The provided tick range is invalid. InvalidTickRange, - /// Maximum user positions exceeded - MaxPositionsExceeded, - - /// Too many swap steps - TooManySwapSteps, - /// Provided liquidity parameter is invalid (likely too small) InvalidLiquidityValue, @@ -281,11 +146,14 @@ mod pallet { /// The subnet does not exist. MechanismDoesNotExist, - /// User liquidity operations are disabled for this subnet - UserLiquidityDisabled, - /// The subnet does not have subtoken enabled SubtokenDisabled, + + /// Swap reserves are too imbalanced + ReservesOutOfBalance, + + /// The extrinsic is deprecated + Deprecated, } #[pallet::call] @@ -316,315 +184,103 @@ mod pallet { Ok(()) } - /// Enable user liquidity operations for a specific subnet. This switches the - /// subnet from V2 to V3 swap mode. Thereafter, adding new user liquidity can be disabled - /// by toggling this flag to false, but the swap mode will remain V3 because of existing - /// user liquidity until all users withdraw their liquidity. - /// - /// Only sudo or subnet owner can enable user liquidity. - /// Only sudo can disable user liquidity. + /// DEPRECATED #[pallet::call_index(4)] - #[pallet::weight(::WeightInfo::toggle_user_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn toggle_user_liquidity( - origin: OriginFor, - netuid: NetUid, - enable: bool, + _origin: OriginFor, + _netuid: NetUid, + _enable: bool, ) -> DispatchResult { - if ensure_root(origin.clone()).is_err() { - let account_id: T::AccountId = ensure_signed(origin)?; - // Only enabling is allowed to subnet owner - ensure!( - T::SubnetInfo::is_owner(&account_id, netuid.into()) && enable, - DispatchError::BadOrigin - ); - } - - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - // EnabledUserLiquidity::::insert(netuid, enable); - - // Self::deposit_event(Event::UserLiquidityToggled { netuid, enable }); - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Add liquidity to a specific price range for a subnet. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - tick_low: Lower bound of the price range - /// - tick_high: Upper bound of the price range - /// - liquidity: Amount of liquidity to add - /// - /// Emits `Event::LiquidityAdded` on success + /// DEPRECATED #[pallet::call_index(1)] - #[pallet::weight(::WeightInfo::add_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn add_liquidity( - origin: OriginFor, + _origin: OriginFor, _hotkey: T::AccountId, _netuid: NetUid, _tick_low: TickIndex, _tick_high: TickIndex, _liquidity: u64, ) -> DispatchResult { - ensure_signed(origin)?; - - // Extrinsic should have no effect. This fix may have to be reverted later, - // so leaving the code in for now. - - // // Ensure that the subnet exists. - // ensure!( - // T::SubnetInfo::exists(netuid.into()), - // Error::::MechanismDoesNotExist - // ); - - // ensure!( - // T::SubnetInfo::is_subtoken_enabled(netuid.into()), - // Error::::SubtokenDisabled - // ); - - // let (position_id, tao, alpha) = Self::do_add_liquidity( - // netuid.into(), - // &coldkey, - // &hotkey, - // tick_low, - // tick_high, - // liquidity, - // )?; - // let alpha = AlphaCurrency::from(alpha); - // let tao = TaoCurrency::from(tao); - - // // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - // let tao_provided = T::BalanceOps::decrease_balance(&coldkey, tao)?; - // ensure!(tao_provided == tao, Error::::InsufficientBalance); - - // let alpha_provided = - // T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; - // ensure!(alpha_provided == alpha, Error::::InsufficientBalance); - - // // Add provided liquidity to user-provided reserves - // T::TaoReserve::increase_provided(netuid.into(), tao_provided); - // T::AlphaReserve::increase_provided(netuid.into(), alpha_provided); - - // // Emit an event - // Self::deposit_event(Event::LiquidityAdded { - // coldkey, - // hotkey, - // netuid, - // position_id, - // liquidity, - // tao, - // alpha, - // tick_low, - // tick_high, - // }); - - // Ok(()) - - Err(Error::::UserLiquidityDisabled.into()) + Err(Error::::Deprecated.into()) } - /// Remove liquidity from a specific position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::remove_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn remove_liquidity( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - position_id: PositionId, + _origin: OriginFor, + _hotkey: T::AccountId, + _netuid: NetUid, + _position_id: PositionId, ) -> DispatchResult { - let coldkey = ensure_signed(origin)?; - - // Ensure that the subnet exists. - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - // Remove liquidity - let result = Self::do_remove_liquidity(netuid, &coldkey, position_id)?; - - // Credit the returned tao and alpha to the account - T::BalanceOps::increase_balance(&coldkey, result.tao.saturating_add(result.fee_tao)); - T::BalanceOps::increase_stake( - &coldkey, - &hotkey, - netuid.into(), - result.alpha.saturating_add(result.fee_alpha), - )?; - - // Remove withdrawn liquidity from user-provided reserves - T::TaoReserve::decrease_provided(netuid.into(), result.tao); - T::AlphaReserve::decrease_provided(netuid.into(), result.alpha); - - // Emit an event - Self::deposit_event(Event::LiquidityRemoved { - coldkey, - hotkey, - netuid: netuid.into(), - position_id, - liquidity: result.liquidity, - tao: result.tao, - alpha: result.alpha, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low.into(), - tick_high: result.tick_high.into(), - }); - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Modify a liquidity position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - liquidity_delta: Liquidity to add (if positive) or remove (if negative) - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::modify_position())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn modify_position( - origin: OriginFor, - hotkey: T::AccountId, - netuid: NetUid, - position_id: PositionId, - liquidity_delta: i64, + _origin: OriginFor, + _hotkey: T::AccountId, + _netuid: NetUid, + _position_id: PositionId, + _liquidity_delta: i64, ) -> DispatchResult { - let coldkey = ensure_signed(origin)?; - - // Ensure that the subnet exists. - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - ensure!( - T::SubnetInfo::is_subtoken_enabled(netuid.into()), - Error::::SubtokenDisabled - ); - - // Add or remove liquidity - let result = - Self::do_modify_position(netuid, &coldkey, &hotkey, position_id, liquidity_delta)?; - - if liquidity_delta > 0 { - // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - let tao_provided = T::BalanceOps::decrease_balance(&coldkey, result.tao)?; - ensure!(tao_provided == result.tao, Error::::InsufficientBalance); - - let alpha_provided = - T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - ensure!( - alpha_provided == result.alpha, - Error::::InsufficientBalance - ); - - // Emit an event - Self::deposit_event(Event::LiquidityModified { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta, - tao: result.tao.to_u64() as i64, - alpha: result.alpha.to_u64() as i64, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } else { - // Credit the returned tao and alpha to the account - T::BalanceOps::increase_balance(&coldkey, result.tao); - T::BalanceOps::increase_stake(&coldkey, &hotkey, netuid.into(), result.alpha)?; - - // Emit an event - if result.removed { - Self::deposit_event(Event::LiquidityRemoved { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta.unsigned_abs(), - tao: result.tao, - alpha: result.alpha, - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } else { - Self::deposit_event(Event::LiquidityModified { - coldkey: coldkey.clone(), - hotkey: hotkey.clone(), - netuid, - position_id, - liquidity: liquidity_delta, - tao: (result.tao.to_u64() as i64).neg(), - alpha: (result.alpha.to_u64() as i64).neg(), - fee_tao: result.fee_tao, - fee_alpha: result.fee_alpha, - tick_low: result.tick_low, - tick_high: result.tick_high, - }); - } - } - - // Credit accrued fees to user account (no matter if liquidity is added or removed) - if result.fee_tao > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&coldkey, result.fee_tao); - } - if !result.fee_alpha.is_zero() { - T::BalanceOps::increase_stake( - &coldkey, - &hotkey.clone(), - netuid.into(), - result.fee_alpha, - )?; - } - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Disable user liquidity in all subnets. - /// - /// Emits `Event::UserLiquidityToggled` on success + /// DEPRECATED #[pallet::call_index(5)] - #[pallet::weight(::WeightInfo::modify_position())] - pub fn disable_lp(origin: OriginFor) -> DispatchResult { - ensure_root(origin)?; - - for netuid in 1..=128 { - let netuid = NetUid::from(netuid as u16); - if EnabledUserLiquidity::::get(netuid) { - EnabledUserLiquidity::::insert(netuid, false); - Self::deposit_event(Event::UserLiquidityToggled { - netuid, - enable: false, - }); - } - - // Remove provided liquidity unconditionally because the network may have - // user liquidity previously disabled - // Ignore result to avoid early stopping - let _ = Self::do_dissolve_all_liquidity_providers(netuid); - } - - Ok(()) + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] + pub fn disable_lp(_origin: OriginFor) -> DispatchResult { + Err(Error::::Deprecated.into()) } } } + +/// Struct representing a tick index, DEPRECATED +#[freeze_struct("7c280c2b3bbbb33e")] +#[derive( + Debug, + Default, + Clone, + Copy, + Decode, + Encode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] +pub struct TickIndex(i32); + +/// Struct representing a liquidity position ID, DEPRECATED +#[freeze_struct("e695cd6455c3f0cb")] +#[derive( + Clone, + Copy, + Decode, + DecodeWithMemTracking, + Default, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + RuntimeDebug, + TypeInfo, +)] +pub struct PositionId(u128); diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 6791835b1a..ddb6a7bccc 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -1,14 +1,11 @@ use core::marker::PhantomData; +use frame_support::ensure; use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaCurrency, Currency, CurrencyReserve, NetUid, TaoCurrency}; use super::pallet::*; -use crate::{ - SqrtPrice, - tick::{ActiveTickIndexManager, TickIndex}, -}; /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep @@ -20,22 +17,16 @@ where // Input parameters netuid: NetUid, drop_fees: bool, + requested_delta_in: PaidIn, + limit_price: U64F64, - // Computed values - current_liquidity: U64F64, - possible_delta_in: PaidIn, - - // Ticks and prices (current, limit, edge, target) - target_sqrt_price: SqrtPrice, - limit_sqrt_price: SqrtPrice, - current_sqrt_price: SqrtPrice, - edge_sqrt_price: SqrtPrice, - edge_tick: TickIndex, + // Intermediate calculations + target_price: U64F64, + current_price: U64F64, // Result values - action: SwapStepAction, delta_in: PaidIn, - final_price: SqrtPrice, + final_price: U64F64, fee: PaidIn, _phantom: PhantomData<(T, PaidIn, PaidOut)>, @@ -52,36 +43,25 @@ where pub(crate) fn new( netuid: NetUid, amount_remaining: PaidIn, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Self { - // Calculate prices and ticks - let current_tick = CurrentTick::::get(netuid); - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let edge_tick = Self::tick_edge(netuid, current_tick); - let edge_sqrt_price = edge_tick.as_sqrt_price_bounded(); - let fee = Pallet::::calculate_fee_amount(netuid, amount_remaining, drop_fees); - let possible_delta_in = amount_remaining.saturating_sub(fee); + let requested_delta_in = amount_remaining.saturating_sub(fee); - // Target price and quantities - let current_liquidity = U64F64::saturating_from_num(CurrentLiquidity::::get(netuid)); - let target_sqrt_price = - Self::sqrt_price_target(current_liquidity, current_sqrt_price, possible_delta_in); + // Target and current prices + let target_price = Self::price_target(netuid, requested_delta_in); + let current_price = Pallet::::current_price(netuid); Self { netuid, drop_fees, - target_sqrt_price, - limit_sqrt_price, - current_sqrt_price, - edge_sqrt_price, - edge_tick, - possible_delta_in, - current_liquidity, - action: SwapStepAction::Stop, + requested_delta_in, + limit_price, + target_price, + current_price, delta_in: PaidIn::ZERO, - final_price: target_sqrt_price, + final_price: target_price, fee, _phantom: PhantomData, } @@ -98,64 +78,25 @@ where let mut recalculate_fee = false; // Calculate the stopping price: The price at which we either reach the limit price, - // exchange the full amount, or reach the edge price. - if Self::price_is_closer(&self.target_sqrt_price, &self.limit_sqrt_price) - && Self::price_is_closer(&self.target_sqrt_price, &self.edge_sqrt_price) - { - // Case 1. target_quantity is the lowest - // The trade completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.target_sqrt_price; - self.delta_in = self.possible_delta_in; - } else if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) - && Self::price_is_closer(&self.limit_sqrt_price, &self.edge_sqrt_price) - { - // Case 2. lim_quantity is the lowest - // The trade also completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.limit_sqrt_price; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.limit_sqrt_price, - ); - recalculate_fee = true; + // or exchange the full amount. + if Self::price_is_closer(&self.target_price, &self.limit_price) { + // Case 1. target_quantity is the lowest, execute in full + self.final_price = self.target_price; + self.delta_in = self.requested_delta_in; } else { - // Case 3. edge_quantity is the lowest - // Tick crossing is likely - self.action = SwapStepAction::Crossing; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.edge_sqrt_price, - ); - self.final_price = self.edge_sqrt_price; + // Case 2. lim_quantity is the lowest + self.final_price = self.limit_price; + self.delta_in = Self::delta_in(self.netuid, self.current_price, self.limit_price); recalculate_fee = true; } - log::trace!("\tAction : {:?}", self.action); - log::trace!( - "\tCurrent Price : {}", - self.current_sqrt_price - .saturating_mul(self.current_sqrt_price) - ); - log::trace!( - "\tTarget Price : {}", - self.target_sqrt_price - .saturating_mul(self.target_sqrt_price) - ); - log::trace!( - "\tLimit Price : {}", - self.limit_sqrt_price.saturating_mul(self.limit_sqrt_price) - ); - log::trace!( - "\tEdge Price : {}", - self.edge_sqrt_price.saturating_mul(self.edge_sqrt_price) - ); + log::trace!("\tCurrent Price : {}", self.current_price); + log::trace!("\tTarget Price : {}", self.target_price); + log::trace!("\tLimit Price : {}", self.limit_price); log::trace!("\tDelta In : {}", self.delta_in); // Because on step creation we calculate fee off the total amount, we might need to - // recalculate it in case if we hit the limit price or the edge price. + // recalculate it in case if we hit the limit price. if recalculate_fee { let u16_max = U64F64::saturating_from_num(u16::MAX); let fee_rate = if self.drop_fees { @@ -169,325 +110,121 @@ where .saturating_to_num::() .into(); } - - // Now correct the action if we stopped exactly at the edge no matter what was the case - // above. Because order type buy moves the price up and tick semi-open interval doesn't - // include its right point, we cross on buys and stop on sells. - let natural_reason_stop_price = - if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) { - self.limit_sqrt_price - } else { - self.target_sqrt_price - }; - if natural_reason_stop_price == self.edge_sqrt_price { - self.action = Self::action_on_edge_sqrt_price(); - } } /// Process a single step of a swap fn process_swap(&self) -> Result, Error> { - // Hold the fees - Self::add_fees( - self.netuid, - Pallet::::current_liquidity_safe(self.netuid), - self.fee, - ); + // Convert amounts, actual swap happens here let delta_out = Self::convert_deltas(self.netuid, self.delta_in); - // log::trace!("\tDelta Out : {delta_out:?}"); - - if self.action == SwapStepAction::Crossing { - let mut tick = Ticks::::get(self.netuid, self.edge_tick).unwrap_or_default(); - tick.fees_out_tao = I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - .saturating_sub(tick.fees_out_tao); - tick.fees_out_alpha = - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - .saturating_sub(tick.fees_out_alpha); - Self::update_liquidity_at_crossing(self.netuid)?; - Ticks::::insert(self.netuid, self.edge_tick, tick); - } - - // Update current price - AlphaSqrtPrice::::set(self.netuid, self.final_price); + log::trace!("\tDelta Out : {delta_out}"); + if self.delta_in > 0.into() { + ensure!(delta_out > 0.into(), Error::::ReservesTooLow); - // Update current tick - let new_current_tick = TickIndex::from_sqrt_price_bounded(self.final_price); - CurrentTick::::set(self.netuid, new_current_tick); + // Hold the fees + Self::add_fees(self.netuid, self.fee); + } Ok(SwapStepResult { - amount_to_take: self.delta_in.saturating_add(self.fee), fee_paid: self.fee, delta_in: self.delta_in, delta_out, }) } - - pub(crate) fn action(&self) -> SwapStepAction { - self.action - } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> TaoCurrency { - liquidity_curr - .saturating_mul(sqrt_price_target.saturating_sub(sqrt_price_curr)) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> TaoCurrency { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + TaoCurrency::from(balancer.calculate_quote_delta_in( + price_curr, + price_target, + tao_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick.next().unwrap_or(TickIndex::MAX), + fn price_target(netuid: NetUid, delta_in: TaoCurrency) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dy = delta_in; + let dx = Self::convert_deltas(netuid, dy); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_sub(dx)), + u64::from(tao_reserve.saturating_add(dy)), ) - .unwrap_or(TickIndex::MAX) } - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: TaoCurrency, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::max_price_inner::().to_u64(), - ); - } - - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(sqrt_price_curr) + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 <= price2 } - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 <= sq_price2 - } - - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Crossing - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: TaoCurrency) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalTao::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn add_fees(netuid: NetUid, fee: TaoCurrency) { + FeesTao::::mutate(netuid, |total| *total = total.saturating_add(fee)) } fn convert_deltas(netuid: NetUid, delta_in: TaoCurrency) -> AlphaCurrency { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return AlphaCurrency::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // (liquidity_curr * sqrt_price_curr + delta_fixed) * sqrt_price_curr; - let a = liquidity_curr - .saturating_mul(sqrt_price_curr) - .saturating_add(delta_fixed) - .saturating_mul(sqrt_price_curr); - // liquidity_curr / a; - let b = liquidity_curr.safe_div(a); - // b * delta_fixed; - b.saturating_mul(delta_fixed) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_higher_active_tick(netuid, current_tick_index), - let upper_tick = ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick_index.next().unwrap_or(TickIndex::MAX), - ) - .unwrap_or(TickIndex::MAX); - Ticks::::get(netuid, upper_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_quote_base(tao_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + AlphaCurrency::from( + alpha_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> AlphaCurrency { - let one = U64F64::saturating_from_num(1); - - liquidity_curr - .saturating_mul( - one.safe_div(sqrt_price_target.into()) - .saturating_sub(one.safe_div(sqrt_price_curr)), - ) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> AlphaCurrency { + let alpha_reserve = T::AlphaReserve::reserve(netuid); + let balancer = SwapBalancer::::get(netuid); + AlphaCurrency::from(balancer.calculate_base_delta_in( + price_curr, + price_target, + alpha_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - let current_price: SqrtPrice = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick); - - if is_active && current_price > current_tick_price { - return ActiveTickIndexManager::::find_closest_lower(netuid, current_tick) - .unwrap_or(TickIndex::MIN); - } - - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick.prev().unwrap_or(TickIndex::MIN), - ) - .unwrap_or(TickIndex::MIN) - } - - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: AlphaCurrency, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - let one = U64F64::saturating_from_num(1); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::min_price_inner::().to_u64(), - ); - } - - one.safe_div( - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(one.safe_div(sqrt_price_curr)), + fn price_target(netuid: NetUid, delta_in: AlphaCurrency) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dx = delta_in; + let dy = Self::convert_deltas(netuid, dx); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_add(dx)), + u64::from(tao_reserve.saturating_sub(dy)), ) } - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 >= sq_price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 >= price2 } - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Stop - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: AlphaCurrency) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalAlpha::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn add_fees(netuid: NetUid, fee: AlphaCurrency) { + FeesAlpha::::mutate(netuid, |total| *total = total.saturating_add(fee)) } fn convert_deltas(netuid: NetUid, delta_in: AlphaCurrency) -> TaoCurrency { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return TaoCurrency::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // liquidity_curr / (liquidity_curr / sqrt_price_curr + delta_fixed); - let denom = liquidity_curr - .safe_div(sqrt_price_curr) - .saturating_add(delta_fixed); - let a = liquidity_curr.safe_div(denom); - // a * sqrt_price_curr; - let b = a.saturating_mul(sqrt_price_curr); - - // delta_fixed * b; - delta_fixed.saturating_mul(b) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_lower_active_tick(netuid, current_tick_index) - let current_price = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick_index.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick_index); - - let lower_tick = if is_active && current_price > current_tick_price { - ActiveTickIndexManager::::find_closest_lower(netuid, current_tick_index) - .unwrap_or(TickIndex::MIN) - } else { - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick_index.prev().unwrap_or(TickIndex::MIN), - ) - .unwrap_or(TickIndex::MIN) - }; - Ticks::::get(netuid, lower_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_base_quote(alpha_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let tao_reserve_fixed = U64F64::from_num(u64::from(tao_reserve)); + TaoCurrency::from( + tao_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } @@ -498,49 +235,25 @@ where PaidOut: Currency, { /// Get the input amount needed to reach the target price - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> PaidIn; + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> PaidIn; - /// Get the tick at the current tick edge. - /// - /// If anything is wrong with tick math and it returns Err, we just abort the deal, i.e. return - /// the edge that is impossible to execute - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex; + /// Get the target price based on the input amount + fn price_target(netuid: NetUid, delta_in: PaidIn) -> U64F64; - /// Get the target square root price based on the input amount - /// - /// This is the price that would be reached if - /// - There are no liquidity positions other than protocol liquidity - /// - Full delta_in amount is executed - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: PaidIn, - ) -> SqrtPrice; - - /// Returns True if sq_price1 is closer to the current price than sq_price2 + /// Returns True if price1 is closer to the current price than price2 /// in terms of order direction. - /// For buying: sq_price1 <= sq_price2 - /// For selling: sq_price1 >= sq_price2 - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool; - - /// Get swap step action on the edge sqrt price. - fn action_on_edge_sqrt_price() -> SwapStepAction; + /// For buying: price1 <= price2 + /// For selling: price1 >= price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool; /// Add fees to the global fee counters - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: PaidIn); + fn add_fees(netuid: NetUid, fee: PaidIn); /// Convert input amount (delta_in) to output amount (delta_out) /// - /// This is the core method of uniswap V3 that tells how much output token is given for an - /// amount of input token within one price tick. + /// This is the core method of the swap that tells how much output token is given for an + /// amount of input token fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; - - /// Update liquidity when crossing a tick - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error>; } #[derive(Debug, PartialEq)] @@ -549,14 +262,7 @@ where PaidIn: Currency, PaidOut: Currency, { - pub(crate) amount_to_take: PaidIn, pub(crate) fee_paid: PaidIn, pub(crate) delta_in: PaidIn, pub(crate) delta_out: PaidOut, } - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SwapStepAction { - Crossing, - Stop, -} diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index cd18e665cd..912c89bbe8 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -6,76 +6,30 @@ )] use approx::assert_abs_diff_eq; -use frame_support::{assert_err, assert_noop, assert_ok}; -use sp_arithmetic::helpers_128bit; +use frame_support::{assert_noop, assert_ok}; +use sp_arithmetic::Perquintill; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; -use subtensor_runtime_common::NetUid; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{Currency, NetUid}; use subtensor_swap_interface::Order as OrderT; use super::*; +use crate::mock::*; use crate::pallet::swap_step::*; -use crate::{SqrtPrice, mock::*}; - -// this function is used to convert price (NON-SQRT price!) to TickIndex. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: SqrtPrice = SqrtPrice::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} - -fn get_ticked_prices_around_current_price() -> (f64, f64) { - // Get current price, ticks around it, and prices on the tick edges for test cases - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let current_tick = CurrentTick::::get(netuid); - // Low and high prices that match to a lower and higher tick that doesn't contain the current price - let current_price_low_sqrt = current_tick.as_sqrt_price_bounded(); - let current_price_high_sqrt = current_tick.next().unwrap().as_sqrt_price_bounded(); - let current_price_low = U96F32::from_num(current_price_low_sqrt * current_price_low_sqrt); - let current_price_high = U96F32::from_num(current_price_high_sqrt * current_price_high_sqrt); +// Run all tests: +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests --nocapture - ( - current_price_low.to_num::(), - current_price_high.to_num::() + 0.000000001, - ) +#[allow(dead_code)] +fn get_min_price() -> U64F64 { + U64F64::from_num(Pallet::::min_price_inner::()) + / U64F64::from_num(1_000_000_000) } -// this function is used to convert tick index NON-SQRT (!) price. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn tick_to_price(tick: TickIndex) -> f64 { - // Handle errors gracefully - match tick.try_to_sqrt_price() { - Ok(price_sqrt) => (price_sqrt * price_sqrt).to_num::(), - Err(_) => { - // Return a sensible default based on whether the tick is above or below the valid range - if tick > TickIndex::MAX { - tick_to_price(TickIndex::MAX) // Use the max valid tick price - } else { - tick_to_price(TickIndex::MIN) // Use the min valid tick price - } - } - } +#[allow(dead_code)] +fn get_max_price() -> U64F64 { + U64F64::from_num(Pallet::::max_price_inner::()) + / U64F64::from_num(1_000_000_000) } mod dispatchables { @@ -106,641 +60,405 @@ mod dispatchables { }); } - // #[test] - // fn test_toggle_user_liquidity() { - // new_test_ext().execute_with(|| { - // let netuid = NetUid::from(101); - - // assert!(!EnabledUserLiquidity::::get(netuid)); - - // assert_ok!(Swap::toggle_user_liquidity( - // RuntimeOrigin::root(), - // netuid.into(), - // true - // )); - - // assert!(EnabledUserLiquidity::::get(netuid)); - - // assert_noop!( - // Swap::toggle_user_liquidity(RuntimeOrigin::signed(666), netuid.into(), true), - // DispatchError::BadOrigin - // ); - - // assert_ok!(Swap::toggle_user_liquidity( - // RuntimeOrigin::signed(1), - // netuid.into(), - // true - // )); - - // assert_noop!( - // Swap::toggle_user_liquidity( - // RuntimeOrigin::root(), - // NON_EXISTENT_NETUID.into(), - // true - // ), - // Error::::MechanismDoesNotExist - // ); - // }); - // } -} + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / 1_000_000_000_000_000_000_f64 + } -#[test] -fn test_swap_initialization() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_happy --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_happy() { + // test case: tao_delta, alpha_delta + [ + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), + ] + .into_iter() + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); - // Get reserves from the mock provider - let tao = TaoReserve::reserve(netuid.into()); - let alpha = AlphaReserve::reserve(netuid.into()); + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + // Initialize reserves and price + let tao = TaoCurrency::from(1_000_000_000_000_u64); + let alpha = AlphaCurrency::from(4_000_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); - assert!(SwapV3Initialized::::get(netuid)); + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); - // Verify current price is set - let sqrt_price = AlphaSqrtPrice::::get(netuid); - let expected_sqrt_price = U64F64::from_num(0.5_f64); - assert_abs_diff_eq!( - sqrt_price.to_num::(), - expected_sqrt_price.to_num::(), - epsilon = 0.000000001 - ); + // Check that price didn't change + let price_after = Swap::current_price(netuid); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); - // Verify that current tick is set - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = TickIndex::from_sqrt_price_bounded(expected_sqrt_price); - assert_eq!(current_tick, expected_current_tick); - - // Calculate expected liquidity - let expected_liquidity = - helpers_128bit::sqrt((tao.to_u64() as u128).saturating_mul(alpha.to_u64() as u128)) - as u64; - - // Get the protocol account - let protocol_account_id = Pallet::::protocol_account_id(); - - // Verify position created for protocol account - let positions = Positions::::iter_prefix_values((netuid, protocol_account_id)) - .collect::>(); - assert_eq!(positions.len(), 1); - - let position = &positions[0]; - assert_eq!(position.liquidity, expected_liquidity); - assert_eq!(position.tick_low, TickIndex::MIN); - assert_eq!(position.tick_high, TickIndex::MAX); - assert_eq!(position.fees_tao, 0); - assert_eq!(position.fees_alpha, 0); - - // Verify ticks were created - let tick_low = Ticks::::get(netuid, TickIndex::MIN).unwrap(); - let tick_high = Ticks::::get(netuid, TickIndex::MAX).unwrap(); - - // Check liquidity values - assert_eq!(tick_low.liquidity_net, expected_liquidity as i128); - assert_eq!(tick_low.liquidity_gross, expected_liquidity); - assert_eq!(tick_high.liquidity_net, -(expected_liquidity as i128)); - assert_eq!(tick_high.liquidity_gross, expected_liquidity); - - // Verify current liquidity is set - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity); - }); -} + // Check that reserve weight was properly updated + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + let expected_quote_weight = + new_tao / (new_alpha * price_before.to_num::() + new_tao); + let expected_quote_weight_delta = expected_quote_weight - 0.5; + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight_delta = + perquintill_to_f64(res_weights.get_quote_weight()) - 0.5; + let eps = expected_quote_weight / 1_000_000_000_000.; + assert_abs_diff_eq!( + expected_quote_weight_delta, + actual_quote_weight_delta, + epsilon = eps + ); + }); + }); + } -// Test adding liquidity on top of the existing protocol liquidity -#[test] -fn test_add_liquidity_basic() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); - - assert_ok!(Pallet::::maybe_initialize_v3(NetUid::from(1))); - let current_price = Pallet::::current_price(NetUid::from(1)).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) + /// This test case verifies that small gradual injections (like emissions in every block) + /// in the worst case + /// - Do not cause price to change + /// - Result in the same weight change as one large injection + /// + /// This is a long test that only tests validity of weights math. Run again if changing + /// Balancer::update_weights_for_added_liquidity + /// + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_deltas --exact --nocapture + #[ignore] + #[test] + fn test_adjust_protocol_liquidity_deltas() { + // The number of times (blocks) over which gradual injections will be made + // One year price drift due to precision is under 1e-6 + const ITERATIONS: u64 = 2_700_000; + const PRICE_PRECISION: f64 = 0.000_001; + const PREC_LARGE_DELTA: f64 = 0.001; + const WEIGHT_PRECISION: f64 = 0.000_000_000_000_000_001; + + let initial_tao_reserve = TaoCurrency::from(1_000_000_000_000_000_u64); + let initial_alpha_reserve = AlphaCurrency::from(10_000_000_000_000_000_u64); + + // test case: tao_delta, alpha_delta, price_precision [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), + (0_u64, 0_u64, PRICE_PRECISION), + (0_u64, 1_u64, PRICE_PRECISION), + (1_u64, 0_u64, PRICE_PRECISION), + (1_u64, 1_u64, PRICE_PRECISION), + (0_u64, 10_u64, PRICE_PRECISION), + (10_u64, 0_u64, PRICE_PRECISION), + (10_u64, 10_u64, PRICE_PRECISION), + (0_u64, 100_u64, PRICE_PRECISION), + (100_u64, 0_u64, PRICE_PRECISION), + (100_u64, 100_u64, PRICE_PRECISION), + (0_u64, 987_u64, PRICE_PRECISION), + (987_u64, 0_u64, PRICE_PRECISION), + (876_u64, 987_u64, PRICE_PRECISION), + (0_u64, 1_000_u64, PRICE_PRECISION), + (1_000_u64, 0_u64, PRICE_PRECISION), + (1_000_u64, 1_000_u64, PRICE_PRECISION), + (0_u64, 1_234_u64, PRICE_PRECISION), + (1_234_u64, 0_u64, PRICE_PRECISION), + (1_234_u64, 4_321_u64, PRICE_PRECISION), + (1_234_000_u64, 4_321_000_u64, PREC_LARGE_DELTA), + (1_234_u64, 4_321_000_u64, PREC_LARGE_DELTA), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each( - |(netuid, price_low, price_high, liquidity, expected_tao, expected_alpha)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - // Get tick infos and liquidity before adding (to account for protocol liquidity) - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, tao, alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - assert_abs_diff_eq!(tao, expected_tao, epsilon = tao / 1000); - assert_abs_diff_eq!(alpha, expected_alpha, epsilon = alpha / 1000); - - // Check that low and high ticks appear in the state and are properly updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = liquidity as i128; - let expected_liquidity_gross_low = liquidity; - let expected_liquidity_net_high = -(liquidity as i128); - let expected_liquidity_gross_high = liquidity; - - assert_eq!( - tick_low_info.liquidity_net - tick_low_info_before.liquidity_net, - expected_liquidity_net_low, - ); - assert_eq!( - tick_low_info.liquidity_gross - tick_low_info_before.liquidity_gross, - expected_liquidity_gross_low, - ); - assert_eq!( - tick_high_info.liquidity_net - tick_high_info_before.liquidity_net, - expected_liquidity_net_high, - ); - assert_eq!( - tick_high_info.liquidity_gross - tick_high_info_before.liquidity_gross, - expected_liquidity_gross_high, - ); + .for_each(|(tao_delta, alpha_delta, price_precision)| { + new_test_ext().execute_with(|| { + let netuid1 = NetUid::from(1); + + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); + + // Initialize realistically large reserves + let mut tao = initial_tao_reserve; + let mut alpha = initial_alpha_reserve; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + let price_before = Swap::current_price(netuid1); + + // Adjust reserves gradually + for _ in 0..ITERATIONS { + Swap::adjust_protocol_liquidity(netuid1, tao_delta, alpha_delta); + tao += tao_delta; + alpha += alpha_delta; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + } - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 + // Check that price didn't change + let price_after = Swap::current_price(netuid1); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_precision ); - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = - if (price_high > current_price) && (price_low <= current_price) { - liquidity_before + liquidity - } else { - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }, - ); - }); -} + ///////////////////////// -#[test] -fn test_add_liquidity_max_limit_enforced() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let liquidity = 2_000_000_000_u64; - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + // Now do one-time big injection with another netuid and compare weights - let limit = MaxPositions::get() as usize; + let netuid2 = NetUid::from(2); - for _ in 0..limit { - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ) - .unwrap(); - } + // Initialize same large reserves + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve); - let test_result = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ); + // Adjust reserves by one large amount at once + let tao_delta_once = TaoCurrency::from(ITERATIONS * u64::from(tao_delta)); + let alpha_delta_once = AlphaCurrency::from(ITERATIONS * u64::from(alpha_delta)); + Swap::adjust_protocol_liquidity(netuid2, tao_delta_once, alpha_delta_once); + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve + tao_delta_once); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve + alpha_delta_once); - assert_err!(test_result, Error::::MaxPositionsExceeded); - }); -} + // Compare reserve weights for netuid 1 and 2 + let res_weights1 = SwapBalancer::::get(netuid1); + let res_weights2 = SwapBalancer::::get(netuid2); + let actual_quote_weight1 = perquintill_to_f64(res_weights1.get_quote_weight()); + let actual_quote_weight2 = perquintill_to_f64(res_weights2.get_quote_weight()); + assert_abs_diff_eq!( + actual_quote_weight1, + actual_quote_weight2, + epsilon = WEIGHT_PRECISION + ); + }); + }); + } -#[test] -fn test_add_liquidity_out_of_bounds() { - new_test_ext().execute_with(|| { + /// Should work ok when initial alpha is zero + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_zero_alpha --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_zero_alpha() { + // test case: tao_delta, alpha_delta [ - // For our tests, we'll construct TickIndex values that are intentionally - // outside the valid range for testing purposes only - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::MAX, - 1_000_000_000_u64, - ), - ( - TickIndex::MIN, - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 100), - TickIndex::new_unchecked(TickIndex::MAX.get() + 100), - 1_000_000_000_u64, - ), - // Inverted ticks: high < low - ( - TickIndex::new_unchecked(-900), - TickIndex::new_unchecked(-1000), - 1_000_000_000_u64, - ), - // Equal ticks: high == low - ( - TickIndex::new_unchecked(-10_000), - TickIndex::new_unchecked(-10_000), - 1_000_000_000_u64, - ), + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, tick_low, tick_high, liquidity)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Swap::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity - ), - Error::::InvalidTickRange, - ); + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao_delta = TaoCurrency::from(tao_delta); + let alpha_delta = AlphaCurrency::from(alpha_delta); + + // Initialize reserves and price + // broken state: Zero price because of zero alpha reserve + let tao = TaoCurrency::from(1_000_000_000_000_u64); + let alpha = AlphaCurrency::from(0_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + assert_eq!(price_before, U64F64::from_num(0)); + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight = perquintill_to_f64(res_weights.get_quote_weight()); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + if new_alpha == 0. { + // If the pool state is still broken (∆x = 0), no change + assert_eq!(actual_quote_weight, 0.5); + assert_eq!(price_after, U64F64::from_num(0)); + } else { + // Price got fixed + let expected_price = new_tao / new_alpha; + assert_abs_diff_eq!( + expected_price, + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); + assert_eq!(actual_quote_weight, 0.5); + } + }); }); - }); -} + } -#[test] -fn test_add_liquidity_over_balance() { - new_test_ext().execute_with(|| { - let coldkey_account_id = 3; - let hotkey_account_id = 1002; + /// Collects the fees and adds them to protocol liquidity + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_collects_fees --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_collects_fees() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); - [ - // Lower than price (not enough tao) - (0.1, 0.2, 100_000_000_000_u64), - // Higher than price (not enough alpha) - (0.3, 0.4, 100_000_000_000_u64), - // Around the price (not enough both) - (0.1, 0.4, 100_000_000_000_u64), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, price_low, price_high, liquidity)| { - // Calculate ticks - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Pallet::::do_add_liquidity( - netuid, - &coldkey_account_id, - &hotkey_account_id, - tick_low, - tick_high, - liquidity - ), - Error::::InsufficientBalance, - ); + let tao_delta = TaoCurrency::ZERO; + let alpha_delta = AlphaCurrency::ZERO; + + // Initialize reserves and price + // 0.1 price + let tao = TaoCurrency::from(1_000_000_000_u64); + let alpha = AlphaCurrency::from(10_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + // Insert fees + let tao_fees = TaoCurrency::from(1_000); + let alpha_fees = AlphaCurrency::from(1_000); + FeesTao::::insert(netuid, tao_fees); + FeesAlpha::::insert(netuid, alpha_fees); + + // Adjust reserves + let (actual_tao_delta, actual_alpha_delta) = + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + // Check that returned reserve deltas are correct (include fees) + assert_eq!(actual_tao_delta, tao_fees); + assert_eq!(actual_alpha_delta, alpha_fees); + + // Check that fees got reset + assert_eq!(FeesTao::::get(netuid), TaoCurrency::ZERO); + assert_eq!(FeesAlpha::::get(netuid), AlphaCurrency::ZERO); }); - }); + } } -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_remove_liquidity_basic --exact --show-output #[test] -fn test_remove_liquidity_basic() { +fn test_swap_initialization() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); - - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); + let netuid = NetUid::from(1); - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each(|(netuid, price_low, price_high, liquidity, tao, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); + // Setup reserves + let tao = TaoCurrency::from(1_000_000_000u64); + let alpha = AlphaCurrency::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // Remove liquidity - let remove_result = - Pallet::::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id) - .unwrap(); - assert_abs_diff_eq!(remove_result.tao.to_u64(), tao, epsilon = tao / 1000); - assert_abs_diff_eq!( - u64::from(remove_result.alpha), - alpha, - epsilon = alpha / 1000 - ); - assert_eq!(remove_result.fee_tao, TaoCurrency::ZERO); - assert_eq!(remove_result.fee_alpha, AlphaCurrency::ZERO); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + assert!(PalSwapInitialized::::get(netuid)); - // Liquidity position is removed - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - assert!(Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).is_none()); + // Verify current price is set + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.25_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 + ); - // Current liquidity is updated (back where it was) - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); - }); + // Verify that swap reserve weight is initialized + let reserve_weight = SwapBalancer::::get(netuid); + assert_eq!( + reserve_weight.get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64), + ); }); } #[test] -fn test_remove_liquidity_nonexisting_position() { +fn test_swap_initialization_with_price() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick.get(), TickIndex::MAX.get()); - - let liquidity = 2_000_000_000_u64; let netuid = NetUid::from(1); - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); + // Setup reserves, tao / alpha = 0.25 + let tao = TaoCurrency::from(1_000_000_000u64); + let alpha = AlphaCurrency::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( + // Initialize with 0.2 price + assert_ok!(Pallet::::maybe_initialize_palswap( netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, + Some(U64F64::from(1u16) / U64F64::from(5u16)) )); + assert!(PalSwapInitialized::::get(netuid)); - assert!(Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID) > 0); - - // Remove liquidity - assert_err!( - Pallet::::do_remove_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - PositionId::new::() - ), - Error::::LiquidityNotFound, + // Verify current price is set to 0.2 + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.2_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 ); }); } -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_modify_position_basic --exact --show-output -#[test] -fn test_modify_position_basic() { - new_test_ext().execute_with(|| { - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let limit_price = 1000.0_f64; - assert_eq!(max_tick, TickIndex::MAX); - let (current_price_low, _current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_low, - max_price, - 2_000_000_000_u64, - 4_000_000_000, - ), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3)) - .for_each(|(netuid, price_low, price_high, liquidity, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Get tick infos before the swap/update - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap(); - - // Swap to create fees on the position - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = GetAlphaForTao::with_amount(liquidity / 10); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - // Modify liquidity (also causes claiming of fees) - let liquidity_before = CurrentLiquidity::::get(netuid); - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 10) as i64), - ) - .unwrap(); - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 10, - epsilon = alpha / 1000 - ); - assert!(modify_result.fee_tao > TaoCurrency::ZERO); - assert_eq!(modify_result.fee_alpha, AlphaCurrency::ZERO); - - // Liquidity position is reduced - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); - - // Current liquidity is reduced with modify_position - assert!(CurrentLiquidity::::get(netuid) < liquidity_before); - - // Position liquidity is reduced - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity * 9 / 10); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - - // Tick liquidity is updated properly for low and high position ticks - let tick_low_info_after = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_after = Ticks::::get(netuid, tick_high).unwrap(); - - assert_eq!( - tick_low_info_before.liquidity_net - (liquidity / 10) as i128, - tick_low_info_after.liquidity_net, - ); - assert_eq!( - tick_low_info_before.liquidity_gross - (liquidity / 10), - tick_low_info_after.liquidity_gross, - ); - assert_eq!( - tick_high_info_before.liquidity_net + (liquidity / 10) as i128, - tick_high_info_after.liquidity_net, - ); - assert_eq!( - tick_high_info_before.liquidity_gross - (liquidity / 10), - tick_high_info_after.liquidity_gross, - ); - - // Modify liquidity again (ensure fees aren't double-collected) - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 100) as i64), - ) - .unwrap(); - - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 100, - epsilon = alpha / 1000 - ); - assert_eq!(modify_result.fee_tao, TaoCurrency::ZERO); - assert_eq!(modify_result.fee_alpha, AlphaCurrency::ZERO); - }); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_basic --exact --show-output +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_basic --exact --nocapture #[test] fn test_swap_basic() { new_test_ext().execute_with(|| { @@ -748,851 +466,277 @@ fn test_swap_basic() { netuid: NetUid, order: Order, limit_price: f64, - output_amount: u64, price_should_grow: bool, ) where Order: OrderT, - Order::PaidIn: GlobalFeeInfo, BasicSwapStep: SwapStep, { - // Consumed liquidity ticks - let tick_low = TickIndex::MIN; - let tick_high = TickIndex::MAX; - let liquidity = order.amount().to_u64(); + let swap_amount = order.amount().to_u64(); // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + // Price is 0.25 + let initial_tao_reserve = TaoCurrency::from(1_000_000_000_u64); + let initial_alpha_reserve = AlphaCurrency::from(4_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid, initial_alpha_reserve); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); // Get current price - let current_price = Pallet::::current_price(netuid); + let current_price_before = Pallet::::current_price(netuid); + + // Get reserves + let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); + let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); + + // Expected fee amount + let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; + let expected_fee = (swap_amount as f64 * fee_rate) as u64; + + // Calculate expected output amount using f64 math + // This is a simple case when w1 = w2 = 0.5, so there's no + // exponentiation needed + let x = alpha_reserve as f64; + let y = tao_reserve as f64; + let expected_output_amount = if price_should_grow { + x * (1.0 - y / (y + (swap_amount - expected_fee) as f64)) + } else { + y * (1.0 - x / (x + (swap_amount - expected_fee) as f64)) + }; // Swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); + let limit_price_fixed = U64F64::from_num(limit_price); let swap_result = - Pallet::::do_swap(netuid, order.clone(), sqrt_limit_price, false, false) + Pallet::::do_swap(netuid, order.clone(), limit_price_fixed, false, false) .unwrap(); assert_abs_diff_eq!( swap_result.amount_paid_out.to_u64(), - output_amount, - epsilon = output_amount / 100 + expected_output_amount as u64, + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_in_reserve_delta() as u64, - liquidity, - epsilon = liquidity / 10 + (swap_amount - expected_fee), + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - - // Check that low and high ticks' fees were updated properly, and liquidity values were not updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, - ); - - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (liquidity as f64 * fee_rate) as u64; - - // Global fees should be updated - let actual_global_fee = (order.amount().global_fee(netuid).to_num::() - * (liquidity_before as f64)) as u64; - - assert!((swap_result.fee_paid.to_u64() as i64 - expected_fee as i64).abs() <= 1); - assert!((actual_global_fee as i64 - expected_fee as i64).abs() <= 1); - - // Tick fees should be updated - - // Liquidity position should not be updated - let protocol_id = Pallet::::protocol_account_id(); - let positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - let position = positions.first().unwrap(); - - assert_eq!( - position.liquidity, - helpers_128bit::sqrt( - TaoReserve::reserve(netuid.into()).to_u64() as u128 - * AlphaReserve::reserve(netuid.into()).to_u64() as u128 - ) as u64 + -(expected_output_amount as i64), + epsilon = 1 ); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - // Current liquidity is not updated - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); + // Update reserves (because it happens outside of do_swap in stake_utils) + if price_should_grow { + TaoReserve::set_mock_reserve( + netuid, + TaoCurrency::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaCurrency::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + } else { + TaoReserve::set_mock_reserve( + netuid, + TaoCurrency::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaCurrency::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + } // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); let current_price_after = Pallet::::current_price(netuid); - assert_eq!(current_price_after >= current_price, price_should_grow); - - // Assert that current tick is updated - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = - TickIndex::from_sqrt_price_bounded(sqrt_current_price_after); - assert_eq!(current_tick, expected_current_tick); + assert_eq!( + current_price_after >= current_price_before, + price_should_grow + ); } // Current price is 0.25 // Test case is (order_type, liquidity, limit_price, output_amount) - perform_test( - 1.into(), - GetAlphaForTao::with_amount(1_000), - 1000.0, - 3990, - true, - ); + perform_test(1.into(), GetAlphaForTao::with_amount(1_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(2_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(123_456), 1000.0, true); + perform_test(2.into(), GetTaoForAlpha::with_amount(1_000), 0.0001, false); + perform_test(2.into(), GetTaoForAlpha::with_amount(2_000), 0.0001, false); perform_test( 2.into(), - GetTaoForAlpha::with_amount(1_000), + GetTaoForAlpha::with_amount(123_456), 0.0001, - 250, false, ); perform_test( 3.into(), - GetAlphaForTao::with_amount(500_000_000), + GetAlphaForTao::with_amount(1_000_000_000), + 1000.0, + true, + ); + perform_test( + 3.into(), + GetAlphaForTao::with_amount(10_000_000_000), 1000.0, - 2_000_000_000, true, ); }); } -// In this test the swap starts and ends within one (large liquidity) position -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_single_position --exact --show-output +// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output #[test] -fn test_swap_single_position() { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - let mut current_price_low = 0_f64; - let mut current_price_high = 0_f64; - let mut current_price = 0_f64; - new_test_ext().execute_with(|| { - let (low, high) = get_ticked_prices_around_current_price(); - current_price_low = low; - current_price_high = high; - current_price = Pallet::::current_price(netuid).to_num::(); - }); - - macro_rules! perform_test { - ($order_t:ident, - $price_low_offset:expr, - $price_high_offset:expr, - $position_liquidity:expr, - $liquidity_fraction:expr, - $limit_price:expr, - $price_should_grow:expr - ) => { - new_test_ext().execute_with(|| { - let price_low_offset = $price_low_offset; - let price_high_offset = $price_high_offset; - let position_liquidity = $position_liquidity; - let order_liquidity_fraction = $liquidity_fraction; - let limit_price = $limit_price; - let price_should_grow = $price_should_grow; - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let protocol_liquidity = (tao_reserve as f64 * alpha_reserve as f64).sqrt(); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid).to_num::(); - - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); +fn test_swap_precision_edge_case() { + // Test case: tao_reserve, alpha_reserve, swap_amount + [ + (1_000_u64, 1_000_u64, 999_500_u64), + (1_000_000_u64, 1_000_000_u64, 999_500_000_u64), + ] + .into_iter() + .for_each(|(tao_reserve, alpha_reserve, swap_amount)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let order = GetTaoForAlpha::with_amount(swap_amount); - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - assert_abs_diff_eq!( - liquidity_before as f64, - protocol_liquidity + position_liquidity as f64, - epsilon = liquidity_before as f64 / 1000. - ); + // Very low reserves + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(tao_reserve)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(alpha_reserve)); - ////////////////////////////////////////////// - // Swap + // Minimum possible limit price + let limit_price: U64F64 = get_min_price(); + println!("limit_price = {:?}", limit_price); - // Calculate the expected output amount for the cornercase of one step - let order_liquidity = order_liquidity_fraction * position_liquidity as f64; + // Swap + let swap_result = + Pallet::::do_swap(netuid, order, limit_price, false, true).unwrap(); - let output_amount = >::approx_expected_swap_output( - sqrt_current_price, - liquidity_before as f64, - order_liquidity, - ); + assert!(swap_result.amount_paid_out > TaoCurrency::ZERO); + }); + }); +} - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity as u64); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); +#[test] +fn test_convert_deltas() { + new_test_ext().execute_with(|| { + for (tao, alpha, w_quote, delta_in) in [ + (1500, 1000, 0.5, 1), + (1500, 1000, 0.5, 10000), + (1500, 1000, 0.5, 1000000), + (1500, 1000, 0.5, u64::MAX), + (1, 1000000, 0.5, 1), + (1, 1000000, 0.5, 10000), + (1, 1000000, 0.5, 1000000), + (1, 1000000, 0.5, u64::MAX), + (1000000, 1, 0.5, 1), + (1000000, 1, 0.5, 10000), + (1000000, 1, 0.5, 1000000), + (1000000, 1, 0.5, u64::MAX), + (1500, 1000, 0.50000001, 1), + (1500, 1000, 0.50000001, 10000), + (1500, 1000, 0.50000001, 1000000), + (1500, 1000, 0.50000001, u64::MAX), + (1, 1000000, 0.50000001, 1), + (1, 1000000, 0.50000001, 10000), + (1, 1000000, 0.50000001, 1000000), + (1, 1000000, 0.50000001, u64::MAX), + (1000000, 1, 0.50000001, 1), + (1000000, 1, 0.50000001, 10000), + (1000000, 1, 0.50000001, 1000000), + (1000000, 1, 0.50000001, u64::MAX), + (1500, 1000, 0.49999999, 1), + (1500, 1000, 0.49999999, 10000), + (1500, 1000, 0.49999999, 1000000), + (1500, 1000, 0.49999999, u64::MAX), + (1, 1000000, 0.49999999, 1), + (1, 1000000, 0.49999999, 10000), + (1, 1000000, 0.49999999, 1000000), + (1, 1000000, 0.49999999, u64::MAX), + (1000000, 1, 0.49999999, 1), + (1000000, 1, 0.49999999, 10000), + (1000000, 1, 0.49999999, 1000000), + (1000000, 1, 0.49999999, u64::MAX), + // Low quote weight + (1500, 1000, 0.1, 1), + (1500, 1000, 0.1, 10000), + (1500, 1000, 0.1, 1000000), + (1500, 1000, 0.1, u64::MAX), + (1, 1000000, 0.1, 1), + (1, 1000000, 0.1, 10000), + (1, 1000000, 0.1, 1000000), + (1, 1000000, 0.1, u64::MAX), + (1000000, 1, 0.1, 1), + (1000000, 1, 0.1, 10000), + (1000000, 1, 0.1, 1000000), + (1000000, 1, 0.1, u64::MAX), + // High quote weight + (1500, 1000, 0.9, 1), + (1500, 1000, 0.9, 10000), + (1500, 1000, 0.9, 1000000), + (1500, 1000, 0.9, u64::MAX), + (1, 1000000, 0.9, 1), + (1, 1000000, 0.9, 10000), + (1, 1000000, 0.9, 1000000), + (1, 1000000, 0.9, u64::MAX), + (1000000, 1, 0.9, 1), + (1000000, 1, 0.9, 10000), + (1000000, 1, 0.9, 1000000), + (1000000, 1, 0.9, u64::MAX), + ] { + // Initialize reserves and weights + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(tao)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(alpha)); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + let w_accuracy = 1_000_000_000_f64; + let w_quote_pt = + Perquintill::from_rational((w_quote * w_accuracy) as u128, w_accuracy as u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); + + // Calculate expected swap results (buy and sell) using f64 math + let y = tao as f64; + let x = alpha as f64; + let d = delta_in as f64; + let w1_div_w2 = (1. - w_quote) / w_quote; + let w2_div_w1 = w_quote / (1. - w_quote); + let expected_sell = y * (1. - (x / (x + d)).powf(w1_div_w2)); + let expected_buy = x * (1. - (y / (y + d)).powf(w2_div_w1)); - if order_liquidity_fraction <= 0.001 { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 10 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - } - - // Assert that price movement is in correct direction - let current_price_after = Pallet::::current_price(netuid); - assert_eq!(price_should_grow, current_price_after > current_price); - - // Assert that for small amounts price stays within the user position - if (order_liquidity_fraction <= 0.001) - && (price_low_offset > 0.0001) - && (price_high_offset > 0.0001) - { - assert!(current_price_after <= price_high); - assert!(current_price_after >= price_low); - } - - // Check that low and high ticks' fees were updated properly - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, - ); - - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (order_liquidity - order_liquidity / (1.0 + fee_rate)) as u64; - - // // Global fees should be updated - let actual_global_fee = ($order_t::with_amount(0) - .amount() - .global_fee(netuid) - .to_num::() - * (liquidity_before as f64)) as u64; - - assert_abs_diff_eq!( - swap_result.fee_paid.to_u64(), - expected_fee, - epsilon = expected_fee / 10 - ); - assert_abs_diff_eq!(actual_global_fee, expected_fee, epsilon = expected_fee / 10); - - // Tick fees should be updated - - // Liquidity position should not be updated - let positions = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - let position = positions.first().unwrap(); - - assert_eq!(position.liquidity, position_liquidity,); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - }); - }; - } - - // Current price is 0.25 - // The test case is based on the current price and position prices are defined as a price - // offset from the current price - // Outer part of test case is Position: (price_low_offset, price_high_offset, liquidity) - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - ( - current_price_high - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at min to current range - ( - min_price - current_price, - current_price_low - current_price, - 2_000_000_000_u64, - ), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - // Inner part of test case is Order: (order_type, order_liquidity, limit_price) - // order_liquidity is represented as a fraction of position_liquidity - for liquidity_fraction in [0.0001, 0.001, 0.01, 0.1, 0.2, 0.5] { - perform_test!( - GetAlphaForTao, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 1000.0_f64, - true - ); - perform_test!( - GetTaoForAlpha, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 0.0001_f64, - false - ); - } - }, - ); -} - -// This test is a sanity check for swap and multiple positions -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_multiple_positions --exact --show-output --nocapture -#[test] -fn test_swap_multiple_positions() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - - // Current price is 0.25 - // All positions below are placed at once - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - (0.0, max_price - current_price, 2_000_000_000_u64), - // Repeat the protocol liquidity at min to current range - (min_price - current_price, 0.0, 2_000_000_000_u64), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - // A few (overlapping) positions up the range - (0.01, 0.02, 100_000_000_000_u64), - (0.02, 0.03, 100_000_000_000_u64), - (0.03, 0.04, 100_000_000_000_u64), - (0.03, 0.05, 100_000_000_000_u64), - // A few (overlapping) positions down the range - (-0.02, -0.01, 100_000_000_000_u64), - (-0.03, -0.02, 100_000_000_000_u64), - (-0.04, -0.03, 100_000_000_000_u64), - (-0.05, -0.03, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - }, - ); - - macro_rules! perform_test { - ($order_t:ident, $order_liquidity:expr, $limit_price:expr, $should_price_grow:expr) => { - ////////////////////////////////////////////// - // Swap - let order_liquidity = $order_liquidity; - let limit_price = $limit_price; - let should_price_grow = $should_price_grow; - - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let liquidity_before = CurrentLiquidity::::get(netuid); - let output_amount = >::approx_expected_swap_output( - sqrt_current_price.to_num(), - liquidity_before as f64, - order_liquidity as f64, - ); - - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); - - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let output_amount = output_amount as u64; - - assert!(output_amount > 0); - - if alpha_reserve > order_liquidity && tao_reserve > order_liquidity { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 100 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 100 - ); - } - - // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_eq!(should_price_grow, current_price_after > current_price); - }; - } - - // All these orders are executed without swap reset - for order_liquidity in [ - (100_000_u64), - (1_000_000), - (10_000_000), - (100_000_000), - (200_000_000), - (500_000_000), - (1_000_000_000), - (10_000_000_000), - ] { - perform_test!(GetAlphaForTao, order_liquidity, 1000.0_f64, true); - perform_test!(GetTaoForAlpha, order_liquidity, 0.0001_f64, false); - } - - // Current price shouldn't be much different from the original - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_abs_diff_eq!( - current_price, - current_price_after, - epsilon = current_price / 10. - ) - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output -#[test] -fn test_swap_precision_edge_case() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(123); // 123 is netuid with low edge case liquidity - let order = GetTaoForAlpha::with_amount(1_000_000_000_000_000_000); - let tick_low = TickIndex::MIN; - - let sqrt_limit_price: SqrtPrice = tick_low.try_to_sqrt_price().unwrap(); - - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Swap - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, true).unwrap(); - - assert!(swap_result.amount_paid_out > TaoCurrency::ZERO); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_price_tick_price_roundtrip --exact --show-output -#[test] -fn test_price_tick_price_roundtrip() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - let current_price = SqrtPrice::from_num(0.500_000_512_192_122_7); - let tick = TickIndex::try_from_sqrt_price(current_price).unwrap(); - - let round_trip_price = TickIndex::try_to_sqrt_price(&tick).unwrap(); - assert!(round_trip_price <= current_price); - - let roundtrip_tick = TickIndex::try_from_sqrt_price(round_trip_price).unwrap(); - assert!(tick == roundtrip_tick); - }); -} - -#[test] -fn test_convert_deltas() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - for (sqrt_price, delta_in, expected_buy, expected_sell) in [ - (SqrtPrice::from_num(1.5), 1, 0, 2), - (SqrtPrice::from_num(1.5), 10000, 4444, 22500), - (SqrtPrice::from_num(1.5), 1000000, 444444, 2250000), - ( - SqrtPrice::from_num(1.5), - u64::MAX, - 2000000000000, - 3000000000000, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1, - 18406523739291577836, - 465, - ), - (TickIndex::MIN.as_sqrt_price_bounded(), 10000, u64::MAX, 465), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1000000, - u64::MAX, - 465, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - u64::MAX, - u64::MAX, - 464, - ), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - 1, - 0, - 18406523745214495085, - ), - (TickIndex::MAX.as_sqrt_price_bounded(), 10000, 0, u64::MAX), - (TickIndex::MAX.as_sqrt_price_bounded(), 1000000, 0, u64::MAX), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - u64::MAX, - 2000000000000, - u64::MAX, - ), - ] { - { - AlphaSqrtPrice::::insert(netuid, sqrt_price); - - assert_abs_diff_eq!( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ), - expected_sell.into(), - epsilon = 2.into() - ); - assert_abs_diff_eq!( - BasicSwapStep::::convert_deltas( - netuid, - delta_in.into() - ), - expected_buy.into(), - epsilon = 2.into() - ); - } - } - }); -} - -// #[test] -// fn test_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// // Use a netuid above 100 since our mock enables liquidity for 0-100 -// let netuid = NetUid::from(101); -// let tick_low = TickIndex::new_unchecked(-1000); -// let tick_high = TickIndex::new_unchecked(1000); -// let position_id = PositionId::from(1); -// let liquidity = 1_000_000_000; -// let liquidity_delta = 500_000_000; - -// assert!(!EnabledUserLiquidity::::get(netuid)); - -// assert_noop!( -// Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_noop!( -// Swap::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id), -// Error::::LiquidityNotFound -// ); - -// assert_noop!( -// Swap::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// position_id, -// liquidity_delta -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); - -// let position_id = Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .unwrap() -// .0; - -// assert_ok!(Swap::do_modify_position( -// netuid.into(), -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// position_id, -// liquidity_delta, -// )); - -// assert_ok!(Swap::do_remove_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// position_id, -// )); -// }); -// } - -/// Test correctness of swap fees: -/// - Fees are distribued to (concentrated) liquidity providers -/// -#[test] -fn test_swap_fee_correctness() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let netuid = NetUid::from(1); - - // Provide very spread liquidity at the range from min to max that matches protocol liquidity - let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - - // Add user liquidity - let (position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Swap buy and swap sell - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(liquidity / 10), - u64::MAX.into(), - false, - false, - ) - .unwrap(); - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(liquidity / 10), - 0_u64.into(), - false, - false, - ) - .unwrap(); - - // Get user position - let mut position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - - // Check that 50% of fees were credited to the position - let fee_rate = FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; - let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); - let expected_fee = (fee_rate * (liquidity / 10) as f64 * 0.5) as u64; - - assert_abs_diff_eq!(actual_fee_tao, expected_fee, epsilon = 1,); - assert_abs_diff_eq!(actual_fee_alpha, expected_fee, epsilon = 1,); - }); -} - -#[test] -fn test_current_liquidity_updates() { - let netuid = NetUid::from(1); - let liquidity = 1_000_000_000; - - // Get current price - let (current_price, current_price_low, current_price_high) = - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - (current_price, current_price_low, current_price_high) - }); - - // Test case: (price_low, price_high, expect_to_update) - [ - // Current price is out of position range (lower), no current lq update - (current_price * 2., current_price * 3., false), - // Current price is out of position range (higher), no current lq update - (current_price / 3., current_price / 2., false), - // Current price is just below position range, no current lq update - (current_price_high, current_price * 3., false), - // Position lower edge is just below the current price, current lq updates - (current_price_low, current_price * 3., true), - // Current price is exactly at lower edge of position range, current lq updates - (current_price, current_price * 3., true), - // Current price is exactly at higher edge of position range, no current lq update - (current_price / 2., current_price, false), - ] - .into_iter() - .for_each(|(price_low, price_high, expect_to_update)| { - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - )); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = if (price_high > current_price) && (price_low <= current_price) - { - assert!(expect_to_update); - liquidity_before + liquidity - } else { - assert!(!expect_to_update); - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }); - }); -} + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_sell as u64, + epsilon = 2u64 + ); + assert_abs_diff_eq!( + u64::from( + BasicSwapStep::::convert_deltas( + netuid, + delta_in.into() + ) + ), + expected_buy as u64, + epsilon = 2u64 + ); + } + }); +} #[test] fn test_rollback_works() { @@ -1620,80 +764,7 @@ fn test_rollback_works() { }) } -/// Test correctness of swap fees: -/// - New LP is not eligible to previously accrued fees -/// -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_new_lp_doesnt_get_old_fees --exact --show-output -#[test] -fn test_new_lp_doesnt_get_old_fees() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let netuid = NetUid::from(1); - - // Provide very spread liquidity at the range from min to max that matches protocol liquidity - let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - - // Add user liquidity - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Swap buy and swap sell - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(liquidity / 10), - u64::MAX.into(), - false, - false, - ) - .unwrap(); - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(liquidity / 10), - 0_u64.into(), - false, - false, - ) - .unwrap(); - - // Add liquidity from a different user to a new tick - let (position_id_2, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_2, - &OK_HOTKEY_ACCOUNT_ID_2, - tick_low.next().unwrap(), - tick_high.prev().unwrap(), - liquidity, - ) - .unwrap(); - - // Get user position - let mut position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID_2, position_id_2)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low.next().unwrap()); - assert_eq!(position.tick_high, tick_high.prev().unwrap()); - - // Check that collected fees are 0 - let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); - assert_abs_diff_eq!(actual_fee_tao, 0, epsilon = 1); - assert_abs_diff_eq!(actual_fee_alpha, 0, epsilon = 1); - }); -} - +#[allow(dead_code)] fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { if t < a { a @@ -1704,955 +775,40 @@ fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { } } +#[allow(dead_code)] fn print_current_price(netuid: NetUid) { - let current_sqrt_price = AlphaSqrtPrice::::get(netuid).to_num::(); - let current_price = current_sqrt_price * current_sqrt_price; + let current_price = Pallet::::current_price(netuid); log::trace!("Current price: {current_price:.6}"); } -/// RUST_LOG=pallet_subtensor_swap=trace cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_wrapping_fees --exact --show-output --nocapture +/// Simple palswap path: PalSwap is initialized. +/// Function must still clear any residual storages and succeed. #[test] -fn test_wrapping_fees() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(WRAPPING_FEES_NETUID); - let position_1_low_price = 0.20; - let position_1_high_price = 0.255; - let position_2_low_price = 0.255; - let position_2_high_price = 0.257; - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_RICH, - &OK_COLDKEY_ACCOUNT_ID_RICH, - price_to_tick(position_1_low_price), - price_to_tick(position_1_high_price), - 1_000_000_000_u64, - ) - .unwrap(); - - print_current_price(netuid); - - let order = GetTaoForAlpha::with_amount(800_000_000); - let sqrt_limit_price = SqrtPrice::from_num(0.000001); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - let order = GetAlphaForTao::with_amount(1_850_000_000); - let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - - print_current_price(netuid); - - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - print_current_price(netuid); - - let add_liquidity_result = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_RICH, - &OK_COLDKEY_ACCOUNT_ID_RICH, - price_to_tick(position_2_low_price), - price_to_tick(position_2_high_price), - 1_000_000_000_u64, - ) - .unwrap(); - - let order = GetTaoForAlpha::with_amount(1_800_000_000); - let sqrt_limit_price = SqrtPrice::from_num(0.000001); - - let initial_sqrt_price = AlphaSqrtPrice::::get(netuid); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - let final_sqrt_price = AlphaSqrtPrice::::get(netuid); - - print_current_price(netuid); - - let mut position = - Positions::::get((netuid, &OK_COLDKEY_ACCOUNT_ID_RICH, add_liquidity_result.0)) - .unwrap(); - - let initial_box_price = bbox( - initial_sqrt_price, - position.tick_low.try_to_sqrt_price().unwrap(), - position.tick_high.try_to_sqrt_price().unwrap(), - ); - - let final_box_price = bbox( - final_sqrt_price, - position.tick_low.try_to_sqrt_price().unwrap(), - position.tick_high.try_to_sqrt_price().unwrap(), - ); - - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - - log::trace!("fee_rate: {fee_rate:.6}"); - log::trace!("position.liquidity: {}", position.liquidity); - log::trace!( - "initial_box_price: {:.6}", - initial_box_price.to_num::() - ); - log::trace!("final_box_price: {:.6}", final_box_price.to_num::()); - - let expected_fee_tao = ((fee_rate / (1.0 - fee_rate)) - * (position.liquidity as f64) - * (final_box_price.to_num::() - initial_box_price.to_num::())) - as u64; - - let expected_fee_alpha = ((fee_rate / (1.0 - fee_rate)) - * (position.liquidity as f64) - * ((1.0 / final_box_price.to_num::()) - (1.0 / initial_box_price.to_num::()))) - as u64; - - log::trace!("Expected ALPHA fee: {:.6}", expected_fee_alpha as f64); - - let (fee_tao, fee_alpha) = position.collect_fees(); - - log::trace!("Collected fees: TAO: {fee_tao}, ALPHA: {fee_alpha}"); - - assert_abs_diff_eq!(fee_tao, expected_fee_tao, epsilon = 1); - assert_abs_diff_eq!(fee_alpha, expected_fee_alpha, epsilon = 1); - }); -} - -/// Test that price moves less with provided liquidity -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_less_price_movement --exact --show-output -#[test] -fn test_less_price_movement() { - let netuid = NetUid::from(1); - let mut last_end_price = U96F32::from_num(0); - let initial_stake_liquidity = 1_000_000_000; - let swapped_liquidity = 1_000_000; - - // Test case is (order_type, provided_liquidity) - // Testing algorithm: - // - Stake initial_stake_liquidity - // - Provide liquidity if iteration provides lq - // - Buy or sell - // - Save end price if iteration doesn't provide lq - macro_rules! perform_test { - ($order_t:ident, $provided_liquidity:expr, $limit_price:expr, $should_price_shrink:expr) => { - let provided_liquidity = $provided_liquidity; - let should_price_shrink = $should_price_shrink; - let limit_price = $limit_price; - new_test_ext().execute_with(|| { - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Buy Alpha - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(initial_stake_liquidity), - SqrtPrice::from_num(10_000_000_000_u64), - false, - false - )); - - // Get current price - let start_price = Pallet::::current_price(netuid); - - // Add liquidity if this test iteration provides - if provided_liquidity > 0 { - let tick_low = price_to_tick(start_price.to_num::() * 0.5); - let tick_high = price_to_tick(start_price.to_num::() * 1.5); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - provided_liquidity, - )); - } - - // Swap - let sqrt_limit_price = SqrtPrice::from_num(limit_price); - assert_ok!(Pallet::::do_swap( - netuid, - $order_t::with_amount(swapped_liquidity), - sqrt_limit_price, - false, - false - )); - - let end_price = Pallet::::current_price(netuid); - - // Save end price if iteration doesn't provide or compare with previous end price if - // it does - if provided_liquidity > 0 { - assert_eq!(should_price_shrink, end_price < last_end_price); - } else { - last_end_price = end_price; - } - }); - }; - } - - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetAlphaForTao, provided_liquidity, 1000.0_f64, true); - } - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetTaoForAlpha, provided_liquidity, 0.001_f64, false); - } -} - -// TODO: Revise when user liquidity is available -// #[test] -// fn test_swap_subtoken_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(SUBTOKEN_DISABLED_NETUID); // Use a netuid not used elsewhere -// let price_low = 0.1; -// let price_high = 0.2; -// let tick_low = price_to_tick(price_low); -// let tick_high = price_to_tick(price_high); -// let liquidity = 1_000_000_u64; - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - -// assert_noop!( -// Pallet::::add_liquidity( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// tick_low, -// tick_high, -// liquidity, -// ), -// Error::::SubtokenDisabled -// ); - -// assert_noop!( -// Pallet::::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// PositionId::from(0), -// liquidity as i64, -// ), -// Error::::SubtokenDisabled -// ); -// }); -// } - -#[test] -fn test_liquidate_v3_removes_positions_ticks_and_state() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Initialize V3 (creates protocol position, ticks, price, liquidity) - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!(SwapV3Initialized::::get(netuid)); - - // Enable user LP - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - - // Add a user position across the full range to ensure ticks/bitmap are populated. - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - let liquidity = 2_000_000_000_u64; - - let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .expect("add liquidity"); - - // Accrue some global fees so we can verify fee storage is cleared later. - let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - sqrt_limit_price, - false, - false - )); - - // Sanity: protocol & user positions exist, ticks exist, liquidity > 0 - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(!prot_positions.is_empty()); - - let user_positions = Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert_eq!(user_positions.len(), 1); - - assert!(Ticks::::get(netuid, TickIndex::MIN).is_some()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); - assert!(CurrentLiquidity::::get(netuid) > 0); - - let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_some(); - assert!(had_bitmap_words); - - // ACT: users-only liquidation then protocol clear - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // ASSERT: positions cleared (both user and protocol) - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(prot_positions_after.is_empty()); - let user_positions_after = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert!(user_positions_after.is_empty()); - - // ASSERT: ticks cleared - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - - // ASSERT: fee globals cleared - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - // ASSERT: price/tick/liquidity flags cleared - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - - // ASSERT: active tick bitmap cleared - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - - // ASSERT: knobs removed on dereg - assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); -} - -// V3 path with user liquidity disabled at teardown: -// must still remove positions and clear state (after protocol clear). -// #[test] -// fn test_liquidate_v3_with_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(101); - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); -// assert!(SwapV3Initialized::::get(netuid)); - -// // Enable temporarily to add a user position -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// true -// )); - -// let min_price = tick_to_price(TickIndex::MIN); -// let max_price = tick_to_price(TickIndex::MAX); -// let tick_low = price_to_tick(min_price); -// let tick_high = price_to_tick(max_price); -// let liquidity = 1_000_000_000_u64; - -// let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .expect("add liquidity"); - -// // Disable user LP *before* liquidation; removal must ignore this flag. -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// false -// )); - -// // Users-only dissolve, then clear protocol liquidity/state. -// assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); -// assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - -// // ASSERT: positions & ticks gone, state reset -// assert_eq!( -// Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), -// 0 -// ); -// assert!( -// Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) -// .next() -// .is_none() -// ); -// assert!(Ticks::::iter_prefix(netuid).next().is_none()); -// assert!( -// TickIndexBitmapWords::::iter_prefix((netuid,)) -// .next() -// .is_none() -// ); -// assert!(!SwapV3Initialized::::contains_key(netuid)); -// assert!(!AlphaSqrtPrice::::contains_key(netuid)); -// assert!(!CurrentTick::::contains_key(netuid)); -// assert!(!CurrentLiquidity::::contains_key(netuid)); -// assert!(!FeeGlobalTao::::contains_key(netuid)); -// assert!(!FeeGlobalAlpha::::contains_key(netuid)); - -// // `EnabledUserLiquidity` is removed by protocol clear stage. -// assert!(!EnabledUserLiquidity::::contains_key(netuid)); -// }); -// } - -/// Non‑V3 path: V3 not initialized (no positions); function must still clear any residual storages and succeed. -#[test] -fn test_liquidate_non_v3_uninitialized_ok_and_clears() { +fn test_liquidate_pal_simple_ok_and_clears() { new_test_ext().execute_with(|| { let netuid = NetUid::from(202); - // Sanity: V3 is not initialized - assert!(!SwapV3Initialized::::get(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - - // ACT - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // ASSERT: Defensive clears leave no residues and do not panic - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - - // All single-key maps should not have the key after liquidation - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); -} - -#[test] -fn test_liquidate_idempotent() { - // V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(7); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add a small user position - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - let tick_low = price_to_tick(0.2); - let tick_high = price_to_tick(0.3); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - 123_456_789 - )); - - // Users-only liquidations are idempotent. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Now clear protocol liquidity/state—also idempotent. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // State remains empty - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); - - // Non‑V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(8); - - // Never initialize V3; both calls no-op and succeed. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn liquidate_v3_refunds_user_funds_and_clears_state() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Enable V3 path & initialize price/ticks (also creates a protocol position). - assert_ok!(Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid, - true - )); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Use distinct cold/hot to demonstrate alpha refund/stake accounting. - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - // Tight in‑range band around current tick. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.saturating_sub(10); - let tick_high = ct.saturating_add(10); - let liquidity: u64 = 1_000_000; - - // Snapshot balances BEFORE. - let tao_before = ::BalanceOps::tao_balance(&cold); - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_total = alpha_before_hot + alpha_before_owner; - - // Create the user position (storage & v3 state only; no balances moved yet). - let (_pos_id, need_tao, need_alpha) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add liquidity"); - - // Mirror extrinsic bookkeeping: withdraw funds & bump provided‑reserve counters. - let tao_taken = ::BalanceOps::decrease_balance(&cold, need_tao.into()) - .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - need_alpha.into(), - ) - .expect("decrease ALPHA"); - TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // Users‑only liquidation. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Expect balances restored to BEFORE snapshots (no swaps ran -> zero fees). - let tao_after = ::BalanceOps::tao_balance(&cold); - assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); - - // ALPHA totals conserved to owner (distribution may differ). - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "ALPHA principal must be refunded/staked for the account (check totals)" - ); - - // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // User position(s) are gone and all V3 state cleared. - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_single_provider_exact() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(11); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // --- Create an alpha‑only position (range entirely above current tick → TAO = 0, ALPHA > 0). - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let liquidity = 1_000_000_u64; - let (_pos_id, tao_needed, alpha_needed) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add alpha-only liquidity"); - assert_eq!(tao_needed, 0, "alpha-only position must not require TAO"); - assert!(alpha_needed > 0, "alpha-only position must require ALPHA"); - - // --- Snapshot BEFORE we withdraw funds (baseline for conservation). - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_total = alpha_before_hot + alpha_before_owner; - - // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - alpha_needed.into(), - ) - .expect("decrease ALPHA"); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // --- Act: users‑only dissolve. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // --- Assert: total α conserved to owner (may be staked to validator). - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "ALPHA principal must be conserved to the account" - ); - - // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // --- State is cleared. - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_multiple_providers_proportional_to_principal() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(12); - let c1 = OK_COLDKEY_ACCOUNT_ID; - let h1 = OK_HOTKEY_ACCOUNT_ID; - let c2 = OK_COLDKEY_ACCOUNT_ID_2; - let h2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Use the same "above current tick" trick for alpha‑only positions. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - // Provider #1 (smaller α) - let liq1 = 700_000_u64; - let (_p1, t1, a1) = - Pallet::::do_add_liquidity(netuid, &c1, &h1, tick_low, tick_high, liq1) - .expect("add alpha-only liquidity #1"); - assert_eq!(t1, 0); - assert!(a1 > 0); - - // Provider #2 (larger α) - let liq2 = 2_100_000_u64; - let (_p2, t2, a2) = - Pallet::::do_add_liquidity(netuid, &c2, &h2, tick_low, tick_high, liq2) - .expect("add alpha-only liquidity #2"); - assert_eq!(t2, 0); - assert!(a2 > 0); - - // Baselines BEFORE withdrawing - let a1_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_before = a1_before_hot + a1_before_owner; - - let a2_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_before = a2_before_hot + a2_before_owner; - - // Withdraw α and account reserves for each provider. - let a1_taken = - ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) - .expect("decrease α #1"); - AlphaReserve::increase_provided(netuid.into(), a1_taken); - - let a2_taken = - ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) - .expect("decrease α #2"); - AlphaReserve::increase_provided(netuid.into(), a2_taken); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Each owner is restored to their exact baseline. - let a1_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_after = a1_after_hot + a1_after_owner; - assert_eq!( - a1_after, a1_before, - "owner #1 must receive their α principal back" - ); + // Insert map values + FeeRate::::insert(netuid, 1_000); + FeesTao::::insert(netuid, TaoCurrency::from(1_000)); + FeesAlpha::::insert(netuid, AlphaCurrency::from(1_000)); + PalSwapInitialized::::insert(netuid, true); + let w_quote_pt = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); - let a2_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_after = a2_after_hot + a2_after_owner; - assert_eq!( - a2_after, a2_before, - "owner #2 must receive their α principal back" - ); - }); -} - -#[test] -fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(13); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot1 = OK_HOTKEY_ACCOUNT_ID; - let hot2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Two alpha‑only positions on different hotkeys of the same owner. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let (_p1, _t1, a1) = - Pallet::::do_add_liquidity(netuid, &cold, &hot1, tick_low, tick_high, 900_000) - .expect("add alpha-only pos (hot1)"); - let (_p2, _t2, a2) = - Pallet::::do_add_liquidity(netuid, &cold, &hot2, tick_low, tick_high, 1_500_000) - .expect("add alpha-only pos (hot2)"); - assert!(a1 > 0 && a2 > 0); - - // Baseline BEFORE: sum over (cold,hot1) + (cold,hot2) + (cold,cold). - let before_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let before_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let before_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let before_total = before_hot1 + before_hot2 + before_owner; - - // Withdraw α from both hotkeys; track provided‑reserve. - let t1 = - ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) - .expect("decr α #hot1"); - AlphaReserve::increase_provided(netuid.into(), t1); - - let t2 = - ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) - .expect("decr α #hot2"); - AlphaReserve::increase_provided(netuid.into(), t2); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // The total α "owned" by the coldkey is conserved (credit may land on (cold,cold)). - let after_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let after_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let after_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let after_total = after_hot1 + after_hot2 + after_owner; + // Sanity: PalSwap is not initialized + assert!(PalSwapInitialized::::get(netuid)); - assert_eq!( - after_total, before_total, - "owner’s α must be conserved across hot ledgers + (owner,owner)" - ); - }); -} - -#[test] -fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { - new_test_ext().execute_with(|| { - // --- Setup --- - let netuid = NetUid::from(42); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!(SwapV3Initialized::::get(netuid)); - - // Tight in‑range band so BOTH τ and α are required. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.saturating_sub(10); - let tick_high = ct.saturating_add(10); - let liquidity: u64 = 1_250_000; - - // Add liquidity and capture required τ/α. - let (_pos_id, tao_needed, alpha_needed) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add in-range liquidity"); - assert!(tao_needed > 0, "in-range pos must require TAO"); - assert!(alpha_needed > 0, "in-range pos must require ALPHA"); - - // Determine the permitted validator with the highest trust (green path). - let trust = ::SubnetInfo::get_validator_trust(netuid.into()); - let permit = ::SubnetInfo::get_validator_permit(netuid.into()); - assert_eq!(trust.len(), permit.len(), "trust/permit must align"); - let target_uid: u16 = trust - .iter() - .zip(permit.iter()) - .enumerate() - .filter(|(_, (_t, p))| **p) - .max_by_key(|(_, (t, _))| *t) - .map(|(i, _)| i as u16) - .expect("at least one permitted validator"); - let validator_hotkey: ::AccountId = - ::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid) - .expect("uid -> hotkey mapping must exist"); - - // --- Snapshot BEFORE we withdraw τ/α to fund the position --- - let tao_before = ::BalanceOps::tao_balance(&cold); - - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_val = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); - - let alpha_before_total = if validator_hotkey == hot { - alpha_before_hot + alpha_before_owner - } else { - alpha_before_hot + alpha_before_owner + alpha_before_val - }; - - // --- Mirror extrinsic bookkeeping: withdraw τ & α; bump provided reserves --- - let tao_taken = ::BalanceOps::decrease_balance(&cold, tao_needed.into()) - .expect("decrease TAO"); - let alpha_taken = ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - alpha_needed.into(), - ) - .expect("decrease ALPHA"); - - TaoReserve::increase_provided(netuid.into(), tao_taken); - AlphaReserve::increase_provided(netuid.into(), alpha_taken); - - // --- Act: dissolve (GREEN PATH: permitted validators exist) --- - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // --- Assert: τ principal refunded to user --- - let tao_after = ::BalanceOps::tao_balance(&cold); - assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); - - // --- α ledger assertions --- - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_val = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); - - // Owner ledger must be unchanged in the green path. - assert_eq!( - alpha_after_owner, alpha_before_owner, - "Owner α ledger must be unchanged (staked to validator, not refunded)" - ); - - if validator_hotkey == hot { - assert_eq!( - alpha_after_hot, alpha_before_hot, - "When validator == hotkey, user's hot ledger must net back to its original balance" - ); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "Total α for the coldkey must be conserved (validator==hotkey)" - ); - } else { - assert!( - alpha_before_hot >= alpha_after_hot, - "hot ledger should not increase" - ); - assert!( - alpha_after_val >= alpha_before_val, - "validator ledger should not decrease" - ); - - let hot_loss = alpha_before_hot - alpha_after_hot; - let val_gain = alpha_after_val - alpha_before_val; - assert_eq!( - val_gain, hot_loss, - "α that left the user's hot ledger must equal α credited to the validator ledger" - ); - - let alpha_after_total = alpha_after_hot + alpha_after_owner + alpha_after_val; - assert_eq!( - alpha_after_total, alpha_before_total, - "Total α for the coldkey must be conserved" - ); - } - - // Now clear protocol liquidity & state and assert full reset. + // ACT assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - let protocol_id = Pallet::::protocol_account_id(); - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!( - prot_positions_after.is_empty(), - "protocol positions must be removed" - ); - - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none(), - "active tick bitmap words must be cleared" - ); - + // All single-key maps should not have the key after liquidation assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); + assert!(!FeesTao::::contains_key(netuid)); + assert!(!FeesAlpha::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); + assert!(!SwapBalancer::::contains_key(netuid)); }); } @@ -2660,84 +816,36 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { fn test_clear_protocol_liquidity_green_path() { new_test_ext().execute_with(|| { // --- Arrange --- - let netuid = NetUid::from(55); - - // Ensure the "user liquidity enabled" flag exists so we can verify it's removed later. - assert_ok!(Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid, - true - )); - - // Initialize V3 state; this should set price/tick flags and create a protocol position. - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!( - SwapV3Initialized::::get(netuid), - "V3 must be initialized" - ); + let netuid = NetUid::from(1); - // Sanity: protocol positions exist before clearing. - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions_before = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + // Initialize swap state + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); assert!( - !prot_positions_before.is_empty(), - "protocol positions should exist after V3 init" + PalSwapInitialized::::get(netuid), + "Swap must be initialized" ); // --- Act --- // Green path: just clear protocol liquidity and wipe all V3 state. assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - // --- Assert: all protocol positions removed --- - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!( - prot_positions_after.is_empty(), - "protocol positions must be removed by do_clear_protocol_liquidity" - ); - - // --- Assert: V3 data wiped (idempotent even if some maps were empty) --- - // Ticks / active tick bitmap - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none(), - "active tick bitmap words must be cleared" - ); - // Fee globals - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); + assert!(!FeesTao::::contains_key(netuid)); + assert!(!FeesAlpha::::contains_key(netuid)); - // Price / tick / liquidity / flags - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); + // Flags + assert!(!PalSwapInitialized::::contains_key(netuid)); // Knobs removed assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); // --- And it's idempotent --- assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, protocol_id)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); }); } +#[allow(dead_code)] fn as_tuple( (t_used, a_used, t_rem, a_rem): (TaoCurrency, AlphaCurrency, TaoCurrency, AlphaCurrency), ) -> (u64, u64, u64, u64) { @@ -2749,160 +857,43 @@ fn as_tuple( ) } +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_migrate_swapv3_to_balancer --exact --nocapture #[test] -fn proportional_when_price_is_one_and_tao_is_plenty() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 10u64.into(); - let amount_alpha: AlphaCurrency = 3u64.into(); - - // alpha * price = 3 * 1 = 3 <= amount_tao(10) - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (3, 3, 7, 0)); -} - -#[test] -fn proportional_when_price_is_one_and_alpha_is_excess() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 5u64.into(); - let amount_alpha: AlphaCurrency = 10u64.into(); - - // tao is limiting: alpha_equiv = floor(5 / 1) = 5 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (5, 5, 0, 5)); -} - -#[test] -fn proportional_with_higher_price_and_alpha_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 85u64.into(); - let amount_alpha: AlphaCurrency = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 < 85 => alpha limits tao - // remainders: tao 5, alpha 0 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (80, 20, 5, 0)); -} - -#[test] -fn proportional_with_higher_price_and_tao_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 50u64.into(); - let amount_alpha: AlphaCurrency = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 > 50 => tao limits alpha - // alpha_equivalent = floor(50 / 4) = 12 - // remainders: tao 0, alpha 20 - 12 = 8 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (50, 12, 0, 8)); -} - -#[test] -fn zero_price_uses_no_tao_and_all_alpha() { - // sqrt_price = 0 => price = 0 - let sqrt = U64F64::from_num(0u64); - let amount_tao: TaoCurrency = 42u64.into(); - let amount_alpha: AlphaCurrency = 17u64.into(); - - // tao_equivalent = 17 * 0 = 0 <= 42 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (0, 17, 42, 0)); -} - -#[test] -fn rounding_down_behavior_when_dividing_by_price() { - // sqrt_price = 2.0 => price = 4.0 - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoCurrency = 13u64.into(); - let amount_alpha: AlphaCurrency = 100u64.into(); - - // tao is limiting; alpha_equiv = floor(13 / 4) = 3 - // remainders: tao 0, alpha 100 - 3 = 97 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (13, 3, 0, 97)); -} - -#[test] -fn exact_fit_when_tao_matches_alpha_times_price() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoCurrency = 9u64.into(); - let amount_alpha: AlphaCurrency = 9u64.into(); - - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (9, 9, 0, 0)); -} - -#[test] -fn handles_zero_balances() { - let sqrt = U64F64::from_num(1u64); - - // Zero TAO, some alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 7u64.into()); - // tao limits; alpha_equiv = floor(0 / 1) = 0 - assert_eq!(as_tuple(out), (0, 0, 0, 7)); - - // Some TAO, zero alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 7u64.into(), 0u64.into()); - // tao_equiv = 0 * 1 = 0 <= 7 - assert_eq!(as_tuple(out), (0, 0, 7, 0)); - - // Both zero - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 0u64.into()); - assert_eq!(as_tuple(out), (0, 0, 0, 0)); -} +fn test_migrate_swapv3_to_balancer() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; -#[test] -fn adjust_protocol_liquidity_uses_and_sets_scrap_reservoirs() { new_test_ext().execute_with(|| { - // --- Arrange - let netuid: NetUid = 1u16.into(); - // Price = 1.0 (since sqrt_price^2 = 1), so proportional match is 1:1 - AlphaSqrtPrice::::insert(netuid, U64F64::saturating_from_num(1u64)); - - // Start with some non-zero scrap reservoirs - ScrapReservoirTao::::insert(netuid, TaoCurrency::from(7u64)); - ScrapReservoirAlpha::::insert(netuid, AlphaCurrency::from(5u64)); - - // Create a minimal protocol position so the function’s body executes. - let protocol = Pallet::::protocol_account_id(); - let position = Position::new( - PositionId::from(0), + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let netuid = NetUid::from(1); + + // Insert deprecated maps values + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoCurrency::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert( netuid, - TickIndex::MIN, - TickIndex::MAX, - 0, + AlphaCurrency::from(9876), ); - // Ensure collect_fees() returns (0,0) via zeroed fees in `position` (default). - Positions::::insert((netuid, protocol, position.id), position.clone()); - // --- Act - // No external deltas or fees; only reservoirs should be considered. - // With price=1, the exact proportional pair uses 5 alpha and 5 tao, - // leaving tao scrap = 7 - 5 = 2, alpha scrap = 5 - 5 = 0. - Pallet::::adjust_protocol_liquidity(netuid, 0u64.into(), 0u64.into()); + // Insert reserves that do not match the 1.23 price + TaoReserve::set_mock_reserve(netuid, TaoCurrency::from(1_000_000_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaCurrency::from(4_000_000_000)); - // --- Assert: reservoirs were READ (used in proportional calc) and then SET (updated) - assert_eq!( - ScrapReservoirTao::::get(netuid), - TaoCurrency::from(2u64) - ); - assert_eq!( - ScrapReservoirAlpha::::get(netuid), - AlphaCurrency::from(0u64) + // Run migration + migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); + + // Test that subnet price is still 1.23^2 + assert_abs_diff_eq!( + Swap::current_price(netuid).to_num::(), + 1.23 * 1.23, + epsilon = 0.1 ); }); } diff --git a/pallets/swap/src/position.rs b/pallets/swap/src/position.rs deleted file mode 100644 index 5a57928a93..0000000000 --- a/pallets/swap/src/position.rs +++ /dev/null @@ -1,198 +0,0 @@ -use core::marker::PhantomData; - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{Config, Error, FeeGlobalAlpha, FeeGlobalTao, LastPositionId}; -use crate::tick::TickIndex; - -/// Position designates one liquidity position. -/// -/// Alpha price is expressed in rao units per one 10^9 unit. For example, -/// price 1_000_000 is equal to 0.001 TAO per Alpha. -#[freeze_struct("27a1bf8c59480f0")] -#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)] -#[scale_info(skip_type_params(T))] -pub struct Position { - /// Unique ID of the position - pub id: PositionId, - /// Network identifier - pub netuid: NetUid, - /// Tick index for lower boundary of price - pub tick_low: TickIndex, - /// Tick index for higher boundary of price - pub tick_high: TickIndex, - /// Position liquidity - pub liquidity: u64, - /// Fees accrued by the position in quote currency (TAO) relative to global fees - pub fees_tao: I64F64, - /// Fees accrued by the position in base currency (Alpha) relative to global fees - pub fees_alpha: I64F64, - /// Phantom marker for generic Config type - pub _phantom: PhantomData, -} - -impl Position { - pub fn new( - id: PositionId, - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Self { - let mut position = Position { - id, - netuid, - tick_low, - tick_high, - liquidity, - fees_tao: I64F64::saturating_from_num(0), - fees_alpha: I64F64::saturating_from_num(0), - _phantom: PhantomData, - }; - - position.fees_tao = position.fees_in_range(true); - position.fees_alpha = position.fees_in_range(false); - - position - } - - /// Converts position to token amounts - /// - /// returns tuple of (TAO, Alpha) - /// - /// Pseudocode: - /// if self.sqrt_price_curr < sqrt_pa: - /// tao = 0 - /// alpha = L * (1 / sqrt_pa - 1 / sqrt_pb) - /// elif self.sqrt_price_curr > sqrt_pb: - /// tao = L * (sqrt_pb - sqrt_pa) - /// alpha = 0 - /// else: - /// tao = L * (self.sqrt_price_curr - sqrt_pa) - /// alpha = L * (1 / self.sqrt_price_curr - 1 / sqrt_pb) - /// - pub fn to_token_amounts(&self, sqrt_price_curr: SqrtPrice) -> Result<(u64, u64), Error> { - let one = U64F64::saturating_from_num(1); - - let sqrt_price_low = self - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_high = self - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let liquidity_fixed = U64F64::saturating_from_num(self.liquidity); - - Ok(if sqrt_price_curr < sqrt_price_low { - ( - 0, - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_low) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - } else if sqrt_price_curr > sqrt_price_high { - ( - liquidity_fixed - .saturating_mul(sqrt_price_high.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - 0, - ) - } else { - ( - liquidity_fixed - .saturating_mul(sqrt_price_curr.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_curr) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - }) - } - - /// Collect fees for a position - /// Updates the position - pub fn collect_fees(&mut self) -> (u64, u64) { - let fee_tao_agg = self.fees_in_range(true); - let fee_alpha_agg = self.fees_in_range(false); - - let mut fee_tao = fee_tao_agg.saturating_sub(self.fees_tao); - let mut fee_alpha = fee_alpha_agg.saturating_sub(self.fees_alpha); - - self.fees_tao = fee_tao_agg; - self.fees_alpha = fee_alpha_agg; - - let liquidity_frac = I64F64::saturating_from_num(self.liquidity); - - fee_tao = liquidity_frac.saturating_mul(fee_tao); - fee_alpha = liquidity_frac.saturating_mul(fee_alpha); - - ( - fee_tao.saturating_to_num::(), - fee_alpha.saturating_to_num::(), - ) - } - - /// Get fees in a position's range - /// - /// If quote flag is true, Tao is returned, otherwise alpha. - fn fees_in_range(&self, quote: bool) -> I64F64 { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - } - .saturating_sub(self.tick_low.fees_below::(self.netuid, quote)) - .saturating_sub(self.tick_high.fees_above::(self.netuid, quote)) - } -} - -#[freeze_struct("8501fa251c9d74c")] -#[derive( - Clone, - Copy, - Decode, - DecodeWithMemTracking, - Default, - Encode, - Eq, - MaxEncodedLen, - PartialEq, - RuntimeDebug, - TypeInfo, -)] -pub struct PositionId(u128); - -impl PositionId { - /// Create a new position ID - pub fn new() -> Self { - let new = LastPositionId::::get().saturating_add(1); - LastPositionId::::put(new); - - Self(new) - } -} - -impl From for PositionId { - fn from(value: u128) -> Self { - Self(value) - } -} - -impl From for u128 { - fn from(value: PositionId) -> Self { - value.0 - } -} diff --git a/pallets/swap/src/tick.rs b/pallets/swap/src/tick.rs deleted file mode 100644 index d3493fde45..0000000000 --- a/pallets/swap/src/tick.rs +++ /dev/null @@ -1,2198 +0,0 @@ -//! The math is adapted from github.com/0xKitsune/uniswap-v3-math -use core::cmp::Ordering; -use core::convert::TryFrom; -use core::error::Error; -use core::fmt; -use core::hash::Hash; -use core::ops::{Add, AddAssign, BitOr, Deref, Neg, Shl, Shr, Sub, SubAssign}; - -use alloy_primitives::{I256, U256}; -use codec::{Decode, DecodeWithMemTracking, Encode, Error as CodecError, Input, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use sp_std::vec; -use sp_std::vec::Vec; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{ - Config, CurrentTick, FeeGlobalAlpha, FeeGlobalTao, TickIndexBitmapWords, Ticks, -}; - -const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); -const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); -const U256_3: U256 = U256::from_limbs([3, 0, 0, 0]); -const U256_4: U256 = U256::from_limbs([4, 0, 0, 0]); -const U256_5: U256 = U256::from_limbs([5, 0, 0, 0]); -const U256_6: U256 = U256::from_limbs([6, 0, 0, 0]); -const U256_7: U256 = U256::from_limbs([7, 0, 0, 0]); -const U256_8: U256 = U256::from_limbs([8, 0, 0, 0]); -const U256_15: U256 = U256::from_limbs([15, 0, 0, 0]); -const U256_16: U256 = U256::from_limbs([16, 0, 0, 0]); -const U256_32: U256 = U256::from_limbs([32, 0, 0, 0]); -const U256_64: U256 = U256::from_limbs([64, 0, 0, 0]); -const U256_127: U256 = U256::from_limbs([127, 0, 0, 0]); -const U256_128: U256 = U256::from_limbs([128, 0, 0, 0]); -const U256_255: U256 = U256::from_limbs([255, 0, 0, 0]); - -const U256_256: U256 = U256::from_limbs([256, 0, 0, 0]); -const U256_512: U256 = U256::from_limbs([512, 0, 0, 0]); -const U256_1024: U256 = U256::from_limbs([1024, 0, 0, 0]); -const U256_2048: U256 = U256::from_limbs([2048, 0, 0, 0]); -const U256_4096: U256 = U256::from_limbs([4096, 0, 0, 0]); -const U256_8192: U256 = U256::from_limbs([8192, 0, 0, 0]); -const U256_16384: U256 = U256::from_limbs([16384, 0, 0, 0]); -const U256_32768: U256 = U256::from_limbs([32768, 0, 0, 0]); -const U256_65536: U256 = U256::from_limbs([65536, 0, 0, 0]); -const U256_131072: U256 = U256::from_limbs([131072, 0, 0, 0]); -const U256_262144: U256 = U256::from_limbs([262144, 0, 0, 0]); -const U256_524288: U256 = U256::from_limbs([524288, 0, 0, 0]); - -const U256_MAX_TICK: U256 = U256::from_limbs([887272, 0, 0, 0]); - -const MIN_TICK: i32 = -887272; -const MAX_TICK: i32 = -MIN_TICK; - -const MIN_SQRT_RATIO: U256 = U256::from_limbs([4295128739, 0, 0, 0]); -const MAX_SQRT_RATIO: U256 = - U256::from_limbs([6743328256752651558, 17280870778742802505, 4294805859, 0]); - -const SQRT_10001: I256 = I256::from_raw(U256::from_limbs([11745905768312294533, 13863, 0, 0])); -const TICK_LOW: I256 = I256::from_raw(U256::from_limbs([ - 6552757943157144234, - 184476617836266586, - 0, - 0, -])); -const TICK_HIGH: I256 = I256::from_raw(U256::from_limbs([ - 4998474450511881007, - 15793544031827761793, - 0, - 0, -])); - -/// Tick is the price range determined by tick index (not part of this struct, but is the key at -/// which the Tick is stored in state hash maps). Tick struct stores liquidity and fee information. -/// -/// - Net liquidity -/// - Gross liquidity -/// - Fees (above global) in both currencies -#[freeze_struct("ff1bce826e64c4aa")] -#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq)] -pub struct Tick { - pub liquidity_net: i128, - pub liquidity_gross: u64, - pub fees_out_tao: I64F64, - pub fees_out_alpha: I64F64, -} - -impl Tick { - pub fn liquidity_net_as_u64(&self) -> u64 { - self.liquidity_net.abs().min(u64::MAX as i128) as u64 - } -} - -/// Struct representing a tick index -#[freeze_struct("13c1f887258657f2")] -#[derive( - Debug, - Default, - Clone, - Copy, - Encode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, -)] -pub struct TickIndex(i32); - -impl Decode for TickIndex { - fn decode(input: &mut I) -> Result { - let raw = i32::decode(input)?; - TickIndex::new(raw).map_err(|_| "TickIndex out of bounds".into()) - } -} - -impl Add for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn add(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_add. - Self::new_unchecked(self.get() + rhs.get()) - } -} - -impl Sub for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn sub(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_sub. - Self::new_unchecked(self.get() - rhs.get()) - } -} - -impl AddAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn add_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() + rhs.get()); - } -} - -impl SubAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn sub_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() - rhs.get()); - } -} - -impl TryFrom for TickIndex { - type Error = TickMathError; - - fn try_from(value: i32) -> Result { - Self::new(value) - } -} - -impl Deref for TickIndex { - type Target = i32; - - fn deref(&self) -> &Self::Target { - // Using get() would create an infinite recursion, so this is one place where we need direct - // field access. This is safe because Self::Target is i32, which is exactly what we're - // storing - &self.0 - } -} - -/// Extension trait to make working with TryFrom more ergonomic -pub trait TryIntoTickIndex { - /// Convert an i32 into a TickIndex, with bounds checking - fn into_tick_index(self) -> Result; -} - -impl TryIntoTickIndex for i32 { - fn into_tick_index(self) -> Result { - TickIndex::try_from(self) - } -} - -impl TickIndex { - /// Minimum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MIN: Self = Self(MIN_TICK.saturating_div(2)); - - /// Maximum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MAX: Self = Self(MAX_TICK.saturating_div(2)); - - /// All tick indexes are offset by this value for storage needs - /// so that tick indexes are positive, which simplifies bit logic - const OFFSET: Self = Self(MAX_TICK); - - /// The MIN sqrt price, which is caclculated at Self::MIN - pub fn min_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(0.0000000002328350195) - } - - /// The MAX sqrt price, which is calculated at Self::MAX - #[allow(clippy::excessive_precision)] - pub fn max_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(4294886577.20989222513899790805) - } - - /// Get fees above a tick - pub fn fees_above(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } else if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } - - /// Get fees below a tick - pub fn fees_below(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } else if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } - - /// Get the current tick index for a subnet, ensuring it's within valid bounds - pub fn current_bounded(netuid: NetUid) -> Self { - let current_tick = CurrentTick::::get(netuid); - if current_tick > Self::MAX { - Self::MAX - } else if current_tick < Self::MIN { - Self::MIN - } else { - current_tick - } - } - - /// Converts a sqrt price to a tick index, ensuring it's within valid bounds - /// - /// If the price is outside the valid range, this function will return the appropriate boundary - /// tick index (MIN or MAX) instead of an error. - /// - /// # Arguments - /// * `sqrt_price` - The square root price to convert to a tick index - /// - /// # Returns - /// * `TickIndex` - A tick index that is guaranteed to be within valid bounds - pub fn from_sqrt_price_bounded(sqrt_price: SqrtPrice) -> Self { - match Self::try_from_sqrt_price(sqrt_price) { - Ok(index) => index, - Err(_) => { - let max_price = Self::MAX.as_sqrt_price_bounded(); - - if sqrt_price > max_price { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Converts a tick index to a sqrt price, ensuring it's within valid bounds - /// - /// Unlike try_to_sqrt_price which returns an error for boundary indices, this function - /// guarantees a valid sqrt price by using fallback values if conversion fails. - /// - /// # Returns - /// * `SqrtPrice` - A sqrt price that is guaranteed to be a valid value - pub fn as_sqrt_price_bounded(&self) -> SqrtPrice { - self.try_to_sqrt_price().unwrap_or_else(|_| { - if *self >= Self::MAX { - Self::max_sqrt_price() - } else { - Self::min_sqrt_price() - } - }) - } - - /// Creates a new TickIndex instance with bounds checking - pub fn new(value: i32) -> Result { - if !(Self::MIN.0..=Self::MAX.0).contains(&value) { - Err(TickMathError::TickOutOfBounds) - } else { - Ok(Self(value)) - } - } - - /// Creates a new TickIndex without bounds checking - /// Use this function with caution, only when you're certain the value is valid - pub fn new_unchecked(value: i32) -> Self { - Self(value) - } - - /// Get the inner value - pub fn get(&self) -> i32 { - self.0 - } - - /// Creates a TickIndex from an offset representation (u32) - /// - /// # Arguments - /// * `offset_index` - An offset index (u32 value) representing a tick index - /// - /// # Returns - /// * `Result` - The corresponding TickIndex if within valid bounds - pub fn from_offset_index(offset_index: u32) -> Result { - // while it's safe, we use saturating math to mute the linter and just in case - let signed_index = ((offset_index as i64).saturating_sub(Self::OFFSET.get() as i64)) as i32; - Self::new(signed_index) - } - - /// Get the next tick index (incrementing by 1) - pub fn next(&self) -> Result { - Self::new(self.0.saturating_add(1)) - } - - /// Get the previous tick index (decrementing by 1) - pub fn prev(&self) -> Result { - Self::new(self.0.saturating_sub(1)) - } - - /// Add a value to this tick index with bounds checking - pub fn checked_add(&self, value: i32) -> Result { - Self::new(self.0.saturating_add(value)) - } - - /// Subtract a value from this tick index with bounds checking - pub fn checked_sub(&self, value: i32) -> Result { - Self::new(self.0.saturating_sub(value)) - } - - /// Add a value to this tick index, saturating at the bounds instead of overflowing - pub fn saturating_add(&self, value: i32) -> Self { - match self.checked_add(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Subtract a value from this tick index, saturating at the bounds instead of overflowing - pub fn saturating_sub(&self, value: i32) -> Self { - match self.checked_sub(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Divide the tick index by a value with bounds checking - #[allow(clippy::arithmetic_side_effects)] - pub fn checked_div(&self, value: i32) -> Result { - if value == 0 { - return Err(TickMathError::DivisionByZero); - } - Self::new(self.0.saturating_div(value)) - } - - /// Divide the tick index by a value, saturating at the bounds - pub fn saturating_div(&self, value: i32) -> Self { - if value == 0 { - return Self::MAX; // Return MAX for division by zero - } - match self.checked_div(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Multiply the tick index by a value with bounds checking - pub fn checked_mul(&self, value: i32) -> Result { - // Check for potential overflow - match self.0.checked_mul(value) { - Some(result) => Self::new(result), - None => Err(TickMathError::Overflow), - } - } - - /// Multiply the tick index by a value, saturating at the bounds - pub fn saturating_mul(&self, value: i32) -> Self { - match self.checked_mul(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Converts tick index into SQRT of lower price of this tick In order to find the higher price - /// of this tick, call tick_index_to_sqrt_price(tick_idx + 1) - pub fn try_to_sqrt_price(&self) -> Result { - // because of u256->u128 conversion we have twice less values for min/max ticks - if !(Self::MIN..=Self::MAX).contains(self) { - return Err(TickMathError::TickOutOfBounds); - } - get_sqrt_ratio_at_tick(self.0).and_then(u256_q64_96_to_u64f64) - } - - /// Converts SQRT price to tick index - /// Because the tick is the range of prices [sqrt_lower_price, sqrt_higher_price), the resulting - /// tick index matches the price by the following inequality: - /// sqrt_lower_price <= sqrt_price < sqrt_higher_price - pub fn try_from_sqrt_price(sqrt_price: SqrtPrice) -> Result { - // price in the native Q64.96 integer format - let price_x96 = u64f64_to_u256_q64_96(sqrt_price); - - // first‑pass estimate from the log calculation - let mut tick = get_tick_at_sqrt_ratio(price_x96)?; - - // post‑verification, *both* directions - let price_at_tick = get_sqrt_ratio_at_tick(tick)?; - if price_at_tick > price_x96 { - tick = tick.saturating_sub(1); // estimate was too high - } else { - // it may still be one too low - let price_at_tick_plus = get_sqrt_ratio_at_tick(tick.saturating_add(1))?; - if price_at_tick_plus <= price_x96 { - tick = tick.saturating_add(1); // step up when required - } - } - - tick.into_tick_index() - } -} - -pub struct ActiveTickIndexManager(PhantomData); - -impl ActiveTickIndexManager { - pub fn insert(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Set bits in each layer - word0_value |= bitmap.bit_mask(LayerLevel::Top); - word1_value |= bitmap.bit_mask(LayerLevel::Middle); - word2_value |= bitmap.bit_mask(LayerLevel::Bottom); - - // Update the storage - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - } - - pub fn remove(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Turn the bit off (& !bit) and save as needed - word2_value &= !bitmap.bit_mask(LayerLevel::Bottom); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - - if word2_value == 0 { - word1_value &= !bitmap.bit_mask(LayerLevel::Middle); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - } - - if word1_value == 0 { - word0_value &= !bitmap.bit_mask(LayerLevel::Top); - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - } - } - - pub fn find_closest_lower(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, true) - } - - pub fn find_closest_higher(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, false) - } - - fn find_closest(netuid: NetUid, index: TickIndex, lower: bool) -> Option { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return None; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - let mut found = false; - let mut result: u32 = 0; - - // Layer positions from bitmap - let layer0_word = bitmap.word_at(LayerLevel::Top); - let layer0_bit = bitmap.bit_at(LayerLevel::Top); - let layer1_word = bitmap.word_at(LayerLevel::Middle); - let layer1_bit = bitmap.bit_at(LayerLevel::Middle); - let layer2_word = bitmap.word_at(LayerLevel::Bottom); - let layer2_bit = bitmap.bit_at(LayerLevel::Bottom); - - // Find the closest active bits in layer 0, then 1, then 2 - - /////////////// - // Level 0 - let word0 = TickIndexBitmapWords::::get((netuid, LayerLevel::Top, layer0_word)); - let closest_bits_l0 = - TickIndexBitmap::find_closest_active_bit_candidates(word0, layer0_bit, lower); - - for closest_bit_l0 in closest_bits_l0.iter() { - /////////////// - // Level 1 - let word1_index = TickIndexBitmap::layer_to_index(BitmapLayer::new(0, *closest_bit_l0)); - - // Layer 1 words are different, shift the bit to the word edge - let start_from_l1_bit = match word1_index.cmp(&layer1_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer1_bit, - }; - let word1_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Middle, word1_index)); - let closest_bits_l1 = TickIndexBitmap::find_closest_active_bit_candidates( - word1_value, - start_from_l1_bit, - lower, - ); - - for closest_bit_l1 in closest_bits_l1.iter() { - /////////////// - // Level 2 - let word2_index = - TickIndexBitmap::layer_to_index(BitmapLayer::new(word1_index, *closest_bit_l1)); - - // Layer 2 words are different, shift the bit to the word edge - let start_from_l2_bit = match word2_index.cmp(&layer2_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer2_bit, - }; - - let word2_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Bottom, word2_index)); - - let closest_bits_l2 = TickIndexBitmap::find_closest_active_bit_candidates( - word2_value, - start_from_l2_bit, - lower, - ); - - if !closest_bits_l2.is_empty() { - // The active tick is found, restore its full index and return - let offset_found_index = TickIndexBitmap::layer_to_index(BitmapLayer::new( - word2_index, - // it's safe to unwrap, because the len is > 0, but to prevent errors in - // refactoring, we use default fallback here for extra safety - closest_bits_l2.first().copied().unwrap_or_default(), - )); - - if lower { - if (offset_found_index > result) || (!found) { - result = offset_found_index; - found = true; - } - } else if (offset_found_index < result) || (!found) { - result = offset_found_index; - found = true; - } - } - } - } - - if !found { - return None; - } - - // Convert the result offset_index back to a tick index - TickIndex::from_offset_index(result).ok() - } - - pub fn tick_is_active(netuid: NetUid, tick: TickIndex) -> bool { - Self::find_closest_lower(netuid, tick).unwrap_or(TickIndex::MAX) == tick - } -} - -/// Represents the three layers in the Uniswap V3 bitmap structure -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum LayerLevel { - /// Top layer (highest level of the hierarchy) - Top = 0, - /// Middle layer - Middle = 1, - /// Bottom layer (contains the actual ticks) - Bottom = 2, -} - -#[freeze_struct("4015a04919eb5e2e")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub(crate) struct BitmapLayer { - word: u32, - bit: u32, -} - -impl BitmapLayer { - pub fn new(word: u32, bit: u32) -> Self { - Self { word, bit } - } -} - -/// A bitmap representation of a tick index position across the three-layer structure -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TickIndexBitmap { - /// The position in layer 0 (top layer) - layer0: BitmapLayer, - /// The position in layer 1 (middle layer) - layer1: BitmapLayer, - /// The position in layer 2 (bottom layer) - layer2: BitmapLayer, -} - -impl TickIndexBitmap { - /// Helper function to convert a bitmap index to a (word, bit) tuple in a bitmap layer using - /// safe methods - /// - /// Note: This function operates on bitmap navigation indices, NOT tick indices. - /// It converts a flat index within the bitmap structure to a (word, bit) position. - fn index_to_layer(index: u32) -> BitmapLayer { - let word = index.safe_div(128); - let bit = index.checked_rem(128).unwrap_or_default(); - BitmapLayer { word, bit } - } - - /// Converts a position (word, bit) within a layer to a word index in the next layer down - /// Note: This returns a bitmap navigation index, NOT a tick index - pub(crate) fn layer_to_index(layer: BitmapLayer) -> u32 { - layer.word.saturating_mul(128).saturating_add(layer.bit) - } - - /// Get the mask for a bit in the specified layer - pub(crate) fn bit_mask(&self, layer: LayerLevel) -> u128 { - match layer { - LayerLevel::Top => 1u128 << self.layer0.bit, - LayerLevel::Middle => 1u128 << self.layer1.bit, - LayerLevel::Bottom => 1u128 << self.layer2.bit, - } - } - - /// Get the word for the specified layer - pub(crate) fn word_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.word, - LayerLevel::Middle => self.layer1.word, - LayerLevel::Bottom => self.layer2.word, - } - } - - /// Get the bit for the specified layer - pub(crate) fn bit_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.bit, - LayerLevel::Middle => self.layer1.bit, - LayerLevel::Bottom => self.layer2.bit, - } - } - - /// Finds the closest active bit in a bitmap word, and if the active bit exactly matches the - /// requested bit, then it finds the next one as well - /// - /// # Arguments - /// * `word` - The bitmap word to search within - /// * `bit` - The bit position to start searching from - /// * `lower` - If true, search for lower bits (decreasing bit position), if false, search for - /// higher bits (increasing bit position) - /// - /// # Returns - /// * Exact match: Vec with [next_bit, bit] - /// * Non-exact match: Vec with [closest_bit] - /// * No match: Empty Vec - pub(crate) fn find_closest_active_bit_candidates( - word: u128, - bit: u32, - lower: bool, - ) -> Vec { - let mut result = vec![]; - let mut mask: u128 = 1_u128.wrapping_shl(bit); - let mut active_bit: u32 = bit; - - while mask > 0 { - if mask & word != 0 { - result.push(active_bit); - if active_bit != bit { - break; - } - } - - mask = if lower { - active_bit = active_bit.saturating_sub(1); - mask.wrapping_shr(1) - } else { - active_bit = active_bit.saturating_add(1); - mask.wrapping_shl(1) - }; - } - - result - } -} - -impl From for TickIndexBitmap { - fn from(tick_index: TickIndex) -> Self { - // Convert to offset index (internal operation only) - let offset_index = (tick_index.get().saturating_add(TickIndex::OFFSET.get())) as u32; - - // Calculate layer positions - let layer2 = Self::index_to_layer(offset_index); - let layer1 = Self::index_to_layer(layer2.word); - let layer0 = Self::index_to_layer(layer1.word); - - Self { - layer0, - layer1, - layer2, - } - } -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_sqrt_ratio_at_tick(tick: i32) -> Result { - let abs_tick = if tick < 0 { - U256::from(tick.neg()) - } else { - U256::from(tick) - }; - - if abs_tick > U256_MAX_TICK { - return Err(TickMathError::TickOutOfBounds); - } - - let mut ratio = if abs_tick & (U256_1) != U256::ZERO { - U256::from_limbs([12262481743371124737, 18445821805675392311, 0, 0]) - } else { - U256::from_limbs([0, 0, 1, 0]) - }; - - if !(abs_tick & U256_2).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 6459403834229662010, - 18444899583751176498, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 17226890335427755468, - 18443055278223354162, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2032852871939366096, - 18439367220385604838, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14545316742740207172, - 18431993317065449817, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 5129152022828963008, - 18417254355718160513, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_64).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4894419605888772193, - 18387811781193591352, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_128).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 1280255884321894483, - 18329067761203520168, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_256).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 15924666964335305636, - 18212142134806087854, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_512).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 8010504389359918676, - 17980523815641551639, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_1024).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10668036004952895731, - 17526086738831147013, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_2048).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4878133418470705625, - 16651378430235024244, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4096).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9537173718739605541, - 15030750278693429944, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8192).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9972618978014552549, - 12247334978882834399, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16384).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10428997489610666743, - 8131365268884726200, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32768).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9305304367709015974, - 3584323654723342297, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_65536).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14301143598189091785, - 696457651847595233, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_131072).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 7393154844743099908, - 26294789957452057, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_262144).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2209338891292245656, - 37481735321082, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_524288).is_zero() { - ratio = - (ratio.saturating_mul(U256::from_limbs([10518117631919034274, 76158723, 0, 0]))) >> 128 - } - - if tick > 0 { - ratio = U256::MAX / ratio; - } - - let shifted: U256 = ratio >> 32; - let ceil = if ratio & U256::from((1u128 << 32) - 1) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - Ok(ceil) -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result { - if !(sqrt_price_x_96 >= MIN_SQRT_RATIO && sqrt_price_x_96 < MAX_SQRT_RATIO) { - return Err(TickMathError::SqrtPriceOutOfBounds); - } - - let ratio: U256 = sqrt_price_x_96.shl(32); - let mut r = ratio; - let mut msb = U256::ZERO; - - let mut f = if r > U256::from_limbs([18446744073709551615, 18446744073709551615, 0, 0]) { - U256_1.shl(U256_7) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([18446744073709551615, 0, 0, 0]) { - U256_1.shl(U256_6) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([4294967295, 0, 0, 0]) { - U256_1.shl(U256_5) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([65535, 0, 0, 0]) { - U256_1.shl(U256_4) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_255 { - U256_1.shl(U256_3) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_15 { - U256_1.shl(U256_2) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_3 { - U256_1.shl(U256_1) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_1 { U256_1 } else { U256::ZERO }; - - msb = msb.bitor(f); - - r = if msb >= U256_128 { - ratio.shr(msb.saturating_sub(U256_127)) - } else { - ratio.shl(U256_127.saturating_sub(msb)) - }; - - let mut log_2: I256 = - (I256::from_raw(msb).saturating_sub(I256::from_limbs([128, 0, 0, 0]))).shl(64); - - for i in (51..=63).rev() { - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(i))); - - r = r.shr(f); - } - - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(50))); - - let log_sqrt10001 = log_2.wrapping_mul(SQRT_10001); - - let tick_low = (log_sqrt10001.saturating_sub(TICK_LOW) >> 128_u8).low_i32(); - - let tick_high = (log_sqrt10001.saturating_add(TICK_HIGH) >> 128_u8).low_i32(); - - let tick = if tick_low == tick_high { - tick_low - } else if get_sqrt_ratio_at_tick(tick_high)? <= sqrt_price_x_96 { - tick_high - } else { - tick_low - }; - - Ok(tick) -} - -// Convert U64F64 to U256 in Q64.96 format (Uniswap's sqrt price format) -fn u64f64_to_u256_q64_96(value: U64F64) -> U256 { - u64f64_to_u256(value, 96) -} - -/// Convert U64F64 to U256 -/// -/// # Arguments -/// * `value` - The U64F64 value to convert -/// * `target_fractional_bits` - Number of fractional bits in the target U256 format -/// -/// # Returns -/// * `U256` - Converted value -#[allow(clippy::arithmetic_side_effects)] -fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 { - let raw = U256::from(value.to_bits()); - - match target_fractional_bits.cmp(&64) { - Ordering::Less => raw >> (64 - target_fractional_bits), - Ordering::Greater => raw.saturating_shl((target_fractional_bits - 64) as usize), - Ordering::Equal => raw, - } -} - -/// Convert U256 in Q64.96 format (Uniswap's sqrt price format) to U64F64 -fn u256_q64_96_to_u64f64(value: U256) -> Result { - q_to_u64f64(value, 96) -} - -#[allow(clippy::arithmetic_side_effects)] -fn q_to_u64f64(x: U256, frac_bits: u32) -> Result { - let diff = frac_bits.saturating_sub(64) as usize; - - // 1. shift right diff bits - let shifted = if diff != 0 { x >> diff } else { x }; - - // 2. **round up** if we threw away any 1‑bits - let mask = if diff != 0 { - (U256_1.saturating_shl(diff)).saturating_sub(U256_1) - } else { - U256::ZERO - }; - let rounded = if diff != 0 && (x & mask) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - - // 3. check that it fits in 128 bits and transmute - if (rounded >> 128) != U256::ZERO { - return Err(TickMathError::Overflow); - } - Ok(U64F64::from_bits(rounded.to::())) -} - -#[derive(Debug, PartialEq, Eq)] -pub enum TickMathError { - TickOutOfBounds, - SqrtPriceOutOfBounds, - ConversionError, - Overflow, - DivisionByZero, -} - -impl fmt::Display for TickMathError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TickOutOfBounds => f.write_str("The given tick is outside of the minimum/maximum values."), - Self::SqrtPriceOutOfBounds =>f.write_str("Second inequality must be < because the price can never reach the price at the max tick"), - Self::ConversionError => f.write_str("Error converting from one number type into another"), - Self::Overflow => f.write_str("Number overflow in arithmetic operation"), - Self::DivisionByZero => f.write_str("Division by zero is not allowed") - } - } -} - -impl Error for TickMathError {} - -#[allow(clippy::unwrap_used)] -#[cfg(test)] -mod tests { - use safe_math::FixedExt; - use std::{ops::Sub, str::FromStr}; - - use super::*; - use crate::mock::*; - - #[test] - fn test_get_sqrt_ratio_at_tick_bounds() { - // the function should return an error if the tick is out of bounds - if let Err(err) = get_sqrt_ratio_at_tick(MIN_TICK - 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect lower tick bound") - } - if let Err(err) = get_sqrt_ratio_at_tick(MAX_TICK + 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect upper tick bound") - } - } - - #[test] - fn test_get_sqrt_ratio_at_tick_values() { - // test individual values for correct results - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK).unwrap(), - U256::from(4295128739u64), - "sqrt ratio at min incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK + 1).unwrap(), - U256::from(4295343490u64), - "sqrt ratio at min + 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK - 1).unwrap(), - U256::from_str("1461373636630004318706518188784493106690254656249").unwrap(), - "sqrt ratio at max - 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK).unwrap(), - U256::from_str("1461446703485210103287273052203988822378723970342").unwrap(), - "sqrt ratio at max incorrect" - ); - // checking hard coded values against solidity results - assert_eq!( - get_sqrt_ratio_at_tick(50).unwrap(), - U256::from(79426470787362580746886972461u128), - "sqrt ratio at 50 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(100).unwrap(), - U256::from(79625275426524748796330556128u128), - "sqrt ratio at 100 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250).unwrap(), - U256::from(80224679980005306637834519095u128), - "sqrt ratio at 250 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500).unwrap(), - U256::from(81233731461783161732293370115u128), - "sqrt ratio at 500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(1000).unwrap(), - U256::from(83290069058676223003182343270u128), - "sqrt ratio at 1000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(2500).unwrap(), - U256::from(89776708723587163891445672585u128), - "sqrt ratio at 2500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(3000).unwrap(), - U256::from(92049301871182272007977902845u128), - "sqrt ratio at 3000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(4000).unwrap(), - U256::from(96768528593268422080558758223u128), - "sqrt ratio at 4000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(5000).unwrap(), - U256::from(101729702841318637793976746270u128), - "sqrt ratio at 5000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(50000).unwrap(), - U256::from(965075977353221155028623082916u128), - "sqrt ratio at 50000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(150000).unwrap(), - U256::from(143194173941309278083010301478497u128), - "sqrt ratio at 150000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250000).unwrap(), - U256::from(21246587762933397357449903968194344u128), - "sqrt ratio at 250000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500000).unwrap(), - U256::from_str("5697689776495288729098254600827762987878").unwrap(), - "sqrt ratio at 500000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(738203).unwrap(), - U256::from_str("847134979253254120489401328389043031315994541").unwrap(), - "sqrt ratio at 738203 incorrect" - ); - } - - #[test] - fn test_get_tick_at_sqrt_ratio() { - //throws for too low - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO.sub(U256_1)); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //throws for too high - let result = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //ratio of min tick - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO).unwrap(); - assert_eq!(result, MIN_TICK); - - //ratio of min tick + 1 - let result = get_tick_at_sqrt_ratio(U256::from_str("4295343490").unwrap()).unwrap(); - assert_eq!(result, MIN_TICK + 1); - } - - #[test] - fn test_roundtrip() { - for tick_index in [ - MIN_TICK + 1, // we can't use extremes because of rounding during roundtrip conversion - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - MAX_TICK - 1, - ] - .iter() - { - let sqrt_price = get_sqrt_ratio_at_tick(*tick_index).unwrap(); - let round_trip_tick_index = get_tick_at_sqrt_ratio(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, *tick_index); - } - } - - #[test] - fn test_u256_to_u64f64_q64_96() { - // Test tick 0 (sqrt price = 1.0 * 2^96) - let tick0_sqrt_price = U256::from(1u128 << 96); - let fixed_price = u256_q64_96_to_u64f64(tick0_sqrt_price).unwrap(); - - // Should be 1.0 in U64F64 - assert_eq!(fixed_price, U64F64::from_num(1.0)); - - // Round trip back to U256 Q64.96 - let back_to_u256 = u64f64_to_u256_q64_96(fixed_price); - assert_eq!(back_to_u256, tick0_sqrt_price); - } - - #[test] - fn test_tick_index_to_sqrt_price() { - let tick_spacing = SqrtPrice::from_num(1.0001); - - // check tick bounds - assert_eq!( - TickIndex(MIN_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds) - ); - - assert_eq!( - TickIndex(MAX_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds), - ); - - assert!( - TickIndex::MAX.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MAX.get() + 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - assert!( - TickIndex::MIN.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - // At tick index 0, the sqrt price should be 1.0 - let sqrt_price = TickIndex(0).try_to_sqrt_price().unwrap(); - assert_eq!(sqrt_price, SqrtPrice::from_num(1.0)); - - let sqrt_price = TickIndex(2).try_to_sqrt_price().unwrap(); - assert!(sqrt_price.abs_diff(tick_spacing) < SqrtPrice::from_num(1e-10)); - - let sqrt_price = TickIndex(4).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^2 - let expected = tick_spacing * tick_spacing; - assert!(sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10)); - - // Test with tick index 10 - let sqrt_price = TickIndex(10).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^5 - let expected = tick_spacing.checked_pow(5).unwrap(); - assert!( - sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10), - "diff: {}", - sqrt_price.abs_diff(expected), - ); - } - - #[test] - fn test_sqrt_price_to_tick_index() { - let tick_spacing = SqrtPrice::from_num(1.0001); - let tick_index = TickIndex::try_from_sqrt_price(SqrtPrice::from_num(1.0)).unwrap(); - assert_eq!(tick_index, TickIndex::new_unchecked(0)); - - // Test with sqrt price equal to tick_spacing_tao (should be tick index 2) - let epsilon = SqrtPrice::from_num(0.0000000000000001); - assert!( - TickIndex::new_unchecked(2) - .as_sqrt_price_bounded() - .abs_diff(tick_spacing) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^2 (should be tick index 4) - let sqrt_price = tick_spacing * tick_spacing; - assert!( - TickIndex::new_unchecked(4) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^5 (should be tick index 10) - let sqrt_price = tick_spacing.checked_pow(5).unwrap(); - assert!( - TickIndex::new_unchecked(10) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - } - - #[test] - fn test_roundtrip_tick_index_sqrt_price() { - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] - .into_iter() - { - let tick_index = TickIndex::new_unchecked(i32_value); - let sqrt_price = tick_index.try_to_sqrt_price().unwrap(); - let round_trip_tick_index = TickIndex::try_from_sqrt_price(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, tick_index); - } - } - - #[test] - fn test_from_offset_index() { - // Test various tick indices - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - 0, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] { - let original_tick = TickIndex::new_unchecked(i32_value); - - // Calculate the offset index (adding OFFSET) - let offset_index = (i32_value + TickIndex::OFFSET.get()) as u32; - - // Convert back from offset index to tick index - let roundtrip_tick = TickIndex::from_offset_index(offset_index).unwrap(); - - // Check that we get the same tick index back - assert_eq!(original_tick, roundtrip_tick); - } - - // Test out of bounds values - let too_large = (TickIndex::MAX.get() + TickIndex::OFFSET.get() + 1) as u32; - assert!(TickIndex::from_offset_index(too_large).is_err()); - } - - #[test] - fn test_tick_price_sanity_check() { - let min_price = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let max_price = TickIndex::MAX.try_to_sqrt_price().unwrap(); - - assert!(min_price > 0.); - assert!(max_price > 0.); - assert!(max_price > min_price); - assert!(min_price < 0.000001); - assert!(max_price > 10.); - - // Roundtrip conversions - let min_price_sqrt = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let min_tick = TickIndex::try_from_sqrt_price(min_price_sqrt).unwrap(); - assert_eq!(min_tick, TickIndex::MIN); - - let max_price_sqrt: SqrtPrice = TickIndex::MAX.try_to_sqrt_price().unwrap(); - let max_tick = TickIndex::try_from_sqrt_price(max_price_sqrt).unwrap(); - assert_eq!(max_tick, TickIndex::MAX); - } - - #[test] - fn test_to_sqrt_price_bounded() { - assert_eq!( - TickIndex::MAX.as_sqrt_price_bounded(), - TickIndex::MAX.try_to_sqrt_price().unwrap() - ); - - assert_eq!( - TickIndex::MIN.as_sqrt_price_bounded(), - TickIndex::MIN.try_to_sqrt_price().unwrap() - ); - } - - mod active_tick_index_manager { - - use super::*; - - #[test] - fn test_tick_search_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ) - .is_none() - ); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MAX - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - }); - } - - #[test] - fn test_tick_search_sparse_queries() { - new_test_ext().execute_with(|| { - let active_index = TickIndex::MIN.saturating_add(10); - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, active_index); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(11) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(12) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(9) - ), - None - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(11) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(12) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(9) - ) - .unwrap(), - active_index - ); - }); - } - - #[test] - fn test_tick_search_many_lows() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - (0..1000).for_each(|i| { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::MIN.saturating_add(i), - ); - }); - - for i in 0..1000 { - let test_index = TickIndex::MIN.saturating_add(i); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, test_index) - .unwrap(), - test_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, test_index) - .unwrap(), - test_index - ); - } - }); - } - - #[test] - fn test_tick_search_many_sparse() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in 0..=count { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_many_lows_sparse_reversed() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in (0..=count).rev() { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_repeated_insertions() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for _ in 0..10 { - for i in 0..=count { - let tick = TickIndex::new_unchecked(i * 10); - ActiveTickIndexManager::::insert(netuid, tick); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick) - .unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_tick - ) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, after_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, after_tick - ) - .unwrap(), - next_tick - ); - } - } - } - }); - } - - #[test] - fn test_tick_search_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - let prev_index = TickIndex::new_unchecked(index.get() - step); - let next_minus_one = TickIndex::new_unchecked(index.get() + step - 1); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, prev_index) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index).unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, next_minus_one) - .unwrap(), - index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, mid_next) - .unwrap(), - index - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index).unwrap(), - index - ); - - let next_index = TickIndex::new_unchecked(index.get() + step); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_index) - .unwrap(), - next_index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, mid_next) - .unwrap(), - next_index - ); - - let next_minus_1 = TickIndex::new_unchecked(index.get() + step - 1); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_minus_1) - .unwrap(), - next_index - ); - for j in 1..=9 { - let before_index = TickIndex::new_unchecked(index.get() - j); - let after_index = TickIndex::new_unchecked(index.get() + j); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_index - ) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_index - ) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - after_index - ) - .unwrap(), - next_index - ); - } - } - }); - } - - #[test] - fn test_tick_remove_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - ActiveTickIndexManager::::remove(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ), - None - ); - }); - } - - #[test] - fn test_tick_remove_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - let remove_frequency = 5; // Remove every 5th tick - - // Insert ticks - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - - // Remove some ticks - for i in 1..count { - if i % remove_frequency == 0 { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::remove(netuid, index); - } - } - - // Verify - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - if i % remove_frequency == 0 { - let lower = - ActiveTickIndexManager::::find_closest_lower(netuid, index); - let higher = - ActiveTickIndexManager::::find_closest_higher(netuid, index); - assert!(lower != Some(index)); - assert!(higher != Some(index)); - } else { - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .unwrap(), - index - ); - } - } - }); - } - } -} diff --git a/pallets/swap/src/weights.rs b/pallets/swap/src/weights.rs index 2bbbb8dbdf..210bf1dc6d 100644 --- a/pallets/swap/src/weights.rs +++ b/pallets/swap/src/weights.rs @@ -15,10 +15,6 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_subtensor_swap. pub trait WeightInfo { fn set_fee_rate() -> Weight; - fn add_liquidity() -> Weight; - fn remove_liquidity() -> Weight; - fn modify_position() -> Weight; - fn toggle_user_liquidity() -> Weight; } /// Default weights for pallet_subtensor_swap. @@ -30,34 +26,6 @@ impl WeightInfo for DefaultWeight { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - - fn add_liquidity() -> Weight { - // Conservative weight estimate for add_liquidity - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn remove_liquidity() -> Weight { - // Conservative weight estimate for remove_liquidity - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn modify_position() -> Weight { - // Conservative weight estimate for modify_position - Weight::from_parts(50_000_000, 0) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(4)) - } - - fn toggle_user_liquidity() -> Weight { - // Conservative weight estimate: one read and one write - Weight::from_parts(10_000_000, 0) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } } // For backwards compatibility and tests @@ -67,28 +35,4 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1)) .saturating_add(RocksDbWeight::get().writes(1)) } - - fn add_liquidity() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(5)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn remove_liquidity() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(4)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn modify_position() -> Weight { - Weight::from_parts(50_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(4)) - .saturating_add(RocksDbWeight::get().writes(4)) - } - - fn toggle_user_liquidity() -> Weight { - Weight::from_parts(10_000_000, 0) - .saturating_add(RocksDbWeight::get().reads(1)) - .saturating_add(RocksDbWeight::get().writes(1)) - } } diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index fc2a16a409..013903ea8e 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -30,7 +30,7 @@ use subtensor_swap_interface::SwapHandler; use core::marker::PhantomData; use smallvec::smallvec; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{Balance, Currency, NetUid}; // Tests @@ -145,7 +145,7 @@ where // This is not ideal because it may not pay all fees, but UX is the priority // and this approach still provides spam protection. alpha_vec.iter().any(|(hotkey, netuid)| { - let alpha_balance = U96F32::saturating_from_num( + let alpha_balance = U64F64::saturating_from_num( pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ), @@ -168,13 +168,13 @@ where alpha_vec.iter().for_each(|(hotkey, netuid)| { // Divide tao_amount evenly among all alpha entries - let alpha_balance = U96F32::saturating_from_num( + let alpha_balance = U64F64::saturating_from_num( pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ), ); let alpha_price = pallet_subtensor_swap::Pallet::::current_alpha_price(*netuid); - let alpha_fee = U96F32::saturating_from_num(tao_per_entry) + let alpha_fee = U64F64::saturating_from_num(tao_per_entry) .checked_div(alpha_price) .unwrap_or(alpha_balance) .min(alpha_balance) diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 7a54f98c5d..56b7b77308 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -387,7 +387,6 @@ impl pallet_balances::Config for Test { 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 = NonZeroU64::new(1_000_000).unwrap(); } @@ -399,7 +398,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index b6697e87f0..7212c91077 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -2,12 +2,11 @@ use crate::TransactionSource; use frame_support::assert_ok; use frame_support::dispatch::GetDispatchInfo; -use pallet_subtensor_swap::AlphaSqrtPrice; use sp_runtime::{ traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use substrate_fixed::types::U64F64; +// use substrate_fixed::types::U64F64; use subtensor_runtime_common::AlphaCurrency; use mock::*; @@ -444,7 +443,9 @@ fn test_remove_stake_edge_alpha() { assert_ok!(result); // Lower Alpha price to 0.0001 so that there is not enough alpha to cover tx fees - AlphaSqrtPrice::::insert(sn.subnets[0].netuid, U64F64::from_num(0.01)); + SubnetTAO::::insert(sn.subnets[0].netuid, TaoCurrency::from(1_000_000)); + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaCurrency::from(10_000_000_000)); + let result_low_alpha_price = ext.validate( RuntimeOrigin::signed(sn.coldkey).into(), &call.clone(), diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index 8dcea0e829..98737f1269 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -5,7 +5,7 @@ use pallet_evm::{BalanceConverter, PrecompileHandle, SubstrateBalance}; use precompile_utils::EvmResult; use sp_core::U256; use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{Currency, NetUid}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -37,7 +37,7 @@ where fn get_alpha_price(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { let current_alpha_price = as SwapHandler>::current_alpha_price(netuid.into()); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) @@ -194,18 +194,18 @@ where .filter(|(netuid, _)| *netuid != NetUid::ROOT) .collect::>(); - let mut sum_alpha_price: U96F32 = U96F32::from_num(0); + let mut sum_alpha_price: U64F64 = U64F64::from_num(0); for (netuid, _) in netuids { let price = as SwapHandler>::current_alpha_price( netuid.into(), ); - if price < U96F32::from_num(1) { + if price < U64F64::from_num(1) { sum_alpha_price = sum_alpha_price.saturating_add(price); } } - let price = sum_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = sum_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3aced2160..d965e5b5c7 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -21,6 +21,7 @@ subtensor-custom-rpc-runtime-api.workspace = true smallvec.workspace = true log.workspace = true codec = { workspace = true, features = ["derive"] } +safe-math.workspace = true scale-info = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["alloc"] } pallet-aura = { workspace = true } @@ -198,6 +199,7 @@ std = [ "pallet-preimage/std", "pallet-commitments/std", "precompile-utils/std", + "safe-math/std", "sp-api/std", "sp-block-builder/std", "sp-core/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 629298a5ab..89735b1011 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -43,9 +43,10 @@ use pallet_subtensor::rpc_info::{ }; use pallet_subtensor::{CommitmentsInterface, ProxyInterface}; use pallet_subtensor_proxy as pallet_proxy; -use pallet_subtensor_swap_runtime_api::SimSwapResult; +use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice}; use pallet_subtensor_utility as pallet_utility; use runtime_common::prod_or_fast; +use safe_math::FixedExt; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_babe::BabeConfiguration; @@ -71,6 +72,7 @@ use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; +use substrate_fixed::types::U64F64; use subtensor_precompiles::Precompiles; use subtensor_runtime_common::{AlphaCurrency, TaoCurrency, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -241,7 +243,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 376, + spec_version: 377, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -1100,7 +1102,6 @@ impl pallet_subtensor::Config for Runtime { 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) }; } @@ -1112,7 +1113,6 @@ impl pallet_subtensor_swap::Config for Runtime { type TaoReserve = pallet_subtensor::TaoCurrencyReserve; type AlphaReserve = pallet_subtensor::AlphaCurrencyReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; // TODO: set measured weights when the pallet been benchmarked and the type is generated @@ -2457,14 +2457,26 @@ impl_runtime_apis! { impl pallet_subtensor_swap_runtime_api::SwapRuntimeApi for Runtime { fn current_alpha_price(netuid: NetUid) -> u64 { - use substrate_fixed::types::U96F32; - pallet_subtensor_swap::Pallet::::current_price(netuid.into()) - .saturating_mul(U96F32::from_num(1_000_000_000)) + .saturating_mul(U64F64::from_num(1_000_000_000)) .saturating_to_num() } + fn current_alpha_price_all() -> Vec { + pallet_subtensor::Pallet::::get_all_subnet_netuids() + .into_iter() + .map(|netuid| { + SubnetPrice { + netuid, + price: Self::current_alpha_price(netuid), + } + }) + .collect() + } + fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoCurrency) -> SimSwapResult { + let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); + let no_slippage_alpha = U64F64::saturating_from_num(u64::from(tao)).safe_div(price).saturating_to_num::(); let order = pallet_subtensor::GetAlphaForTao::::with_amount(tao); pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), @@ -2476,17 +2488,23 @@ impl_runtime_apis! { alpha_amount: 0.into(), tao_fee: 0.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: 0.into(), }, |sr| SimSwapResult { tao_amount: sr.amount_paid_in.into(), alpha_amount: sr.amount_paid_out.into(), tao_fee: sr.fee_paid.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: no_slippage_alpha.saturating_sub(sr.amount_paid_out.into()).into(), }, ) } fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaCurrency) -> SimSwapResult { + let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); + let no_slippage_tao = U64F64::saturating_from_num(u64::from(alpha)).saturating_mul(price).saturating_to_num::(); let order = pallet_subtensor::GetTaoForAlpha::::with_amount(alpha); pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), @@ -2498,12 +2516,16 @@ impl_runtime_apis! { alpha_amount: 0.into(), tao_fee: 0.into(), alpha_fee: 0.into(), + tao_slippage: 0.into(), + alpha_slippage: 0.into(), }, |sr| SimSwapResult { tao_amount: sr.amount_paid_out.into(), alpha_amount: sr.amount_paid_in.into(), tao_fee: 0.into(), alpha_fee: sr.fee_paid.into(), + tao_slippage: no_slippage_tao.saturating_sub(sr.amount_paid_out.into()).into(), + alpha_slippage: 0.into(), }, ) }