add genesis liquidity implementation

This commit is contained in:
akildemir
2024-03-27 14:46:15 +03:00
parent 63521f6a96
commit c1bcb0f6c7
12 changed files with 453 additions and 1 deletions

View File

@@ -0,0 +1,46 @@
[package]
name = "serai-genesis-liquidity-pallet"
version = "0.1.0"
description = "Genesis liquidity pallet for Serai"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity/pallet"
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
rust-version = "1.77"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-machete]
ignored = ["scale", "scale-info"]
[lints]
workspace = true
[dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../primitives", default-features = false }
[features]
std = [
"frame-system/std",
"frame-support/std",
"coins-pallet/std",
"dex-pallet/std",
]
default = ["std"]

View File

@@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2024 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,271 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_system::{pallet_prelude::*, RawOrigin};
use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion};
use sp_std::{vec, collections::btree_map::BTreeMap};
use dex_pallet::{Pallet as Dex, Config as DexConfig};
use coins_pallet::{
primitives::{OutInstructionWithBalance, OutInstruction},
Config as CoinsConfig, Pallet as Coins, AllowMint,
};
use serai_primitives::*;
pub use genesis_liquidity_primitives as primitives;
use primitives::*;
/// LiquidityTokens Pallet as an instance of coins pallet.
pub type LiquidityTokens<T> = coins_pallet::Pallet<T, coins_pallet::Instance1>;
#[pallet::config]
pub trait Config:
frame_system::Config + DexConfig + CoinsConfig + coins_pallet::Config<coins_pallet::Instance1>
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[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: Balance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance },
GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance },
EconomicSecurityReached { network: NetworkId },
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::storage]
pub(crate) type Liquidity<T: Config> =
StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type LiquidityTokensPerAddress<T: Config> =
StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type EconomicSecurityReached<T: Config> =
StorageMap<_, Identity, NetworkId, BlockNumberFor<T>, ValueQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_finalize(n: BlockNumberFor<T>) {
// Distribute the genesis sri to pools after a month
if n > BLOCKS_PER_MONTH.into() {
// mint the SRI
Coins::<T>::mint(
GENESIS_LIQUIDITY_ACCOUNT.into(),
Balance { coin: Coin::Serai, amount: Amount(GENESIS_SRI) },
)
.unwrap();
// get coin values & total
let mut account_values = BTreeMap::new();
let mut pool_values = BTreeMap::new();
let mut total_value: u64 = 0;
for coin in COINS {
// TODO: following line is just a place holder till we get the actual coin value
// in terms of btc.
let value = Dex::<T>::security_oracle_value(coin).unwrap_or(Amount(0)).0;
account_values.insert(coin, vec![]);
let mut pool_amount: u64 = 0;
for (account, amount) in Liquidity::<T>::iter_prefix(coin) {
pool_amount += amount;
let value_this_addr = amount * value;
account_values.get_mut(&coin).unwrap().push((account, value_this_addr))
}
let pool_value = pool_amount * value;
total_value += pool_value;
pool_values.insert(coin, (pool_amount, pool_value));
}
// add the liquidity per pool
for (coin, (amount, value)) in &pool_values {
let sri_amount = (GENESIS_SRI * value) / total_value;
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
Dex::<T>::add_liquidity(
origin.into(),
*coin,
*amount,
sri_amount,
*amount,
sri_amount,
GENESIS_LIQUIDITY_ACCOUNT.into(),
)
.unwrap();
// set liquidity tokens per account
let tokens = LiquidityTokens::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), *coin).0;
let mut total_tokens_this_coin: u64 = 0;
for (acc, value) in account_values.get(coin).unwrap() {
let liq_tokens_this_acc = (tokens * value) / pool_values.get(coin).unwrap().1;
total_tokens_this_coin += liq_tokens_this_acc;
LiquidityTokensPerAddress::<T>::set(coin, acc, Some(liq_tokens_this_acc));
}
assert_eq!(tokens, total_tokens_this_coin);
}
// we shouldn't have any coin left in our account at this moment, including SRI.
for coin in COINS {
assert_eq!(Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0));
}
}
// we accept we reached economic security once we can mint smallest amount of a network's coin
for coin in COINS {
let existing = EconomicSecurityReached::<T>::get(coin.network());
if existing == 0u32.into() &&
<T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
{
EconomicSecurityReached::<T>::set(coin.network(), n);
Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() });
}
}
}
}
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: Balance) -> DispatchResult {
// check we are still in genesis period
if Self::genesis_ended() {
Err(Error::<T>::GenesisPeriodEnded)?;
}
// mint the coins
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance)?;
// save
let existing = Liquidity::<T>::get(balance.coin, account).unwrap_or(0);
let new = existing.checked_add(balance.amount.0).ok_or(Error::<T>::AmountOverflowed)?;
Liquidity::<T>::set(balance.coin, account, Some(new));
Self::deposit_event(Event::GenesisLiquidityAdded { by: account.into(), balance });
Ok(())
}
/// Remove the provided genesis liquidity for an account. If called pre-economic security era,
pub fn remove_coin_liquidity(
account: PublicKey,
balance: Balance,
out_address: ExternalAddress,
) -> DispatchResult {
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
// check we are still in genesis period
if Self::genesis_ended() {
// check user have enough to remove
let existing = LiquidityTokensPerAddress::<T>::get(balance.coin, account).unwrap_or(0);
if balance.amount.0 > existing {
Err(Error::<T>::NotEnoughLiquidity)?;
}
// 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);
Dex::<T>::remove_liquidity(
origin.clone().into(),
balance.coin,
balance.amount.0,
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);
// burn the SRI if necessary
let mut sri = current_sri.0 - prev_sri.0;
let burn_sri_amount = (sri *
(GENESIS_SRI_TRICKLE_FEED - Self::blocks_since_ec_security(&balance.coin))) /
GENESIS_SRI_TRICKLE_FEED;
Coins::<T>::burn(
origin.clone().into(),
Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) },
)?;
sri -= burn_sri_amount;
// transfer to owner
let coin_out = current_coin.0 - prev_coin.0;
Coins::<T>::transfer(
origin.clone().into(),
account,
Balance { coin: balance.coin, amount: Amount(coin_out) },
)?;
Coins::<T>::transfer(
origin.into(),
account,
Balance { coin: Coin::Serai, amount: Amount(sri) },
)?;
// save
let existing = LiquidityTokensPerAddress::<T>::get(balance.coin, account).unwrap_or(0);
let new = existing.checked_sub(balance.amount.0).ok_or(Error::<T>::AmountOverflowed)?;
LiquidityTokensPerAddress::<T>::set(balance.coin, account, Some(new));
} else {
let existing = Liquidity::<T>::get(balance.coin, account).unwrap_or(0);
if balance.amount.0 > existing || balance.amount.0 == 0 {
Err(Error::<T>::NotEnoughLiquidity)?;
}
if balance.amount.0 < existing {
Err(Error::<T>::CanOnlyRemoveFullAmount)?;
}
// TODO: do internal transfer instead?
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
let instruction = OutInstructionWithBalance {
instruction: OutInstruction { address: out_address, data: None },
balance,
};
Coins::<T>::burn_with_instruction(origin.into(), instruction)?;
// save
Liquidity::<T>::set(balance.coin, account, None);
}
Self::deposit_event(Event::GenesisLiquidityRemoved { by: account.into(), balance });
Ok(())
}
// Returns the number of blocks since the coin's network reached economic security first time.
// If the network is yet to be reached that threshold 0 is returned, and maximum of
// GENESIS_SRI_TRICKLE_FEED returned.
fn blocks_since_ec_security(coin: &Coin) -> u64 {
let ec_security_block =
EconomicSecurityReached::<T>::get(coin.network()).saturated_into::<u64>();
let current = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
if ec_security_block > 0 {
let diff = current - ec_security_block;
if diff > GENESIS_SRI_TRICKLE_FEED {
return GENESIS_SRI_TRICKLE_FEED;
}
return diff;
}
0
}
fn genesis_ended() -> bool {
<frame_system::Pallet<T>>::block_number() >= BLOCKS_PER_MONTH.into()
}
}
}
pub use pallet::*;

View File

@@ -0,0 +1,25 @@
[package]
name = "serai-genesis-liquidity-primitives"
version = "0.1.0"
description = "Serai genesis liquidity primitives"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity/primitives"
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
rust-version = "1.77"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
serai-primitives = { path = "../../primitives", default-features = false }
[features]
std = [
"serai-primitives/std",
]
default = ["std"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,17 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
use serai_primitives::*;
// amount of blocks in 30 days for 6s per block.
pub const BLOCKS_PER_MONTH: u32 = 10 * 60 * 24 * 30;
/// 180 days of blocks
pub const GENESIS_SRI_TRICKLE_FEED: u64 = 10 * 60 * 24 * 180;
// 100 Million SRI
pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8);
// This is the account which will be the origin for any dispatched `InInstruction`s.
pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"Genesis-liquidity-account");

View File

@@ -37,6 +37,7 @@ in-instructions-primitives = { package = "serai-in-instructions-primitives", pat
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false }
[features]
std = [
@@ -58,5 +59,6 @@ std = [
"coins-pallet/std",
"dex-pallet/std",
"validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
]
default = ["std"]

View File

@@ -33,10 +33,14 @@ pub mod pallet {
Config as ValidatorSetsConfig, Pallet as ValidatorSets,
};
use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig};
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig {
pub trait Config:
frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig + GenesisLiqConfig
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
@@ -200,6 +204,14 @@ pub mod pallet {
}
}
}
InInstruction::GenesisLiquidity(ops) => match ops {
GenesisLiquidityOperation::Add(address, balance) => {
GenesisLiq::<T>::add_coin_liquidity(address.into(), balance)?;
}
GenesisLiquidityOperation::Remove(address, balance, out_address) => {
GenesisLiq::<T>::remove_coin_liquidity(address.into(), balance, out_address)?;
}
},
}
Ok(())
}

View File

@@ -71,6 +71,15 @@ pub enum DexCall {
Swap(Balance, OutAddress),
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Zeroize))]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GenesisLiquidityOperation {
Add(SeraiAddress, Balance),
Remove(SeraiAddress, Balance, ExternalAddress),
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Zeroize))]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
@@ -78,6 +87,7 @@ pub enum DexCall {
pub enum InInstruction {
Transfer(SeraiAddress),
Dex(DexCall),
GenesisLiquidity(GenesisLiquidityOperation),
}
#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)]

View File

@@ -59,6 +59,7 @@ coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", defau
dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false }
genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity/pallet", default-features = false }
in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false }
@@ -112,6 +113,7 @@ std = [
"dex-pallet/std",
"validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
"in-instructions-pallet/std",

View File

@@ -32,6 +32,8 @@ pub use signals_pallet as signals;
pub use pallet_babe as babe;
pub use pallet_grandpa as grandpa;
pub use genesis_liquidity_pallet as genesis_liquidity;
// Actually used by the runtime
use sp_core::OpaqueMetadata;
use sp_std::prelude::*;
@@ -288,6 +290,10 @@ impl in_instructions::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl genesis_liquidity::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
// for publishing equivocation evidences.
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where
@@ -362,6 +368,7 @@ construct_runtime!(
Coins: coins,
LiquidityTokens: coins::<Instance1>::{Pallet, Call, Storage, Event<T>},
Dex: dex,
GenesisLiquidity: genesis_liquidity,
ValidatorSets: validator_sets,