mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Stub the genesis-liquidity pallet
This commit is contained in:
@@ -1,23 +1,38 @@
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
|
||||
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.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
|
||||
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 {
|
||||
/// The values of the non-Bitcoin external coins.
|
||||
values: GenesisValues,
|
||||
/// The signature by the genesis validators for these values.
|
||||
signature: Signature,
|
||||
},
|
||||
/// Remove liquidity.
|
||||
remove_liquidity {
|
||||
/// Transfer genesis 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.
|
||||
balance: ExternalBalance,
|
||||
genesis_liquidity: ExternalBalance,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,7 +40,7 @@ impl Call {
|
||||
pub(crate) fn is_signed(&self) -> bool {
|
||||
match self {
|
||||
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.
|
||||
recipient: SeraiAddress,
|
||||
/// 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.
|
||||
GenesisLiquidityRemoved {
|
||||
/// The account which removed the genesis liquidity.
|
||||
origin: SeraiAddress,
|
||||
from: SeraiAddress,
|
||||
/// The amount of genesis liquidity removed.
|
||||
balance: ExternalBalance,
|
||||
genesis_liquidity: ExternalBalance,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ rust-version = "1.85"
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["scale"]
|
||||
|
||||
[lints]
|
||||
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-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-core-pallet = { path = "../core", default-features = false }
|
||||
serai-coins-pallet = { path = "../coins", default-features = false }
|
||||
@@ -45,8 +46,6 @@ std = [
|
||||
"frame-system/std",
|
||||
"frame-support/std",
|
||||
|
||||
"substrate-median/std",
|
||||
|
||||
"serai-abi/std",
|
||||
"serai-core-pallet/std",
|
||||
"serai-coins-pallet/std",
|
||||
|
||||
@@ -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;
|
||||
|
||||
#[pallet::call]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "serai-genesis-liquidity-pallet"
|
||||
version = "0.1.0"
|
||||
description = "Genesis liquidity pallet for Serai"
|
||||
description = "Genesis Liquidity pallet for Serai"
|
||||
license = "AGPL-3.0-only"
|
||||
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"
|
||||
rust-version = "1.85"
|
||||
|
||||
@@ -21,40 +21,48 @@ workspace = true
|
||||
[dependencies]
|
||||
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-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 }
|
||||
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
|
||||
sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", 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 }
|
||||
serai-abi = { path = "../abi", default-features = false, features = ["substrate"] }
|
||||
serai-core-pallet = { path = "../core", default-features = false }
|
||||
serai-coins-pallet = { path = "../coins", default-features = false }
|
||||
serai-dex-pallet = { path = "../dex", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"scale/std",
|
||||
|
||||
"sp-core/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",
|
||||
|
||||
"economic-security-pallet/std",
|
||||
|
||||
"serai-primitives/std",
|
||||
"serai-abi/std",
|
||||
"serai-core-pallet/std",
|
||||
"serai-coins-pallet/std",
|
||||
"serai-dex-pallet/std",
|
||||
]
|
||||
|
||||
try-runtime = [
|
||||
"frame-system/try-runtime",
|
||||
"frame-support/try-runtime",
|
||||
|
||||
"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"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
it under the terms of the GNU Affero General Public License Version 3 as
|
||||
|
||||
3
substrate/genesis-liquidity/README.md
Normal file
3
substrate/genesis-liquidity/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Genesis Liquidity Pallet
|
||||
|
||||
Pallet implementing the Serai protocol's genesis liquidity.
|
||||
@@ -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(
|
||||
unreachable_patterns,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::no_effect_underscore_binding,
|
||||
clippy::empty_docs
|
||||
)]
|
||||
extern crate alloc;
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[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 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]
|
||||
pub trait Config:
|
||||
frame_system::Config
|
||||
+ VsConfig
|
||||
+ DexConfig
|
||||
+ EconomicSecurityConfig
|
||||
+ CoinsConfig
|
||||
+ coins_pallet::Config<coins_pallet::Instance1>
|
||||
+ serai_core_pallet::Config
|
||||
+ serai_coins_pallet::Config<serai_coins_pallet::CoinsInstance>
|
||||
+ serai_coins_pallet::Config<serai_coins_pallet::LiquidityTokensInstance>
|
||||
+ serai_dex_pallet::Config
|
||||
{
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
}
|
||||
|
||||
/// An error incurred.
|
||||
#[pallet::error]
|
||||
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 },
|
||||
}
|
||||
pub enum Error<T> {}
|
||||
|
||||
/// The Pallet struct.
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(PhantomData<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
|
||||
}
|
||||
}
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// 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: 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(())
|
||||
fn emit_event(event: Event) {
|
||||
Core::<T>::emit_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
/// 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 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
|
||||
}
|
||||
|
||||
fn mul_div(a: u64, b: u64, c: u64) -> Result<u64, Error<T>> {
|
||||
let a = u128::from(a);
|
||||
let b = u128::from(b);
|
||||
let c = u128::from(c);
|
||||
|
||||
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)
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Add liquidity on behalf of the specified address.
|
||||
pub fn add_liquidity(to: SeraiAddress, balance: ExternalBalance) -> Result<(), Error<T>> {
|
||||
todo!("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
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::weight((0, DispatchClass::Operational))] // 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
|
||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||
pub fn oraclize_values(
|
||||
origin: OriginFor<T>,
|
||||
values: Values,
|
||||
_signature: Signature,
|
||||
values: GenesisValues,
|
||||
signature: Signature,
|
||||
) -> DispatchResult {
|
||||
ensure_none(origin)?;
|
||||
|
||||
// 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(())
|
||||
todo!("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::validate_unsigned]
|
||||
impl<T: Config> ValidateUnsigned for Pallet<T> {
|
||||
type Call = Call<T>;
|
||||
/// Transfer genesis liquidity.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||
pub fn transfer_genesis_liquidity(
|
||||
origin: OriginFor<T>,
|
||||
to: SeraiAddress,
|
||||
genesis_liquidity: ExternalBalance,
|
||||
) -> DispatchResult {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
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::<T>::session(network) else {
|
||||
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.
|
||||
// we don't need this check for tests.
|
||||
#[cfg(not(feature = "fast-epoch"))]
|
||||
if <frame_system::Pallet<T>>::block_number().saturated_into::<u64>() < 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!(),
|
||||
}
|
||||
/// Remove genesis liquidity.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||
pub fn remove_genesis_liquidity(
|
||||
origin: OriginFor<T>,
|
||||
genesis_liquidity: ExternalBalance,
|
||||
) -> DispatchResult {
|
||||
todo!("TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ use crate::balance::Amount;
|
||||
|
||||
/// The value of non-Bitcoin externals coins present at genesis, relative to Bitcoin.
|
||||
#[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 {
|
||||
/// The value of Ether, relative to Bitcoin.
|
||||
pub ether: Amount,
|
||||
@@ -29,8 +29,8 @@ pub mod coin;
|
||||
/// The `Amount`, `ExternalBalance`, and `Balance` types.
|
||||
pub mod balance;
|
||||
|
||||
/// Types for genesis.
|
||||
pub mod genesis;
|
||||
/// Types for the genesis liquidity functionality.
|
||||
pub mod genesis_liquidity;
|
||||
|
||||
/// Types for identifying networks and their properties.
|
||||
pub mod network_id;
|
||||
|
||||
@@ -54,6 +54,7 @@ serai-coins-pallet = { path = "../coins", default-features = false }
|
||||
serai-validator-sets-pallet = { path = "../validator-sets", default-features = false }
|
||||
serai-signals-pallet = { path = "../signals", default-features = false }
|
||||
serai-dex-pallet = { path = "../dex", default-features = false }
|
||||
serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk" }
|
||||
@@ -90,6 +91,7 @@ std = [
|
||||
"serai-validator-sets-pallet/std",
|
||||
"serai-signals-pallet/std",
|
||||
"serai-dex-pallet/std",
|
||||
"serai-genesis-liquidity-pallet/std",
|
||||
]
|
||||
|
||||
try-runtime = [
|
||||
@@ -110,6 +112,7 @@ try-runtime = [
|
||||
"serai-validator-sets-pallet/try-runtime",
|
||||
"serai-signals-pallet/try-runtime",
|
||||
"serai-dex-pallet/try-runtime",
|
||||
"serai-genesis-liquidity-pallet/try-runtime",
|
||||
]
|
||||
|
||||
runtime-benchmarks = [
|
||||
@@ -127,6 +130,7 @@ runtime-benchmarks = [
|
||||
"serai-validator-sets-pallet/runtime-benchmarks",
|
||||
"serai-signals-pallet/runtime-benchmarks",
|
||||
"serai-dex-pallet/runtime-benchmarks",
|
||||
"serai-genesis-liquidity-pallet/runtime-benchmarks",
|
||||
]
|
||||
|
||||
default = ["std"]
|
||||
|
||||
@@ -96,6 +96,9 @@ mod runtime {
|
||||
#[runtime::pallet_index(6)]
|
||||
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::disable_inherent]
|
||||
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;
|
||||
}
|
||||
impl serai_dex_pallet::Config for Runtime {}
|
||||
impl serai_genesis_liquidity_pallet::Config for Runtime {}
|
||||
|
||||
impl pallet_timestamp::Config for Runtime {
|
||||
type Moment = u64;
|
||||
@@ -329,7 +333,23 @@ impl From<serai_abi::Call> for RuntimeCall {
|
||||
serai_abi::Call::GenesisLiquidity(call) => {
|
||||
use serai_abi::genesis_liquidity::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) => {
|
||||
|
||||
Reference in New Issue
Block a user