Median by Position (#533)

* use median price instead of the highest sustained

* add test for lexicographically reversing a byte slice

* fix pr comments

* fix CI fail

* fix dex tests

* Use a fuzz-tested list of prices

* Working median algorithm based on position + lints

---------

Co-authored-by: akildemir <aeg_asd@hotmail.com>
This commit is contained in:
Luke Parker
2024-02-19 20:50:04 -05:00
committed by GitHub
parent 34b93b882c
commit 6f5d794f10
12 changed files with 334 additions and 92 deletions

3
Cargo.lock generated
View File

@@ -7487,6 +7487,7 @@ dependencies = [
"frame-support", "frame-support",
"frame-system", "frame-system",
"parity-scale-codec", "parity-scale-codec",
"rand_core",
"scale-info", "scale-info",
"serai-coins-pallet", "serai-coins-pallet",
"serai-primitives", "serai-primitives",
@@ -7687,10 +7688,12 @@ dependencies = [
"borsh", "borsh",
"frame-support", "frame-support",
"parity-scale-codec", "parity-scale-codec",
"rand_core",
"scale-info", "scale-info",
"serde", "serde",
"sp-application-crypto", "sp-application-crypto",
"sp-core", "sp-core",
"sp-io",
"sp-runtime", "sp-runtime",
"zeroize", "zeroize",
] ]

View File

@@ -9,6 +9,7 @@ macro_rules! serai_test {
$( $(
#[tokio::test] #[tokio::test]
async fn $name() { async fn $name() {
use std::collections::HashMap;
use dockertest::{ use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest, TestBodySpecification, DockerTest,
@@ -28,6 +29,7 @@ macro_rules! serai_test {
"--rpc-cors".to_string(), "--rpc-cors".to_string(),
"all".to_string(), "all".to_string(),
]) ])
.replace_env(HashMap::from([("RUST_LOG".to_string(), "runtime=debug".to_string())]))
.set_publish_all_ports(true) .set_publish_all_ports(true)
.set_handle(handle) .set_handle(handle)
.set_start_policy(StartPolicy::Strict) .set_start_policy(StartPolicy::Strict)

View File

@@ -244,8 +244,8 @@ serai_test!(
// add liquidity // add liquidity
common_add_liquidity(&serai, common_add_liquidity(&serai,
coin, coin,
Amount(50_000_000_000_000), Amount(5_000_000_000_000),
Amount(50_000_000_000_000), Amount(500_000_000_000),
0, 0,
pair.clone() pair.clone()
).await; ).await;
@@ -274,8 +274,8 @@ serai_test!(
mint_to: pair.public().into(), mint_to: pair.public().into(),
pool_id: Coin::Bitcoin, pool_id: Coin::Bitcoin,
coin_amount: 10_000_000_000_000, // half of sent amount coin_amount: 10_000_000_000_000, // half of sent amount
sri_amount: 6_947_918_403_646, sri_amount: 111_333_778_668,
lp_token_minted: 8333333333332 lp_token_minted: 1_054_092_553_383
}] }]
); );
}) })
@@ -290,7 +290,7 @@ serai_test!(
// mint coins // mint coins
mint_coin( mint_coin(
&serai, &serai,
Balance { coin: coin1, amount: Amount(100_000_000_000_000) }, Balance { coin: coin1, amount: Amount(10_000_000_000_000_000) },
NetworkId::Monero, NetworkId::Monero,
coin1_batch_id, coin1_batch_id,
pair.clone().public().into(), pair.clone().public().into(),
@@ -310,15 +310,15 @@ serai_test!(
// add liquidity to pools // add liquidity to pools
common_add_liquidity(&serai, common_add_liquidity(&serai,
coin1, coin1,
Amount(50_000_000_000_000), Amount(5_000_000_000_000_000), // monero has 12 decimals
Amount(50_000_000_000_000), Amount(50_000_000_000),
0, 0,
pair.clone() pair.clone()
).await; ).await;
common_add_liquidity(&serai, common_add_liquidity(&serai,
coin2, coin2,
Amount(50_000_000_000_000), Amount(5_000_000_000_000), // ether still has 8 in our codebase
Amount(50_000_000_000_000), Amount(500_000_000_000),
1, 1,
pair.clone() pair.clone()
).await; ).await;
@@ -344,7 +344,7 @@ serai_test!(
block: block_hash, block: block_hash,
instructions: vec![InInstructionWithBalance { instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)), 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, who: IN_INSTRUCTION_EXECUTOR,
send_to: IN_INSTRUCTION_EXECUTOR, send_to: IN_INSTRUCTION_EXECUTOR,
path, path,
amount_in: 20_000_000_000_000, amount_in: 200_000_000_000_000,
amount_out: 11066655622377 amount_out: 19_044_944_233
}] }]
); );
} }
@@ -384,7 +384,7 @@ serai_test!(
block: block_hash, block: block_hash,
instructions: vec![InInstructionWithBalance { instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), 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, who: IN_INSTRUCTION_EXECUTOR,
send_to: out_address.as_native().unwrap(), send_to: out_address.as_native().unwrap(),
path, path,
amount_in: 20_000_000_000_000, amount_in: 200_000_000_000,
amount_out: 26440798801319 amount_out: 1487294253782353
}] }]
); );
} }
@@ -422,7 +422,7 @@ serai_test!(
block: block_hash, block: block_hash,
instructions: vec![InInstructionWithBalance { instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())), 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, who: IN_INSTRUCTION_EXECUTOR,
send_to: out_address.as_native().unwrap(), send_to: out_address.as_native().unwrap(),
path, path,
amount_in: 10_000_000_000_000, amount_in: 100_000_000_000_000,
amount_out: 10711005507065 amount_out: 1_762_662_819
}] }]
); );
} }

View File

@@ -36,6 +36,9 @@ coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", de
serai-primitives = { path = "../../primitives", default-features = false } serai-primitives = { path = "../../primitives", default-features = false }
[dev-dependencies]
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
[features] [features]
default = ["std"] default = ["std"]
std = [ std = [

View File

@@ -106,7 +106,7 @@ pub mod pallet {
use coins_pallet::{Pallet as CoinsPallet, Config as CoinsConfig}; 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. /// Pool ID.
/// ///
@@ -144,6 +144,10 @@ pub mod pallet {
#[pallet::constant] #[pallet::constant]
type MaxSwapPathLength: Get<u32>; type MaxSwapPathLength: Get<u32>;
/// Last N number of blocks that oracle keeps track of the prices.
#[pallet::constant]
type MedianPriceWindowLength: Get<u16>;
/// Weight information for extrinsics in this pallet. /// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo; type WeightInfo: WeightInfo;
} }
@@ -156,33 +160,156 @@ pub mod pallet {
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn spot_price_for_block)] #[pallet::getter(fn spot_price_for_block)]
pub type SpotPriceForBlock<T: Config> = pub type SpotPriceForBlock<T: Config> =
StorageDoubleMap<_, Identity, BlockNumberFor<T>, Identity, Coin, [u8; 8], ValueQuery>; StorageDoubleMap<_, Identity, BlockNumberFor<T>, 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 /// The [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions in this
/// in this multi-set. /// multi-set. Since the underlying map is lexicographically sorted, this map stores amounts from
/// low to high.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn oracle_prices)] pub type SpotPrices<T: Config> =
pub type OraclePrices<T: Config> =
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>; StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>;
// SpotPrices, yet with keys stored in reverse lexicographic order.
#[pallet::storage]
pub type ReverseSpotPrices<T: Config> =
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], (), OptionQuery>;
/// Current length of the `SpotPrices` map.
#[pallet::storage]
pub type SpotPricesLength<T: Config> = StorageMap<_, Identity, Coin, u16, OptionQuery>;
/// Current position of the median within the `SpotPrices` map;
#[pallet::storage]
pub type CurrentMedianPosition<T: Config> = 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<T: Config> = 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<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
// TODO: consider an algorithm which removes outliers? This algorithm might work a good bit fn restore_median(
// better if we remove the bottom n values (so some value sustained over 90% of blocks instead coin: Coin,
// of all blocks in the window). mut current_median_pos: u16,
/// Get the highest sustained value for this window. mut current_median: Amount,
/// This is actually the lowest price observed during the windows, as it's the price length: u16,
/// all prices are greater than or equal to. ) {
pub fn highest_sustained_price(coin: &Coin) -> Option<Amount> { // 1 -> 0 (the only value)
let mut iter = OraclePrices::<T>::iter_key_prefix(coin); // 2 -> 1 (the higher element), 4 -> 2 (the higher element)
// the first key will be the lowest price due to the keys being lexicographically ordered. // 3 -> 1 (the true median)
iter.next().map(|amount| Amount(u64::from_be_bytes(amount))) 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::<T>::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::<T>::hashed_key_for(coin, key);
let next_price = SpotPrices::<T>::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::<T>::hashed_key_for(coin, key);
let next_price = ReverseSpotPrices::<T>::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::<T>::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;
} }
} }
#[pallet::storage] CurrentMedianPosition::<T>::set(coin, Some(current_median_pos));
#[pallet::getter(fn oracle_value)] MedianPrice::<T>::set(coin, Some(current_median));
pub type OracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>; }
pub(crate) fn insert_into_median(coin: Coin, amount: Amount) {
let new_quantity_of_presences =
SpotPrices::<T>::get(coin, amount.0.to_be_bytes()).unwrap_or(0) + 1;
SpotPrices::<T>::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences));
if new_quantity_of_presences == 1 {
ReverseSpotPrices::<T>::set(
coin,
reverse_lexicographic_order(amount.0.to_be_bytes()),
Some(()),
);
}
let new_length = SpotPricesLength::<T>::get(coin).unwrap_or(0) + 1;
SpotPricesLength::<T>::set(coin, Some(new_length));
let Some(current_median) = MedianPrice::<T>::get(coin) else {
MedianPrice::<T>::set(coin, Some(amount));
CurrentMedianPosition::<T>::set(coin, Some(0));
return;
};
let mut current_median_pos = CurrentMedianPosition::<T>::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::<T>::get(coin).unwrap();
let mut current_median_pos = CurrentMedianPosition::<T>::get(coin).unwrap();
if amount < current_median {
current_median_pos -= 1;
}
let new_quantity_of_presences =
SpotPrices::<T>::get(coin, amount.0.to_be_bytes()).unwrap() - 1;
if new_quantity_of_presences == 0 {
let normal_key = amount.0.to_be_bytes();
SpotPrices::<T>::remove(coin, normal_key);
ReverseSpotPrices::<T>::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::<T>::hashed_key_for(coin, normal_key);
current_median = Amount(u64::from_be_bytes(
SpotPrices::<T>::iter_key_prefix_from(coin, key).next().unwrap(),
));
}
} else {
SpotPrices::<T>::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences));
}
let new_length = SpotPricesLength::<T>::get(coin).unwrap() - 1;
SpotPricesLength::<T>::set(coin, Some(new_length));
Self::restore_median(coin, current_median_pos, current_median, new_length);
}
}
// Pallet's events. // Pallet's events.
#[pallet::event] #[pallet::event]
@@ -264,12 +391,6 @@ pub mod pallet {
#[pallet::genesis_build] #[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) { 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 // create the pools
for coin in &self.pools { for coin in &self.pools {
Pallet::<T>::create_pool(*coin).unwrap(); Pallet::<T>::create_pool(*coin).unwrap();
@@ -362,35 +483,22 @@ pub mod pallet {
} else { } else {
0 0
}; };
let sri_per_coin = sri_per_coin.to_be_bytes();
SpotPriceForBlock::<T>::set(n, coin, sri_per_coin); let sri_per_coin = Amount(sri_per_coin);
SpotPriceForBlock::<T>::set(n, coin, Some(sri_per_coin));
// Include this spot price into the multiset Self::insert_into_median(coin, sri_per_coin);
{ if SpotPricesLength::<T>::get(coin).unwrap() > T::MedianPriceWindowLength::get() {
let observed = OraclePrices::<T>::get(coin, sri_per_coin).unwrap_or(0); let old = n - T::MedianPriceWindowLength::get().into();
OraclePrices::<T>::set(coin, sri_per_coin, Some(observed + 1)); let old_price = SpotPriceForBlock::<T>::get(old, coin).unwrap();
} SpotPriceForBlock::<T>::remove(old, coin);
Self::remove_from_median(coin, old_price);
// 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::<T>::remove(start_of_window, coin);
// Remove this price from the multiset
OraclePrices::<T>::mutate_exists(coin, start_spot_price, |v| {
*v = Some(v.unwrap_or(1) - 1);
if *v == Some(0) {
*v = None;
}
});
} }
// update the oracle value // update the oracle value
let highest_sustained = Self::highest_sustained_price(&coin).unwrap_or(Amount(0)); let median = Self::median_price(coin).unwrap_or(Amount(0));
let oracle_value = Self::oracle_value(coin).unwrap_or(Amount(0)); let oracle_value = Self::security_oracle_value(coin).unwrap_or(Amount(0));
if highest_sustained > oracle_value { if median > oracle_value {
OracleValue::<T>::set(coin, Some(highest_sustained)); SecurityOracleValue::<T>::set(coin, Some(median));
} }
} }
} }
@@ -422,7 +530,7 @@ pub mod pallet {
pub fn on_new_session(network: NetworkId) { pub fn on_new_session(network: NetworkId) {
// reset the oracle value // reset the oracle value
for coin in network.coins() { for coin in network.coins() {
OracleValue::<T>::set(*coin, Self::highest_sustained_price(coin)); SecurityOracleValue::<T>::set(*coin, Self::median_price(coin));
} }
} }
} }

View File

@@ -25,7 +25,7 @@ use crate as dex;
use frame_support::{ use frame_support::{
construct_runtime, construct_runtime,
traits::{ConstU32, ConstU64}, traits::{ConstU16, ConstU32, ConstU64},
}; };
use sp_core::{H256, sr25519::Public}; use sp_core::{H256, sr25519::Public};
@@ -40,6 +40,8 @@ pub use coins_pallet as coins;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10;
construct_runtime!( construct_runtime!(
pub enum Test pub enum Test
{ {
@@ -92,6 +94,9 @@ impl Config for Test {
type WeightInfo = (); type WeightInfo = ();
type LPFee = ConstU32<3>; // means 0.3% type LPFee = ConstU32<3>; // means 0.3%
type MaxSwapPathLength = ConstU32<4>; type MaxSwapPathLength = ConstU32<4>;
type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>;
// 100 is good enough when the main currency has 12 decimals. // 100 is good enough when the main currency has 12 decimals.
type MintMinLiquidity = ConstU64<100>; type MintMinLiquidity = ConstU64<100>;
} }

View File

@@ -1267,3 +1267,53 @@ fn cannot_block_pool_creation() {
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 100, 9900, 10, 9900, user,)); 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::<Test>::from(u32::try_from(i).unwrap());
SpotPriceForBlock::<Test>::set(n, coin, Some(price));
Dex::insert_into_median(coin, price);
if SpotPricesLength::<Test>::get(coin).unwrap() > MEDIAN_PRICE_WINDOW_LENGTH {
let old = n - u64::from(MEDIAN_PRICE_WINDOW_LENGTH);
let old_price = SpotPriceForBlock::<Test>::get(old, coin).unwrap();
SpotPriceForBlock::<Test>::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]));
}
});
}

View File

@@ -20,10 +20,6 @@
use super::*; 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. /// Trait for providing methods to swap between the various coin classes.
pub trait Swap<AccountId, Balance, MultiCoinId> { pub trait Swap<AccountId, Balance, MultiCoinId> {
/// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`. /// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`.

View File

@@ -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-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-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { 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 } 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] [features]
std = ["zeroize", "scale/std", "borsh?/std", "serde?/std", "scale-info/std", "sp-core/std", "sp-runtime/std", "frame-support/std"] std = ["zeroize", "scale/std", "borsh?/std", "serde?/std", "scale-info/std", "sp-core/std", "sp-runtime/std", "frame-support/std"]
borsh = ["dep:borsh"] borsh = ["dep:borsh"]

View File

@@ -13,8 +13,13 @@ use serde::{Serialize, Deserialize};
use scale::{Encode, Decode, MaxEncodedLen}; use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo; 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; pub use sp_application_crypto as crypto;
mod amount; mod amount;
@@ -35,6 +40,9 @@ pub use account::*;
mod tx; mod tx;
pub use tx::*; pub use tx::*;
pub type BlockNumber = u64;
pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>;
#[cfg(feature = "borsh")] #[cfg(feature = "borsh")]
pub fn borsh_serialize_bounded_vec<W: borsh::io::Write, T: BorshSerialize, const B: u32>( pub fn borsh_serialize_bounded_vec<W: borsh::io::Write, T: BorshSerialize, const B: u32>(
bounded: &BoundedVec<T, ConstU32<B>>, bounded: &BoundedVec<T, ConstU32<B>>,
@@ -143,5 +151,66 @@ impl AsRef<[u8]> for Data {
} }
} }
pub type BlockNumber = u64; /// Lexicographically reverses a given byte array.
pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>; pub fn reverse_lexicographic_order<const N: usize>(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<Storage, Identity, [u8; 8], (), OptionQuery>;
struct StorageReverse;
impl StorageInstance for StorageReverse {
fn pallet_prefix() -> &'static str {
"LexicographicOrder"
}
const STORAGE_PREFIX: &'static str = "storagereverse";
}
type MapReverse = StorageMap<StorageReverse, Identity, [u8; 8], (), OptionQuery>;
// 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)]
);
}
});
}

View File

@@ -50,7 +50,7 @@ use sp_runtime::{
use primitives::{PublicKey, AccountLookup, SubstrateAmount}; use primitives::{PublicKey, AccountLookup, SubstrateAmount};
use support::{ use support::{
traits::{ConstU8, ConstU32, ConstU64, Contains}, traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains},
weights::{ weights::{
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
IdentityFee, Weight, IdentityFee, Weight,
@@ -124,6 +124,16 @@ pub const DAYS: BlockNumber = HOURS * 24;
pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); 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 = pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration =
sp_consensus_babe::BabeEpochConfiguration { sp_consensus_babe::BabeEpochConfiguration {
c: PRIMARY_PROBABILITY, c: PRIMARY_PROBABILITY,
@@ -246,6 +256,8 @@ impl dex::Config for Runtime {
type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2
type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>;
type WeightInfo = dex::weights::SubstrateWeight<Runtime>; type WeightInfo = dex::weights::SubstrateWeight<Runtime>;
} }

View File

@@ -216,23 +216,13 @@ pub mod pallet {
type SortedAllocations<T: Config> = type SortedAllocations<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 8], [u8; 16], Public), (), OptionQuery>; StorageMap<_, Identity, (NetworkId, [u8; 8], [u8; 16], Public), (), OptionQuery>;
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// 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] #[inline]
fn sorted_allocation_key( fn sorted_allocation_key(
network: NetworkId, network: NetworkId,
key: Public, key: Public,
amount: Amount, amount: Amount,
) -> (NetworkId, [u8; 8], [u8; 16], Public) { ) -> (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()); let hash = sp_io::hashing::blake2_128(&(network, amount, key).encode());
(network, amount, hash, key) (network, amount, hash, key)
} }
@@ -769,7 +759,7 @@ pub mod pallet {
use dex_pallet::HigherPrecisionBalance; use dex_pallet::HigherPrecisionBalance;
// This is inclusive to an increase in accuracy // This is inclusive to an increase in accuracy
let sri_per_coin = Dex::<T>::oracle_value(balance.coin).unwrap_or(Amount(0)); let sri_per_coin = Dex::<T>::security_oracle_value(balance.coin).unwrap_or(Amount(0));
// See dex-pallet for the reasoning on these // See dex-pallet for the reasoning on these
let coin_decimals = balance.coin.decimals().max(5); let coin_decimals = balance.coin.decimals().max(5);