#![cfg_attr(not(feature = "std"), no_std)] #[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)] #[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::*; 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] #[pallet::getter(fn genesis_complete_block)] pub(crate) type GenesisCompleteBlock = StorageValue<_, u64, 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() && GenesisCompleteBlock::::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)); } GenesisCompleteBlock::::set(Some(n.saturated_into::())); } // 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::*;