diff --git a/Cargo.lock b/Cargo.lock index b8f4efacf..38e1f8d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7929,6 +7929,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serai-coins-primitives", + "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-signals-primitives", @@ -8127,6 +8128,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serai-genesis-liquidity-pallet" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "serai-coins-pallet", + "serai-dex-pallet", + "serai-genesis-liquidity-primitives", + "serai-primitives", + "serai-validator-sets-pallet", + "serai-validator-sets-primitives", + "sp-application-crypto", + "sp-core", + "sp-std", +] + +[[package]] +name = "serai-genesis-liquidity-primitives" +version = "0.1.0" +dependencies = [ + "borsh", + "parity-scale-codec", + "scale-info", + "serai-primitives", + "serai-validator-sets-primitives", + "serde", + "sp-std", + "zeroize", +] + [[package]] name = "serai-in-instructions-pallet" version = "0.1.0" @@ -8137,6 +8171,8 @@ dependencies = [ "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-genesis-liquidity-pallet", + "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", @@ -8406,6 +8442,7 @@ dependencies = [ "serai-abi", "serai-coins-pallet", "serai-dex-pallet", + "serai-genesis-liquidity-pallet", "serai-in-instructions-pallet", "serai-primitives", "serai-signals-pallet", diff --git a/deny.toml b/deny.toml index 3359de0f4..960250516 100644 --- a/deny.toml +++ b/deny.toml @@ -53,6 +53,8 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-coins-pallet" }, { allow = ["AGPL-3.0"], name = "serai-dex-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index 547761d88..c2947aaab 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -33,6 +33,7 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur serai-primitives = { path = "../primitives", version = "0.1", default-features = false } serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false } serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false } +serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false } serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false } serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false } @@ -55,6 +56,7 @@ std = [ "serai-primitives/std", "serai-coins-primitives/std", "serai-validator-sets-primitives/std", + "serai-genesis-liquidity-primitives/std", "serai-in-instructions-primitives/std", "serai-signals-primitives/std", ] @@ -63,6 +65,7 @@ borsh = [ "serai-primitives/borsh", "serai-coins-primitives/borsh", "serai-validator-sets-primitives/borsh", + "serai-genesis-liquidity-primitives/borsh", "serai-in-instructions-primitives/borsh", "serai-signals-primitives/borsh", ] @@ -71,6 +74,7 @@ serde = [ "serai-primitives/serde", "serai-coins-primitives/serde", "serai-validator-sets-primitives/serde", + "serai-genesis-liquidity-primitives/serde", "serai-in-instructions-primitives/serde", "serai-signals-primitives/serde", ] diff --git a/substrate/abi/src/coins.rs b/substrate/abi/src/coins.rs index 56255b0a8..9466db0f9 100644 --- a/substrate/abi/src/coins.rs +++ b/substrate/abi/src/coins.rs @@ -13,15 +13,6 @@ pub enum Call { burn_with_instruction { instruction: OutInstructionWithBalance }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub enum LiquidityTokensCall { - transfer { to: SeraiAddress, balance: Balance }, - burn { balance: Balance }, -} - #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize))] diff --git a/substrate/abi/src/genesis_liquidity.rs b/substrate/abi/src/genesis_liquidity.rs new file mode 100644 index 000000000..461284141 --- /dev/null +++ b/substrate/abi/src/genesis_liquidity.rs @@ -0,0 +1,21 @@ +pub use serai_genesis_liquidity_primitives as primitives; + +use serai_primitives::*; +use primitives::*; + +#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Call { + remove_coin_liquidity { balance: Balance }, + oraclize_values { values: Values, signature: Signature }, +} + +#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Event { + GenesisLiquidityAdded { by: SeraiAddress, balance: Balance }, + GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance }, + GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance }, + EconomicSecurityReached { network: NetworkId }, +} diff --git a/substrate/abi/src/lib.rs b/substrate/abi/src/lib.rs index 2670bef78..ac8b8824c 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -12,12 +12,15 @@ pub mod system; pub mod timestamp; pub mod coins; +pub mod liquidity_tokens; pub mod dex; pub mod validator_sets; pub mod in_instructions; pub mod signals; +pub mod genesis_liquidity; + pub mod babe; pub mod grandpa; @@ -27,8 +30,9 @@ pub mod tx; pub enum Call { Timestamp(timestamp::Call), Coins(coins::Call), - LiquidityTokens(coins::LiquidityTokensCall), + LiquidityTokens(liquidity_tokens::Call), Dex(dex::Call), + GenesisLiquidity(genesis_liquidity::Call), ValidatorSets(validator_sets::Call), InInstructions(in_instructions::Call), Signals(signals::Call), @@ -48,8 +52,9 @@ pub enum Event { Timestamp, TransactionPayment(TransactionPaymentEvent), Coins(coins::Event), - LiquidityTokens(coins::Event), + LiquidityTokens(liquidity_tokens::Event), Dex(dex::Event), + GenesisLiquidity(genesis_liquidity::Event), ValidatorSets(validator_sets::Event), InInstructions(in_instructions::Event), Signals(signals::Event), diff --git a/substrate/abi/src/liquidity_tokens.rs b/substrate/abi/src/liquidity_tokens.rs new file mode 100644 index 000000000..6bdc651b6 --- /dev/null +++ b/substrate/abi/src/liquidity_tokens.rs @@ -0,0 +1,18 @@ +use serai_primitives::{Balance, SeraiAddress}; + +#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Call { + burn { balance: Balance }, + transfer { to: SeraiAddress, balance: Balance }, +} + +#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Event { + Mint { to: SeraiAddress, balance: Balance }, + Burn { from: SeraiAddress, balance: Balance }, + Transfer { from: SeraiAddress, to: SeraiAddress, balance: Balance }, +} diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs index 00108dfef..d9edc56b6 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -1,7 +1,9 @@ use sp_core::bounded_vec::BoundedVec; use serai_abi::primitives::{SeraiAddress, Amount, Coin}; -use crate::{SeraiError, TemporalSerai}; +use scale::{decode_from_bytes, Encode}; + +use crate::{Serai, SeraiError, TemporalSerai}; pub type DexEvent = serai_abi::dex::Event; @@ -57,4 +59,20 @@ impl<'a> SeraiDex<'a> { send_to: address, }) } + + /// Returns the reserves of `coin:SRI` pool. + pub async fn get_reserves(&self, coin: Coin) -> Result, SeraiError> { + let reserves = self + .0 + .serai + .call( + "state_call", + ["DexApi_get_reserves".to_string(), hex::encode((coin, Coin::Serai).encode())], + ) + .await?; + let bytes = Serai::hex_decode(reserves)?; + let result = decode_from_bytes::>(bytes.into()) + .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; + Ok(result.map(|amounts| (Amount(amounts.0), Amount(amounts.1)))) + } } diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs new file mode 100644 index 000000000..04e80d745 --- /dev/null +++ b/substrate/client/src/serai/genesis_liquidity.rs @@ -0,0 +1,65 @@ +pub use serai_abi::genesis_liquidity::primitives; +use primitives::{Values, LiquidityAmount}; + +use serai_abi::primitives::*; + +use sp_core::sr25519::Signature; + +use scale::Encode; + +use crate::{Serai, SeraiError, TemporalSerai, Transaction}; + +pub type GenesisLiquidityEvent = serai_abi::genesis_liquidity::Event; + +const PALLET: &str = "GenesisLiquidity"; + +#[derive(Clone, Copy)] +pub struct SeraiGenesisLiquidity<'a>(pub(crate) &'a TemporalSerai<'a>); +impl<'a> SeraiGenesisLiquidity<'a> { + pub async fn events(&self) -> Result, SeraiError> { + self + .0 + .events(|event| { + if let serai_abi::Event::GenesisLiquidity(event) = event { + Some(event.clone()) + } else { + None + } + }) + .await + } + + pub fn oraclize_values(values: Values, signature: Signature) -> Transaction { + Serai::unsigned(serai_abi::Call::GenesisLiquidity( + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature }, + )) + } + + pub fn remove_coin_liquidity(balance: Balance) -> serai_abi::Call { + serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity { + balance, + }) + } + + pub async fn liquidity( + &self, + address: &SeraiAddress, + coin: Coin, + ) -> Result { + Ok( + self + .0 + .storage( + PALLET, + "Liquidity", + (coin, sp_core::hashing::blake2_128(&address.encode()), &address.0), + ) + .await? + .unwrap_or(LiquidityAmount::zero()), + ) + } + + pub async fn supply(&self, coin: Coin) -> Result { + Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) + } +} diff --git a/substrate/client/src/serai/liquidity_tokens.rs b/substrate/client/src/serai/liquidity_tokens.rs new file mode 100644 index 000000000..3e9052b2c --- /dev/null +++ b/substrate/client/src/serai/liquidity_tokens.rs @@ -0,0 +1,41 @@ +use scale::Encode; + +use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance}; + +use crate::{TemporalSerai, SeraiError}; + +const PALLET: &str = "LiquidityTokens"; + +#[derive(Clone, Copy)] +pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>); +impl<'a> SeraiLiquidityTokens<'a> { + pub async fn token_supply(&self, coin: Coin) -> Result { + Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0))) + } + + pub async fn token_balance( + &self, + coin: Coin, + address: SeraiAddress, + ) -> Result { + Ok( + self + .0 + .storage( + PALLET, + "Balances", + (sp_core::hashing::blake2_128(&address.encode()), &address.0, coin), + ) + .await? + .unwrap_or(Amount(0)), + ) + } + + pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call { + serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer { to, balance }) + } + + pub fn burn(balance: Balance) -> serai_abi::Call { + serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn { balance }) + } +} diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index b1b8b0418..dfd14779e 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -26,6 +26,10 @@ pub mod in_instructions; pub use in_instructions::SeraiInInstructions; pub mod validator_sets; pub use validator_sets::SeraiValidatorSets; +pub mod genesis_liquidity; +pub use genesis_liquidity::SeraiGenesisLiquidity; +pub mod liquidity_tokens; +pub use liquidity_tokens::SeraiLiquidityTokens; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)] pub struct Block { @@ -194,6 +198,7 @@ impl Serai { Ok(()) } + // TODO: move this into substrate/client/src/validator_sets.rs async fn active_network_validators(&self, network: NetworkId) -> Result, SeraiError> { let validators: String = self .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) @@ -388,4 +393,12 @@ impl<'a> TemporalSerai<'a> { pub fn validator_sets(&'a self) -> SeraiValidatorSets<'a> { SeraiValidatorSets(self) } + + pub fn genesis_liquidity(&'a self) -> SeraiGenesisLiquidity { + SeraiGenesisLiquidity(self) + } + + pub fn liquidity_tokens(&'a self) -> SeraiLiquidityTokens { + SeraiLiquidityTokens(self) + } } diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index d7e8436b2..e9d88594c 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -66,3 +66,67 @@ macro_rules! serai_test { )* } } + +#[macro_export] +macro_rules! serai_test_fast_epoch { + ($($name: ident: $test: expr)*) => { + $( + #[tokio::test] + async fn $name() { + use std::collections::HashMap; + use dockertest::{ + PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, + TestBodySpecification, DockerTest, + }; + + serai_docker_tests::build("serai-fast-epoch".to_string()); + + let handle = concat!("serai_client-serai_node-", stringify!($name)); + + let composition = TestBodySpecification::with_image( + Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never), + ) + .replace_cmd(vec![ + "serai-node".to_string(), + "--dev".to_string(), + "--unsafe-rpc-external".to_string(), + "--rpc-cors".to_string(), + "all".to_string(), + ]) + .replace_env( + HashMap::from([ + ("RUST_LOG".to_string(), "runtime=debug".to_string()), + ("KEY".to_string(), " ".to_string()), + ]) + ) + .set_publish_all_ports(true) + .set_handle(handle) + .set_start_policy(StartPolicy::Strict) + .set_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::Always, + source: LogSource::Both, + })); + + let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); + test.provide_container(composition); + test.run_async(|ops| async move { + // Sleep until the Substrate RPC starts + let serai_rpc = ops.handle(handle).host_port(9944).unwrap(); + let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1); + // Bound execution to 60 seconds + for _ in 0 .. 60 { + tokio::time::sleep(core::time::Duration::from_secs(1)).await; + let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue }; + if client.latest_finalized_block_hash().await.is_err() { + continue; + } + break; + } + #[allow(clippy::redundant_closure_call)] + $test(Serai::new(serai_rpc).await.unwrap()).await; + }).await; + } + )* + } +} diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs new file mode 100644 index 000000000..8edc87d3d --- /dev/null +++ b/substrate/client/tests/genesis_liquidity.rs @@ -0,0 +1,216 @@ +use std::{time::Duration, collections::HashMap}; + +use rand_core::{RngCore, OsRng}; +use zeroize::Zeroizing; + +use ciphersuite::{Ciphersuite, Ristretto}; +use frost::dkg::musig::musig; +use schnorrkel::Schnorrkel; + +use serai_client::{ + genesis_liquidity::{ + primitives::{GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES}, + SeraiGenesisLiquidity, + }, + validator_sets::primitives::{musig_context, Session, ValidatorSet}, +}; + +use serai_abi::{ + genesis_liquidity::primitives::{oraclize_values_message, Values}, + primitives::COINS, +}; + +use sp_core::{sr25519::Signature, Pair as PairTrait}; + +use serai_client::{ + primitives::{ + Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, GENESIS_SRI, + }, + in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, + Serai, +}; + +mod common; +use common::{in_instructions::provide_batch, tx::publish_tx}; + +serai_test_fast_epoch!( + genesis_liquidity: (|serai: Serai| async move { + test_genesis_liquidity(serai).await; + }) +); + +async fn test_genesis_liquidity(serai: Serai) { + // all coins except the native + let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); + + // make accounts with amounts + let mut accounts = HashMap::new(); + for coin in coins.clone() { + // make 5 accounts per coin + let mut values = vec![]; + for _ in 0 .. 5 { + let mut address = SeraiAddress::new([0; 32]); + OsRng.fill_bytes(&mut address.0); + values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); + } + accounts.insert(coin, values); + } + + // send a batch per coin + let mut batch_ids: HashMap = HashMap::new(); + for coin in coins.clone() { + // set up instructions + let instructions = accounts[&coin] + .iter() + .map(|(addr, amount)| InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(*addr), + balance: Balance { coin, amount: *amount }, + }) + .collect::>(); + + // set up bloch hash + let mut block = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block.0); + + // set up batch id + batch_ids + .entry(coin.network()) + .and_modify(|v| { + *v += 1; + }) + .or_insert(0); + + let batch = + Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions }; + provide_batch(&serai, batch).await; + } + + // wait until genesis ends + let genesis_blocks = 10; // TODO + let block_time = 6; // TODO + tokio::time::timeout( + tokio::time::Duration::from_secs(3 * (genesis_blocks * block_time)), + async { + while serai.latest_finalized_block().await.unwrap().number() < 10 { + tokio::time::sleep(Duration::from_secs(6)).await; + } + }, + ) + .await + .unwrap(); + + // set values relative to each other + // TODO: Random values here + let values = Values { monero: 184100, ether: 4785000, dai: 1500 }; + set_values(&serai, &values).await; + let values_map = HashMap::from([ + (Coin::Monero, values.monero), + (Coin::Ether, values.ether), + (Coin::Dai, values.dai), + ]); + + // wait a little bit.. + tokio::time::sleep(Duration::from_secs(12)).await; + + // check total SRI supply is +100M + // there are 6 endowed accounts in dev-net. Take this into consideration when checking + // for the total sri minted at this time. + let serai = serai.as_of_latest_finalized_block().await.unwrap(); + let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); + let endowed_amount: u64 = 1 << 60; + let total_sri = (6 * endowed_amount) + GENESIS_SRI; + assert_eq!(sri, Amount(total_sri)); + + // check genesis account has no coins, all transferred to pools. + for coin in COINS { + let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap(); + assert_eq!(amount.0, 0); + } + + // check pools has proper liquidity + let mut pool_amounts = HashMap::new(); + let mut total_value = 0u128; + for coin in coins.clone() { + let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); + let value = if coin != Coin::Bitcoin { + (total_coin * u128::from(values_map[&coin])) / 10u128.pow(coin.decimals()) + } else { + total_coin + }; + + total_value += value; + pool_amounts.insert(coin, (total_coin, value)); + } + + // check distributed SRI per pool + let mut total_sri_distributed = 0u128; + for coin in coins.clone() { + let sri = if coin == *COINS.last().unwrap() { + u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap() + } else { + (pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value + }; + total_sri_distributed += sri; + + let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap(); + assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side + assert_eq!(u128::from(reserves.1 .0), sri); // SRI side + } + + // check each liquidity provider got liquidity tokens proportional to their value + for coin in coins { + let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap(); + for (acc, amount) in &accounts[&coin] { + let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares; + + // since we can't test the ratios directly(due to integer division giving 0) + // we test whether they give the same result when multiplied by another constant. + // Following test ensures the account in fact has the right amount of shares. + let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares; + let amounts_ratio = + (INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap(); + + // we can tolerate 1 unit diff between them due to integer division. + if shares_ratio.abs_diff(amounts_ratio) == 1 { + shares_ratio = amounts_ratio; + } + + assert_eq!(shares_ratio, amounts_ratio); + } + } + + // TODO: test remove the liq before/after genesis ended. +} + +async fn set_values(serai: &Serai, values: &Values) { + // prepare a Musig tx to oraclize the relative values + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); + // we publish the tx in set 4 + let set = ValidatorSet { session: Session(4), network: NetworkId::Serai }; + + let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); + let secret_key = ::read_F::<&[u8]>( + &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + + assert_eq!(Ristretto::generator() * secret_key, public_key); + let threshold_keys = + musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); + + let sig = frost::tests::sign_without_caching( + &mut OsRng, + frost::tests::algorithm_machines( + &mut OsRng, + &Schnorrkel::new(b"substrate"), + &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), + ), + &oraclize_values_message(&set, values), + ); + + // oraclize values + let _ = + publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes()))) + .await; +} diff --git a/substrate/dex/pallet/src/tests.rs b/substrate/dex/pallet/src/tests.rs index 80b45464d..b00141997 100644 --- a/substrate/dex/pallet/src/tests.rs +++ b/substrate/dex/pallet/src/tests.rs @@ -25,7 +25,7 @@ pub use coins_pallet as coins; use coins::Pallet as CoinsPallet; -use serai_primitives::*; +use serai_primitives::{Balance, COINS, PublicKey, system_address, Amount}; type LiquidityTokens = coins_pallet::Pallet; type LiquidityTokensError = coins_pallet::Error; diff --git a/substrate/genesis-liquidity/pallet/Cargo.toml b/substrate/genesis-liquidity/pallet/Cargo.toml new file mode 100644 index 000000000..99b71c4ce --- /dev/null +++ b/substrate/genesis-liquidity/pallet/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "serai-genesis-liquidity-pallet" +version = "0.1.0" +description = "Genesis liquidity pallet for Serai" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity/pallet" +authors = ["Akil Demir "] +edition = "2021" +rust-version = "1.77" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.cargo-machete] +ignored = ["scale", "scale-info"] + +[lints] +workspace = true + +[dependencies] +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } + +frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } +frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } + +sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false } + +dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } +coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } +validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } + +serai-primitives = { path = "../../primitives", default-features = false } +genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../primitives", default-features = false } +validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false } + +[features] +std = [ + "scale/std", + "scale-info/std", + + "frame-system/std", + "frame-support/std", + + "sp-std/std", + "sp-core/std", + "sp-application-crypto/std", + + "coins-pallet/std", + "dex-pallet/std", + "validator-sets-pallet/std", + + "serai-primitives/std", + "genesis-liquidity-primitives/std", + "validator-sets-primitives/std", +] +try-runtime = [] # TODO +fast-epoch = [] + +default = ["std"] diff --git a/substrate/genesis-liquidity/pallet/LICENSE b/substrate/genesis-liquidity/pallet/LICENSE new file mode 100644 index 000000000..e091b1498 --- /dev/null +++ b/substrate/genesis-liquidity/pallet/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs new file mode 100644 index 000000000..ad371ae41 --- /dev/null +++ b/substrate/genesis-liquidity/pallet/src/lib.rs @@ -0,0 +1,470 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)] +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_system::{pallet_prelude::*, RawOrigin}; + use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; + + use sp_std::{vec, vec::Vec}; + use sp_core::sr25519::Signature; + use sp_application_crypto::RuntimePublic; + + use dex_pallet::{Pallet as Dex, Config as DexConfig}; + use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; + use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets}; + + use serai_primitives::{Coin, COINS, *}; + use validator_sets_primitives::{ValidatorSet, musig_key}; + pub use genesis_liquidity_primitives as primitives; + use primitives::*; + + // TODO: Have a more robust way of accessing LiquidityTokens pallet. + /// LiquidityTokens Pallet as an instance of coins pallet. + pub type LiquidityTokens = coins_pallet::Pallet; + + #[pallet::config] + pub trait Config: + frame_system::Config + + VsConfig + + DexConfig + + CoinsConfig + + coins_pallet::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::error] + pub enum Error { + GenesisPeriodEnded, + AmountOverflowed, + NotEnoughLiquidity, + CanOnlyRemoveFullAmount, + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + GenesisLiquidityAdded { by: SeraiAddress, balance: Balance }, + GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance }, + GenesisLiquidityAddedToPool { coin1: Balance, sri: Amount }, + EconomicSecurityReached { network: NetworkId }, + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// Keeps shares and the amount of coins per account. + #[pallet::storage] + pub(crate) type Liquidity = + StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, LiquidityAmount, OptionQuery>; + + /// Keeps the total shares and the total amount of coins per coin. + #[pallet::storage] + pub(crate) type Supply = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>; + + #[pallet::storage] + pub(crate) type EconomicSecurityReached = + StorageMap<_, Identity, NetworkId, BlockNumberFor, OptionQuery>; + + #[pallet::storage] + pub(crate) type Oracle = StorageMap<_, Identity, Coin, u64, OptionQuery>; + + #[pallet::storage] + pub(crate) type GenesisComplete = StorageValue<_, (), OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + #[cfg(feature = "fast-epoch")] + let final_block = 10u64; + + #[cfg(not(feature = "fast-epoch"))] + let final_block = MONTHS; + + // Distribute the genesis sri to pools after a month + if (n.saturated_into::() >= final_block) && + Self::oraclization_is_done() && + GenesisComplete::::get().is_none() + { + // mint the SRI + Coins::::mint( + GENESIS_LIQUIDITY_ACCOUNT.into(), + Balance { coin: Coin::Serai, amount: Amount(GENESIS_SRI) }, + ) + .unwrap(); + + // get pool & total values + let mut pool_values = vec![]; + let mut total_value: u128 = 0; + for coin in COINS { + if coin == Coin::Serai { + continue; + } + + // initial coin value in terms of btc + let Some(value) = Oracle::::get(coin) else { + continue; + }; + + let pool_amount = + u128::from(Supply::::get(coin).unwrap_or(LiquidityAmount::zero()).coins); + let pool_value = pool_amount + .checked_mul(value.into()) + .unwrap() + .checked_div(10u128.pow(coin.decimals())) + .unwrap(); + total_value = total_value.checked_add(pool_value).unwrap(); + pool_values.push((coin, pool_amount, pool_value)); + } + + // add the liquidity per pool + let mut total_sri_distributed = 0; + let pool_values_len = pool_values.len(); + for (i, (coin, pool_amount, pool_value)) in pool_values.into_iter().enumerate() { + // whatever sri left for the last coin should be ~= it's ratio + let sri_amount = if i == (pool_values_len - 1) { + GENESIS_SRI.checked_sub(total_sri_distributed).unwrap() + } else { + u64::try_from( + u128::from(GENESIS_SRI) + .checked_mul(pool_value) + .unwrap() + .checked_div(total_value) + .unwrap(), + ) + .unwrap() + }; + total_sri_distributed = total_sri_distributed.checked_add(sri_amount).unwrap(); + + // actually add the liquidity to dex + let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + let Ok(()) = Dex::::add_liquidity( + origin.into(), + coin, + u64::try_from(pool_amount).unwrap(), + sri_amount, + u64::try_from(pool_amount).unwrap(), + sri_amount, + GENESIS_LIQUIDITY_ACCOUNT.into(), + ) else { + continue; + }; + + // let everyone know about the event + Self::deposit_event(Event::GenesisLiquidityAddedToPool { + coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) }, + sri: Amount(sri_amount), + }); + } + assert_eq!(total_sri_distributed, GENESIS_SRI); + + // we shouldn't have left any coin in genesis account at this moment, including SRI. + // All transferred to the pools. + for coin in COINS { + assert_eq!(Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0)); + } + + GenesisComplete::::set(Some(())); + } + + // we accept we reached economic security once we can mint smallest amount of a network's coin + // TODO: move EconomicSecurity to a separate pallet + for coin in COINS { + let existing = EconomicSecurityReached::::get(coin.network()); + if existing.is_none() && + ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) + { + EconomicSecurityReached::::set(coin.network(), Some(n)); + Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() }); + } + } + + Weight::zero() // TODO + } + } + + impl Pallet { + /// Add genesis liquidity for the given account. All accounts that provide liquidity + /// will receive the genesis SRI according to their liquidity ratio. + pub fn add_coin_liquidity(account: PublicKey, balance: Balance) -> DispatchResult { + // check we are still in genesis period + if Self::genesis_ended() { + Err(Error::::GenesisPeriodEnded)?; + } + + // calculate new shares & supply + let (new_liquidity, new_supply) = if let Some(supply) = Supply::::get(balance.coin) { + // calculate amount of shares for this amount + let shares = Self::mul_div(supply.shares, balance.amount.0, supply.coins)?; + + // get new shares for this account + let existing = + Liquidity::::get(balance.coin, account).unwrap_or(LiquidityAmount::zero()); + ( + LiquidityAmount { + shares: existing.shares.checked_add(shares).ok_or(Error::::AmountOverflowed)?, + coins: existing + .coins + .checked_add(balance.amount.0) + .ok_or(Error::::AmountOverflowed)?, + }, + LiquidityAmount { + shares: supply.shares.checked_add(shares).ok_or(Error::::AmountOverflowed)?, + coins: supply + .coins + .checked_add(balance.amount.0) + .ok_or(Error::::AmountOverflowed)?, + }, + ) + } else { + let first_amount = + LiquidityAmount { shares: INITIAL_GENESIS_LP_SHARES, coins: balance.amount.0 }; + (first_amount, first_amount) + }; + + // save + Liquidity::::set(balance.coin, account, Some(new_liquidity)); + Supply::::set(balance.coin, Some(new_supply)); + Self::deposit_event(Event::GenesisLiquidityAdded { by: account.into(), balance }); + Ok(()) + } + + /// Returns the number of blocks since the all networks reached economic security first time. + /// If networks is yet to be reached that threshold, None is returned. + fn blocks_since_ec_security() -> Option { + let mut min = u64::MAX; + for n in NETWORKS { + let ec_security_block = EconomicSecurityReached::::get(n)?.saturated_into::(); + let current = >::block_number().saturated_into::(); + let diff = current.saturating_sub(ec_security_block); + min = diff.min(min); + } + Some(min) + } + + fn genesis_ended() -> bool { + Self::oraclization_is_done() && + >::block_number().saturated_into::() >= MONTHS + } + + fn oraclization_is_done() -> bool { + for c in COINS { + if c == Coin::Serai { + continue; + } + + if Oracle::::get(c).is_none() { + return false; + } + } + + true + } + + fn mul_div(a: u64, b: u64, c: u64) -> Result> { + let a = u128::from(a); + let b = u128::from(b); + let c = u128::from(c); + + let result = a + .checked_mul(b) + .ok_or(Error::::AmountOverflowed)? + .checked_div(c) + .ok_or(Error::::AmountOverflowed)?; + + result.try_into().map_err(|_| Error::::AmountOverflowed) + } + } + + #[pallet::call] + impl Pallet { + /// Remove the provided genesis liquidity for an account. + #[pallet::call_index(0)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn remove_coin_liquidity(origin: OriginFor, balance: Balance) -> DispatchResult { + let account = ensure_signed(origin)?; + let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + let supply = Supply::::get(balance.coin).ok_or(Error::::NotEnoughLiquidity)?; + + // check we are still in genesis period + let (new_liquidity, new_supply) = if Self::genesis_ended() { + // see how much liq tokens we have + let total_liq_tokens = + LiquidityTokens::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai).0; + + // get how much user wants to remove + let LiquidityAmount { shares, coins } = + Liquidity::::get(balance.coin, account).unwrap_or(LiquidityAmount::zero()); + let total_shares = Supply::::get(balance.coin).unwrap_or(LiquidityAmount::zero()).shares; + let user_liq_tokens = Self::mul_div(total_liq_tokens, shares, total_shares)?; + let amount_to_remove = + Self::mul_div(user_liq_tokens, balance.amount.0, INITIAL_GENESIS_LP_SHARES)?; + + // remove liquidity from pool + let prev_sri = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai); + let prev_coin = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin); + Dex::::remove_liquidity( + origin.clone().into(), + balance.coin, + amount_to_remove, + 1, + 1, + GENESIS_LIQUIDITY_ACCOUNT.into(), + )?; + let current_sri = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai); + let current_coin = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin); + + // burn the SRI if necessary + // TODO: take into consideration movement between pools. + let mut sri: u64 = current_sri.0.saturating_sub(prev_sri.0); + let distance_to_full_pay = + GENESIS_SRI_TRICKLE_FEED.saturating_sub(Self::blocks_since_ec_security().unwrap_or(0)); + let burn_sri_amount = u64::try_from( + u128::from(sri) + .checked_mul(u128::from(distance_to_full_pay)) + .ok_or(Error::::AmountOverflowed)? + .checked_div(u128::from(GENESIS_SRI_TRICKLE_FEED)) + .ok_or(Error::::AmountOverflowed)?, + ) + .map_err(|_| Error::::AmountOverflowed)?; + Coins::::burn( + origin.clone().into(), + Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) }, + )?; + sri = sri.checked_sub(burn_sri_amount).ok_or(Error::::AmountOverflowed)?; + + // transfer to owner + let coin_out = current_coin.0.saturating_sub(prev_coin.0); + Coins::::transfer( + origin.clone().into(), + account, + Balance { coin: balance.coin, amount: Amount(coin_out) }, + )?; + Coins::::transfer( + origin.into(), + account, + Balance { coin: Coin::Serai, amount: Amount(sri) }, + )?; + + // return new amounts + ( + LiquidityAmount { + shares: shares.checked_sub(amount_to_remove).ok_or(Error::::AmountOverflowed)?, + coins: coins.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, + }, + LiquidityAmount { + shares: supply + .shares + .checked_sub(amount_to_remove) + .ok_or(Error::::AmountOverflowed)?, + coins: supply.coins.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, + }, + ) + } else { + if balance.amount.0 != INITIAL_GENESIS_LP_SHARES { + Err(Error::::CanOnlyRemoveFullAmount)?; + } + let existing = + Liquidity::::get(balance.coin, account).ok_or(Error::::NotEnoughLiquidity)?; + + // transfer to the user + Coins::::transfer( + origin.into(), + account, + Balance { coin: balance.coin, amount: Amount(existing.coins) }, + )?; + + ( + LiquidityAmount::zero(), + LiquidityAmount { + shares: supply + .shares + .checked_sub(existing.shares) + .ok_or(Error::::AmountOverflowed)?, + coins: supply.coins.checked_sub(existing.coins).ok_or(Error::::AmountOverflowed)?, + }, + ) + }; + + // save + if new_liquidity == LiquidityAmount::zero() { + Liquidity::::set(balance.coin, account, None); + } else { + Liquidity::::set(balance.coin, account, Some(new_liquidity)); + } + Supply::::set(balance.coin, Some(new_supply)); + + Self::deposit_event(Event::GenesisLiquidityRemoved { by: account.into(), balance }); + Ok(()) + } + + /// A call to submit the initial coin values in terms of BTC. + #[pallet::call_index(1)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn oraclize_values( + origin: OriginFor, + values: Values, + _signature: Signature, + ) -> DispatchResult { + ensure_none(origin)?; + + // set their relative values + Oracle::::set(Coin::Bitcoin, Some(10u64.pow(Coin::Bitcoin.decimals()))); + Oracle::::set(Coin::Monero, Some(values.monero)); + Oracle::::set(Coin::Ether, Some(values.ether)); + Oracle::::set(Coin::Dai, Some(values.dai)); + Ok(()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { + match call { + Call::oraclize_values { ref values, ref signature } => { + let network = NetworkId::Serai; + let Some(session) = ValidatorSets::::session(network) else { + return Err(TransactionValidityError::from(InvalidTransaction::Custom(0))); + }; + + let set = ValidatorSet { network, session }; + let signers = ValidatorSets::::participants_for_latest_decided_set(network) + .expect("no participant in the current set") + .into_iter() + .map(|(p, _)| p) + .collect::>(); + + // check this didn't get called before + if Self::oraclization_is_done() { + Err(InvalidTransaction::Custom(1))?; + } + + // make sure signers settings the value at the end of the genesis period. + // we don't need this check for tests. + #[cfg(not(feature = "fast-epoch"))] + if >::block_number().saturated_into::() < MONTHS { + Err(InvalidTransaction::Custom(2))?; + } + + if !musig_key(set, &signers).verify(&oraclize_values_message(&set, values), signature) { + Err(InvalidTransaction::BadProof)?; + } + + ValidTransaction::with_tag_prefix("GenesisLiquidity") + .and_provides((0, set)) + .longevity(u64::MAX) + .propagate(true) + .build() + } + Call::remove_coin_liquidity { .. } => Err(InvalidTransaction::Call)?, + Call::__Ignore(_, _) => unreachable!(), + } + } + } +} + +pub use pallet::*; diff --git a/substrate/genesis-liquidity/primitives/Cargo.toml b/substrate/genesis-liquidity/primitives/Cargo.toml new file mode 100644 index 000000000..e795ff24b --- /dev/null +++ b/substrate/genesis-liquidity/primitives/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "serai-genesis-liquidity-primitives" +version = "0.1.0" +description = "Serai genesis liquidity primitives" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity/primitives" +authors = ["Akil Demir "] +edition = "2021" +rust-version = "1.77" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +zeroize = { version = "^1.5", features = ["derive"], optional = true } + +borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } + +sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } + +serai-primitives = { path = "../../primitives", default-features = false } +validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false } + +[features] +std = [ + "zeroize", + "scale/std", + "borsh?/std", + "serde?/std", + "scale-info/std", + + "serai-primitives/std", + "validator-sets-primitives/std", + + "sp-std/std" +] +default = ["std"] diff --git a/substrate/genesis-liquidity/primitives/LICENSE b/substrate/genesis-liquidity/primitives/LICENSE new file mode 100644 index 000000000..659881f1a --- /dev/null +++ b/substrate/genesis-liquidity/primitives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/substrate/genesis-liquidity/primitives/src/lib.rs b/substrate/genesis-liquidity/primitives/src/lib.rs new file mode 100644 index 000000000..4e4c277ca --- /dev/null +++ b/substrate/genesis-liquidity/primitives/src/lib.rs @@ -0,0 +1,54 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +use zeroize::Zeroize; + +#[cfg(feature = "borsh")] +use borsh::{BorshSerialize, BorshDeserialize}; +#[cfg(feature = "serde")] +use serde::{Serialize, Deserialize}; + +use sp_std::vec::Vec; + +use scale::{Encode, Decode, MaxEncodedLen}; +use scale_info::TypeInfo; + +use serai_primitives::*; +use validator_sets_primitives::ValidatorSet; + +pub const INITIAL_GENESIS_LP_SHARES: u64 = 10_000; + +// This is the account to hold and manage the genesis liquidity. +pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"GenesisLiquidity-account"); + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Zeroize))] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Values { + pub monero: u64, + pub ether: u64, + pub dai: u64, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Zeroize))] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LiquidityAmount { + pub shares: u64, + pub coins: u64, +} + +impl LiquidityAmount { + pub fn zero() -> Self { + LiquidityAmount { shares: 0, coins: 0 } + } +} + +/// The message for the oraclize_values signature. +pub fn oraclize_values_message(set: &ValidatorSet, values: &Values) -> Vec { + (b"GenesisLiquidity-oraclize_values", set, values).encode() +} diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index 676d11f5e..4eafd199e 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -33,10 +33,12 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur serai-primitives = { path = "../../primitives", default-features = false } in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../primitives", default-features = false } +genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../../genesis-liquidity/primitives", default-features = false } coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } +genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false } [features] std = [ @@ -54,10 +56,12 @@ std = [ "serai-primitives/std", "in-instructions-primitives/std", + "genesis-liquidity-primitives/std", "coins-pallet/std", "dex-pallet/std", "validator-sets-pallet/std", + "genesis-liquidity-pallet/std", ] default = ["std"] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 955a54df7..2667b13df 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -19,6 +19,7 @@ pub mod pallet { use sp_core::sr25519::Public; use serai_primitives::{Coin, Amount, Balance}; + use genesis_liquidity_primitives::GENESIS_LIQUIDITY_ACCOUNT; use frame_support::pallet_prelude::*; use frame_system::{pallet_prelude::*, RawOrigin}; @@ -33,10 +34,14 @@ pub mod pallet { Config as ValidatorSetsConfig, Pallet as ValidatorSets, }; + use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig}; + use super::*; #[pallet::config] - pub trait Config: frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig { + pub trait Config: + frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig + GenesisLiqConfig + { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } @@ -200,6 +205,10 @@ pub mod pallet { } } } + InInstruction::GenesisLiquidity(address) => { + Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; + GenesisLiq::::add_coin_liquidity(address.into(), instruction.balance)?; + } } Ok(()) } diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index afa11cac1..87d9ce373 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -78,6 +78,7 @@ pub enum DexCall { pub enum InInstruction { Transfer(SeraiAddress), Dex(DexCall), + GenesisLiquidity(SeraiAddress), } #[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs new file mode 100644 index 000000000..c5c53d75d --- /dev/null +++ b/substrate/primitives/src/constants.rs @@ -0,0 +1,29 @@ +use crate::BlockNumber; + +// 1 MB +pub const BLOCK_SIZE: u32 = 1024 * 1024; +// 6 seconds +pub const TARGET_BLOCK_TIME: u64 = 6; + +/// Measured in blocks. +pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; +pub const HOURS: BlockNumber = MINUTES * 60; +pub const DAYS: BlockNumber = HOURS * 24; +pub const WEEKS: BlockNumber = DAYS * 7; +pub const MONTHS: BlockNumber = WEEKS * 4; + +/// 6 months of blocks +pub const GENESIS_SRI_TRICKLE_FEED: u64 = MONTHS * 6; + +// 100 Million SRI +pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); + +/// 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; diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 2af36e22d..d2c52219e 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -37,6 +37,9 @@ pub use balance::*; mod account; pub use account::*; +mod constants; +pub use constants::*; + pub type BlockNumber = u64; pub type Header = sp_runtime::generic::Header; diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 54869f6df..a8e60174f 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -60,6 +60,7 @@ coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", defau dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-features = false } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false } +genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity/pallet", default-features = false } in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false } @@ -115,6 +116,7 @@ std = [ "dex-pallet/std", "validator-sets-pallet/std", + "genesis-liquidity-pallet/std", "in-instructions-pallet/std", @@ -127,7 +129,7 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", ] -fast-epoch = [] +fast-epoch = ["genesis-liquidity-pallet/fast-epoch"] runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs index 45c2aa334..b479036d2 100644 --- a/substrate/runtime/src/abi.rs +++ b/substrate/runtime/src/abi.rs @@ -7,7 +7,7 @@ use serai_abi::Call; use crate::{ Vec, primitives::{PublicKey, SeraiAddress}, - timestamp, coins, dex, + timestamp, coins, dex, genesis_liquidity, validator_sets::{self, MembershipProof}, in_instructions, signals, babe, grandpa, RuntimeCall, }; @@ -30,10 +30,10 @@ impl From for RuntimeCall { } }, Call::LiquidityTokens(lt) => match lt { - serai_abi::coins::LiquidityTokensCall::transfer { to, balance } => { + serai_abi::liquidity_tokens::Call::transfer { to, balance } => { RuntimeCall::LiquidityTokens(coins::Call::transfer { to: to.into(), balance }) } - serai_abi::coins::LiquidityTokensCall::burn { balance } => { + serai_abi::liquidity_tokens::Call::burn { balance } => { RuntimeCall::LiquidityTokens(coins::Call::burn { balance }) } }, @@ -89,6 +89,17 @@ impl From for RuntimeCall { send_to: send_to.into(), }), }, + Call::GenesisLiquidity(gl) => match gl { + serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance }) + } + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values { + values, + signature, + }) + } + }, Call::ValidatorSets(vs) => match vs { serai_abi::validator_sets::Call::set_keys { network, @@ -209,9 +220,9 @@ impl TryInto for RuntimeCall { }), RuntimeCall::LiquidityTokens(call) => Call::LiquidityTokens(match call { coins::Call::transfer { to, balance } => { - serai_abi::coins::LiquidityTokensCall::transfer { to: to.into(), balance } + serai_abi::liquidity_tokens::Call::transfer { to: to.into(), balance } } - coins::Call::burn { balance } => serai_abi::coins::LiquidityTokensCall::burn { balance }, + coins::Call::burn { balance } => serai_abi::liquidity_tokens::Call::burn { balance }, _ => Err(())?, }), RuntimeCall::Dex(call) => Call::Dex(match call { @@ -261,6 +272,15 @@ impl TryInto for RuntimeCall { } _ => Err(())?, }), + RuntimeCall::GenesisLiquidity(call) => Call::GenesisLiquidity(match call { + genesis_liquidity::Call::remove_coin_liquidity { balance } => { + serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } + } + genesis_liquidity::Call::oraclize_values { values, signature } => { + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } + } + _ => Err(())?, + }), RuntimeCall::ValidatorSets(call) => Call::ValidatorSets(match call { validator_sets::Call::set_keys { network, removed_participants, key_pair, signature } => { serai_abi::validator_sets::Call::set_keys { diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index bef2c062a..5301f0432 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -11,7 +11,6 @@ use core::marker::PhantomData; // Re-export all components pub use serai_primitives as primitives; pub use primitives::{BlockNumber, Header}; -use primitives::{NetworkId, NETWORKS}; pub use frame_system as system; pub use frame_support as support; @@ -32,6 +31,8 @@ pub use signals_pallet as signals; pub use pallet_babe as babe; pub use pallet_grandpa as grandpa; +pub use genesis_liquidity_pallet as genesis_liquidity; + // Actually used by the runtime use sp_core::OpaqueMetadata; use sp_std::prelude::*; @@ -47,7 +48,11 @@ use sp_runtime::{ BoundedVec, Perbill, ApplyExtrinsicResult, }; -use primitives::{PublicKey, AccountLookup, SubstrateAmount}; +#[allow(unused_imports)] +use primitives::{ + NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH, + HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, +}; use support::{ traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains}, @@ -114,28 +119,7 @@ pub fn native_version() -> NativeVersion { NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } } -// 1 MB -pub const BLOCK_SIZE: u32 = 1024 * 1024; -// 6 seconds -pub const TARGET_BLOCK_TIME: u64 = 6; - -/// Measured in blocks. -pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; -pub const HOURS: BlockNumber = MINUTES * 60; -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, @@ -264,6 +248,10 @@ impl in_instructions::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +impl genesis_liquidity::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + // for publishing equivocation evidences. impl frame_system::offchain::SendTransactionTypes for Runtime where @@ -338,6 +326,7 @@ construct_runtime!( Coins: coins, LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, Dex: dex, + GenesisLiquidity: genesis_liquidity, ValidatorSets: validator_sets, @@ -604,4 +593,28 @@ sp_api::impl_runtime_apis! { } } } + + impl dex::DexApi for Runtime { + fn quote_price_exact_tokens_for_tokens( + asset1: Coin, + asset2: Coin, + amount: SubstrateAmount, + include_fee: bool + ) -> Option { + Dex::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee) + } + + fn quote_price_tokens_for_exact_tokens( + asset1: Coin, + asset2: Coin, + amount: SubstrateAmount, + include_fee: bool + ) -> Option { + Dex::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee) + } + + fn get_reserves(asset1: Coin, asset2: Coin) -> Option<(SubstrateAmount, SubstrateAmount)> { + Dex::get_reserves(&asset1, &asset2).ok() + } + } }