Implement genesis liquidity protocol (#545)
Some checks failed
Full Stack Tests / build (push) Waiting to run
Lint / fmt (push) Waiting to run
Lint / machete (push) Waiting to run
Lint / clippy (macos-13) (push) Waiting to run
Lint / clippy (macos-14) (push) Waiting to run
Lint / clippy (ubuntu-latest) (push) Waiting to run
Lint / clippy (windows-latest) (push) Waiting to run
Lint / deny (push) Waiting to run
Reproducible Runtime / build (push) Has been cancelled
Tests / test-infra (push) Has been cancelled
Tests / test-substrate (push) Has been cancelled
Tests / test-serai-client (push) Has been cancelled

* add genesis liquidity implementation

* add missing deposit event

* fix CI issues

* minor fixes

* make math safer

* fix fmt

* make remove liquidity an authorized call

* implement setting initial values for coins

* add genesis liquidity test & misc fixes

* updato develop latest

* fix rotation test

* Finish merging develop

* Remove accidentally committed ETH files

* fix pr comments

* further bug fixes

* fix last pr comments

* tidy up

* Misc

---------

Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
akildemir 2024-07-19 02:30:19 +03:00 committed by GitHub
parent 2ccb0cd90d
commit 1493f49416
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1287 additions and 43 deletions

37
Cargo.lock generated
View file

@ -7929,6 +7929,7 @@ dependencies = [
"parity-scale-codec", "parity-scale-codec",
"scale-info", "scale-info",
"serai-coins-primitives", "serai-coins-primitives",
"serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives", "serai-in-instructions-primitives",
"serai-primitives", "serai-primitives",
"serai-signals-primitives", "serai-signals-primitives",
@ -8127,6 +8128,39 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "serai-genesis-liquidity-pallet"
version = "0.1.0"
dependencies = [
"frame-support",
"frame-system",
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-genesis-liquidity-primitives",
"serai-primitives",
"serai-validator-sets-pallet",
"serai-validator-sets-primitives",
"sp-application-crypto",
"sp-core",
"sp-std",
]
[[package]]
name = "serai-genesis-liquidity-primitives"
version = "0.1.0"
dependencies = [
"borsh",
"parity-scale-codec",
"scale-info",
"serai-primitives",
"serai-validator-sets-primitives",
"serde",
"sp-std",
"zeroize",
]
[[package]] [[package]]
name = "serai-in-instructions-pallet" name = "serai-in-instructions-pallet"
version = "0.1.0" version = "0.1.0"
@ -8137,6 +8171,8 @@ dependencies = [
"scale-info", "scale-info",
"serai-coins-pallet", "serai-coins-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives", "serai-in-instructions-primitives",
"serai-primitives", "serai-primitives",
"serai-validator-sets-pallet", "serai-validator-sets-pallet",
@ -8406,6 +8442,7 @@ dependencies = [
"serai-abi", "serai-abi",
"serai-coins-pallet", "serai-coins-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-in-instructions-pallet", "serai-in-instructions-pallet",
"serai-primitives", "serai-primitives",
"serai-signals-pallet", "serai-signals-pallet",

View file

@ -53,6 +53,8 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-coins-pallet" }, { allow = ["AGPL-3.0"], name = "serai-coins-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-dex-pallet" }, { allow = ["AGPL-3.0"], name = "serai-dex-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" },

View file

@ -33,6 +33,7 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur
serai-primitives = { path = "../primitives", version = "0.1", default-features = false } serai-primitives = { path = "../primitives", version = "0.1", default-features = false }
serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false } serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false }
serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false } serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false }
serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false }
serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false } serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false }
serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false } serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false }
@ -55,6 +56,7 @@ std = [
"serai-primitives/std", "serai-primitives/std",
"serai-coins-primitives/std", "serai-coins-primitives/std",
"serai-validator-sets-primitives/std", "serai-validator-sets-primitives/std",
"serai-genesis-liquidity-primitives/std",
"serai-in-instructions-primitives/std", "serai-in-instructions-primitives/std",
"serai-signals-primitives/std", "serai-signals-primitives/std",
] ]
@ -63,6 +65,7 @@ borsh = [
"serai-primitives/borsh", "serai-primitives/borsh",
"serai-coins-primitives/borsh", "serai-coins-primitives/borsh",
"serai-validator-sets-primitives/borsh", "serai-validator-sets-primitives/borsh",
"serai-genesis-liquidity-primitives/borsh",
"serai-in-instructions-primitives/borsh", "serai-in-instructions-primitives/borsh",
"serai-signals-primitives/borsh", "serai-signals-primitives/borsh",
] ]
@ -71,6 +74,7 @@ serde = [
"serai-primitives/serde", "serai-primitives/serde",
"serai-coins-primitives/serde", "serai-coins-primitives/serde",
"serai-validator-sets-primitives/serde", "serai-validator-sets-primitives/serde",
"serai-genesis-liquidity-primitives/serde",
"serai-in-instructions-primitives/serde", "serai-in-instructions-primitives/serde",
"serai-signals-primitives/serde", "serai-signals-primitives/serde",
] ]

View file

@ -13,15 +13,6 @@ pub enum Call {
burn_with_instruction { instruction: OutInstructionWithBalance }, burn_with_instruction { instruction: OutInstructionWithBalance },
} }
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))]
pub enum LiquidityTokensCall {
transfer { to: SeraiAddress, balance: Balance },
burn { balance: Balance },
}
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize))]

View file

@ -0,0 +1,21 @@
pub use serai_genesis_liquidity_primitives as primitives;
use serai_primitives::*;
use primitives::*;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Call {
remove_coin_liquidity { balance: Balance },
oraclize_values { values: Values, signature: Signature },
}
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Event {
GenesisLiquidityAdded { by: SeraiAddress, balance: Balance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance },
GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance },
EconomicSecurityReached { network: NetworkId },
}

View file

@ -12,12 +12,15 @@ pub mod system;
pub mod timestamp; pub mod timestamp;
pub mod coins; pub mod coins;
pub mod liquidity_tokens;
pub mod dex; pub mod dex;
pub mod validator_sets; pub mod validator_sets;
pub mod in_instructions; pub mod in_instructions;
pub mod signals; pub mod signals;
pub mod genesis_liquidity;
pub mod babe; pub mod babe;
pub mod grandpa; pub mod grandpa;
@ -27,8 +30,9 @@ pub mod tx;
pub enum Call { pub enum Call {
Timestamp(timestamp::Call), Timestamp(timestamp::Call),
Coins(coins::Call), Coins(coins::Call),
LiquidityTokens(coins::LiquidityTokensCall), LiquidityTokens(liquidity_tokens::Call),
Dex(dex::Call), Dex(dex::Call),
GenesisLiquidity(genesis_liquidity::Call),
ValidatorSets(validator_sets::Call), ValidatorSets(validator_sets::Call),
InInstructions(in_instructions::Call), InInstructions(in_instructions::Call),
Signals(signals::Call), Signals(signals::Call),
@ -48,8 +52,9 @@ pub enum Event {
Timestamp, Timestamp,
TransactionPayment(TransactionPaymentEvent), TransactionPayment(TransactionPaymentEvent),
Coins(coins::Event), Coins(coins::Event),
LiquidityTokens(coins::Event), LiquidityTokens(liquidity_tokens::Event),
Dex(dex::Event), Dex(dex::Event),
GenesisLiquidity(genesis_liquidity::Event),
ValidatorSets(validator_sets::Event), ValidatorSets(validator_sets::Event),
InInstructions(in_instructions::Event), InInstructions(in_instructions::Event),
Signals(signals::Event), Signals(signals::Event),

View file

@ -0,0 +1,18 @@
use serai_primitives::{Balance, SeraiAddress};
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Call {
burn { balance: Balance },
transfer { to: SeraiAddress, balance: Balance },
}
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Event {
Mint { to: SeraiAddress, balance: Balance },
Burn { from: SeraiAddress, balance: Balance },
Transfer { from: SeraiAddress, to: SeraiAddress, balance: Balance },
}

View file

@ -1,7 +1,9 @@
use sp_core::bounded_vec::BoundedVec; use sp_core::bounded_vec::BoundedVec;
use serai_abi::primitives::{SeraiAddress, Amount, Coin}; use serai_abi::primitives::{SeraiAddress, Amount, Coin};
use crate::{SeraiError, TemporalSerai}; use scale::{decode_from_bytes, Encode};
use crate::{Serai, SeraiError, TemporalSerai};
pub type DexEvent = serai_abi::dex::Event; pub type DexEvent = serai_abi::dex::Event;
@ -57,4 +59,20 @@ impl<'a> SeraiDex<'a> {
send_to: address, send_to: address,
}) })
} }
/// Returns the reserves of `coin:SRI` pool.
pub async fn get_reserves(&self, coin: Coin) -> Result<Option<(Amount, Amount)>, SeraiError> {
let reserves = self
.0
.serai
.call(
"state_call",
["DexApi_get_reserves".to_string(), hex::encode((coin, Coin::Serai).encode())],
)
.await?;
let bytes = Serai::hex_decode(reserves)?;
let result = decode_from_bytes::<Option<(u64, u64)>>(bytes.into())
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(result.map(|amounts| (Amount(amounts.0), Amount(amounts.1))))
}
} }

View file

@ -0,0 +1,65 @@
pub use serai_abi::genesis_liquidity::primitives;
use primitives::{Values, LiquidityAmount};
use serai_abi::primitives::*;
use sp_core::sr25519::Signature;
use scale::Encode;
use crate::{Serai, SeraiError, TemporalSerai, Transaction};
pub type GenesisLiquidityEvent = serai_abi::genesis_liquidity::Event;
const PALLET: &str = "GenesisLiquidity";
#[derive(Clone, Copy)]
pub struct SeraiGenesisLiquidity<'a>(pub(crate) &'a TemporalSerai<'a>);
impl<'a> SeraiGenesisLiquidity<'a> {
pub async fn events(&self) -> Result<Vec<GenesisLiquidityEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::GenesisLiquidity(event) = event {
Some(event.clone())
} else {
None
}
})
.await
}
pub fn oraclize_values(values: Values, signature: Signature) -> Transaction {
Serai::unsigned(serai_abi::Call::GenesisLiquidity(
serai_abi::genesis_liquidity::Call::oraclize_values { values, signature },
))
}
pub fn remove_coin_liquidity(balance: Balance) -> serai_abi::Call {
serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity {
balance,
})
}
pub async fn liquidity(
&self,
address: &SeraiAddress,
coin: Coin,
) -> Result<LiquidityAmount, SeraiError> {
Ok(
self
.0
.storage(
PALLET,
"Liquidity",
(coin, sp_core::hashing::blake2_128(&address.encode()), &address.0),
)
.await?
.unwrap_or(LiquidityAmount::zero()),
)
}
pub async fn supply(&self, coin: Coin) -> Result<LiquidityAmount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero()))
}
}

View file

@ -0,0 +1,41 @@
use scale::Encode;
use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance};
use crate::{TemporalSerai, SeraiError};
const PALLET: &str = "LiquidityTokens";
#[derive(Clone, Copy)]
pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>);
impl<'a> SeraiLiquidityTokens<'a> {
pub async fn token_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
}
pub async fn token_balance(
&self,
coin: Coin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(
self
.0
.storage(
PALLET,
"Balances",
(sp_core::hashing::blake2_128(&address.encode()), &address.0, coin),
)
.await?
.unwrap_or(Amount(0)),
)
}
pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer { to, balance })
}
pub fn burn(balance: Balance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn { balance })
}
}

View file

@ -26,6 +26,10 @@ pub mod in_instructions;
pub use in_instructions::SeraiInInstructions; pub use in_instructions::SeraiInInstructions;
pub mod validator_sets; pub mod validator_sets;
pub use validator_sets::SeraiValidatorSets; pub use validator_sets::SeraiValidatorSets;
pub mod genesis_liquidity;
pub use genesis_liquidity::SeraiGenesisLiquidity;
pub mod liquidity_tokens;
pub use liquidity_tokens::SeraiLiquidityTokens;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)] #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)]
pub struct Block { pub struct Block {
@ -194,6 +198,7 @@ impl Serai {
Ok(()) Ok(())
} }
// TODO: move this into substrate/client/src/validator_sets.rs
async fn active_network_validators(&self, network: NetworkId) -> Result<Vec<Public>, SeraiError> { async fn active_network_validators(&self, network: NetworkId) -> Result<Vec<Public>, SeraiError> {
let validators: String = self let validators: String = self
.call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())])
@ -388,4 +393,12 @@ impl<'a> TemporalSerai<'a> {
pub fn validator_sets(&'a self) -> SeraiValidatorSets<'a> { pub fn validator_sets(&'a self) -> SeraiValidatorSets<'a> {
SeraiValidatorSets(self) SeraiValidatorSets(self)
} }
pub fn genesis_liquidity(&'a self) -> SeraiGenesisLiquidity {
SeraiGenesisLiquidity(self)
}
pub fn liquidity_tokens(&'a self) -> SeraiLiquidityTokens {
SeraiLiquidityTokens(self)
}
} }

View file

@ -66,3 +66,67 @@ macro_rules! serai_test {
)* )*
} }
} }
#[macro_export]
macro_rules! serai_test_fast_epoch {
($($name: ident: $test: expr)*) => {
$(
#[tokio::test]
async fn $name() {
use std::collections::HashMap;
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest,
};
serai_docker_tests::build("serai-fast-epoch".to_string());
let handle = concat!("serai_client-serai_node-", stringify!($name));
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
)
.replace_cmd(vec![
"serai-node".to_string(),
"--dev".to_string(),
"--unsafe-rpc-external".to_string(),
"--rpc-cors".to_string(),
"all".to_string(),
])
.replace_env(
HashMap::from([
("RUST_LOG".to_string(), "runtime=debug".to_string()),
("KEY".to_string(), " ".to_string()),
])
)
.set_publish_all_ports(true)
.set_handle(handle)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::Always,
source: LogSource::Both,
}));
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition);
test.run_async(|ops| async move {
// Sleep until the Substrate RPC starts
let serai_rpc = ops.handle(handle).host_port(9944).unwrap();
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
// Bound execution to 60 seconds
for _ in 0 .. 60 {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue };
if client.latest_finalized_block_hash().await.is_err() {
continue;
}
break;
}
#[allow(clippy::redundant_closure_call)]
$test(Serai::new(serai_rpc).await.unwrap()).await;
}).await;
}
)*
}
}

View file

@ -0,0 +1,216 @@
use std::{time::Duration, collections::HashMap};
use rand_core::{RngCore, OsRng};
use zeroize::Zeroizing;
use ciphersuite::{Ciphersuite, Ristretto};
use frost::dkg::musig::musig;
use schnorrkel::Schnorrkel;
use serai_client::{
genesis_liquidity::{
primitives::{GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES},
SeraiGenesisLiquidity,
},
validator_sets::primitives::{musig_context, Session, ValidatorSet},
};
use serai_abi::{
genesis_liquidity::primitives::{oraclize_values_message, Values},
primitives::COINS,
};
use sp_core::{sr25519::Signature, Pair as PairTrait};
use serai_client::{
primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, GENESIS_SRI,
},
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
Serai,
};
mod common;
use common::{in_instructions::provide_batch, tx::publish_tx};
serai_test_fast_epoch!(
genesis_liquidity: (|serai: Serai| async move {
test_genesis_liquidity(serai).await;
})
);
async fn test_genesis_liquidity(serai: Serai) {
// all coins except the native
let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>();
// make accounts with amounts
let mut accounts = HashMap::new();
for coin in coins.clone() {
// make 5 accounts per coin
let mut values = vec![];
for _ in 0 .. 5 {
let mut address = SeraiAddress::new([0; 32]);
OsRng.fill_bytes(&mut address.0);
values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals()))));
}
accounts.insert(coin, values);
}
// send a batch per coin
let mut batch_ids: HashMap<NetworkId, u32> = HashMap::new();
for coin in coins.clone() {
// set up instructions
let instructions = accounts[&coin]
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: Balance { coin, amount: *amount },
})
.collect::<Vec<_>>();
// set up bloch hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
// set up batch id
batch_ids
.entry(coin.network())
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
let batch =
Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions };
provide_batch(&serai, batch).await;
}
// wait until genesis ends
let genesis_blocks = 10; // TODO
let block_time = 6; // TODO
tokio::time::timeout(
tokio::time::Duration::from_secs(3 * (genesis_blocks * block_time)),
async {
while serai.latest_finalized_block().await.unwrap().number() < 10 {
tokio::time::sleep(Duration::from_secs(6)).await;
}
},
)
.await
.unwrap();
// set values relative to each other
// TODO: Random values here
let values = Values { monero: 184100, ether: 4785000, dai: 1500 };
set_values(&serai, &values).await;
let values_map = HashMap::from([
(Coin::Monero, values.monero),
(Coin::Ether, values.ether),
(Coin::Dai, values.dai),
]);
// wait a little bit..
tokio::time::sleep(Duration::from_secs(12)).await;
// check total SRI supply is +100M
// there are 6 endowed accounts in dev-net. Take this into consideration when checking
// for the total sri minted at this time.
let serai = serai.as_of_latest_finalized_block().await.unwrap();
let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap();
let endowed_amount: u64 = 1 << 60;
let total_sri = (6 * endowed_amount) + GENESIS_SRI;
assert_eq!(sri, Amount(total_sri));
// check genesis account has no coins, all transferred to pools.
for coin in COINS {
let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap();
assert_eq!(amount.0, 0);
}
// check pools has proper liquidity
let mut pool_amounts = HashMap::new();
let mut total_value = 0u128;
for coin in coins.clone() {
let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let value = if coin != Coin::Bitcoin {
(total_coin * u128::from(values_map[&coin])) / 10u128.pow(coin.decimals())
} else {
total_coin
};
total_value += value;
pool_amounts.insert(coin, (total_coin, value));
}
// check distributed SRI per pool
let mut total_sri_distributed = 0u128;
for coin in coins.clone() {
let sri = if coin == *COINS.last().unwrap() {
u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap()
} else {
(pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value
};
total_sri_distributed += sri;
let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap();
assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side
assert_eq!(u128::from(reserves.1 .0), sri); // SRI side
}
// check each liquidity provider got liquidity tokens proportional to their value
for coin in coins {
let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap();
for (acc, amount) in &accounts[&coin] {
let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares;
// since we can't test the ratios directly(due to integer division giving 0)
// we test whether they give the same result when multiplied by another constant.
// Following test ensures the account in fact has the right amount of shares.
let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares;
let amounts_ratio =
(INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap();
// we can tolerate 1 unit diff between them due to integer division.
if shares_ratio.abs_diff(amounts_ratio) == 1 {
shares_ratio = amounts_ratio;
}
assert_eq!(shares_ratio, amounts_ratio);
}
}
// TODO: test remove the liq before/after genesis ended.
}
async fn set_values(serai: &Serai, values: &Values) {
// prepare a Musig tx to oraclize the relative values
let pair = insecure_pair_from_name("Alice");
let public = pair.public();
// we publish the tx in set 4
let set = ValidatorSet { session: Session(4), network: NetworkId::Serai };
let public_key = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap();
let secret_key = <Ristretto as Ciphersuite>::read_F::<&[u8]>(
&mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(),
)
.unwrap();
assert_eq!(Ristretto::generator() * secret_key, public_key);
let threshold_keys =
musig::<Ristretto>(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap();
let sig = frost::tests::sign_without_caching(
&mut OsRng,
frost::tests::algorithm_machines(
&mut OsRng,
&Schnorrkel::new(b"substrate"),
&HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]),
),
&oraclize_values_message(&set, values),
);
// oraclize values
let _ =
publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes())))
.await;
}

View file

@ -25,7 +25,7 @@ pub use coins_pallet as coins;
use coins::Pallet as CoinsPallet; use coins::Pallet as CoinsPallet;
use serai_primitives::*; use serai_primitives::{Balance, COINS, PublicKey, system_address, Amount};
type LiquidityTokens<T> = coins_pallet::Pallet<T, coins::Instance1>; type LiquidityTokens<T> = coins_pallet::Pallet<T, coins::Instance1>;
type LiquidityTokensError<T> = coins_pallet::Error<T, coins::Instance1>; type LiquidityTokensError<T> = coins_pallet::Error<T, coins::Instance1>;

View file

@ -0,0 +1,63 @@
[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 }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-application-crypto = { 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 }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../primitives", default-features = false }
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false }
[features]
std = [
"scale/std",
"scale-info/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",
"serai-primitives/std",
"genesis-liquidity-primitives/std",
"validator-sets-primitives/std",
]
try-runtime = [] # TODO
fast-epoch = []
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,470 @@
#![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, vec::Vec};
use sp_core::sr25519::Signature;
use sp_application_crypto::RuntimePublic;
use dex_pallet::{Pallet as Dex, Config as DexConfig};
use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint};
use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets};
use serai_primitives::{Coin, COINS, *};
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>;
#[pallet::config]
pub trait Config:
frame_system::Config
+ VsConfig
+ 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, sri: Amount },
EconomicSecurityReached { network: NetworkId },
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
/// Keeps shares and the amount of coins per account.
#[pallet::storage]
pub(crate) type Liquidity<T: Config> =
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<T: Config> = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type EconomicSecurityReached<T: Config> =
StorageMap<_, Identity, NetworkId, BlockNumberFor<T>, OptionQuery>;
#[pallet::storage]
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
#[pallet::storage]
pub(crate) type GenesisComplete<T: Config> = StorageValue<_, (), 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() &&
GenesisComplete::<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 COINS {
if coin == Coin::Serai {
continue;
}
// 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 {
coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) },
sri: Amount(sri_amount),
});
}
assert_eq!(total_sri_distributed, GENESIS_SRI);
// we shouldn't have left any coin in genesis account at this moment, including SRI.
// All transferred to the pools.
for coin in COINS {
assert_eq!(Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0));
}
GenesisComplete::<T>::set(Some(()));
}
// 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::<T>::get(coin.network());
if existing.is_none() &&
<T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
{
EconomicSecurityReached::<T>::set(coin.network(), Some(n));
Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() });
}
}
Weight::zero() // TODO
}
}
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)?;
}
// 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 NETWORKS {
let ec_security_block = EconomicSecurityReached::<T>::get(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 COINS {
if c == Coin::Serai {
continue;
}
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)
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Remove the provided genesis liquidity for an account.
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn remove_coin_liquidity(origin: OriginFor<T>, balance: Balance) -> 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);
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);
// 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, 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, 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(
origin: OriginFor<T>,
values: Values,
_signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
// set their relative values
Oracle::<T>::set(Coin::Bitcoin, Some(10u64.pow(Coin::Bitcoin.decimals())));
Oracle::<T>::set(Coin::Monero, Some(values.monero));
Oracle::<T>::set(Coin::Ether, Some(values.ether));
Oracle::<T>::set(Coin::Dai, Some(values.dai));
Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
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!(),
}
}
}
}
pub use pallet::*;

View file

@ -0,0 +1,45 @@
[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]
zeroize = { version = "^1.5", features = ["derive"], optional = true }
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false }
[features]
std = [
"zeroize",
"scale/std",
"borsh?/std",
"serde?/std",
"scale-info/std",
"serai-primitives/std",
"validator-sets-primitives/std",
"sp-std/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,54 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
use zeroize::Zeroize;
#[cfg(feature = "borsh")]
use borsh::{BorshSerialize, BorshDeserialize};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use sp_std::vec::Vec;
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
use serai_primitives::*;
use validator_sets_primitives::ValidatorSet;
pub const INITIAL_GENESIS_LP_SHARES: u64 = 10_000;
// This is the account to hold and manage the genesis liquidity.
pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"GenesisLiquidity-account");
#[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 Values {
pub monero: u64,
pub ether: u64,
pub dai: u64,
}
#[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, values: &Values) -> Vec<u8> {
(b"GenesisLiquidity-oraclize_values", set, values).encode()
}

View file

@ -33,10 +33,12 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur
serai-primitives = { path = "../../primitives", default-features = false } serai-primitives = { path = "../../primitives", default-features = false }
in-instructions-primitives = { package = "serai-in-instructions-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 } coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/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 } 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] [features]
std = [ std = [
@ -54,10 +56,12 @@ std = [
"serai-primitives/std", "serai-primitives/std",
"in-instructions-primitives/std", "in-instructions-primitives/std",
"genesis-liquidity-primitives/std",
"coins-pallet/std", "coins-pallet/std",
"dex-pallet/std", "dex-pallet/std",
"validator-sets-pallet/std", "validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
] ]
default = ["std"] default = ["std"]

View file

@ -19,6 +19,7 @@ pub mod pallet {
use sp_core::sr25519::Public; use sp_core::sr25519::Public;
use serai_primitives::{Coin, Amount, Balance}; use serai_primitives::{Coin, Amount, Balance};
use genesis_liquidity_primitives::GENESIS_LIQUIDITY_ACCOUNT;
use frame_support::pallet_prelude::*; use frame_support::pallet_prelude::*;
use frame_system::{pallet_prelude::*, RawOrigin}; use frame_system::{pallet_prelude::*, RawOrigin};
@ -33,10 +34,14 @@ pub mod pallet {
Config as ValidatorSetsConfig, Pallet as ValidatorSets, Config as ValidatorSetsConfig, Pallet as ValidatorSets,
}; };
use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig};
use super::*; use super::*;
#[pallet::config] #[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>; type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
} }
@ -200,6 +205,10 @@ pub mod pallet {
} }
} }
} }
InInstruction::GenesisLiquidity(address) => {
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?;
GenesisLiq::<T>::add_coin_liquidity(address.into(), instruction.balance)?;
}
} }
Ok(()) Ok(())
} }

View file

@ -78,6 +78,7 @@ pub enum DexCall {
pub enum InInstruction { pub enum InInstruction {
Transfer(SeraiAddress), Transfer(SeraiAddress),
Dex(DexCall), Dex(DexCall),
GenesisLiquidity(SeraiAddress),
} }
#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] #[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)]

View file

@ -0,0 +1,29 @@
use crate::BlockNumber;
// 1 MB
pub const BLOCK_SIZE: u32 = 1024 * 1024;
// 6 seconds
pub const TARGET_BLOCK_TIME: u64 = 6;
/// Measured in blocks.
pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME;
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
pub const WEEKS: BlockNumber = DAYS * 7;
pub const MONTHS: BlockNumber = WEEKS * 4;
/// 6 months of blocks
pub const GENESIS_SRI_TRICKLE_FEED: u64 = MONTHS * 6;
// 100 Million SRI
pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8);
/// This needs to be long enough for arbitrage to occur and make holding any fake price up
/// sufficiently unrealistic.
#[allow(clippy::cast_possible_truncation)]
pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16;
/// Since we use the median price, double the window length.
///
/// We additionally +1 so there is a true median.
pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1;

View file

@ -37,6 +37,9 @@ pub use balance::*;
mod account; mod account;
pub use account::*; pub use account::*;
mod constants;
pub use constants::*;
pub type BlockNumber = u64; pub type BlockNumber = u64;
pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>; pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>;

View file

@ -60,6 +60,7 @@ coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", defau
dex-pallet = { package = "serai-dex-pallet", path = "../dex/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 } 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 } in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false }
@ -115,6 +116,7 @@ std = [
"dex-pallet/std", "dex-pallet/std",
"validator-sets-pallet/std", "validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
"in-instructions-pallet/std", "in-instructions-pallet/std",
@ -127,7 +129,7 @@ std = [
"pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment-rpc-runtime-api/std",
] ]
fast-epoch = [] fast-epoch = ["genesis-liquidity-pallet/fast-epoch"]
runtime-benchmarks = [ runtime-benchmarks = [
"sp-runtime/runtime-benchmarks", "sp-runtime/runtime-benchmarks",

View file

@ -7,7 +7,7 @@ use serai_abi::Call;
use crate::{ use crate::{
Vec, Vec,
primitives::{PublicKey, SeraiAddress}, primitives::{PublicKey, SeraiAddress},
timestamp, coins, dex, timestamp, coins, dex, genesis_liquidity,
validator_sets::{self, MembershipProof}, validator_sets::{self, MembershipProof},
in_instructions, signals, babe, grandpa, RuntimeCall, in_instructions, signals, babe, grandpa, RuntimeCall,
}; };
@ -30,10 +30,10 @@ impl From<Call> for RuntimeCall {
} }
}, },
Call::LiquidityTokens(lt) => match lt { Call::LiquidityTokens(lt) => match lt {
serai_abi::coins::LiquidityTokensCall::transfer { to, balance } => { serai_abi::liquidity_tokens::Call::transfer { to, balance } => {
RuntimeCall::LiquidityTokens(coins::Call::transfer { to: to.into(), balance }) RuntimeCall::LiquidityTokens(coins::Call::transfer { to: to.into(), balance })
} }
serai_abi::coins::LiquidityTokensCall::burn { balance } => { serai_abi::liquidity_tokens::Call::burn { balance } => {
RuntimeCall::LiquidityTokens(coins::Call::burn { balance }) RuntimeCall::LiquidityTokens(coins::Call::burn { balance })
} }
}, },
@ -89,6 +89,17 @@ impl From<Call> for RuntimeCall {
send_to: send_to.into(), send_to: send_to.into(),
}), }),
}, },
Call::GenesisLiquidity(gl) => match gl {
serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => {
RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance })
}
serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } => {
RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values {
values,
signature,
})
}
},
Call::ValidatorSets(vs) => match vs { Call::ValidatorSets(vs) => match vs {
serai_abi::validator_sets::Call::set_keys { serai_abi::validator_sets::Call::set_keys {
network, network,
@ -209,9 +220,9 @@ impl TryInto<Call> for RuntimeCall {
}), }),
RuntimeCall::LiquidityTokens(call) => Call::LiquidityTokens(match call { RuntimeCall::LiquidityTokens(call) => Call::LiquidityTokens(match call {
coins::Call::transfer { to, balance } => { coins::Call::transfer { to, balance } => {
serai_abi::coins::LiquidityTokensCall::transfer { to: to.into(), balance } serai_abi::liquidity_tokens::Call::transfer { to: to.into(), balance }
} }
coins::Call::burn { balance } => serai_abi::coins::LiquidityTokensCall::burn { balance }, coins::Call::burn { balance } => serai_abi::liquidity_tokens::Call::burn { balance },
_ => Err(())?, _ => Err(())?,
}), }),
RuntimeCall::Dex(call) => Call::Dex(match call { RuntimeCall::Dex(call) => Call::Dex(match call {
@ -261,6 +272,15 @@ impl TryInto<Call> for RuntimeCall {
} }
_ => Err(())?, _ => Err(())?,
}), }),
RuntimeCall::GenesisLiquidity(call) => Call::GenesisLiquidity(match call {
genesis_liquidity::Call::remove_coin_liquidity { balance } => {
serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance }
}
genesis_liquidity::Call::oraclize_values { values, signature } => {
serai_abi::genesis_liquidity::Call::oraclize_values { values, signature }
}
_ => Err(())?,
}),
RuntimeCall::ValidatorSets(call) => Call::ValidatorSets(match call { RuntimeCall::ValidatorSets(call) => Call::ValidatorSets(match call {
validator_sets::Call::set_keys { network, removed_participants, key_pair, signature } => { validator_sets::Call::set_keys { network, removed_participants, key_pair, signature } => {
serai_abi::validator_sets::Call::set_keys { serai_abi::validator_sets::Call::set_keys {

View file

@ -11,7 +11,6 @@ use core::marker::PhantomData;
// Re-export all components // Re-export all components
pub use serai_primitives as primitives; pub use serai_primitives as primitives;
pub use primitives::{BlockNumber, Header}; pub use primitives::{BlockNumber, Header};
use primitives::{NetworkId, NETWORKS};
pub use frame_system as system; pub use frame_system as system;
pub use frame_support as support; pub use frame_support as support;
@ -32,6 +31,8 @@ pub use signals_pallet as signals;
pub use pallet_babe as babe; pub use pallet_babe as babe;
pub use pallet_grandpa as grandpa; pub use pallet_grandpa as grandpa;
pub use genesis_liquidity_pallet as genesis_liquidity;
// Actually used by the runtime // Actually used by the runtime
use sp_core::OpaqueMetadata; use sp_core::OpaqueMetadata;
use sp_std::prelude::*; use sp_std::prelude::*;
@ -47,7 +48,11 @@ use sp_runtime::{
BoundedVec, Perbill, ApplyExtrinsicResult, BoundedVec, Perbill, ApplyExtrinsicResult,
}; };
use primitives::{PublicKey, AccountLookup, SubstrateAmount}; #[allow(unused_imports)]
use primitives::{
NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH,
HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE,
};
use support::{ use support::{
traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains}, traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains},
@ -114,28 +119,7 @@ pub fn native_version() -> NativeVersion {
NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } NativeVersion { runtime_version: VERSION, can_author_with: Default::default() }
} }
// 1 MB
pub const BLOCK_SIZE: u32 = 1024 * 1024;
// 6 seconds
pub const TARGET_BLOCK_TIME: u64 = 6;
/// Measured in blocks.
pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME;
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4);
/// This needs to be long enough for arbitrage to occur and make holding any fake price up
/// sufficiently unrealistic.
#[allow(clippy::cast_possible_truncation)]
pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16;
/// Since we use the median price, double the window length.
///
/// We additionally +1 so there is a true median.
pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1;
pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration = pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration =
sp_consensus_babe::BabeEpochConfiguration { sp_consensus_babe::BabeEpochConfiguration {
c: PRIMARY_PROBABILITY, c: PRIMARY_PROBABILITY,
@ -264,6 +248,10 @@ impl in_instructions::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
} }
impl genesis_liquidity::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
// for publishing equivocation evidences. // for publishing equivocation evidences.
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where where
@ -338,6 +326,7 @@ construct_runtime!(
Coins: coins, Coins: coins,
LiquidityTokens: coins::<Instance1>::{Pallet, Call, Storage, Event<T>}, LiquidityTokens: coins::<Instance1>::{Pallet, Call, Storage, Event<T>},
Dex: dex, Dex: dex,
GenesisLiquidity: genesis_liquidity,
ValidatorSets: validator_sets, ValidatorSets: validator_sets,
@ -604,4 +593,28 @@ sp_api::impl_runtime_apis! {
} }
} }
} }
impl dex::DexApi<Block> for Runtime {
fn quote_price_exact_tokens_for_tokens(
asset1: Coin,
asset2: Coin,
amount: SubstrateAmount,
include_fee: bool
) -> Option<SubstrateAmount> {
Dex::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee)
}
fn quote_price_tokens_for_exact_tokens(
asset1: Coin,
asset2: Coin,
amount: SubstrateAmount,
include_fee: bool
) -> Option<SubstrateAmount> {
Dex::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee)
}
fn get_reserves(asset1: Coin, asset2: Coin) -> Option<(SubstrateAmount, SubstrateAmount)> {
Dex::get_reserves(&asset1, &asset2).ok()
}
}
} }