diff --git a/Cargo.lock b/Cargo.lock index 1ae1d463..fde62b3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7526,6 +7526,28 @@ 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", + "sp-std", +] + +[[package]] +name = "serai-genesis-liquidity-primitives" +version = "0.1.0" +dependencies = [ + "serai-primitives", +] + [[package]] name = "serai-in-instructions-pallet" version = "0.1.0" @@ -7536,6 +7558,7 @@ dependencies = [ "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-genesis-liquidity-pallet", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", @@ -7797,6 +7820,7 @@ dependencies = [ "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-genesis-liquidity-pallet", "serai-in-instructions-pallet", "serai-primitives", "serai-signals-pallet", diff --git a/substrate/genesis-liquidity/pallet/Cargo.toml b/substrate/genesis-liquidity/pallet/Cargo.toml new file mode 100644 index 00000000..35551626 --- /dev/null +++ b/substrate/genesis-liquidity/pallet/Cargo.toml @@ -0,0 +1,46 @@ +[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 } + +dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } +coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } + +serai-primitives = { path = "../../primitives", default-features = false } +genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../primitives", default-features = false } + +[features] +std = [ + "frame-system/std", + "frame-support/std", + + "coins-pallet/std", + "dex-pallet/std", +] + +default = ["std"] \ No newline at end of file diff --git a/substrate/genesis-liquidity/pallet/LICENSE b/substrate/genesis-liquidity/pallet/LICENSE new file mode 100644 index 00000000..e091b149 --- /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 00000000..f43df0b4 --- /dev/null +++ b/substrate/genesis-liquidity/pallet/src/lib.rs @@ -0,0 +1,271 @@ +#![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, collections::btree_map::BTreeMap}; + + use dex_pallet::{Pallet as Dex, Config as DexConfig}; + use coins_pallet::{ + primitives::{OutInstructionWithBalance, OutInstruction}, + Config as CoinsConfig, Pallet as Coins, AllowMint, + }; + + use serai_primitives::*; + pub use genesis_liquidity_primitives as primitives; + use primitives::*; + + /// LiquidityTokens Pallet as an instance of coins pallet. + pub type LiquidityTokens = coins_pallet::Pallet; + + #[pallet::config] + pub trait Config: + frame_system::Config + 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, coin2: Balance }, + EconomicSecurityReached { network: NetworkId }, + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::storage] + pub(crate) type Liquidity = + StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>; + + #[pallet::storage] + pub(crate) type LiquidityTokensPerAddress = + StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>; + + #[pallet::storage] + pub(crate) type EconomicSecurityReached = + StorageMap<_, Identity, NetworkId, BlockNumberFor, ValueQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(n: BlockNumberFor) { + // Distribute the genesis sri to pools after a month + if n > BLOCKS_PER_MONTH.into() { + // mint the SRI + Coins::::mint( + GENESIS_LIQUIDITY_ACCOUNT.into(), + Balance { coin: Coin::Serai, amount: Amount(GENESIS_SRI) }, + ) + .unwrap(); + + // get coin values & total + let mut account_values = BTreeMap::new(); + let mut pool_values = BTreeMap::new(); + let mut total_value: u64 = 0; + for coin in COINS { + // TODO: following line is just a place holder till we get the actual coin value + // in terms of btc. + let value = Dex::::security_oracle_value(coin).unwrap_or(Amount(0)).0; + account_values.insert(coin, vec![]); + let mut pool_amount: u64 = 0; + for (account, amount) in Liquidity::::iter_prefix(coin) { + pool_amount += amount; + let value_this_addr = amount * value; + account_values.get_mut(&coin).unwrap().push((account, value_this_addr)) + } + + let pool_value = pool_amount * value; + total_value += pool_value; + pool_values.insert(coin, (pool_amount, pool_value)); + } + + // add the liquidity per pool + for (coin, (amount, value)) in &pool_values { + let sri_amount = (GENESIS_SRI * value) / total_value; + let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + Dex::::add_liquidity( + origin.into(), + *coin, + *amount, + sri_amount, + *amount, + sri_amount, + GENESIS_LIQUIDITY_ACCOUNT.into(), + ) + .unwrap(); + + // set liquidity tokens per account + let tokens = LiquidityTokens::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), *coin).0; + let mut total_tokens_this_coin: u64 = 0; + for (acc, value) in account_values.get(coin).unwrap() { + let liq_tokens_this_acc = (tokens * value) / pool_values.get(coin).unwrap().1; + total_tokens_this_coin += liq_tokens_this_acc; + LiquidityTokensPerAddress::::set(coin, acc, Some(liq_tokens_this_acc)); + } + assert_eq!(tokens, total_tokens_this_coin); + } + + // we shouldn't have any coin left in our account at this moment, including SRI. + for coin in COINS { + assert_eq!(Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0)); + } + } + + // we accept we reached economic security once we can mint smallest amount of a network's coin + for coin in COINS { + let existing = EconomicSecurityReached::::get(coin.network()); + if existing == 0u32.into() && + ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) + { + EconomicSecurityReached::::set(coin.network(), n); + Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() }); + } + } + } + } + + 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)?; + } + + // mint the coins + Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance)?; + + // save + let existing = Liquidity::::get(balance.coin, account).unwrap_or(0); + let new = existing.checked_add(balance.amount.0).ok_or(Error::::AmountOverflowed)?; + Liquidity::::set(balance.coin, account, Some(new)); + + Self::deposit_event(Event::GenesisLiquidityAdded { by: account.into(), balance }); + Ok(()) + } + + /// Remove the provided genesis liquidity for an account. If called pre-economic security era, + pub fn remove_coin_liquidity( + account: PublicKey, + balance: Balance, + out_address: ExternalAddress, + ) -> DispatchResult { + let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + + // check we are still in genesis period + if Self::genesis_ended() { + // check user have enough to remove + let existing = LiquidityTokensPerAddress::::get(balance.coin, account).unwrap_or(0); + if balance.amount.0 > existing { + Err(Error::::NotEnoughLiquidity)?; + } + + // 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, + balance.amount.0, + 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 + let mut sri = current_sri.0 - prev_sri.0; + let burn_sri_amount = (sri * + (GENESIS_SRI_TRICKLE_FEED - Self::blocks_since_ec_security(&balance.coin))) / + GENESIS_SRI_TRICKLE_FEED; + Coins::::burn( + origin.clone().into(), + Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) }, + )?; + sri -= burn_sri_amount; + + // transfer to owner + let coin_out = current_coin.0 - 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) }, + )?; + + // save + let existing = LiquidityTokensPerAddress::::get(balance.coin, account).unwrap_or(0); + let new = existing.checked_sub(balance.amount.0).ok_or(Error::::AmountOverflowed)?; + LiquidityTokensPerAddress::::set(balance.coin, account, Some(new)); + } else { + let existing = Liquidity::::get(balance.coin, account).unwrap_or(0); + if balance.amount.0 > existing || balance.amount.0 == 0 { + Err(Error::::NotEnoughLiquidity)?; + } + if balance.amount.0 < existing { + Err(Error::::CanOnlyRemoveFullAmount)?; + } + + // TODO: do internal transfer instead? + let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + let instruction = OutInstructionWithBalance { + instruction: OutInstruction { address: out_address, data: None }, + balance, + }; + Coins::::burn_with_instruction(origin.into(), instruction)?; + + // save + Liquidity::::set(balance.coin, account, None); + } + + Self::deposit_event(Event::GenesisLiquidityRemoved { by: account.into(), balance }); + Ok(()) + } + + // Returns the number of blocks since the coin's network reached economic security first time. + // If the network is yet to be reached that threshold 0 is returned, and maximum of + // GENESIS_SRI_TRICKLE_FEED returned. + fn blocks_since_ec_security(coin: &Coin) -> u64 { + let ec_security_block = + EconomicSecurityReached::::get(coin.network()).saturated_into::(); + let current = >::block_number().saturated_into::(); + if ec_security_block > 0 { + let diff = current - ec_security_block; + if diff > GENESIS_SRI_TRICKLE_FEED { + return GENESIS_SRI_TRICKLE_FEED; + } + + return diff; + } + + 0 + } + + fn genesis_ended() -> bool { + >::block_number() >= BLOCKS_PER_MONTH.into() + } + } +} + +pub use pallet::*; diff --git a/substrate/genesis-liquidity/primitives/Cargo.toml b/substrate/genesis-liquidity/primitives/Cargo.toml new file mode 100644 index 00000000..1e10e840 --- /dev/null +++ b/substrate/genesis-liquidity/primitives/Cargo.toml @@ -0,0 +1,25 @@ +[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] +serai-primitives = { path = "../../primitives", default-features = false } + +[features] +std = [ + "serai-primitives/std", +] +default = ["std"] diff --git a/substrate/genesis-liquidity/primitives/LICENSE b/substrate/genesis-liquidity/primitives/LICENSE new file mode 100644 index 00000000..659881f1 --- /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 00000000..40c8ecb3 --- /dev/null +++ b/substrate/genesis-liquidity/primitives/src/lib.rs @@ -0,0 +1,17 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +use serai_primitives::*; + +// amount of blocks in 30 days for 6s per block. +pub const BLOCKS_PER_MONTH: u32 = 10 * 60 * 24 * 30; + +/// 180 days of blocks +pub const GENESIS_SRI_TRICKLE_FEED: u64 = 10 * 60 * 24 * 180; + +// 100 Million SRI +pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); + +// This is the account which will be the origin for any dispatched `InInstruction`s. +pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"Genesis-liquidity-account"); diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index f313a22a..c91c3250 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -37,6 +37,7 @@ in-instructions-primitives = { package = "serai-in-instructions-primitives", pat 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 = [ @@ -58,5 +59,6 @@ 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 3ec63ae5..2e45cffa 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -33,10 +33,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 +204,14 @@ pub mod pallet { } } } + InInstruction::GenesisLiquidity(ops) => match ops { + GenesisLiquidityOperation::Add(address, balance) => { + GenesisLiq::::add_coin_liquidity(address.into(), balance)?; + } + GenesisLiquidityOperation::Remove(address, balance, out_address) => { + GenesisLiq::::remove_coin_liquidity(address.into(), balance, out_address)?; + } + }, } Ok(()) } diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index afa11cac..fb2e9503 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -71,6 +71,15 @@ pub enum DexCall { Swap(Balance, OutAddress), } +#[derive(Clone, 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 enum GenesisLiquidityOperation { + Add(SeraiAddress, Balance), + Remove(SeraiAddress, Balance, ExternalAddress), +} + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] @@ -78,6 +87,7 @@ pub enum DexCall { pub enum InInstruction { Transfer(SeraiAddress), Dex(DexCall), + GenesisLiquidity(GenesisLiquidityOperation), } #[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index e4b7d639..8efe0c85 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -59,6 +59,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 } @@ -112,6 +113,7 @@ std = [ "dex-pallet/std", "validator-sets-pallet/std", + "genesis-liquidity-pallet/std", "in-instructions-pallet/std", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 9a534a72..d79f2f03 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -32,6 +32,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::*; @@ -288,6 +290,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 @@ -362,6 +368,7 @@ construct_runtime!( Coins: coins, LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, Dex: dex, + GenesisLiquidity: genesis_liquidity, ValidatorSets: validator_sets,