diff --git a/Cargo.lock b/Cargo.lock index 7d661d29..d4a18fe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7998,6 +7998,7 @@ dependencies = [ "serai-coins-pallet", "serai-dex-pallet", "serai-genesis-liquidity-pallet", + "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", diff --git a/substrate/abi/src/genesis_liquidity.rs b/substrate/abi/src/genesis_liquidity.rs index 2b0c208c..2047bc2b 100644 --- a/substrate/abi/src/genesis_liquidity.rs +++ b/substrate/abi/src/genesis_liquidity.rs @@ -7,7 +7,7 @@ use primitives::*; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Call { remove_coin_liquidity { balance: Balance }, - set_initial_price { prices: Prices, signature: Signature }, + oraclize_values { prices: Prices, signature: Signature }, } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs index 23e942aa..431ce90b 100644 --- a/substrate/client/src/serai/genesis_liquidity.rs +++ b/substrate/client/src/serai/genesis_liquidity.rs @@ -29,9 +29,9 @@ impl<'a> SeraiGenesisLiquidity<'a> { .await } - pub fn set_initial_price(prices: Prices, signature: Signature) -> Transaction { + pub fn oraclize_values(prices: Prices, signature: Signature) -> Transaction { Serai::unsigned(serai_abi::Call::GenesisLiquidity( - serai_abi::genesis_liquidity::Call::set_initial_price { prices, signature }, + serai_abi::genesis_liquidity::Call::oraclize_values { prices, signature }, )) } diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs index 0e5b787f..ae0f27eb 100644 --- a/substrate/client/tests/genesis_liquidity.rs +++ b/substrate/client/tests/genesis_liquidity.rs @@ -16,7 +16,7 @@ use serai_client::{ }; use serai_abi::{ - genesis_liquidity::primitives::{set_initial_price_message, Prices}, + genesis_liquidity::primitives::{oraclize_values_message, Prices}, primitives::COINS, }; @@ -101,7 +101,7 @@ async fn test_genesis_liquidity(serai: Serai) { .unwrap(); // set prices - let prices = Prices { bitcoin: 10u64.pow(8), monero: 184100, ethereum: 4785000, dai: 1500 }; + let prices = Prices { monero: 184100, ethereum: 4785000, dai: 1500 }; set_prices(&serai, &prices).await; // wait little bit.. @@ -127,7 +127,7 @@ async fn test_genesis_liquidity(serai: Serai) { let pool_btc = btc_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); let pool_xmr = xmr_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); - let pool_btc_value = (pool_btc * u128::from(prices.bitcoin)) / 10u128.pow(8); + let pool_btc_value = pool_btc; let pool_xmr_value = (pool_xmr * u128::from(prices.monero)) / 10u128.pow(12); let total_value = pool_btc_value + pool_xmr_value; @@ -176,7 +176,8 @@ async fn set_prices(serai: &Serai, prices: &Prices) { // prepare a Musig tx to set the initial prices let pair = insecure_pair_from_name("Alice"); let public = pair.public(); - let set = ValidatorSet { session: Session(0), network: NetworkId::Serai }; + // we publish the tx in set 2 + let set = ValidatorSet { session: Session(2), network: NetworkId::Serai }; let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); let secret_key = ::read_F::<&[u8]>( @@ -195,13 +196,11 @@ async fn set_prices(serai: &Serai, prices: &Prices) { &Schnorrkel::new(b"substrate"), &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), ), - &set_initial_price_message(&set, prices), + &oraclize_values_message(&set, prices), ); // set initial prices - let _ = publish_tx( - serai, - &SeraiGenesisLiquidity::set_initial_price(*prices, Signature(sig.to_bytes())), - ) - .await; + let _ = + publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*prices, Signature(sig.to_bytes()))) + .await; } diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs index b5ee14f2..86f2a908 100644 --- a/substrate/genesis-liquidity/pallet/src/lib.rs +++ b/substrate/genesis-liquidity/pallet/src/lib.rs @@ -47,7 +47,7 @@ pub mod pallet { pub enum Event { GenesisLiquidityAdded { by: SeraiAddress, balance: Balance }, GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance }, - GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance }, + GenesisLiquidityAddedToPool { coin1: Balance, sri: Amount }, EconomicSecurityReached { network: NetworkId }, } @@ -56,20 +56,12 @@ pub mod pallet { /// Keeps shares and the amount of coins per account. #[pallet::storage] - pub(crate) type Liquidity = StorageDoubleMap< - _, - Identity, - Coin, - Blake2_128Concat, - PublicKey, - (SubstrateAmount, SubstrateAmount), - OptionQuery, - >; + 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, (SubstrateAmount, SubstrateAmount), OptionQuery>; + pub(crate) type Supply = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>; #[pallet::storage] pub(crate) type EconomicSecurityReached = @@ -83,7 +75,7 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { - fn on_finalize(n: BlockNumberFor) { + fn on_initialize(n: BlockNumberFor) -> Weight { #[cfg(feature = "fast-epoch")] let final_block = 10u64; @@ -115,9 +107,14 @@ pub mod pallet { continue; }; - let pool_amount = u128::from(Supply::::get(coin).unwrap_or((0, 0)).1); - let pool_value = pool_amount.saturating_mul(value.into()) / 10u128.pow(coin.decimals()); - total_value = total_value.saturating_add(pool_value); + 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)); } @@ -127,11 +124,18 @@ pub mod pallet { 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 - total_sri_distributed + GENESIS_SRI.checked_sub(total_sri_distributed).unwrap() } else { - u64::try_from(u128::from(GENESIS_SRI).saturating_mul(pool_value) / total_value).unwrap() + u64::try_from( + u128::from(GENESIS_SRI) + .checked_mul(pool_value) + .unwrap() + .checked_div(total_value) + .unwrap(), + ) + .unwrap() }; - total_sri_distributed += sri_amount; + total_sri_distributed = total_sri_distributed.checked_add(sri_amount).unwrap(); // we can't add 0 liquidity if !(pool_amount > 0 && sri_amount > 0) { @@ -154,7 +158,7 @@ pub mod pallet { // let everyone know about the event Self::deposit_event(Event::GenesisLiquidityAddedToPool { coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) }, - coin2: Balance { coin: Coin::Serai, amount: Amount(sri_amount) }, + sri: Amount(sri_amount), }); } assert_eq!(total_sri_distributed, GENESIS_SRI); @@ -169,6 +173,7 @@ pub mod pallet { } // 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() && @@ -178,6 +183,8 @@ pub mod pallet { Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() }); } } + + Weight::zero() // TODO } } @@ -190,33 +197,37 @@ pub mod pallet { Err(Error::::GenesisPeriodEnded)?; } - // mint the coins - Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance)?; - // calculate new shares & supply - let (new_shares, new_supply) = if let Some(supply) = Supply::::get(balance.coin) { + 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.0, balance.amount.0, supply.1)?; + 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((0, 0)); + let existing = + Liquidity::::get(balance.coin, account).unwrap_or(LiquidityAmount::zero()); ( - ( - existing.0.checked_add(shares).ok_or(Error::::AmountOverflowed)?, - existing.1.checked_add(balance.amount.0).ok_or(Error::::AmountOverflowed)?, - ), - ( - supply.0.checked_add(shares).ok_or(Error::::AmountOverflowed)?, - supply.1.checked_add(balance.amount.0).ok_or(Error::::AmountOverflowed)?, - ), + 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_amounts = (GENESIS_LP_SHARES, balance.amount.0); - (first_amounts, first_amounts) + let first_amount = LiquidityAmount { shares: GENESIS_LP_SHARES, coins: balance.amount.0 }; + (first_amount, first_amount) }; // save - Liquidity::::set(balance.coin, account, Some(new_shares)); + 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(()) @@ -280,16 +291,16 @@ pub mod pallet { let supply = Supply::::get(balance.coin).ok_or(Error::::NotEnoughLiquidity)?; // check we are still in genesis period - let (new_shares, new_supply) = if Self::genesis_ended() { + 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 (user_shares, user_coins) = - Liquidity::::get(balance.coin, account).unwrap_or((0, 0)); - let total_shares = Supply::::get(balance.coin).unwrap_or((0, 0)).0; - let user_liq_tokens = Self::mul_div(total_liq_tokens, user_shares, total_shares)?; + 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, GENESIS_LP_SHARES)?; // remove liquidity from pool @@ -308,18 +319,25 @@ pub mod pallet { // burn the SRI if necessary // TODO: take into consideration movement between pools. - let mut sri = current_sri.0.saturating_sub(prev_sri.0); + 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 = sri.saturating_mul(distance_to_full_pay) / GENESIS_SRI_TRICKLE_FEED; + 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 -= burn_sri_amount; + sri = sri.checked_sub(burn_sri_amount).ok_or(Error::::AmountOverflowed)?; // transfer to owner - let coin_out = current_coin.0 - prev_coin.0; + let coin_out = current_coin.0.saturating_sub(prev_coin.0); Coins::::transfer( origin.clone().into(), account, @@ -333,14 +351,17 @@ pub mod pallet { // return new amounts ( - ( - user_shares.checked_sub(amount_to_remove).ok_or(Error::::AmountOverflowed)?, - user_coins.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, - ), - ( - supply.0.checked_sub(amount_to_remove).ok_or(Error::::AmountOverflowed)?, - supply.1.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, - ), + 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 != GENESIS_LP_SHARES { @@ -353,23 +374,26 @@ pub mod pallet { Coins::::transfer( origin.into(), account, - Balance { coin: balance.coin, amount: Amount(existing.1) }, + Balance { coin: balance.coin, amount: Amount(existing.coins) }, )?; ( - (0, 0), - ( - supply.0.checked_sub(existing.0).ok_or(Error::::AmountOverflowed)?, - supply.1.checked_sub(existing.1).ok_or(Error::::AmountOverflowed)?, - ), + 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_shares.0 == 0 { + if new_liquidity == LiquidityAmount::zero() { Liquidity::::set(balance.coin, account, None); } else { - Liquidity::::set(balance.coin, account, Some(new_shares)); + Liquidity::::set(balance.coin, account, Some(new_liquidity)); } Supply::::set(balance.coin, Some(new_supply)); @@ -380,7 +404,7 @@ pub mod pallet { /// A call to submit the initial coin values in terms of BTC. #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn set_initial_price( + pub fn oraclize_values( origin: OriginFor, prices: Prices, _signature: Signature, @@ -388,7 +412,7 @@ pub mod pallet { ensure_none(origin)?; // set the prices - Oracle::::set(Coin::Bitcoin, Some(prices.bitcoin)); + Oracle::::set(Coin::Bitcoin, Some(10u64.pow(8))); Oracle::::set(Coin::Monero, Some(prices.monero)); Oracle::::set(Coin::Ether, Some(prices.ethereum)); Oracle::::set(Coin::Dai, Some(prices.dai)); @@ -402,11 +426,14 @@ pub mod pallet { fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { - Call::set_initial_price { ref prices, ref signature } => { - // TODO: if this is supposed to be called after a month, serai set won't still be - // in the session 0? Ideally this should pull the session from Vs pallet? - let set = ValidatorSet { network: NetworkId::Serai, session: Session(0) }; - let signers = ValidatorSets::::participants_for_latest_decided_set(NetworkId::Serai) + Call::oraclize_values { ref prices, 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) @@ -414,17 +441,17 @@ pub mod pallet { // check this didn't get called before if Self::oraclization_is_done() { - Err(InvalidTransaction::Custom(0))?; + Err(InvalidTransaction::Custom(1))?; } // make sure signers settings the price 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(1))?; + Err(InvalidTransaction::Custom(2))?; } - if !musig_key(set, &signers).verify(&set_initial_price_message(&set, prices), signature) { + if !musig_key(set, &signers).verify(&oraclize_values_message(&set, prices), signature) { Err(InvalidTransaction::BadProof)?; } diff --git a/substrate/genesis-liquidity/primitives/src/lib.rs b/substrate/genesis-liquidity/primitives/src/lib.rs index 7053e3f5..0468fdca 100644 --- a/substrate/genesis-liquidity/primitives/src/lib.rs +++ b/substrate/genesis-liquidity/primitives/src/lib.rs @@ -28,13 +28,27 @@ pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"Genesis-liq #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Prices { - pub bitcoin: u64, pub monero: u64, pub ethereum: u64, pub dai: u64, } -/// The message for the set_initial_price signature. -pub fn set_initial_price_message(set: &ValidatorSet, prices: &Prices) -> Vec { - (b"GenesisLiquidity-set_initial_price", set, prices).encode() +#[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, prices: &Prices) -> Vec { + (b"GenesisLiquidity-oraclize_values", set, prices).encode() } diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index f684cbaa..4eafd199 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -33,6 +33,7 @@ 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 } @@ -55,6 +56,7 @@ std = [ "serai-primitives/std", "in-instructions-primitives/std", + "genesis-liquidity-primitives/std", "coins-pallet/std", "dex-pallet/std", diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 007147d6..2667b13d 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}; @@ -205,6 +206,7 @@ pub mod pallet { } } InInstruction::GenesisLiquidity(address) => { + Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; GenesisLiq::::add_coin_liquidity(address.into(), instruction.balance)?; } } diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs index 812cb58d..aa5f1a82 100644 --- a/substrate/runtime/src/abi.rs +++ b/substrate/runtime/src/abi.rs @@ -93,8 +93,8 @@ impl From for RuntimeCall { serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => { RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance }) } - serai_abi::genesis_liquidity::Call::set_initial_price { prices, signature } => { - RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::set_initial_price { + serai_abi::genesis_liquidity::Call::oraclize_values { prices, signature } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values { prices, signature, }) @@ -276,8 +276,8 @@ impl TryInto for RuntimeCall { genesis_liquidity::Call::remove_coin_liquidity { balance } => { serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } } - genesis_liquidity::Call::set_initial_price { prices, signature } => { - serai_abi::genesis_liquidity::Call::set_initial_price { prices, signature } + genesis_liquidity::Call::oraclize_values { prices, signature } => { + serai_abi::genesis_liquidity::Call::oraclize_values { prices, signature } } _ => Err(())?, }),