Implement block emissions (#551)
Some checks are pending
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) Waiting to run
Tests / test-infra (push) Waiting to run
Tests / test-substrate (push) Waiting to run
Tests / test-serai-client (push) Waiting to run

* add genesis liquidity implementation

* add missing deposit event

* fix CI issues

* minor fixes

* make math safer

* fix fmt

* implement block emissions

* make remove liquidity an authorized call

* implement setting initial values for coins

* add genesis liquidity test & misc fixes

* updato develop latest

* fix rotation test

* fix licencing

* add fast-epoch feature

* only create the pool when adding liquidity first time

* add initial reward era test

* test whole pre ec security emissions

* fix clippy

* add swap-to-staked-sri feature

* rebase changes

* fix tests

* Remove accidentally commited ETH ABI files

* fix some pr comments

* Finish up fixing pr comments

* exclude SRI from is_allowed check

* Misc changes

---------

Co-authored-by: akildemir <aeg_asd@hotmail.com>
Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
akildemir 2024-08-15 06:12:04 +03:00 committed by GitHub
parent bf1c493d9a
commit cccc1fc7e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1279 additions and 302 deletions

30
Cargo.lock generated
View file

@ -7930,6 +7930,7 @@ dependencies = [
"parity-scale-codec", "parity-scale-codec",
"scale-info", "scale-info",
"serai-coins-primitives", "serai-coins-primitives",
"serai-emissions-primitives",
"serai-genesis-liquidity-primitives", "serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives", "serai-in-instructions-primitives",
"serai-primitives", "serai-primitives",
@ -8089,6 +8090,32 @@ dependencies = [
"chrono", "chrono",
] ]
[[package]]
name = "serai-emissions-pallet"
version = "0.1.0"
dependencies = [
"frame-support",
"frame-system",
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-emissions-primitives",
"serai-genesis-liquidity-pallet",
"serai-primitives",
"serai-validator-sets-pallet",
"serai-validator-sets-primitives",
"sp-runtime",
"sp-std",
]
[[package]]
name = "serai-emissions-primitives"
version = "0.1.0"
dependencies = [
"serai-primitives",
]
[[package]] [[package]]
name = "serai-env" name = "serai-env"
version = "0.1.0" version = "0.1.0"
@ -8172,8 +8199,8 @@ dependencies = [
"scale-info", "scale-info",
"serai-coins-pallet", "serai-coins-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-emissions-pallet",
"serai-genesis-liquidity-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",
@ -8442,6 +8469,7 @@ dependencies = [
"serai-abi", "serai-abi",
"serai-coins-pallet", "serai-coins-pallet",
"serai-dex-pallet", "serai-dex-pallet",
"serai-emissions-pallet",
"serai-genesis-liquidity-pallet", "serai-genesis-liquidity-pallet",
"serai-in-instructions-pallet", "serai-in-instructions-pallet",
"serai-primitives", "serai-primitives",

View file

@ -54,6 +54,7 @@ exceptions = [
{ 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-genesis-liquidity-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-emissions-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" },

View file

@ -34,6 +34,7 @@ serai-primitives = { path = "../primitives", version = "0.1", default-features =
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-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false }
serai-emissions-primitives = { path = "../emissions/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 }
@ -57,6 +58,7 @@ 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-genesis-liquidity-primitives/std",
"serai-emissions-primitives/std",
"serai-in-instructions-primitives/std", "serai-in-instructions-primitives/std",
"serai-signals-primitives/std", "serai-signals-primitives/std",
] ]

View file

@ -0,0 +1 @@
pub use serai_emissions_primitives as primitives;

View file

@ -16,10 +16,13 @@ pub mod liquidity_tokens;
pub mod dex; pub mod dex;
pub mod validator_sets; pub mod validator_sets;
pub mod in_instructions;
pub mod signals;
pub mod genesis_liquidity; pub mod genesis_liquidity;
pub mod emissions;
pub mod in_instructions;
pub mod signals;
pub mod babe; pub mod babe;
pub mod grandpa; pub mod grandpa;
@ -32,8 +35,8 @@ pub enum Call {
Coins(coins::Call), Coins(coins::Call),
LiquidityTokens(liquidity_tokens::Call), LiquidityTokens(liquidity_tokens::Call),
Dex(dex::Call), Dex(dex::Call),
GenesisLiquidity(genesis_liquidity::Call),
ValidatorSets(validator_sets::Call), ValidatorSets(validator_sets::Call),
GenesisLiquidity(genesis_liquidity::Call),
InInstructions(in_instructions::Call), InInstructions(in_instructions::Call),
Signals(signals::Call), Signals(signals::Call),
Babe(babe::Call), Babe(babe::Call),
@ -54,8 +57,9 @@ pub enum Event {
Coins(coins::Event), Coins(coins::Event),
LiquidityTokens(liquidity_tokens::Event), LiquidityTokens(liquidity_tokens::Event),
Dex(dex::Event), Dex(dex::Event),
GenesisLiquidity(genesis_liquidity::Event),
ValidatorSets(validator_sets::Event), ValidatorSets(validator_sets::Event),
GenesisLiquidity(genesis_liquidity::Event),
Emissions,
InInstructions(in_instructions::Event), InInstructions(in_instructions::Event),
Signals(signals::Event), Signals(signals::Event),
Babe, Babe,

View file

@ -1,12 +1,12 @@
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 scale::{decode_from_bytes, Encode}; use crate::{SeraiError, TemporalSerai};
use crate::{Serai, SeraiError, TemporalSerai};
pub type DexEvent = serai_abi::dex::Event; pub type DexEvent = serai_abi::dex::Event;
const PALLET: &str = "Dex";
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>); pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>);
impl<'a> SeraiDex<'a> { impl<'a> SeraiDex<'a> {
@ -62,17 +62,10 @@ impl<'a> SeraiDex<'a> {
/// Returns the reserves of `coin:SRI` pool. /// Returns the reserves of `coin:SRI` pool.
pub async fn get_reserves(&self, coin: Coin) -> Result<Option<(Amount, Amount)>, SeraiError> { pub async fn get_reserves(&self, coin: Coin) -> Result<Option<(Amount, Amount)>, SeraiError> {
let reserves = self self.0.runtime_api("DexApi_get_reserves", (coin, Coin::Serai)).await
.0 }
.serai
.call( pub async fn oracle_value(&self, coin: Coin) -> Result<Option<Amount>, SeraiError> {
"state_call", self.0.storage(PALLET, "SecurityOracleValue", coin).await
["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

@ -62,4 +62,9 @@ impl<'a> SeraiGenesisLiquidity<'a> {
pub async fn supply(&self, coin: Coin) -> Result<LiquidityAmount, SeraiError> { pub async fn supply(&self, coin: Coin) -> Result<LiquidityAmount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero()))
} }
pub async fn genesis_complete(&self) -> Result<bool, SeraiError> {
let result: Option<()> = self.0.storage(PALLET, "GenesisComplete", ()).await?;
Ok(result.is_some())
}
} }

View file

@ -198,17 +198,6 @@ 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> {
let validators: String = self
.call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())])
.await?;
let bytes = Self::hex_decode(validators)?;
let r = Vec::<Public>::decode(&mut bytes.as_slice())
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(r)
}
pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> { pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> {
let hash: String = self.call("chain_getFinalizedHead", ()).await?; let hash: String = self.call("chain_getFinalizedHead", ()).await?;
Self::hex_decode(hash)?.try_into().map_err(|_| { Self::hex_decode(hash)?.try_into().map_err(|_| {
@ -378,6 +367,28 @@ impl<'a> TemporalSerai<'a> {
})?)) })?))
} }
async fn runtime_api<P: Encode, R: Decode>(
&self,
method: &'static str,
params: P,
) -> Result<R, SeraiError> {
let result: String = self
.serai
.call(
"state_call",
[method.to_string(), hex::encode(params.encode()), hex::encode(self.block)],
)
.await?;
let bytes = Serai::hex_decode(result.clone())?;
R::decode(&mut bytes.as_slice()).map_err(|_| {
SeraiError::InvalidRuntime(format!(
"different type than what is expected to be returned, raw value: {}",
hex::encode(result)
))
})
}
pub fn coins(&'a self) -> SeraiCoins<'a> { pub fn coins(&'a self) -> SeraiCoins<'a> {
SeraiCoins(self) SeraiCoins(self)
} }

View file

@ -163,7 +163,7 @@ impl<'a> SeraiValidatorSets<'a> {
&self, &self,
network: NetworkId, network: NetworkId,
) -> Result<Vec<Public>, SeraiError> { ) -> Result<Vec<Public>, SeraiError> {
self.0.serai.active_network_validators(network).await self.0.runtime_api("SeraiRuntimeApi_validators", network).await
} }
// TODO: Store these separately since we almost never need both at once? // TODO: Store these separately since we almost never need both at once?
@ -178,6 +178,14 @@ impl<'a> SeraiValidatorSets<'a> {
self.0.storage(PALLET, "PendingSlashReport", network).await self.0.storage(PALLET, "PendingSlashReport", network).await
} }
pub async fn session_begin_block(
&self,
network: NetworkId,
session: Session,
) -> Result<Option<u64>, SeraiError> {
self.0.storage(PALLET, "SessionBeginBlock", (network, session)).await
}
pub fn set_keys( pub fn set_keys(
network: NetworkId, network: NetworkId,
removed_participants: sp_runtime::BoundedVec< removed_participants: sp_runtime::BoundedVec<

View file

@ -0,0 +1,115 @@
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use zeroize::Zeroizing;
use ciphersuite::{Ciphersuite, Ristretto};
use frost::dkg::musig::musig;
use schnorrkel::Schnorrkel;
use sp_core::{sr25519::Signature, Pair as PairTrait};
use serai_abi::{
genesis_liquidity::primitives::{oraclize_values_message, Values},
validator_sets::primitives::{musig_context, Session, ValidatorSet},
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name,
},
};
use serai_client::{Serai, SeraiGenesisLiquidity};
use crate::common::{in_instructions::provide_batch, tx::publish_tx};
#[allow(dead_code)]
pub async fn set_up_genesis(
serai: &Serai,
coins: &[Coin],
values: &HashMap<Coin, u64>,
) -> (HashMap<Coin, Vec<(SeraiAddress, Amount)>>, HashMap<NetworkId, u32>) {
// make accounts with amounts
let mut accounts = HashMap::new();
for coin in coins {
// 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 {
// set up instructions
let instructions = accounts[coin]
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: Balance { coin: *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;
}
// set values relative to each other. We can do that without checking for genesis period blocks
// since we are running in test(fast-epoch) mode.
// TODO: Random values here
let values =
Values { monero: values[&Coin::Monero], ether: values[&Coin::Ether], dai: values[&Coin::Dai] };
set_values(serai, &values).await;
(accounts, batch_ids)
}
#[allow(dead_code)]
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 1
let set = ValidatorSet { session: Session(1), 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

@ -10,7 +10,7 @@ use sp_core::Pair;
use serai_client::{ use serai_client::{
primitives::{insecure_pair_from_name, BlockHash, NetworkId, Balance, SeraiAddress}, primitives::{insecure_pair_from_name, BlockHash, NetworkId, Balance, SeraiAddress},
validator_sets::primitives::{Session, ValidatorSet, KeyPair}, validator_sets::primitives::{ValidatorSet, KeyPair},
in_instructions::{ in_instructions::{
primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance}, primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance},
InInstructionsEvent, InInstructionsEvent,
@ -22,12 +22,12 @@ use crate::common::{tx::publish_tx, validator_sets::set_keys};
#[allow(dead_code)] #[allow(dead_code)]
pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] { pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
// TODO: Get the latest session let serai_latest = serai.as_of_latest_finalized_block().await.unwrap();
let set = ValidatorSet { session: Session(0), network: batch.network }; let session = serai_latest.validator_sets().session(batch.network).await.unwrap().unwrap();
let set = ValidatorSet { session, network: batch.network };
let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}")); let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}"));
let keys = if let Some(keys) = let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() {
serai.as_of_latest_finalized_block().await.unwrap().validator_sets().keys(set).await.unwrap()
{
keys keys
} else { } else {
let keys = KeyPair(pair.public(), vec![].try_into().unwrap()); let keys = KeyPair(pair.public(), vec![].try_into().unwrap());

View file

@ -2,6 +2,7 @@ pub mod tx;
pub mod validator_sets; pub mod validator_sets;
pub mod in_instructions; pub mod in_instructions;
pub mod dex; pub mod dex;
pub mod genesis_liquidity;
#[macro_export] #[macro_export]
macro_rules! serai_test { macro_rules! serai_test {

View file

@ -1,14 +1,13 @@
use rand_core::{RngCore, OsRng}; use rand_core::{RngCore, OsRng};
use scale::Encode;
use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec, hashing::blake2_256}; use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec};
use serai_abi::in_instructions::primitives::DexCall; use serai_abi::in_instructions::primitives::DexCall;
use serai_client::{ use serai_client::{
primitives::{ primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, insecure_pair_from_name, ExternalAddress, Amount, NetworkId, Coin, Balance, BlockHash, insecure_pair_from_name, ExternalAddress,
SeraiAddress, PublicKey, SeraiAddress,
}, },
in_instructions::primitives::{ in_instructions::primitives::{
InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress, InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress,
@ -28,33 +27,6 @@ use common::{
// TODO: Modularize common code // TODO: Modularize common code
// TODO: Check Transfer events // TODO: Check Transfer events
serai_test!( serai_test!(
create_pool: (|serai: Serai| async move {
let block = serai.finalized_block_by_number(0).await.unwrap().unwrap().hash();
let events = serai.as_of(block).dex().events().await.unwrap();
assert_eq!(
events,
vec![
DexEvent::PoolCreated {
pool_id: Coin::Bitcoin,
pool_account: PublicKey::from_raw(blake2_256(&Coin::Bitcoin.encode())).into(),
},
DexEvent::PoolCreated {
pool_id: Coin::Ether,
pool_account: PublicKey::from_raw(blake2_256(&Coin::Ether.encode())).into(),
},
DexEvent::PoolCreated {
pool_id: Coin::Dai,
pool_account: PublicKey::from_raw(blake2_256(&Coin::Dai.encode())).into(),
},
DexEvent::PoolCreated {
pool_id: Coin::Monero,
pool_account: PublicKey::from_raw(blake2_256(&Coin::Monero.encode())).into(),
},
]
);
})
add_liquidity: (|serai: Serai| async move { add_liquidity: (|serai: Serai| async move {
let coin = Coin::Monero; let coin = Coin::Monero;
let pair = insecure_pair_from_name("Ferdie"); let pair = insecure_pair_from_name("Ferdie");

View file

@ -0,0 +1,257 @@
use std::{time::Duration, collections::HashMap};
use rand_core::{RngCore, OsRng};
use serai_client::TemporalSerai;
use serai_abi::{
emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY},
in_instructions::primitives::Batch,
primitives::{
BlockHash, Coin, COINS, FAST_EPOCH_DURATION, FAST_EPOCH_INITIAL_PERIOD, NETWORKS,
TARGET_BLOCK_TIME,
},
validator_sets::primitives::Session,
};
use serai_client::{
primitives::{Amount, NetworkId, Balance},
Serai,
};
mod common;
use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch};
serai_test_fast_epoch!(
emissions: (|serai: Serai| async move {
test_emissions(serai).await;
})
);
async fn send_batches(serai: &Serai, ids: &mut HashMap<NetworkId, u32>) {
for network in NETWORKS {
if network != NetworkId::Serai {
// set up batch id
ids
.entry(network)
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
// set up block hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
provide_batch(serai, Batch { network, id: ids[&network], block, instructions: vec![] }).await;
}
}
}
async fn test_emissions(serai: Serai) {
// set up the genesis
let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>();
let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]);
let (_, mut batch_ids) = set_up_genesis(&serai, &coins, &values).await;
// wait until genesis is complete
while !serai
.as_of_latest_finalized_block()
.await
.unwrap()
.genesis_liquidity()
.genesis_complete()
.await
.unwrap()
{
tokio::time::sleep(Duration::from_secs(1)).await;
}
let genesis_complete_block = serai.latest_finalized_block().await.unwrap().number();
for _ in 0 .. 3 {
// get current stakes
let mut current_stake = HashMap::new();
for n in NETWORKS {
// TODO: investigate why serai network TAS isn't visible at session 0.
let stake = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.total_allocated_stake(n)
.await
.unwrap()
.unwrap_or(Amount(0))
.0;
current_stake.insert(n, stake);
}
// wait for a session change
let current_session = wait_for_session_change(&serai).await;
// get last block
let last_block = serai.latest_finalized_block().await.unwrap();
let serai_latest = serai.as_of(last_block.hash());
let change_block_number = last_block.number();
// get distances to ec security & block count of the previous session
let (distances, total_distance) = get_distances(&serai_latest, &current_stake).await;
let block_count = get_session_blocks(&serai_latest, current_session - 1).await;
// calculate how much reward in this session
let reward_this_epoch =
if change_block_number < (genesis_complete_block + FAST_EPOCH_INITIAL_PERIOD) {
block_count * INITIAL_REWARD_PER_BLOCK
} else {
let blocks_until = SECURE_BY - change_block_number;
let block_reward = total_distance / blocks_until;
block_count * block_reward
};
let reward_per_network = distances
.into_iter()
.map(|(n, distance)| {
let reward = u64::try_from(
u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) /
u128::from(total_distance),
)
.unwrap();
(n, reward)
})
.collect::<HashMap<NetworkId, u64>>();
// retire the prev-set so that TotalAllocatedStake updated.
send_batches(&serai, &mut batch_ids).await;
for (n, reward) in reward_per_network {
let stake = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.total_allocated_stake(n)
.await
.unwrap()
.unwrap_or(Amount(0))
.0;
// all reward should automatically staked for the network since we are in initial period.
assert_eq!(stake, *current_stake.get(&n).unwrap() + reward);
}
// TODO: check stake per address?
// TODO: check post ec security era
}
}
/// Returns the required stake in terms SRI for a given `Balance`.
async fn required_stake(serai: &TemporalSerai<'_>, balance: Balance) -> u64 {
// This is inclusive to an increase in accuracy
let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0));
// See dex-pallet for the reasoning on these
let coin_decimals = balance.coin.decimals().max(5);
let accuracy_increase = u128::from(10u64.pow(coin_decimals));
let total_coin_value =
u64::try_from(u128::from(balance.amount.0) * u128::from(sri_per_coin.0) / accuracy_increase)
.unwrap_or(u64::MAX);
// required stake formula (COIN_VALUE * 1.5) + margin(20%)
let required_stake = total_coin_value.saturating_mul(3).saturating_div(2);
required_stake.saturating_add(total_coin_value.saturating_div(5))
}
async fn wait_for_session_change(serai: &Serai) -> u32 {
let current_session = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(NetworkId::Serai)
.await
.unwrap()
.unwrap()
.0;
let next_session = current_session + 1;
// lets wait double the epoch time.
tokio::time::timeout(
tokio::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
async {
while serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(NetworkId::Serai)
.await
.unwrap()
.unwrap()
.0 <
next_session
{
tokio::time::sleep(Duration::from_secs(6)).await;
}
},
)
.await
.unwrap();
next_session
}
async fn get_distances(
serai: &TemporalSerai<'_>,
current_stake: &HashMap<NetworkId, u64>,
) -> (HashMap<NetworkId, u64>, u64) {
// we should be in the initial period, so calculate how much each network supposedly get..
// we can check the supply to see how much coin hence liability we have.
let mut distances: HashMap<NetworkId, u64> = HashMap::new();
let mut total_distance = 0;
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
let mut required = 0;
for c in n.coins() {
let amount = serai.coins().coin_supply(*c).await.unwrap();
required += required_stake(serai, Balance { coin: *c, amount }).await;
}
let mut current = *current_stake.get(&n).unwrap();
if current > required {
current = required;
}
let distance = required - current;
total_distance += distance;
distances.insert(n, distance);
}
// add serai network portion(20%)
let new_total_distance = total_distance.saturating_mul(10) / 8;
distances.insert(NetworkId::Serai, new_total_distance - total_distance);
total_distance = new_total_distance;
(distances, total_distance)
}
async fn get_session_blocks(serai: &TemporalSerai<'_>, session: u32) -> u64 {
let begin_block = serai
.validator_sets()
.session_begin_block(NetworkId::Serai, Session(session))
.await
.unwrap()
.unwrap();
let next_begin_block = serai
.validator_sets()
.session_begin_block(NetworkId::Serai, Session(session + 1))
.await
.unwrap()
.unwrap();
next_begin_block.saturating_sub(begin_block)
}

View file

@ -1,37 +1,15 @@
use std::{time::Duration, collections::HashMap}; use std::{time::Duration, collections::HashMap};
use rand_core::{RngCore, OsRng}; use serai_client::Serai;
use zeroize::Zeroizing;
use ciphersuite::{Ciphersuite, Ristretto}; use serai_abi::primitives::{Coin, COINS, Amount, GENESIS_SRI};
use frost::dkg::musig::musig;
use schnorrkel::Schnorrkel;
use serai_client::{ use serai_client::genesis_liquidity::primitives::{
genesis_liquidity::{ GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES,
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; mod common;
use common::{in_instructions::provide_batch, tx::publish_tx}; use common::genesis_liquidity::set_up_genesis;
serai_test_fast_epoch!( serai_test_fast_epoch!(
genesis_liquidity: (|serai: Serai| async move { genesis_liquidity: (|serai: Serai| async move {
@ -39,78 +17,24 @@ serai_test_fast_epoch!(
}) })
); );
async fn test_genesis_liquidity(serai: Serai) { pub async fn test_genesis_liquidity(serai: Serai) {
// all coins except the native // set up the genesis
let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>(); let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>();
let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]);
let (accounts, _) = set_up_genesis(&serai, &coins, &values).await;
// make accounts with amounts // wait until genesis is complete
let mut accounts = HashMap::new(); while !serai
for coin in coins.clone() { .as_of_latest_finalized_block()
// 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 .await
.unwrap(); .unwrap()
.genesis_liquidity()
// set values relative to each other .genesis_complete()
// TODO: Random values here .await
let values = Values { monero: 184100, ether: 4785000, dai: 1500 }; .unwrap()
set_values(&serai, &values).await; {
let values_map = HashMap::from([ tokio::time::sleep(Duration::from_secs(1)).await;
(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 // check total SRI supply is +100M
// there are 6 endowed accounts in dev-net. Take this into consideration when checking // there are 6 endowed accounts in dev-net. Take this into consideration when checking
@ -133,7 +57,7 @@ async fn test_genesis_liquidity(serai: Serai) {
for coin in coins.clone() { for coin in coins.clone() {
let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let value = if coin != Coin::Bitcoin { let value = if coin != Coin::Bitcoin {
(total_coin * u128::from(values_map[&coin])) / 10u128.pow(coin.decimals()) (total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals())
} else { } else {
total_coin total_coin
}; };
@ -181,36 +105,3 @@ async fn test_genesis_liquidity(serai: Serai) {
// TODO: test remove the liq before/after genesis ended. // 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

@ -6,7 +6,9 @@ use sp_core::{
}; };
use serai_client::{ use serai_client::{
primitives::{NETWORKS, NetworkId, BlockHash, insecure_pair_from_name}, primitives::{
NETWORKS, NetworkId, BlockHash, insecure_pair_from_name, FAST_EPOCH_DURATION, TARGET_BLOCK_TIME,
},
validator_sets::{ validator_sets::{
primitives::{Session, ValidatorSet, KeyPair}, primitives::{Session, ValidatorSet, KeyPair},
ValidatorSetsEvent, ValidatorSetsEvent,
@ -326,13 +328,15 @@ async fn verify_session_and_active_validators(
session: u32, session: u32,
participants: &[Public], participants: &[Public],
) { ) {
// wait until the active session. This wait should be max 30 secs since the epoch time. // wait until the active session.
let block = tokio::time::timeout(core::time::Duration::from_secs(2 * 60), async move { let block = tokio::time::timeout(
core::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
async move {
loop { loop {
let mut block = serai.latest_finalized_block_hash().await.unwrap(); let mut block = serai.latest_finalized_block_hash().await.unwrap();
if session_for_block(serai, block, network).await < session { if session_for_block(serai, block, network).await < session {
// Sleep a block // Sleep a block
tokio::time::sleep(core::time::Duration::from_secs(6)).await; tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
continue; continue;
} }
while session_for_block(serai, block, network).await > session { while session_for_block(serai, block, network).await > session {
@ -341,7 +345,8 @@ async fn verify_session_and_active_validators(
assert_eq!(session_for_block(serai, block, network).await, session); assert_eq!(session_for_block(serai, block, network).await, session);
break block; break block;
} }
}) },
)
.await .await
.unwrap(); .unwrap();
let serai_for_block = serai.as_of(block); let serai_for_block = serai.as_of(block);
@ -358,10 +363,10 @@ async fn verify_session_and_active_validators(
// make sure finalization continues as usual after the changes // make sure finalization continues as usual after the changes
let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number; let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
tokio::time::timeout(core::time::Duration::from_secs(60), async move { tokio::time::timeout(core::time::Duration::from_secs(TARGET_BLOCK_TIME * 10), async move {
let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
while finalized_block <= current_finalized_block + 2 { while finalized_block <= current_finalized_block + 2 {
tokio::time::sleep(core::time::Duration::from_secs(6)).await; tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
finalized_block = serai.latest_finalized_block().await.unwrap().header.number; finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
} }
}) })

View file

@ -159,7 +159,9 @@ pub mod pallet {
/// ///
/// Errors if any amount overflows. /// Errors if any amount overflows.
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> { pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
if !T::AllowMint::is_allowed(&balance) { // If the coin isn't Serai, which we're always allowed to mint, and the mint isn't explicitly
// allowed, error
if (balance.coin != Coin::Serai) && (!T::AllowMint::is_allowed(&balance)) {
Err(Error::<T, I>::MintNotAllowed)?; Err(Error::<T, I>::MintNotAllowed)?;
} }

View file

@ -194,6 +194,11 @@ pub mod pallet {
#[pallet::getter(fn security_oracle_value)] #[pallet::getter(fn security_oracle_value)]
pub type SecurityOracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>; pub type SecurityOracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
/// Total swap volume of a given pool in terms of SRI.
#[pallet::storage]
#[pallet::getter(fn swap_volume)]
pub type SwapVolume<T: Config> = StorageMap<_, Identity, PoolId, u64, OptionQuery>;
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
fn restore_median( fn restore_median(
coin: Coin, coin: Coin,
@ -373,31 +378,6 @@ pub mod pallet {
}, },
} }
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
/// Pools to create at launch.
pub pools: Vec<Coin>,
/// field just to have T.
pub _ignore: PhantomData<T>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { pools: Default::default(), _ignore: Default::default() }
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// create the pools
for coin in &self.pools {
Pallet::<T>::create_pool(*coin).unwrap();
}
}
}
#[pallet::error] #[pallet::error]
pub enum Error<T> { pub enum Error<T> {
/// Provided coins are equal. /// Provided coins are equal.
@ -510,19 +490,15 @@ pub mod pallet {
/// ///
/// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it.
pub(crate) fn create_pool(coin: Coin) -> DispatchResult { pub(crate) fn create_pool(coin: Coin) -> DispatchResult {
ensure!(coin != Coin::Serai, Error::<T>::EqualCoins); // get pool_id
let pool_id = Self::get_pool_id(coin, Coin::Serai)?;
// prepare pool_id
let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap();
ensure!(!Pools::<T>::contains_key(pool_id), Error::<T>::PoolExists); ensure!(!Pools::<T>::contains_key(pool_id), Error::<T>::PoolExists);
let pool_account = Self::get_pool_account(pool_id); let pool_account = Self::get_pool_account(pool_id);
frame_system::Pallet::<T>::inc_providers(&pool_account); frame_system::Pallet::<T>::inc_providers(&pool_account);
Pools::<T>::insert(pool_id, ()); Pools::<T>::insert(pool_id, ());
Self::deposit_event(Event::PoolCreated { pool_id, pool_account }); Self::deposit_event(Event::PoolCreated { pool_id, pool_account });
Ok(()) Ok(())
} }
@ -561,11 +537,14 @@ pub mod pallet {
) -> DispatchResult { ) -> DispatchResult {
let sender = ensure_signed(origin)?; let sender = ensure_signed(origin)?;
ensure!((sri_desired > 0) && (coin_desired > 0), Error::<T>::WrongDesiredAmount); ensure!((sri_desired > 0) && (coin_desired > 0), Error::<T>::WrongDesiredAmount);
ensure!(coin != Coin::Serai, Error::<T>::EqualCoins);
let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap(); let pool_id = Self::get_pool_id(coin, Coin::Serai)?;
Pools::<T>::get(pool_id).as_ref().ok_or(Error::<T>::PoolNotFound)?; // create the pool if it doesn't exist. We can just attempt to do that because our checks
// far enough to allow that.
if Pools::<T>::get(pool_id).is_none() {
Self::create_pool(coin)?;
}
let pool_account = Self::get_pool_account(pool_id); let pool_account = Self::get_pool_account(pool_id);
let sri_reserve = Self::get_balance(&pool_account, Coin::Serai); let sri_reserve = Self::get_balance(&pool_account, Coin::Serai);
@ -887,9 +866,20 @@ pub mod pallet {
&to, &to,
Balance { coin: *coin2, amount: Amount(*amount_out) }, Balance { coin: *coin2, amount: Amount(*amount_out) },
)?; )?;
// update the volume
let swap_volume = if *coin1 == Coin::Serai {
amounts.get(i as usize).ok_or(Error::<T>::CorrespondenceError)?
} else {
amount_out
};
let existing = SwapVolume::<T>::get(pool_id).unwrap_or(0);
let new_volume = existing.saturating_add(*swap_volume);
SwapVolume::<T>::set(pool_id, Some(new_volume));
} }
i += 1; i += 1;
} }
Self::deposit_event(Event::SwapExecuted { Self::deposit_event(Event::SwapExecuted {
who: sender, who: sender,
send_to, send_to,

View file

@ -1155,16 +1155,8 @@ fn can_not_swap_same_coin() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
let user = system_address(b"user1").into(); let user = system_address(b"user1").into();
let coin1 = Coin::Dai; let coin1 = Coin::Dai;
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(1000) })); assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(1000) }));
let liquidity1 = 1000;
let liquidity2 = 20;
assert_noop!(
Dex::add_liquidity(RuntimeOrigin::signed(user), coin1, liquidity2, liquidity1, 1, 1, user,),
Error::<Test>::PoolNotFound
);
let exchange_amount = 10; let exchange_amount = 10;
assert_noop!( assert_noop!(
Dex::swap_exact_tokens_for_tokens( Dex::swap_exact_tokens_for_tokens(

View file

@ -0,0 +1,61 @@
[package]
name = "serai-emissions-pallet"
version = "0.1.0"
description = "Emissions pallet for Serai"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/emissions/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-runtime = { git = "https://github.com/serai-dex/substrate", 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 }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", 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 }
emissions-primitives = { package = "serai-emissions-primitives", path = "../primitives", default-features = false }
[features]
std = [
"scale/std",
"scale-info/std",
"frame-system/std",
"frame-support/std",
"sp-std/std",
"sp-runtime/std",
"coins-pallet/std",
"validator-sets-pallet/std",
"dex-pallet/std",
"genesis-liquidity-pallet/std",
"serai-primitives/std",
"emissions-primitives/std",
]
fast-epoch = []
try-runtime = [] # TODO
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,460 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)]
#[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, ops::Mul, collections::btree_map::BTreeMap};
use sp_runtime;
use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint};
use dex_pallet::{Config as DexConfig, Pallet as Dex};
use validator_sets_pallet::{Pallet as ValidatorSets, Config as ValidatorSetsConfig};
use genesis_liquidity_pallet::{Pallet as GenesisLiquidity, Config as GenesisLiquidityConfig};
use serai_primitives::*;
use validator_sets_primitives::{MAX_KEY_SHARES_PER_SET, Session};
pub use emissions_primitives as primitives;
use primitives::*;
#[pallet::config]
pub trait Config:
frame_system::Config<AccountId = PublicKey>
+ ValidatorSetsConfig
+ CoinsConfig
+ DexConfig
+ GenesisLiquidityConfig
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
/// Networks to spawn Serai with.
pub networks: Vec<(NetworkId, Amount)>,
/// List of participants to place in the initial validator sets.
pub participants: Vec<T::AccountId>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { networks: Default::default(), participants: Default::default() }
}
}
#[pallet::error]
pub enum Error<T> {
NetworkHasEconomicSecurity,
NoValueForCoin,
InsufficientAllocation,
}
#[pallet::event]
pub enum Event<T: Config> {}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
// TODO: Remove this. This should be the sole domain of validator-sets
#[pallet::storage]
#[pallet::getter(fn participants)]
pub(crate) type Participants<T: Config> = StorageMap<
_,
Identity,
NetworkId,
BoundedVec<(PublicKey, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>,
OptionQuery,
>;
// TODO: Remove this too
#[pallet::storage]
#[pallet::getter(fn session)]
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, u32, ValueQuery>;
// TODO: Find a better place for this
#[pallet::storage]
#[pallet::getter(fn economic_security_reached)]
pub(crate) type EconomicSecurityReached<T: Config> =
StorageMap<_, Identity, NetworkId, bool, ValueQuery>;
// TODO: Find a better place for this
#[pallet::storage]
pub(crate) type GenesisCompleteBlock<T: Config> = StorageValue<_, u64, OptionQuery>;
#[pallet::storage]
pub(crate) type LastSwapVolume<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for (id, stake) in self.networks.clone() {
let mut participants = vec![];
for p in self.participants.clone() {
participants.push((p, stake.0));
}
Participants::<T>::set(id, Some(participants.try_into().unwrap()));
CurrentSession::<T>::set(id, 0);
EconomicSecurityReached::<T>::set(id, false);
}
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
if GenesisCompleteBlock::<T>::get().is_none() &&
GenesisLiquidity::<T>::genesis_complete().is_some()
{
GenesisCompleteBlock::<T>::set(Some(n.saturated_into::<u64>()));
}
// we wait 1 extra block after genesis ended to see the changes. We only need this extra
// block in dev&test networks where we start the chain with accounts that already has some
// staked SRI. So when we check for ec-security pre-genesis we look like we are economically
// secure. The reason for this although we only check for it once the genesis is complete(so
// if the genesis complete we shouldn't be economically secure because the funds are not
// enough) is because ValidatorSets pallet runs before the genesis pallet in runtime.
// So ValidatorSets pallet sees the old state until next block.
// TODO: revisit this when mainnet genesis validator stake code is done.
let gcb = GenesisCompleteBlock::<T>::get();
let genesis_ended = gcb.is_some() && (n.saturated_into::<u64>() > gcb.unwrap());
// we accept we reached economic security once we can mint smallest amount of a network's coin
for coin in COINS {
let check = genesis_ended && !Self::economic_security_reached(coin.network());
if check && <T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
{
EconomicSecurityReached::<T>::set(coin.network(), true);
}
}
// check if we got a new session
let mut session_changed = false;
let session = ValidatorSets::<T>::session(NetworkId::Serai).unwrap_or(Session(0));
if session.0 > Self::session(NetworkId::Serai) {
session_changed = true;
CurrentSession::<T>::set(NetworkId::Serai, session.0);
}
// update participants per session before the genesis
// after the genesis, we update them after reward distribution.
if (!genesis_ended) && session_changed {
Self::update_participants();
}
// We only want to distribute emissions if the genesis period is over AND the session has
// ended
if !(genesis_ended && session_changed) {
return Weight::zero(); // TODO
}
// figure out the amount of blocks in the last session
// Since the session has changed, we're now at least at session 1
let block_count = ValidatorSets::<T>::session_begin_block(NetworkId::Serai, session) -
ValidatorSets::<T>::session_begin_block(NetworkId::Serai, Session(session.0 - 1));
// get total reward for this epoch
let pre_ec_security = Self::pre_ec_security();
let mut distances = BTreeMap::new();
let mut total_distance: u64 = 0;
let reward_this_epoch = if pre_ec_security {
// calculate distance to economic security per network
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
let required = ValidatorSets::<T>::required_stake_for_network(n);
let mut current = ValidatorSets::<T>::total_allocated_stake(n).unwrap_or(Amount(0)).0;
if current > required {
current = required;
}
let distance = required - current;
distances.insert(n, distance);
total_distance = total_distance.saturating_add(distance);
}
// add serai network portion (20%)
let new_total_distance =
total_distance.saturating_mul(100) / (100 - SERAI_VALIDATORS_DESIRED_PERCENTAGE);
distances.insert(NetworkId::Serai, new_total_distance - total_distance);
total_distance = new_total_distance;
if Self::initial_period(n) {
// rewards are fixed for initial period
block_count * INITIAL_REWARD_PER_BLOCK
} else {
// rewards for pre-economic security is
// (STAKE_REQUIRED - CURRENT_STAKE) / blocks_until(SECURE_BY).
let block_reward = total_distance / Self::blocks_until(SECURE_BY);
block_count * block_reward
}
} else {
// post ec security
block_count * REWARD_PER_BLOCK
};
// map epoch ec-security-distance/volume to rewards
let (rewards_per_network, volume_per_network, volume_per_coin) = if pre_ec_security {
(
distances
.into_iter()
.map(|(n, distance)| {
// calculate how much each network gets based on distance to ec-security
let reward = u64::try_from(
u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) /
u128::from(total_distance),
)
.unwrap();
(n, reward)
})
.collect::<BTreeMap<NetworkId, u64>>(),
None,
None,
)
} else {
// get swap volumes
let mut volume_per_coin: BTreeMap<Coin, u64> = BTreeMap::new();
for c in COINS {
// this should return 0 for SRI and so it shouldn't affect the total volume.
let current_volume = Dex::<T>::swap_volume(c).unwrap_or(0);
let last_volume = LastSwapVolume::<T>::get(c).unwrap_or(0);
let vol_this_epoch = current_volume.saturating_sub(last_volume);
// update the current volume
LastSwapVolume::<T>::set(c, Some(current_volume));
volume_per_coin.insert(c, vol_this_epoch);
}
// aggregate per network
let mut total_volume = 0u64;
let mut volume_per_network: BTreeMap<NetworkId, u64> = BTreeMap::new();
for (c, vol) in &volume_per_coin {
volume_per_network.insert(
c.network(),
(*volume_per_network.get(&c.network()).unwrap_or(&0)).saturating_add(*vol),
);
total_volume = total_volume.saturating_add(*vol);
}
(
volume_per_network
.iter()
.map(|(n, vol)| {
// 20% of the reward goes to the Serai network and rest is distributed among others
// based on swap-volume.
let reward = if *n == NetworkId::Serai {
reward_this_epoch / 5
} else {
let reward = reward_this_epoch - (reward_this_epoch / 5);
// TODO: It is highly unlikely but what to do in case of 0 total volume?
if total_volume != 0 {
u64::try_from(
u128::from(reward).saturating_mul(u128::from(*vol)) / u128::from(total_volume),
)
.unwrap()
} else {
0
}
};
(*n, reward)
})
.collect::<BTreeMap<NetworkId, u64>>(),
Some(volume_per_network),
Some(volume_per_coin),
)
};
// distribute the rewards within the network
for (n, reward) in rewards_per_network {
let (validators_reward, network_pool_reward) = if n == NetworkId::Serai {
(reward, 0)
} else {
// calculate pool vs validator share
let capacity = ValidatorSets::<T>::total_allocated_stake(n).unwrap_or(Amount(0)).0;
let required = ValidatorSets::<T>::required_stake_for_network(n);
let unused_capacity = capacity.saturating_sub(required);
let distribution = unused_capacity.saturating_mul(ACCURACY_MULTIPLIER) / capacity;
let total = DESIRED_DISTRIBUTION.saturating_add(distribution);
let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total;
let network_pool_reward = reward.saturating_sub(validators_reward);
(validators_reward, network_pool_reward)
};
// distribute validators rewards
Self::distribute_to_validators(n, validators_reward);
// send the rest to the pool
if network_pool_reward != 0 {
// these should be available to unwrap if we have a network_pool_reward. Because that
// means we had an unused capacity hence in a post-ec era.
let vpn = volume_per_network.as_ref().unwrap();
let vpc = volume_per_coin.as_ref().unwrap();
for c in n.coins() {
let pool_reward = u64::try_from(
u128::from(network_pool_reward).saturating_mul(u128::from(vpc[c])) /
u128::from(vpn[&n]),
)
.unwrap();
if Coins::<T>::mint(
Dex::<T>::get_pool_account(*c),
Balance { coin: Coin::Serai, amount: Amount(pool_reward) },
)
.is_err()
{
// TODO: log the failure
continue;
}
}
}
}
// TODO: we have the past session participants here in the emissions pallet so that we can
// distribute rewards to them in the next session. Ideally we should be able to fetch this
// information from valiadtor sets pallet.
Self::update_participants();
Weight::zero() // TODO
}
}
impl<T: Config> Pallet<T> {
fn blocks_until(block: u64) -> u64 {
let current = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
block.saturating_sub(current)
}
fn initial_period(n: BlockNumberFor<T>) -> bool {
#[cfg(feature = "fast-epoch")]
let initial_period_duration = FAST_EPOCH_INITIAL_PERIOD;
#[cfg(not(feature = "fast-epoch"))]
let initial_period_duration = 2 * MONTHS;
let genesis_complete_block = GenesisCompleteBlock::<T>::get();
genesis_complete_block.is_some() &&
(n.saturated_into::<u64>() < (genesis_complete_block.unwrap() + initial_period_duration))
}
/// Returns true if any of the external networks haven't reached economic security yet.
fn pre_ec_security() -> bool {
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
if !Self::economic_security_reached(n) {
return true;
}
}
false
}
// Distribute the reward among network's set based on
// -> (key shares * stake per share) + ((stake % stake per share) / 2)
fn distribute_to_validators(n: NetworkId, reward: u64) {
let stake_per_share = ValidatorSets::<T>::allocation_per_key_share(n).unwrap().0;
let mut scores = vec![];
let mut total_score = 0u64;
for (p, amount) in Self::participants(n).unwrap() {
let remainder = amount % stake_per_share;
let score = amount - (remainder / 2);
total_score = total_score.saturating_add(score);
scores.push((p, score));
}
// stake the rewards
for (p, score) in scores {
let p_reward = u64::try_from(
u128::from(reward).saturating_mul(u128::from(score)) / u128::from(total_score),
)
.unwrap();
Coins::<T>::mint(p, Balance { coin: Coin::Serai, amount: Amount(p_reward) }).unwrap();
if ValidatorSets::<T>::distribute_block_rewards(n, p, Amount(p_reward)).is_err() {
// TODO: log the failure
continue;
}
}
}
pub fn swap_to_staked_sri(
to: PublicKey,
network: NetworkId,
balance: Balance,
) -> DispatchResult {
// check the network didn't reach the economic security yet
if Self::economic_security_reached(network) {
Err(Error::<T>::NetworkHasEconomicSecurity)?;
}
// swap half of the liquidity for SRI to form PoL.
let half = balance.amount.0 / 2;
let path = BoundedVec::try_from(vec![balance.coin, Coin::Serai]).unwrap();
let origin = RawOrigin::Signed(POL_ACCOUNT.into());
Dex::<T>::swap_exact_tokens_for_tokens(
origin.clone().into(),
path,
half,
1, // minimum out, so we accept whatever we get.
POL_ACCOUNT.into(),
)?;
// get how much we got for our swap
let sri_amount = Coins::<T>::balance(POL_ACCOUNT.into(), Coin::Serai).0;
// add liquidity
Dex::<T>::add_liquidity(
origin.clone().into(),
balance.coin,
half,
sri_amount,
1,
1,
POL_ACCOUNT.into(),
)?;
// use last block spot price to calculate how much SRI the balance makes.
let last_block = <frame_system::Pallet<T>>::block_number() - 1u32.into();
let value = Dex::<T>::spot_price_for_block(last_block, balance.coin)
.ok_or(Error::<T>::NoValueForCoin)?;
// TODO: may panic? It might be best for this math ops to return the result as is instead of
// doing an unwrap so that it can be properly dealt with.
let sri_amount = balance.amount.mul(value);
// Mint
Coins::<T>::mint(to, Balance { coin: Coin::Serai, amount: sri_amount })?;
// Stake the SRI for the network.
ValidatorSets::<T>::allocate(
frame_system::RawOrigin::Signed(to).into(),
network,
sri_amount,
)?;
Ok(())
}
fn update_participants() {
for n in NETWORKS {
let participants = ValidatorSets::<T>::participants_for_latest_decided_set(n)
.unwrap()
.into_iter()
.map(|(key, _)| (key, ValidatorSets::<T>::allocation((n, key)).unwrap_or(Amount(0)).0))
.collect::<Vec<_>>();
Participants::<T>::set(n, Some(participants.try_into().unwrap()));
}
}
}
}
pub use pallet::*;

View file

@ -0,0 +1,23 @@
[package]
name = "serai-emissions-primitives"
version = "0.1.0"
description = "Serai emissions primitives"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/emissions/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,26 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
use serai_primitives::{DAYS, YEARS, SeraiAddress, system_address};
// Protocol owned liquidity account.
pub const POL_ACCOUNT: SeraiAddress = system_address(b"Serai-protocol_owned_liquidity");
/// INITIAL_REWARD = 100,000 SRI / BLOCKS_PER_DAY for 60 days
pub const INITIAL_REWARD_PER_BLOCK: u64 = (100_000 * 10u64.pow(8)) / DAYS;
/// REWARD = 20M SRI / BLOCKS_PER_YEAR
pub const REWARD_PER_BLOCK: u64 = (20_000_000 * 10u64.pow(8)) / YEARS;
/// 20% of all stake desired to be for Serai network
pub const SERAI_VALIDATORS_DESIRED_PERCENTAGE: u64 = 20;
/// Desired unused capacity ratio for a network assuming capacity is 10,000.
pub const DESIRED_DISTRIBUTION: u64 = 1_000;
/// Percentage scale for the validator vs. pool reward distribution.
pub const ACCURACY_MULTIPLIER: u64 = 10_000;
/// The block to target for economic security
pub const SECURE_BY: u64 = YEARS;

View file

@ -1,6 +1,6 @@
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)] #[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs)]
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { pub mod pallet {
use super::*; use super::*;
@ -15,7 +15,7 @@ pub mod pallet {
use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint};
use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets}; use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets};
use serai_primitives::{Coin, COINS, *}; use serai_primitives::*;
use validator_sets_primitives::{ValidatorSet, musig_key}; use validator_sets_primitives::{ValidatorSet, musig_key};
pub use genesis_liquidity_primitives as primitives; pub use genesis_liquidity_primitives as primitives;
use primitives::*; use primitives::*;
@ -72,6 +72,7 @@ pub mod pallet {
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>; pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn genesis_complete)]
pub(crate) type GenesisComplete<T: Config> = StorageValue<_, (), OptionQuery>; pub(crate) type GenesisComplete<T: Config> = StorageValue<_, (), OptionQuery>;
#[pallet::hooks] #[pallet::hooks]

View file

@ -33,12 +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 } genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false }
emissions-pallet = { package = "serai-emissions-pallet", path = "../../emissions/pallet", default-features = false }
[features] [features]
std = [ std = [
@ -56,12 +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", "genesis-liquidity-pallet/std",
"emissions-pallet/std",
] ]
default = ["std"] default = ["std"]

View file

@ -19,7 +19,6 @@ 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};
@ -34,13 +33,21 @@ 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 genesis_liquidity_pallet::{
Pallet as GenesisLiq, Config as GenesisLiqConfig, primitives::GENESIS_LIQUIDITY_ACCOUNT,
};
use emissions_pallet::{Pallet as Emissions, Config as EmissionsConfig, primitives::POL_ACCOUNT};
use super::*; use super::*;
#[pallet::config] #[pallet::config]
pub trait Config: pub trait Config:
frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig + GenesisLiqConfig frame_system::Config
+ CoinsConfig
+ DexConfig
+ ValidatorSetsConfig
+ GenesisLiqConfig
+ EmissionsConfig
{ {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
} }
@ -209,6 +216,10 @@ pub mod pallet {
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?;
GenesisLiq::<T>::add_coin_liquidity(address.into(), instruction.balance)?; GenesisLiq::<T>::add_coin_liquidity(address.into(), instruction.balance)?;
} }
InInstruction::SwapToStakedSRI(address, network) => {
Coins::<T>::mint(POL_ACCOUNT.into(), instruction.balance)?;
Emissions::<T>::swap_to_staked_sri(address.into(), network, instruction.balance)?;
}
} }
Ok(()) Ok(())
} }

View file

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

View file

@ -7,7 +7,7 @@ use sc_service::ChainType;
use serai_runtime::{ use serai_runtime::{
primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig,
CoinsConfig, DexConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, CoinsConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, EmissionsConfig,
}; };
pub type ChainSpec = sc_service::GenericChainSpec<RuntimeGenesisConfig>; pub type ChainSpec = sc_service::GenericChainSpec<RuntimeGenesisConfig>;
@ -46,11 +46,6 @@ fn devnet_genesis(
_ignore: Default::default(), _ignore: Default::default(),
}, },
dex: DexConfig {
pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero],
_ignore: Default::default(),
},
validator_sets: ValidatorSetsConfig { validator_sets: ValidatorSetsConfig {
networks: serai_runtime::primitives::NETWORKS networks: serai_runtime::primitives::NETWORKS
.iter() .iter()
@ -63,6 +58,18 @@ fn devnet_genesis(
.collect(), .collect(),
participants: validators.clone(), participants: validators.clone(),
}, },
emissions: EmissionsConfig {
networks: serai_runtime::primitives::NETWORKS
.iter()
.map(|network| match network {
NetworkId::Serai => (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))),
NetworkId::Bitcoin => (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))),
NetworkId::Ethereum => (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))),
NetworkId::Monero => (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))),
})
.collect(),
participants: validators.clone(),
},
signals: SignalsConfig::default(), signals: SignalsConfig::default(),
babe: BabeConfig { babe: BabeConfig {
authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(),
@ -97,11 +104,6 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime
_ignore: Default::default(), _ignore: Default::default(),
}, },
dex: DexConfig {
pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero],
_ignore: Default::default(),
},
validator_sets: ValidatorSetsConfig { validator_sets: ValidatorSetsConfig {
networks: serai_runtime::primitives::NETWORKS networks: serai_runtime::primitives::NETWORKS
.iter() .iter()
@ -114,6 +116,18 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime
.collect(), .collect(),
participants: validators.clone(), participants: validators.clone(),
}, },
emissions: EmissionsConfig {
networks: serai_runtime::primitives::NETWORKS
.iter()
.map(|network| match network {
NetworkId::Serai => (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))),
NetworkId::Bitcoin => (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))),
NetworkId::Ethereum => (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))),
NetworkId::Monero => (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))),
})
.collect(),
participants: validators.clone(),
},
signals: SignalsConfig::default(), signals: SignalsConfig::default(),
babe: BabeConfig { babe: BabeConfig {
authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(),

View file

@ -7,13 +7,16 @@ pub const TARGET_BLOCK_TIME: u64 = 6;
/// Measured in blocks. /// Measured in blocks.
pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME;
pub const HOURS: BlockNumber = MINUTES * 60; pub const HOURS: BlockNumber = 60 * MINUTES;
pub const DAYS: BlockNumber = HOURS * 24; pub const DAYS: BlockNumber = 24 * HOURS;
pub const WEEKS: BlockNumber = DAYS * 7; pub const WEEKS: BlockNumber = 7 * DAYS;
pub const MONTHS: BlockNumber = WEEKS * 4; // Defines a month as 30 days, which is slightly inaccurate
pub const MONTHS: BlockNumber = 30 * DAYS;
// Defines a year as 12 inaccurate months, which is 360 days literally (~1.5% off)
pub const YEARS: BlockNumber = 12 * MONTHS;
/// 6 months of blocks /// 6 months of blocks
pub const GENESIS_SRI_TRICKLE_FEED: u64 = MONTHS * 6; pub const GENESIS_SRI_TRICKLE_FEED: u64 = 6 * MONTHS;
// 100 Million SRI // 100 Million SRI
pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8);
@ -27,3 +30,9 @@ pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16;
/// ///
/// We additionally +1 so there is a true median. /// We additionally +1 so there is a true median.
pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1;
/// Amount of blocks per epoch in the fast-epoch feature that is used in tests.
pub const FAST_EPOCH_DURATION: u64 = 2 * MINUTES;
/// Amount of blocks for the initial period era of the emissions under the fast-epoch feature.
pub const FAST_EPOCH_INITIAL_PERIOD: u64 = 2 * FAST_EPOCH_DURATION;

View file

@ -15,7 +15,9 @@ use sp_core::{ConstU32, bounded::BoundedVec};
use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec}; use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec};
/// The type used to identify networks. /// The type used to identify networks.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[derive(
Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, PartialOrd, Ord, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "std", derive(Zeroize))]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

View file

@ -61,6 +61,7 @@ dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-fea
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 } genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity/pallet", default-features = false }
emissions-pallet = { package = "serai-emissions-pallet", path = "../emissions/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 }
@ -117,6 +118,7 @@ std = [
"validator-sets-pallet/std", "validator-sets-pallet/std",
"genesis-liquidity-pallet/std", "genesis-liquidity-pallet/std",
"emissions-pallet/std",
"in-instructions-pallet/std", "in-instructions-pallet/std",
@ -129,7 +131,10 @@ std = [
"pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment-rpc-runtime-api/std",
] ]
fast-epoch = ["genesis-liquidity-pallet/fast-epoch"] fast-epoch = [
"genesis-liquidity-pallet/fast-epoch",
"emissions-pallet/fast-epoch",
]
runtime-benchmarks = [ runtime-benchmarks = [
"sp-runtime/runtime-benchmarks", "sp-runtime/runtime-benchmarks",

View file

@ -89,17 +89,6 @@ 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,
@ -138,6 +127,17 @@ impl From<Call> for RuntimeCall {
RuntimeCall::ValidatorSets(validator_sets::Call::claim_deallocation { network, session }) RuntimeCall::ValidatorSets(validator_sets::Call::claim_deallocation { network, session })
} }
}, },
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::InInstructions(ii) => match ii { Call::InInstructions(ii) => match ii {
serai_abi::in_instructions::Call::execute_batch { batch } => { serai_abi::in_instructions::Call::execute_batch { batch } => {
RuntimeCall::InInstructions(in_instructions::Call::execute_batch { batch }) RuntimeCall::InInstructions(in_instructions::Call::execute_batch { batch })

View file

@ -32,6 +32,7 @@ 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; pub use genesis_liquidity_pallet as genesis_liquidity;
pub use emissions_pallet as emissions;
// Actually used by the runtime // Actually used by the runtime
use sp_core::OpaqueMetadata; use sp_core::OpaqueMetadata;
@ -51,7 +52,7 @@ use sp_runtime::{
#[allow(unused_imports)] #[allow(unused_imports)]
use primitives::{ use primitives::{
NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH, NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH,
HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, FAST_EPOCH_DURATION,
}; };
use support::{ use support::{
@ -252,6 +253,10 @@ impl genesis_liquidity::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
} }
impl emissions::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
@ -278,7 +283,7 @@ pub type ReportLongevity = <Runtime as pallet_babe::Config>::EpochDuration;
impl babe::Config for Runtime { impl babe::Config for Runtime {
#[cfg(feature = "fast-epoch")] #[cfg(feature = "fast-epoch")]
type EpochDuration = ConstU64<{ MINUTES / 2 }>; // 30 seconds type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>;
#[cfg(not(feature = "fast-epoch"))] #[cfg(not(feature = "fast-epoch"))]
type EpochDuration = ConstU64<{ 4 * 7 * DAYS }>; type EpochDuration = ConstU64<{ 4 * 7 * DAYS }>;
@ -326,9 +331,10 @@ 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,
GenesisLiquidity: genesis_liquidity,
Emissions: emissions,
InInstructions: in_instructions, InInstructions: in_instructions,

View file

@ -15,6 +15,7 @@ use sp_staking::offence::{ReportOffence, Offence, OffenceError};
use frame_system::{pallet_prelude::*, RawOrigin}; use frame_system::{pallet_prelude::*, RawOrigin};
use frame_support::{ use frame_support::{
pallet_prelude::*, pallet_prelude::*,
sp_runtime::SaturatedConversion,
traits::{DisabledValidators, KeyOwnerProofSystem, FindAuthor}, traits::{DisabledValidators, KeyOwnerProofSystem, FindAuthor},
BoundedVec, WeakBoundedVec, StoragePrefixedMap, BoundedVec, WeakBoundedVec, StoragePrefixedMap,
}; };
@ -262,12 +263,20 @@ pub mod pallet {
_t: PhantomData<T>, _t: PhantomData<T>,
prefix: Vec<u8>, prefix: Vec<u8>,
last: Vec<u8>, last: Vec<u8>,
allocation_per_key_share: Amount,
} }
impl<T: Config> SortedAllocationsIter<T> { impl<T: Config> SortedAllocationsIter<T> {
fn new(network: NetworkId) -> Self { fn new(network: NetworkId) -> Self {
let mut prefix = SortedAllocations::<T>::final_prefix().to_vec(); let mut prefix = SortedAllocations::<T>::final_prefix().to_vec();
prefix.extend(&network.encode()); prefix.extend(&network.encode());
Self { _t: PhantomData, prefix: prefix.clone(), last: prefix } Self {
_t: PhantomData,
prefix: prefix.clone(),
last: prefix,
allocation_per_key_share: Pallet::<T>::allocation_per_key_share(network).expect(
"SortedAllocationsIter iterating over a network without a set allocation per key share",
),
}
} }
} }
impl<T: Config> Iterator for SortedAllocationsIter<T> { impl<T: Config> Iterator for SortedAllocationsIter<T> {
@ -275,10 +284,17 @@ pub mod pallet {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let next = sp_io::storage::next_key(&self.last)?; let next = sp_io::storage::next_key(&self.last)?;
if !next.starts_with(&self.prefix) { if !next.starts_with(&self.prefix) {
return None; None?;
} }
let key = Pallet::<T>::recover_key_from_sorted_allocation_key(&next); let key = Pallet::<T>::recover_key_from_sorted_allocation_key(&next);
let amount = Pallet::<T>::recover_amount_from_sorted_allocation_key(&next); let amount = Pallet::<T>::recover_amount_from_sorted_allocation_key(&next);
// We may have validators present, with less than the minimum allocation, due to block
// rewards
if amount.0 < self.allocation_per_key_share.0 {
None?;
}
self.last = next; self.last = next;
Some((key, amount)) Some((key, amount))
} }
@ -309,6 +325,12 @@ pub mod pallet {
#[pallet::storage] #[pallet::storage]
pub type SeraiDisabledIndices<T: Config> = StorageMap<_, Identity, u32, Public, OptionQuery>; pub type SeraiDisabledIndices<T: Config> = StorageMap<_, Identity, u32, Public, OptionQuery>;
/// Mapping from session to its starting block number.
#[pallet::storage]
#[pallet::getter(fn session_begin_block)]
pub type SessionBeginBlock<T: Config> =
StorageDoubleMap<_, Identity, NetworkId, Identity, Session, u64, ValueQuery>;
#[pallet::event] #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> { pub enum Event<T: Config> {
@ -391,6 +413,11 @@ pub mod pallet {
Pallet::<T>::deposit_event(Event::NewSet { set }); Pallet::<T>::deposit_event(Event::NewSet { set });
Participants::<T>::set(network, Some(participants.try_into().unwrap())); Participants::<T>::set(network, Some(participants.try_into().unwrap()));
SessionBeginBlock::<T>::set(
network,
session,
<frame_system::Pallet<T>>::block_number().saturated_into::<u64>(),
);
} }
} }
@ -490,11 +517,13 @@ pub mod pallet {
network: NetworkId, network: NetworkId,
account: T::AccountId, account: T::AccountId,
amount: Amount, amount: Amount,
block_reward: bool,
) -> DispatchResult { ) -> DispatchResult {
let old_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0; let old_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0;
let new_allocation = old_allocation + amount.0; let new_allocation = old_allocation + amount.0;
let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0;
if new_allocation < allocation_per_key_share { // If this is a block reward, we always allow it to be allocated
if (new_allocation < allocation_per_key_share) && (!block_reward) {
Err(Error::<T>::InsufficientAllocation)?; Err(Error::<T>::InsufficientAllocation)?;
} }
@ -819,6 +848,21 @@ pub mod pallet {
total_required total_required
} }
pub fn distribute_block_rewards(
network: NetworkId,
account: T::AccountId,
amount: Amount,
) -> DispatchResult {
// TODO: Should this call be part of the `increase_allocation` since we have to have it
// before each call to it?
Coins::<T>::transfer_internal(
account,
Self::account(),
Balance { coin: Coin::Serai, amount },
)?;
Self::increase_allocation(network, account, amount, true)
}
fn can_slash_serai_validator(validator: Public) -> bool { fn can_slash_serai_validator(validator: Public) -> bool {
// Checks if they're active or actively deallocating (letting us still slash them) // Checks if they're active or actively deallocating (letting us still slash them)
// We could check if they're upcoming/still allocating, yet that'd mean the equivocation is // We could check if they're upcoming/still allocating, yet that'd mean the equivocation is
@ -966,7 +1010,7 @@ pub mod pallet {
Self::account(), Self::account(),
Balance { coin: Coin::Serai, amount }, Balance { coin: Coin::Serai, amount },
)?; )?;
Self::increase_allocation(network, validator, amount) Self::increase_allocation(network, validator, amount, false)
} }
#[pallet::call_index(3)] #[pallet::call_index(3)]