Stub the genesis-liquidity pallet

This commit is contained in:
Luke Parker
2025-12-02 16:24:17 -05:00
parent af74c318aa
commit eb04f873d5
12 changed files with 180 additions and 479 deletions

9
Cargo.lock generated
View File

@@ -8386,7 +8386,6 @@ dependencies = [
"serai-core-pallet", "serai-core-pallet",
"sp-core", "sp-core",
"sp-io", "sp-io",
"substrate-median",
] ]
[[package]] [[package]]
@@ -8509,14 +8508,11 @@ dependencies = [
"frame-support", "frame-support",
"frame-system", "frame-system",
"parity-scale-codec", "parity-scale-codec",
"serai-abi",
"serai-coins-pallet", "serai-coins-pallet",
"serai-core-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-economic-security-pallet",
"serai-primitives",
"serai-validator-sets-pallet",
"sp-application-crypto",
"sp-core", "sp-core",
"sp-std",
] ]
[[package]] [[package]]
@@ -9028,6 +9024,7 @@ dependencies = [
"serai-coins-pallet", "serai-coins-pallet",
"serai-core-pallet", "serai-core-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-signals-pallet", "serai-signals-pallet",
"serai-validator-sets-pallet", "serai-validator-sets-pallet",
"sp-api", "sp-api",

View File

@@ -1,23 +1,38 @@
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{ use serai_primitives::{
crypto::Signature, address::SeraiAddress, balance::ExternalBalance, genesis::GenesisValues, crypto::Signature, address::SeraiAddress, coin::ExternalCoin, balance::ExternalBalance,
genesis_liquidity::GenesisValues,
}; };
/// The address used for to hold genesis liquidity for a pool.
pub fn address(coin: ExternalCoin) -> SeraiAddress {
SeraiAddress::system(borsh::to_vec(&(b"GenesisLiquidity", coin)).unwrap())
}
/// A call to the genesis liquidity. /// A call to the genesis liquidity.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call { pub enum Call {
/// Oraclize the value of non-Bitcoin external coins relative to Bitcoin. /// Oraclize the values of the coins available on genesis, relative to BTC.
///
/// This will trigger the addition of the liquidity into the pools and their initialization.
oraclize_values { oraclize_values {
/// The values of the non-Bitcoin external coins. /// The values of the non-Bitcoin external coins.
values: GenesisValues, values: GenesisValues,
/// The signature by the genesis validators for these values. /// The signature by the genesis validators for these values.
signature: Signature, signature: Signature,
}, },
/// Remove liquidity. /// Transfer genesis liquidity.
remove_liquidity { transfer_genesis_liquidity {
/// The account to transfer the liquidity to.
to: SeraiAddress,
/// The genesis liquidity to transfer.
genesis_liquidity: ExternalBalance,
},
/// Remove genesis liquidity.
remove_genesis_liquidity {
/// The genesis liquidity to remove. /// The genesis liquidity to remove.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
}, },
} }
@@ -25,7 +40,7 @@ impl Call {
pub(crate) fn is_signed(&self) -> bool { pub(crate) fn is_signed(&self) -> bool {
match self { match self {
Call::oraclize_values { .. } => false, Call::oraclize_values { .. } => false,
Call::remove_liquidity { .. } => true, Call::transfer_genesis_liquidity { .. } | Call::remove_genesis_liquidity { .. } => true,
} }
} }
} }
@@ -38,13 +53,22 @@ pub enum Event {
/// The recipient of the genesis liquidity. /// The recipient of the genesis liquidity.
recipient: SeraiAddress, recipient: SeraiAddress,
/// The coins added as genesis liquidity. /// The coins added as genesis liquidity.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
},
/// Genesis liquidity added.
GenesisLiquidityTransferred {
/// The address transferred from.
from: SeraiAddress,
/// The address transferred to.
to: SeraiAddress,
/// The genesis liquidity transferred.
genesis_liquidity: ExternalBalance,
}, },
/// Genesis liquidity removed. /// Genesis liquidity removed.
GenesisLiquidityRemoved { GenesisLiquidityRemoved {
/// The account which removed the genesis liquidity. /// The account which removed the genesis liquidity.
origin: SeraiAddress, from: SeraiAddress,
/// The amount of genesis liquidity removed. /// The amount of genesis liquidity removed.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
}, },
} }

View File

@@ -12,6 +12,9 @@ rust-version = "1.85"
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-machete]
ignored = ["scale"]
[lints] [lints]
workspace = true workspace = true
@@ -23,8 +26,6 @@ sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-fea
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
substrate-median = { path = "../median", default-features = false }
serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } serai-abi = { path = "../abi", default-features = false, features = ["substrate"] }
serai-core-pallet = { path = "../core", default-features = false } serai-core-pallet = { path = "../core", default-features = false }
serai-coins-pallet = { path = "../coins", default-features = false } serai-coins-pallet = { path = "../coins", default-features = false }
@@ -45,8 +46,6 @@ std = [
"frame-system/std", "frame-system/std",
"frame-support/std", "frame-support/std",
"substrate-median/std",
"serai-abi/std", "serai-abi/std",
"serai-core-pallet/std", "serai-core-pallet/std",
"serai-coins-pallet/std", "serai-coins-pallet/std",

View File

@@ -78,6 +78,10 @@ mod pallet {
} }
} }
/// The minimum amount of liquidity allowed to be initially added.
///
/// This should be sufficiently low it isn't inaccessible, yet sufficiently high that future
/// additions can be reasonably grained when their share of the new supply is calculated.
const MINIMUM_LIQUIDITY: u64 = 1 << 16; const MINIMUM_LIQUIDITY: u64 = 1 << 16;
#[pallet::call] #[pallet::call]

View File

@@ -1,10 +1,10 @@
[package] [package]
name = "serai-genesis-liquidity-pallet" name = "serai-genesis-liquidity-pallet"
version = "0.1.0" version = "0.1.0"
description = "Genesis liquidity pallet for Serai" description = "Genesis Liquidity pallet for Serai"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity"
authors = ["Akil Demir <akildemir72@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.85"
@@ -21,40 +21,48 @@ workspace = true
[dependencies] [dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
sp-std = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } serai-abi = { path = "../abi", default-features = false, features = ["substrate"] }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } serai-core-pallet = { path = "../core", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } serai-coins-pallet = { path = "../coins", default-features = false }
serai-dex-pallet = { path = "../dex", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../dex", default-features = false }
coins-pallet = { package = "serai-coins-pallet", path = "../coins", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets", default-features = false }
economic-security-pallet = { package = "serai-economic-security-pallet", path = "../economic-security", default-features = false }
serai-primitives = { path = "../primitives", default-features = false }
[features] [features]
std = [ std = [
"scale/std", "scale/std",
"sp-core/std",
"frame-system/std", "frame-system/std",
"frame-support/std", "frame-support/std",
"sp-std/std", "serai-abi/std",
"sp-core/std", "serai-core-pallet/std",
"sp-application-crypto/std", "serai-coins-pallet/std",
"serai-dex-pallet/std",
"coins-pallet/std", ]
"dex-pallet/std",
"validator-sets-pallet/std", try-runtime = [
"frame-system/try-runtime",
"economic-security-pallet/std", "frame-support/try-runtime",
"serai-primitives/std", "serai-abi/try-runtime",
"serai-core-pallet/try-runtime",
"serai-coins-pallet/try-runtime",
"serai-dex-pallet/try-runtime",
]
runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"serai-core-pallet/runtime-benchmarks",
"serai-coins-pallet/runtime-benchmarks",
"serai-dex-pallet/runtime-benchmarks",
] ]
try-runtime = [] # TODO
default = ["std"] default = ["std"]

View File

@@ -1,6 +1,6 @@
AGPL-3.0-only license AGPL-3.0-only license
Copyright (c) 2024-2025 Luke Parker Copyright (c) 2023-2025 Luke Parker
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License Version 3 as

View File

@@ -0,0 +1,3 @@
# Genesis Liquidity Pallet
Pallet implementing the Serai protocol's genesis liquidity.

View File

@@ -1,464 +1,102 @@
#![cfg_attr(not(feature = "std"), no_std)] #![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(any(feature = "std", test)), no_std)]
#[allow( extern crate alloc;
unreachable_patterns,
clippy::cast_possible_truncation, #[expect(clippy::cast_possible_truncation)]
clippy::no_effect_underscore_binding,
clippy::empty_docs
)]
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { mod pallet {
use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*;
use serai_abi::{
primitives::{prelude::*, crypto::Signature, genesis_liquidity::GenesisValues},
genesis_liquidity::Event,
};
use serai_core_pallet::Pallet as Core;
type Coins<T> = serai_coins_pallet::Pallet<T, serai_coins_pallet::CoinsInstance>;
type LiquidityTokens<T> =
serai_coins_pallet::Pallet<T, serai_coins_pallet::LiquidityTokensInstance>;
use super::*; 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};
use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets};
use economic_security_pallet::{Config as EconomicSecurityConfig, Pallet as EconomicSecurity};
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<T> = coins_pallet::Pallet<T, coins_pallet::Instance1>;
/// The configuration of this pallet.
#[pallet::config] #[pallet::config]
pub trait Config: pub trait Config:
frame_system::Config frame_system::Config
+ VsConfig + serai_core_pallet::Config
+ DexConfig + serai_coins_pallet::Config<serai_coins_pallet::CoinsInstance>
+ EconomicSecurityConfig + serai_coins_pallet::Config<serai_coins_pallet::LiquidityTokensInstance>
+ CoinsConfig + serai_dex_pallet::Config
+ coins_pallet::Config<coins_pallet::Instance1>
{ {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
} }
/// An error incurred.
#[pallet::error] #[pallet::error]
pub enum Error<T> { pub enum Error<T> {}
GenesisPeriodEnded,
AmountOverflowed,
NotEnoughLiquidity,
CanOnlyRemoveFullAmount,
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
GenesisLiquidityAdded { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityAddedToPool { coin: ExternalBalance, sri: Amount },
}
/// The Pallet struct.
#[pallet::pallet] #[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>); pub struct Pallet<T>(_);
/// Keeps shares and the amount of coins per account.
#[pallet::storage]
#[pallet::getter(fn liquidity)]
pub(crate) type Liquidity<T: Config> = StorageDoubleMap<
_,
Identity,
ExternalCoin,
Blake2_128Concat,
PublicKey,
LiquidityAmount,
OptionQuery,
>;
/// Keeps the total shares and the total amount of coins per coin.
#[pallet::storage]
#[pallet::getter(fn supply)]
pub(crate) type Supply<T: Config> =
StorageMap<_, Identity, ExternalCoin, LiquidityAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, ExternalCoin, u64, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn genesis_complete_block)]
pub(crate) type GenesisCompleteBlock<T: Config> = StorageValue<_, u64, OptionQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> 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::<u64>() >= final_block) &&
Self::oraclization_is_done() &&
GenesisCompleteBlock::<T>::get().is_none()
{
// mint the SRI
Coins::<T>::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 EXTERNAL_COINS {
// initial coin value in terms of btc
let Some(value) = Oracle::<T>::get(coin) else {
continue;
};
let pool_amount =
u128::from(Supply::<T>::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::<T>::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 {
coin: ExternalBalance { 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::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0));
}
GenesisCompleteBlock::<T>::set(Some(n.saturated_into::<u64>()));
}
Weight::zero() // TODO
}
}
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Add genesis liquidity for the given account. All accounts that provide liquidity fn emit_event(event: Event) {
/// will receive the genesis SRI according to their liquidity ratio. Core::<T>::emit_event(event)
pub fn add_coin_liquidity(account: PublicKey, balance: ExternalBalance) -> DispatchResult {
// check we are still in genesis period
if Self::genesis_ended() {
Err(Error::<T>::GenesisPeriodEnded)?;
}
// calculate new shares & supply
let (new_liquidity, new_supply) = if let Some(supply) = Supply::<T>::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::<T>::get(balance.coin, account).unwrap_or(LiquidityAmount::zero());
(
LiquidityAmount {
shares: existing.shares.checked_add(shares).ok_or(Error::<T>::AmountOverflowed)?,
coins: existing
.coins
.checked_add(balance.amount.0)
.ok_or(Error::<T>::AmountOverflowed)?,
},
LiquidityAmount {
shares: supply.shares.checked_add(shares).ok_or(Error::<T>::AmountOverflowed)?,
coins: supply
.coins
.checked_add(balance.amount.0)
.ok_or(Error::<T>::AmountOverflowed)?,
},
)
} else {
let first_amount =
LiquidityAmount { shares: INITIAL_GENESIS_LP_SHARES, coins: balance.amount.0 };
(first_amount, first_amount)
};
// save
Liquidity::<T>::set(balance.coin, account, Some(new_liquidity));
Supply::<T>::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<u64> {
let mut min = u64::MAX;
for n in EXTERNAL_NETWORKS {
let ec_security_block =
EconomicSecurity::<T>::economic_security_block(n)?.saturated_into::<u64>();
let current = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
let diff = current.saturating_sub(ec_security_block);
min = diff.min(min);
}
Some(min)
}
fn genesis_ended() -> bool {
Self::oraclization_is_done() &&
<frame_system::Pallet<T>>::block_number().saturated_into::<u64>() >= MONTHS
}
fn oraclization_is_done() -> bool {
for c in EXTERNAL_COINS {
if Oracle::<T>::get(c).is_none() {
return false;
} }
} }
true /// The minimum amount of liquidity allowed to be initially added.
} ///
/// This should be sufficiently low it isn't inaccessible, yet sufficiently high that future
/// additions can be reasonably grained when their share of the new supply is calculated.
///
/// This constant is duplicated with `serai-dex-pallet` intentionally as while they have the same
/// value, they are distinct constants and don't require being equivalent.
const MINIMUM_LIQUIDITY: u64 = 1 << 16;
fn mul_div(a: u64, b: u64, c: u64) -> Result<u64, Error<T>> { impl<T: Config> Pallet<T> {
let a = u128::from(a); /// Add liquidity on behalf of the specified address.
let b = u128::from(b); pub fn add_liquidity(to: SeraiAddress, balance: ExternalBalance) -> Result<(), Error<T>> {
let c = u128::from(c); todo!("TODO")
let result = a
.checked_mul(b)
.ok_or(Error::<T>::AmountOverflowed)?
.checked_div(c)
.ok_or(Error::<T>::AmountOverflowed)?;
result.try_into().map_err(|_| Error::<T>::AmountOverflowed)
} }
} }
#[pallet::call] #[pallet::call]
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Remove the provided genesis liquidity for an account. /// Oraclize the values of the coins available on genesis, relative to BTC.
///
/// This will trigger the addition of the liquidity into the pools and their initialization.
#[pallet::call_index(0)] #[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO #[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn remove_coin_liquidity(origin: OriginFor<T>, balance: ExternalBalance) -> DispatchResult {
let account = ensure_signed(origin)?;
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
let supply = Supply::<T>::get(balance.coin).ok_or(Error::<T>::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::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai).0;
// get how much user wants to remove
let LiquidityAmount { shares, coins } =
Liquidity::<T>::get(balance.coin, account).unwrap_or(LiquidityAmount::zero());
let total_shares = Supply::<T>::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::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let prev_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin.into());
Dex::<T>::remove_liquidity(
origin.clone().into(),
balance.coin,
amount_to_remove,
1,
1,
GENESIS_LIQUIDITY_ACCOUNT.into(),
)?;
let current_sri = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let current_coin =
Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin.into());
// 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::<T>::AmountOverflowed)?
.checked_div(u128::from(GENESIS_SRI_TRICKLE_FEED))
.ok_or(Error::<T>::AmountOverflowed)?,
)
.map_err(|_| Error::<T>::AmountOverflowed)?;
Coins::<T>::burn(
origin.clone().into(),
Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) },
)?;
sri = sri.checked_sub(burn_sri_amount).ok_or(Error::<T>::AmountOverflowed)?;
// transfer to owner
let coin_out = current_coin.0.saturating_sub(prev_coin.0);
Coins::<T>::transfer(
origin.clone().into(),
account,
Balance { coin: balance.coin.into(), amount: Amount(coin_out) },
)?;
Coins::<T>::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::<T>::AmountOverflowed)?,
coins: coins.checked_sub(coin_out).ok_or(Error::<T>::AmountOverflowed)?,
},
LiquidityAmount {
shares: supply
.shares
.checked_sub(amount_to_remove)
.ok_or(Error::<T>::AmountOverflowed)?,
coins: supply.coins.checked_sub(coin_out).ok_or(Error::<T>::AmountOverflowed)?,
},
)
} else {
if balance.amount.0 != INITIAL_GENESIS_LP_SHARES {
Err(Error::<T>::CanOnlyRemoveFullAmount)?;
}
let existing =
Liquidity::<T>::get(balance.coin, account).ok_or(Error::<T>::NotEnoughLiquidity)?;
// transfer to the user
Coins::<T>::transfer(
origin.into(),
account,
Balance { coin: balance.coin.into(), amount: Amount(existing.coins) },
)?;
(
LiquidityAmount::zero(),
LiquidityAmount {
shares: supply
.shares
.checked_sub(existing.shares)
.ok_or(Error::<T>::AmountOverflowed)?,
coins: supply.coins.checked_sub(existing.coins).ok_or(Error::<T>::AmountOverflowed)?,
},
)
};
// save
if new_liquidity == LiquidityAmount::zero() {
Liquidity::<T>::set(balance.coin, account, None);
} else {
Liquidity::<T>::set(balance.coin, account, Some(new_liquidity));
}
Supply::<T>::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( pub fn oraclize_values(
origin: OriginFor<T>, origin: OriginFor<T>,
values: Values, values: GenesisValues,
_signature: Signature, signature: Signature,
) -> DispatchResult { ) -> DispatchResult {
ensure_none(origin)?; todo!("TODO")
// set their relative values
Oracle::<T>::set(ExternalCoin::Bitcoin, Some(10u64.pow(ExternalCoin::Bitcoin.decimals())));
Oracle::<T>::set(ExternalCoin::Monero, Some(values.monero));
Oracle::<T>::set(ExternalCoin::Ether, Some(values.ether));
Oracle::<T>::set(ExternalCoin::Dai, Some(values.dai));
Ok(())
}
} }
#[pallet::validate_unsigned] /// Transfer genesis liquidity.
impl<T: Config> ValidateUnsigned for Pallet<T> { #[pallet::call_index(1)]
type Call = Call<T>; #[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn transfer_genesis_liquidity(
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { origin: OriginFor<T>,
match call { to: SeraiAddress,
Call::oraclize_values { ref values, ref signature } => { genesis_liquidity: ExternalBalance,
let network = NetworkId::Serai; ) -> DispatchResult {
let Some(session) = ValidatorSets::<T>::session(network) else { todo!("TODO")
return Err(TransactionValidityError::from(InvalidTransaction::Custom(0)));
};
let set = ValidatorSet { network, session };
let signers = ValidatorSets::<T>::participants_for_latest_decided_set(network)
.expect("no participant in the current set")
.into_iter()
.map(|(p, _)| p)
.collect::<Vec<_>>();
// 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. /// Remove genesis liquidity.
// we don't need this check for tests. #[pallet::call_index(2)]
#[cfg(not(feature = "fast-epoch"))] #[pallet::weight((0, DispatchClass::Normal))] // TODO
if <frame_system::Pallet<T>>::block_number().saturated_into::<u64>() < MONTHS { pub fn remove_genesis_liquidity(
Err(InvalidTransaction::Custom(2))?; origin: OriginFor<T>,
} genesis_liquidity: ExternalBalance,
) -> DispatchResult {
if !musig_key(set, &signers).verify(&oraclize_values_message(&set, values), signature) { todo!("TODO")
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!(),
}
} }
} }
} }

View File

@@ -7,6 +7,10 @@ use crate::balance::Amount;
/// The value of non-Bitcoin externals coins present at genesis, relative to Bitcoin. /// The value of non-Bitcoin externals coins present at genesis, relative to Bitcoin.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
#[cfg_attr(
feature = "non_canonical_scale_derivations",
derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking)
)]
pub struct GenesisValues { pub struct GenesisValues {
/// The value of Ether, relative to Bitcoin. /// The value of Ether, relative to Bitcoin.
pub ether: Amount, pub ether: Amount,

View File

@@ -29,8 +29,8 @@ pub mod coin;
/// The `Amount`, `ExternalBalance`, and `Balance` types. /// The `Amount`, `ExternalBalance`, and `Balance` types.
pub mod balance; pub mod balance;
/// Types for genesis. /// Types for the genesis liquidity functionality.
pub mod genesis; pub mod genesis_liquidity;
/// Types for identifying networks and their properties. /// Types for identifying networks and their properties.
pub mod network_id; pub mod network_id;

View File

@@ -54,6 +54,7 @@ serai-coins-pallet = { path = "../coins", default-features = false }
serai-validator-sets-pallet = { path = "../validator-sets", default-features = false } serai-validator-sets-pallet = { path = "../validator-sets", default-features = false }
serai-signals-pallet = { path = "../signals", default-features = false } serai-signals-pallet = { path = "../signals", default-features = false }
serai-dex-pallet = { path = "../dex", default-features = false } serai-dex-pallet = { path = "../dex", default-features = false }
serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false }
[build-dependencies] [build-dependencies]
substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk" }
@@ -90,6 +91,7 @@ std = [
"serai-validator-sets-pallet/std", "serai-validator-sets-pallet/std",
"serai-signals-pallet/std", "serai-signals-pallet/std",
"serai-dex-pallet/std", "serai-dex-pallet/std",
"serai-genesis-liquidity-pallet/std",
] ]
try-runtime = [ try-runtime = [
@@ -110,6 +112,7 @@ try-runtime = [
"serai-validator-sets-pallet/try-runtime", "serai-validator-sets-pallet/try-runtime",
"serai-signals-pallet/try-runtime", "serai-signals-pallet/try-runtime",
"serai-dex-pallet/try-runtime", "serai-dex-pallet/try-runtime",
"serai-genesis-liquidity-pallet/try-runtime",
] ]
runtime-benchmarks = [ runtime-benchmarks = [
@@ -127,6 +130,7 @@ runtime-benchmarks = [
"serai-validator-sets-pallet/runtime-benchmarks", "serai-validator-sets-pallet/runtime-benchmarks",
"serai-signals-pallet/runtime-benchmarks", "serai-signals-pallet/runtime-benchmarks",
"serai-dex-pallet/runtime-benchmarks", "serai-dex-pallet/runtime-benchmarks",
"serai-genesis-liquidity-pallet/runtime-benchmarks",
] ]
default = ["std"] default = ["std"]

View File

@@ -96,6 +96,9 @@ mod runtime {
#[runtime::pallet_index(6)] #[runtime::pallet_index(6)]
pub type Dex = serai_dex_pallet::Pallet<Runtime>; pub type Dex = serai_dex_pallet::Pallet<Runtime>;
#[runtime::pallet_index(7)]
pub type GenesisLiquidity = serai_genesis_liquidity_pallet::Pallet<Runtime>;
#[runtime::pallet_index(0xfd)] #[runtime::pallet_index(0xfd)]
#[runtime::disable_inherent] #[runtime::disable_inherent]
pub type Timestamp = pallet_timestamp::Pallet<Runtime>; pub type Timestamp = pallet_timestamp::Pallet<Runtime>;
@@ -174,6 +177,7 @@ impl serai_coins_pallet::Config<LiquidityTokensInstance> for Runtime {
type AllowMint = serai_coins_pallet::AlwaysAllowMint; type AllowMint = serai_coins_pallet::AlwaysAllowMint;
} }
impl serai_dex_pallet::Config for Runtime {} impl serai_dex_pallet::Config for Runtime {}
impl serai_genesis_liquidity_pallet::Config for Runtime {}
impl pallet_timestamp::Config for Runtime { impl pallet_timestamp::Config for Runtime {
type Moment = u64; type Moment = u64;
@@ -329,7 +333,23 @@ impl From<serai_abi::Call> for RuntimeCall {
serai_abi::Call::GenesisLiquidity(call) => { serai_abi::Call::GenesisLiquidity(call) => {
use serai_abi::genesis_liquidity::Call; use serai_abi::genesis_liquidity::Call;
match call { match call {
Call::oraclize_values { .. } | Call::remove_liquidity { .. } => todo!("TODO"), Call::oraclize_values { values, signature } => {
RuntimeCall::GenesisLiquidity(serai_genesis_liquidity_pallet::Call::oraclize_values {
values,
signature,
})
}
Call::transfer_genesis_liquidity { to, genesis_liquidity } => {
RuntimeCall::GenesisLiquidity(
serai_genesis_liquidity_pallet::Call::transfer_genesis_liquidity {
to,
genesis_liquidity,
},
)
}
Call::remove_genesis_liquidity { genesis_liquidity } => RuntimeCall::GenesisLiquidity(
serai_genesis_liquidity_pallet::Call::remove_genesis_liquidity { genesis_liquidity },
),
} }
} }
serai_abi::Call::InInstructions(call) => { serai_abi::Call::InInstructions(call) => {