diff --git a/Cargo.lock b/Cargo.lock index cadd1188..5d4e75f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7487,6 +7487,7 @@ dependencies = [ "frame-support", "frame-system", "parity-scale-codec", + "rand_core", "scale-info", "serai-coins-pallet", "serai-primitives", @@ -7687,10 +7688,12 @@ dependencies = [ "borsh", "frame-support", "parity-scale-codec", + "rand_core", "scale-info", "serde", "sp-application-crypto", "sp-core", + "sp-io", "sp-runtime", "zeroize", ] diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index 73fe52cb..d887b0b1 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -9,6 +9,7 @@ macro_rules! serai_test { $( #[tokio::test] async fn $name() { + use std::collections::HashMap; use dockertest::{ PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, TestBodySpecification, DockerTest, @@ -28,6 +29,7 @@ macro_rules! serai_test { "--rpc-cors".to_string(), "all".to_string(), ]) + .replace_env(HashMap::from([("RUST_LOG".to_string(), "runtime=debug".to_string())])) .set_publish_all_ports(true) .set_handle(handle) .set_start_policy(StartPolicy::Strict) diff --git a/substrate/client/tests/dex.rs b/substrate/client/tests/dex.rs index 8796fe0b..da0270ff 100644 --- a/substrate/client/tests/dex.rs +++ b/substrate/client/tests/dex.rs @@ -244,8 +244,8 @@ serai_test!( // add liquidity common_add_liquidity(&serai, coin, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), + Amount(5_000_000_000_000), + Amount(500_000_000_000), 0, pair.clone() ).await; @@ -274,8 +274,8 @@ serai_test!( mint_to: pair.public().into(), pool_id: Coin::Bitcoin, coin_amount: 10_000_000_000_000, // half of sent amount - sri_amount: 6_947_918_403_646, - lp_token_minted: 8333333333332 + sri_amount: 111_333_778_668, + lp_token_minted: 1_054_092_553_383 }] ); }) @@ -290,7 +290,7 @@ serai_test!( // mint coins mint_coin( &serai, - Balance { coin: coin1, amount: Amount(100_000_000_000_000) }, + Balance { coin: coin1, amount: Amount(10_000_000_000_000_000) }, NetworkId::Monero, coin1_batch_id, pair.clone().public().into(), @@ -310,15 +310,15 @@ serai_test!( // add liquidity to pools common_add_liquidity(&serai, coin1, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), + Amount(5_000_000_000_000_000), // monero has 12 decimals + Amount(50_000_000_000), 0, pair.clone() ).await; common_add_liquidity(&serai, coin2, - Amount(50_000_000_000_000), - Amount(50_000_000_000_000), + Amount(5_000_000_000_000), // ether still has 8 in our codebase + Amount(500_000_000_000), 1, pair.clone() ).await; @@ -344,7 +344,7 @@ serai_test!( block: block_hash, instructions: vec![InInstructionWithBalance { instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)), - balance: Balance { coin: coin1, amount: Amount(20_000_000_000_000) }, + balance: Balance { coin: coin1, amount: Amount(200_000_000_000_000) }, }], }; @@ -360,8 +360,8 @@ serai_test!( who: IN_INSTRUCTION_EXECUTOR, send_to: IN_INSTRUCTION_EXECUTOR, path, - amount_in: 20_000_000_000_000, - amount_out: 11066655622377 + amount_in: 200_000_000_000_000, + amount_out: 19_044_944_233 }] ); } @@ -384,7 +384,7 @@ serai_test!( block: block_hash, instructions: vec![InInstructionWithBalance { instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), - balance: Balance { coin: coin2, amount: Amount(20_000_000_000_000) }, + balance: Balance { coin: coin2, amount: Amount(200_000_000_000) }, }], }; @@ -399,8 +399,8 @@ serai_test!( who: IN_INSTRUCTION_EXECUTOR, send_to: out_address.as_native().unwrap(), path, - amount_in: 20_000_000_000_000, - amount_out: 26440798801319 + amount_in: 200_000_000_000, + amount_out: 1487294253782353 }] ); } @@ -422,7 +422,7 @@ serai_test!( block: block_hash, instructions: vec![InInstructionWithBalance { instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), - balance: Balance { coin: coin1, amount: Amount(10_000_000_000_000) }, + balance: Balance { coin: coin1, amount: Amount(100_000_000_000_000) }, }], }; @@ -437,8 +437,8 @@ serai_test!( who: IN_INSTRUCTION_EXECUTOR, send_to: out_address.as_native().unwrap(), path, - amount_in: 10_000_000_000_000, - amount_out: 10711005507065 + amount_in: 100_000_000_000_000, + amount_out: 1_762_662_819 }] ); } diff --git a/substrate/dex/pallet/Cargo.toml b/substrate/dex/pallet/Cargo.toml index 83192d6e..6a2eadb8 100644 --- a/substrate/dex/pallet/Cargo.toml +++ b/substrate/dex/pallet/Cargo.toml @@ -36,6 +36,9 @@ coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", de serai-primitives = { path = "../../primitives", default-features = false } +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } + [features] default = ["std"] std = [ diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs index 35220502..f296a262 100644 --- a/substrate/dex/pallet/src/lib.rs +++ b/substrate/dex/pallet/src/lib.rs @@ -106,7 +106,7 @@ pub mod pallet { use coins_pallet::{Pallet as CoinsPallet, Config as CoinsConfig}; - use serai_primitives::{Coin, Amount, Balance, SubstrateAmount}; + use serai_primitives::{Coin, Amount, Balance, SubstrateAmount, reverse_lexicographic_order}; /// Pool ID. /// @@ -144,6 +144,10 @@ pub mod pallet { #[pallet::constant] type MaxSwapPathLength: Get; + /// Last N number of blocks that oracle keeps track of the prices. + #[pallet::constant] + type MedianPriceWindowLength: Get; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } @@ -156,34 +160,157 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn spot_price_for_block)] pub type SpotPriceForBlock = - StorageDoubleMap<_, Identity, BlockNumberFor, Identity, Coin, [u8; 8], ValueQuery>; + StorageDoubleMap<_, Identity, BlockNumberFor, Identity, Coin, Amount, OptionQuery>; - /// Moving window of oracle prices. + /// Moving window of prices from each block. /// - /// The second [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions - /// in this multi-set. + /// The [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions in this + /// multi-set. Since the underlying map is lexicographically sorted, this map stores amounts from + /// low to high. #[pallet::storage] - #[pallet::getter(fn oracle_prices)] - pub type OraclePrices = + pub type SpotPrices = StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>; + + // SpotPrices, yet with keys stored in reverse lexicographic order. + #[pallet::storage] + pub type ReverseSpotPrices = + StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], (), OptionQuery>; + + /// Current length of the `SpotPrices` map. + #[pallet::storage] + pub type SpotPricesLength = StorageMap<_, Identity, Coin, u16, OptionQuery>; + + /// Current position of the median within the `SpotPrices` map; + #[pallet::storage] + pub type CurrentMedianPosition = StorageMap<_, Identity, Coin, u16, OptionQuery>; + + /// Current median price of the prices in the `SpotPrices` map at any given time. + #[pallet::storage] + #[pallet::getter(fn median_price)] + pub type MedianPrice = StorageMap<_, Identity, Coin, Amount, OptionQuery>; + + /// The price used for evaluating economic security, which is the highest observed median price. + #[pallet::storage] + #[pallet::getter(fn security_oracle_value)] + pub type SecurityOracleValue = StorageMap<_, Identity, Coin, Amount, OptionQuery>; + impl Pallet { - // TODO: consider an algorithm which removes outliers? This algorithm might work a good bit - // better if we remove the bottom n values (so some value sustained over 90% of blocks instead - // of all blocks in the window). - /// Get the highest sustained value for this window. - /// This is actually the lowest price observed during the windows, as it's the price - /// all prices are greater than or equal to. - pub fn highest_sustained_price(coin: &Coin) -> Option { - let mut iter = OraclePrices::::iter_key_prefix(coin); - // the first key will be the lowest price due to the keys being lexicographically ordered. - iter.next().map(|amount| Amount(u64::from_be_bytes(amount))) + fn restore_median( + coin: Coin, + mut current_median_pos: u16, + mut current_median: Amount, + length: u16, + ) { + // 1 -> 0 (the only value) + // 2 -> 1 (the higher element), 4 -> 2 (the higher element) + // 3 -> 1 (the true median) + let target_median_pos = length / 2; + while current_median_pos < target_median_pos { + // Get the amount of presences for the current element + let key = current_median.0.to_be_bytes(); + let presences = SpotPrices::::get(coin, key).unwrap(); + // > is correct, not >=. + // Consider: + // - length = 1, current_median_pos = 0, presences = 1, target_median_pos = 0 + // - length = 2, current_median_pos = 0, presences = 2, target_median_pos = 1 + // - length = 2, current_median_pos = 0, presences = 1, target_median_pos = 1 + if (current_median_pos + presences) > target_median_pos { + break; + } + current_median_pos += presences; + + let key = SpotPrices::::hashed_key_for(coin, key); + let next_price = SpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(); + current_median = Amount(u64::from_be_bytes(next_price)); + } + + while current_median_pos > target_median_pos { + // Get the next element + let key = reverse_lexicographic_order(current_median.0.to_be_bytes()); + let key = ReverseSpotPrices::::hashed_key_for(coin, key); + let next_price = ReverseSpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(); + let next_price = reverse_lexicographic_order(next_price); + current_median = Amount(u64::from_be_bytes(next_price)); + + // Get its amount of presences + let presences = SpotPrices::::get(coin, current_median.0.to_be_bytes()).unwrap(); + // Adjust from next_value_first_pos to this_value_first_pos by substracting this value's + // amount of times present + current_median_pos -= presences; + + if current_median_pos <= target_median_pos { + break; + } + } + + CurrentMedianPosition::::set(coin, Some(current_median_pos)); + MedianPrice::::set(coin, Some(current_median)); + } + + pub(crate) fn insert_into_median(coin: Coin, amount: Amount) { + let new_quantity_of_presences = + SpotPrices::::get(coin, amount.0.to_be_bytes()).unwrap_or(0) + 1; + SpotPrices::::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences)); + if new_quantity_of_presences == 1 { + ReverseSpotPrices::::set( + coin, + reverse_lexicographic_order(amount.0.to_be_bytes()), + Some(()), + ); + } + + let new_length = SpotPricesLength::::get(coin).unwrap_or(0) + 1; + SpotPricesLength::::set(coin, Some(new_length)); + + let Some(current_median) = MedianPrice::::get(coin) else { + MedianPrice::::set(coin, Some(amount)); + CurrentMedianPosition::::set(coin, Some(0)); + return; + }; + + let mut current_median_pos = CurrentMedianPosition::::get(coin).unwrap(); + // If this is being inserted before the current median, the current median's position has + // increased + if amount < current_median { + current_median_pos += 1; + } + Self::restore_median(coin, current_median_pos, current_median, new_length); + } + + pub(crate) fn remove_from_median(coin: Coin, amount: Amount) { + let mut current_median = MedianPrice::::get(coin).unwrap(); + + let mut current_median_pos = CurrentMedianPosition::::get(coin).unwrap(); + if amount < current_median { + current_median_pos -= 1; + } + + let new_quantity_of_presences = + SpotPrices::::get(coin, amount.0.to_be_bytes()).unwrap() - 1; + if new_quantity_of_presences == 0 { + let normal_key = amount.0.to_be_bytes(); + SpotPrices::::remove(coin, normal_key); + ReverseSpotPrices::::remove(coin, reverse_lexicographic_order(amount.0.to_be_bytes())); + + // If we've removed the current item at this position, update to the item now at this + // position + if amount == current_median { + let key = SpotPrices::::hashed_key_for(coin, normal_key); + current_median = Amount(u64::from_be_bytes( + SpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(), + )); + } + } else { + SpotPrices::::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences)); + } + + let new_length = SpotPricesLength::::get(coin).unwrap() - 1; + SpotPricesLength::::set(coin, Some(new_length)); + + Self::restore_median(coin, current_median_pos, current_median, new_length); } } - #[pallet::storage] - #[pallet::getter(fn oracle_value)] - pub type OracleValue = StorageMap<_, Identity, Coin, Amount, OptionQuery>; - // Pallet's events. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -264,12 +391,6 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - // assert that oracle windows size can fit into u16. Otherwise number of observants - // for a price in the `OraclePrices` map can overflow - // We don't want to make this const directly a u16 because it is used the block number - // calculations (which are done as u32s) - u16::try_from(ORACLE_WINDOW_SIZE).unwrap(); - // create the pools for coin in &self.pools { Pallet::::create_pool(*coin).unwrap(); @@ -362,35 +483,22 @@ pub mod pallet { } else { 0 }; - let sri_per_coin = sri_per_coin.to_be_bytes(); - SpotPriceForBlock::::set(n, coin, sri_per_coin); - - // Include this spot price into the multiset - { - let observed = OraclePrices::::get(coin, sri_per_coin).unwrap_or(0); - OraclePrices::::set(coin, sri_per_coin, Some(observed + 1)); - } - - // pop the earliest key from the window once we reach its full size. - if n >= ORACLE_WINDOW_SIZE.into() { - let start_of_window = n - ORACLE_WINDOW_SIZE.into(); - let start_spot_price = Self::spot_price_for_block(start_of_window, coin); - SpotPriceForBlock::::remove(start_of_window, coin); - // Remove this price from the multiset - OraclePrices::::mutate_exists(coin, start_spot_price, |v| { - *v = Some(v.unwrap_or(1) - 1); - if *v == Some(0) { - *v = None; - } - }); + let sri_per_coin = Amount(sri_per_coin); + SpotPriceForBlock::::set(n, coin, Some(sri_per_coin)); + Self::insert_into_median(coin, sri_per_coin); + if SpotPricesLength::::get(coin).unwrap() > T::MedianPriceWindowLength::get() { + let old = n - T::MedianPriceWindowLength::get().into(); + let old_price = SpotPriceForBlock::::get(old, coin).unwrap(); + SpotPriceForBlock::::remove(old, coin); + Self::remove_from_median(coin, old_price); } // update the oracle value - let highest_sustained = Self::highest_sustained_price(&coin).unwrap_or(Amount(0)); - let oracle_value = Self::oracle_value(coin).unwrap_or(Amount(0)); - if highest_sustained > oracle_value { - OracleValue::::set(coin, Some(highest_sustained)); + let median = Self::median_price(coin).unwrap_or(Amount(0)); + let oracle_value = Self::security_oracle_value(coin).unwrap_or(Amount(0)); + if median > oracle_value { + SecurityOracleValue::::set(coin, Some(median)); } } } @@ -422,7 +530,7 @@ pub mod pallet { pub fn on_new_session(network: NetworkId) { // reset the oracle value for coin in network.coins() { - OracleValue::::set(*coin, Self::highest_sustained_price(coin)); + SecurityOracleValue::::set(*coin, Self::median_price(coin)); } } } diff --git a/substrate/dex/pallet/src/mock.rs b/substrate/dex/pallet/src/mock.rs index 8c1863e1..666c0324 100644 --- a/substrate/dex/pallet/src/mock.rs +++ b/substrate/dex/pallet/src/mock.rs @@ -25,7 +25,7 @@ use crate as dex; use frame_support::{ construct_runtime, - traits::{ConstU32, ConstU64}, + traits::{ConstU16, ConstU32, ConstU64}, }; use sp_core::{H256, sr25519::Public}; @@ -40,6 +40,8 @@ pub use coins_pallet as coins; type Block = frame_system::mocking::MockBlock; +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; + construct_runtime!( pub enum Test { @@ -92,6 +94,9 @@ impl Config for Test { type WeightInfo = (); type LPFee = ConstU32<3>; // means 0.3% type MaxSwapPathLength = ConstU32<4>; + + type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; + // 100 is good enough when the main currency has 12 decimals. type MintMinLiquidity = ConstU64<100>; } diff --git a/substrate/dex/pallet/src/tests.rs b/substrate/dex/pallet/src/tests.rs index a1809b73..80b45464 100644 --- a/substrate/dex/pallet/src/tests.rs +++ b/substrate/dex/pallet/src/tests.rs @@ -1267,3 +1267,53 @@ fn cannot_block_pool_creation() { assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 100, 9900, 10, 9900, user,)); }); } + +#[test] +fn test_median_price() { + new_test_ext().execute_with(|| { + use rand_core::{RngCore, OsRng}; + + let mut prices = vec![]; + for i in 0 .. 100 { + // Randomly use an active number + if (i != 0) && (OsRng.next_u64() % u64::from(MEDIAN_PRICE_WINDOW_LENGTH / 3) == 0) { + let old_index = usize::try_from( + OsRng.next_u64() % + u64::from(MEDIAN_PRICE_WINDOW_LENGTH) % + u64::try_from(prices.len()).unwrap(), + ) + .unwrap(); + let window_base = prices.len().saturating_sub(MEDIAN_PRICE_WINDOW_LENGTH.into()); + prices.push(prices[window_base + old_index]); + } else { + prices.push(OsRng.next_u64()); + } + } + let coin = Coin::Bitcoin; + + assert!(prices.len() >= (2 * usize::from(MEDIAN_PRICE_WINDOW_LENGTH))); + for i in 0 .. prices.len() { + let price = Amount(prices[i]); + + let n = BlockNumberFor::::from(u32::try_from(i).unwrap()); + SpotPriceForBlock::::set(n, coin, Some(price)); + Dex::insert_into_median(coin, price); + if SpotPricesLength::::get(coin).unwrap() > MEDIAN_PRICE_WINDOW_LENGTH { + let old = n - u64::from(MEDIAN_PRICE_WINDOW_LENGTH); + let old_price = SpotPriceForBlock::::get(old, coin).unwrap(); + SpotPriceForBlock::::remove(old, coin); + Dex::remove_from_median(coin, old_price); + } + + // get the current window (cloning so our sort doesn't affect the original array) + let window_base = (i + 1).saturating_sub(MEDIAN_PRICE_WINDOW_LENGTH.into()); + let mut window = Vec::from(&prices[window_base ..= i]); + assert!(window.len() <= MEDIAN_PRICE_WINDOW_LENGTH.into()); + + // get the median + window.sort(); + let median_index = window.len() / 2; + assert_eq!(Dex::median_price(coin).unwrap(), Amount(window[median_index])); + } + }); +} diff --git a/substrate/dex/pallet/src/types.rs b/substrate/dex/pallet/src/types.rs index ee344564..818f7567 100644 --- a/substrate/dex/pallet/src/types.rs +++ b/substrate/dex/pallet/src/types.rs @@ -20,10 +20,6 @@ use super::*; -/// This needs to be long enough for arbitrage to occur and make holding -/// any fake price up sufficiently unprofitable. -pub const ORACLE_WINDOW_SIZE: u32 = 1000; - /// Trait for providing methods to swap between the various coin classes. pub trait Swap { /// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`. diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index 54137aba..22fc4709 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -27,9 +27,13 @@ serde = { version = "1", default-features = false, features = ["derive", "alloc" sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } + [features] std = ["zeroize", "scale/std", "borsh?/std", "serde?/std", "scale-info/std", "sp-core/std", "sp-runtime/std", "frame-support/std"] borsh = ["dep:borsh"] diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 0e05c9b1..970cf46e 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -13,8 +13,13 @@ use serde::{Serialize, Deserialize}; use scale::{Encode, Decode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_core::{ConstU32, bounded::BoundedVec}; +#[cfg(test)] +use sp_io::TestExternalities; +#[cfg(test)] +use frame_support::{pallet_prelude::*, Identity, traits::StorageInstance}; + +use sp_core::{ConstU32, bounded::BoundedVec}; pub use sp_application_crypto as crypto; mod amount; @@ -35,6 +40,9 @@ pub use account::*; mod tx; pub use tx::*; +pub type BlockNumber = u64; +pub type Header = sp_runtime::generic::Header; + #[cfg(feature = "borsh")] pub fn borsh_serialize_bounded_vec( bounded: &BoundedVec>, @@ -143,5 +151,66 @@ impl AsRef<[u8]> for Data { } } -pub type BlockNumber = u64; -pub type Header = sp_runtime::generic::Header; +/// Lexicographically reverses a given byte array. +pub fn reverse_lexicographic_order(bytes: [u8; N]) -> [u8; N] { + let mut res = [0u8; N]; + for (i, byte) in bytes.iter().enumerate() { + res[i] = !*byte; + } + res +} + +#[test] +fn test_reverse_lexicographic_order() { + TestExternalities::default().execute_with(|| { + use rand_core::{RngCore, OsRng}; + + struct Storage; + impl StorageInstance for Storage { + fn pallet_prefix() -> &'static str { + "LexicographicOrder" + } + + const STORAGE_PREFIX: &'static str = "storage"; + } + type Map = StorageMap; + + struct StorageReverse; + impl StorageInstance for StorageReverse { + fn pallet_prefix() -> &'static str { + "LexicographicOrder" + } + + const STORAGE_PREFIX: &'static str = "storagereverse"; + } + type MapReverse = StorageMap; + + // populate the maps + let mut amounts = vec![]; + for _ in 0 .. 100 { + amounts.push(OsRng.next_u64()); + } + + let mut amounts_sorted = amounts.clone(); + amounts_sorted.sort(); + for a in amounts { + Map::set(a.to_be_bytes(), Some(())); + MapReverse::set(reverse_lexicographic_order(a.to_be_bytes()), Some(())); + } + + // retrive back and check whether they are sorted as expected + let total_size = amounts_sorted.len(); + let mut map_iter = Map::iter_keys(); + let mut reverse_map_iter = MapReverse::iter_keys(); + for i in 0 .. amounts_sorted.len() { + let first = map_iter.next().unwrap(); + let second = reverse_map_iter.next().unwrap(); + + assert_eq!(u64::from_be_bytes(first), amounts_sorted[i]); + assert_eq!( + u64::from_be_bytes(reverse_lexicographic_order(second)), + amounts_sorted[total_size - (i + 1)] + ); + } + }); +} diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index f083befb..afc8349b 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -50,7 +50,7 @@ use sp_runtime::{ use primitives::{PublicKey, AccountLookup, SubstrateAmount}; use support::{ - traits::{ConstU8, ConstU32, ConstU64, Contains}, + traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, IdentityFee, Weight, @@ -124,6 +124,16 @@ pub const DAYS: BlockNumber = HOURS * 24; pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); +/// This needs to be long enough for arbitrage to occur and make holding any fake price up +/// sufficiently unrealistic. +#[allow(clippy::cast_possible_truncation)] +pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16; + +/// Since we use the median price, double the window length. +/// +/// We additionally +1 so there is a true median. +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; + pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration = sp_consensus_babe::BabeEpochConfiguration { c: PRIMARY_PROBABILITY, @@ -246,6 +256,8 @@ impl dex::Config for Runtime { type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 + type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; + type WeightInfo = dex::weights::SubstrateWeight; } diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 3c8418bf..a460c928 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -216,23 +216,13 @@ pub mod pallet { type SortedAllocations = StorageMap<_, Identity, (NetworkId, [u8; 8], [u8; 16], Public), (), OptionQuery>; impl Pallet { - /// A function which takes an amount and generates a byte array with a lexicographic order from - /// high amount to low amount. - #[inline] - fn lexicographic_amount(amount: Amount) -> [u8; 8] { - let mut bytes = amount.0.to_be_bytes(); - for byte in &mut bytes { - *byte = !*byte; - } - bytes - } #[inline] fn sorted_allocation_key( network: NetworkId, key: Public, amount: Amount, ) -> (NetworkId, [u8; 8], [u8; 16], Public) { - let amount = Self::lexicographic_amount(amount); + let amount = reverse_lexicographic_order(amount.0.to_be_bytes()); let hash = sp_io::hashing::blake2_128(&(network, amount, key).encode()); (network, amount, hash, key) } @@ -769,7 +759,7 @@ pub mod pallet { use dex_pallet::HigherPrecisionBalance; // This is inclusive to an increase in accuracy - let sri_per_coin = Dex::::oracle_value(balance.coin).unwrap_or(Amount(0)); + let sri_per_coin = Dex::::security_oracle_value(balance.coin).unwrap_or(Amount(0)); // See dex-pallet for the reasoning on these let coin_decimals = balance.coin.decimals().max(5);