28 Commits

Author SHA1 Message Date
akildemir
84b534a4f4 rebase changes 2024-07-19 15:48:43 +03:00
akildemir
d40df53916 rebase to develop latest 2024-07-19 15:48:09 +03:00
akildemir
929e66c607 add swap-to-staked-sri feature 2024-05-16 15:57:03 +03:00
akildemir
904c6ddbe3 Merge branch 'emissions' of https://github.com/akildemir/serai into block-emissions 2024-05-16 11:35:56 +03:00
akildemir
929d0bf48c Merge branch 'develop' of https://github.com/akildemir/serai into emissions 2024-05-16 11:35:28 +03:00
akildemir
b8a9d204db fix clippy 2024-05-10 14:57:19 +03:00
akildemir
317b92983f test whole pre ec security emissions 2024-05-10 14:54:10 +03:00
akildemir
30df83786e add initial reward era test 2024-05-10 12:22:33 +03:00
akildemir
971b93b43d only create the pool when adding liquidity first time 2024-05-10 12:14:07 +03:00
akildemir
1d5a53dad4 add fast-epoch feature 2024-05-05 11:49:37 +03:00
akildemir
3ed6bd3b76 fix licencing 2024-05-05 11:46:37 +03:00
akildemir
33fcd27dd1 Merge branch 'emissions' of https://github.com/akildemir/serai into block-emissions 2024-05-03 13:50:52 +03:00
akildemir
04fcb2bba3 fix rotation test 2024-05-03 13:34:01 +03:00
akildemir
90a5232bbd Merge branch 'develop' of https://github.com/serai-dex/serai into emissions 2024-05-03 12:08:52 +03:00
akildemir
20cf4c930c updato develop latest 2024-04-30 10:00:21 +03:00
akildemir
7ecbfde936 Merge branch 'develop' of https://github.com/serai-dex/serai into emissions 2024-04-30 09:42:04 +03:00
akildemir
926ddd09db add genesis liquidity test & misc fixes 2024-04-29 23:19:55 +03:00
akildemir
826f9986e4 implement setting initial values for coins 2024-04-20 10:53:25 +03:00
akildemir
7041dbeb0b make remove liquidity an authorized call 2024-04-17 13:54:23 +03:00
akildemir
df774d153c Merge branch 'develop' of https://github.com/serai-dex/serai into emissions 2024-04-17 13:34:46 +03:00
akildemir
1cd9868433 implement block emissions 2024-04-09 11:14:07 +03:00
akildemir
8be0538eba fix fmt 2024-04-09 10:51:28 +03:00
akildemir
c32040240c make math safer 2024-04-08 12:08:48 +03:00
akildemir
207f2bd28a minor fixes 2024-03-29 11:38:48 +03:00
akildemir
2b32fe90ca fix CI issues 2024-03-28 11:06:04 +03:00
akildemir
b9df73e418 add missing deposit event 2024-03-27 15:26:14 +03:00
akildemir
da51543588 Merge branch 'develop' of https://github.com/serai-dex/serai into emissions 2024-03-27 15:16:39 +03:00
akildemir
c1bcb0f6c7 add genesis liquidity implementation 2024-03-27 14:46:15 +03:00
29 changed files with 2653 additions and 272 deletions

26
Cargo.lock generated
View File

@@ -7929,6 +7929,7 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serai-coins-primitives",
"serai-emissions-primitives",
"serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives",
"serai-primitives",
@@ -8088,6 +8089,29 @@ dependencies = [
"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"
[[package]]
name = "serai-env"
version = "0.1.0"
@@ -8171,6 +8195,7 @@ dependencies = [
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-emissions-pallet",
"serai-genesis-liquidity-pallet",
"serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives",
@@ -8442,6 +8467,7 @@ dependencies = [
"serai-abi",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-emissions-pallet",
"serai-genesis-liquidity-pallet",
"serai-in-instructions-pallet",
"serai-primitives",

View File

@@ -52,6 +52,9 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-coins-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-dex-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-emissions-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" },

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,410 @@
pub use schnorr::*;
/// This module was auto-generated with ethers-rs Abigen.
/// More information at: <https://github.com/gakonst/ethers-rs>
#[allow(
clippy::enum_variant_names,
clippy::too_many_arguments,
clippy::upper_case_acronyms,
clippy::type_complexity,
dead_code,
non_camel_case_types,
)]
pub mod schnorr {
#[allow(deprecated)]
fn __abi() -> ::ethers_core::abi::Abi {
::ethers_core::abi::ethabi::Contract {
constructor: ::core::option::Option::None,
functions: ::core::convert::From::from([
(
::std::borrow::ToOwned::to_owned("Q"),
::std::vec![
::ethers_core::abi::ethabi::Function {
name: ::std::borrow::ToOwned::to_owned("Q"),
inputs: ::std::vec![],
outputs: ::std::vec![
::ethers_core::abi::ethabi::Param {
name: ::std::string::String::new(),
kind: ::ethers_core::abi::ethabi::ParamType::Uint(256usize),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("uint256"),
),
},
],
constant: ::core::option::Option::None,
state_mutability: ::ethers_core::abi::ethabi::StateMutability::View,
},
],
),
(
::std::borrow::ToOwned::to_owned("verify"),
::std::vec![
::ethers_core::abi::ethabi::Function {
name: ::std::borrow::ToOwned::to_owned("verify"),
inputs: ::std::vec![
::ethers_core::abi::ethabi::Param {
name: ::std::borrow::ToOwned::to_owned("parity"),
kind: ::ethers_core::abi::ethabi::ParamType::Uint(8usize),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("uint8"),
),
},
::ethers_core::abi::ethabi::Param {
name: ::std::borrow::ToOwned::to_owned("px"),
kind: ::ethers_core::abi::ethabi::ParamType::FixedBytes(
32usize,
),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("bytes32"),
),
},
::ethers_core::abi::ethabi::Param {
name: ::std::borrow::ToOwned::to_owned("message"),
kind: ::ethers_core::abi::ethabi::ParamType::FixedBytes(
32usize,
),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("bytes32"),
),
},
::ethers_core::abi::ethabi::Param {
name: ::std::borrow::ToOwned::to_owned("c"),
kind: ::ethers_core::abi::ethabi::ParamType::FixedBytes(
32usize,
),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("bytes32"),
),
},
::ethers_core::abi::ethabi::Param {
name: ::std::borrow::ToOwned::to_owned("s"),
kind: ::ethers_core::abi::ethabi::ParamType::FixedBytes(
32usize,
),
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("bytes32"),
),
},
],
outputs: ::std::vec![
::ethers_core::abi::ethabi::Param {
name: ::std::string::String::new(),
kind: ::ethers_core::abi::ethabi::ParamType::Bool,
internal_type: ::core::option::Option::Some(
::std::borrow::ToOwned::to_owned("bool"),
),
},
],
constant: ::core::option::Option::None,
state_mutability: ::ethers_core::abi::ethabi::StateMutability::View,
},
],
),
]),
events: ::std::collections::BTreeMap::new(),
errors: ::core::convert::From::from([
(
::std::borrow::ToOwned::to_owned("InvalidSOrA"),
::std::vec![
::ethers_core::abi::ethabi::AbiError {
name: ::std::borrow::ToOwned::to_owned("InvalidSOrA"),
inputs: ::std::vec![],
},
],
),
(
::std::borrow::ToOwned::to_owned("InvalidSignature"),
::std::vec![
::ethers_core::abi::ethabi::AbiError {
name: ::std::borrow::ToOwned::to_owned("InvalidSignature"),
inputs: ::std::vec![],
},
],
),
]),
receive: false,
fallback: false,
}
}
///The parsed JSON ABI of the contract.
pub static SCHNORR_ABI: ::ethers_contract::Lazy<::ethers_core::abi::Abi> = ::ethers_contract::Lazy::new(
__abi,
);
pub struct Schnorr<M>(::ethers_contract::Contract<M>);
impl<M> ::core::clone::Clone for Schnorr<M> {
fn clone(&self) -> Self {
Self(::core::clone::Clone::clone(&self.0))
}
}
impl<M> ::core::ops::Deref for Schnorr<M> {
type Target = ::ethers_contract::Contract<M>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<M> ::core::ops::DerefMut for Schnorr<M> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<M> ::core::fmt::Debug for Schnorr<M> {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple(::core::stringify!(Schnorr)).field(&self.address()).finish()
}
}
impl<M: ::ethers_providers::Middleware> Schnorr<M> {
/// Creates a new contract instance with the specified `ethers` client at
/// `address`. The contract derefs to a `ethers::Contract` object.
pub fn new<T: Into<::ethers_core::types::Address>>(
address: T,
client: ::std::sync::Arc<M>,
) -> Self {
Self(
::ethers_contract::Contract::new(
address.into(),
SCHNORR_ABI.clone(),
client,
),
)
}
///Calls the contract's `Q` (0xe493ef8c) function
pub fn q(
&self,
) -> ::ethers_contract::builders::ContractCall<M, ::ethers_core::types::U256> {
self.0
.method_hash([228, 147, 239, 140], ())
.expect("method not found (this should never happen)")
}
///Calls the contract's `verify` (0x9186da4c) function
pub fn verify(
&self,
parity: u8,
px: [u8; 32],
message: [u8; 32],
c: [u8; 32],
s: [u8; 32],
) -> ::ethers_contract::builders::ContractCall<M, bool> {
self.0
.method_hash([145, 134, 218, 76], (parity, px, message, c, s))
.expect("method not found (this should never happen)")
}
}
impl<M: ::ethers_providers::Middleware> From<::ethers_contract::Contract<M>>
for Schnorr<M> {
fn from(contract: ::ethers_contract::Contract<M>) -> Self {
Self::new(contract.address(), contract.client())
}
}
///Custom Error type `InvalidSOrA` with signature `InvalidSOrA()` and selector `0x4e99a12e`
#[derive(
Clone,
::ethers_contract::EthError,
::ethers_contract::EthDisplay,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
#[etherror(name = "InvalidSOrA", abi = "InvalidSOrA()")]
pub struct InvalidSOrA;
///Custom Error type `InvalidSignature` with signature `InvalidSignature()` and selector `0x8baa579f`
#[derive(
Clone,
::ethers_contract::EthError,
::ethers_contract::EthDisplay,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
#[etherror(name = "InvalidSignature", abi = "InvalidSignature()")]
pub struct InvalidSignature;
///Container type for all of the contract's custom errors
#[derive(Clone, ::ethers_contract::EthAbiType, Debug, PartialEq, Eq, Hash)]
pub enum SchnorrErrors {
InvalidSOrA(InvalidSOrA),
InvalidSignature(InvalidSignature),
/// The standard solidity revert string, with selector
/// Error(string) -- 0x08c379a0
RevertString(::std::string::String),
}
impl ::ethers_core::abi::AbiDecode for SchnorrErrors {
fn decode(
data: impl AsRef<[u8]>,
) -> ::core::result::Result<Self, ::ethers_core::abi::AbiError> {
let data = data.as_ref();
if let Ok(decoded) = <::std::string::String as ::ethers_core::abi::AbiDecode>::decode(
data,
) {
return Ok(Self::RevertString(decoded));
}
if let Ok(decoded) = <InvalidSOrA as ::ethers_core::abi::AbiDecode>::decode(
data,
) {
return Ok(Self::InvalidSOrA(decoded));
}
if let Ok(decoded) = <InvalidSignature as ::ethers_core::abi::AbiDecode>::decode(
data,
) {
return Ok(Self::InvalidSignature(decoded));
}
Err(::ethers_core::abi::Error::InvalidData.into())
}
}
impl ::ethers_core::abi::AbiEncode for SchnorrErrors {
fn encode(self) -> ::std::vec::Vec<u8> {
match self {
Self::InvalidSOrA(element) => {
::ethers_core::abi::AbiEncode::encode(element)
}
Self::InvalidSignature(element) => {
::ethers_core::abi::AbiEncode::encode(element)
}
Self::RevertString(s) => ::ethers_core::abi::AbiEncode::encode(s),
}
}
}
impl ::ethers_contract::ContractRevert for SchnorrErrors {
fn valid_selector(selector: [u8; 4]) -> bool {
match selector {
[0x08, 0xc3, 0x79, 0xa0] => true,
_ if selector
== <InvalidSOrA as ::ethers_contract::EthError>::selector() => true,
_ if selector
== <InvalidSignature as ::ethers_contract::EthError>::selector() => {
true
}
_ => false,
}
}
}
impl ::core::fmt::Display for SchnorrErrors {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
Self::InvalidSOrA(element) => ::core::fmt::Display::fmt(element, f),
Self::InvalidSignature(element) => ::core::fmt::Display::fmt(element, f),
Self::RevertString(s) => ::core::fmt::Display::fmt(s, f),
}
}
}
impl ::core::convert::From<::std::string::String> for SchnorrErrors {
fn from(value: String) -> Self {
Self::RevertString(value)
}
}
impl ::core::convert::From<InvalidSOrA> for SchnorrErrors {
fn from(value: InvalidSOrA) -> Self {
Self::InvalidSOrA(value)
}
}
impl ::core::convert::From<InvalidSignature> for SchnorrErrors {
fn from(value: InvalidSignature) -> Self {
Self::InvalidSignature(value)
}
}
///Container type for all input parameters for the `Q` function with signature `Q()` and selector `0xe493ef8c`
#[derive(
Clone,
::ethers_contract::EthCall,
::ethers_contract::EthDisplay,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
#[ethcall(name = "Q", abi = "Q()")]
pub struct QCall;
///Container type for all input parameters for the `verify` function with signature `verify(uint8,bytes32,bytes32,bytes32,bytes32)` and selector `0x9186da4c`
#[derive(
Clone,
::ethers_contract::EthCall,
::ethers_contract::EthDisplay,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
#[ethcall(name = "verify", abi = "verify(uint8,bytes32,bytes32,bytes32,bytes32)")]
pub struct VerifyCall {
pub parity: u8,
pub px: [u8; 32],
pub message: [u8; 32],
pub c: [u8; 32],
pub s: [u8; 32],
}
///Container type for all of the contract's call
#[derive(Clone, ::ethers_contract::EthAbiType, Debug, PartialEq, Eq, Hash)]
pub enum SchnorrCalls {
Q(QCall),
Verify(VerifyCall),
}
impl ::ethers_core::abi::AbiDecode for SchnorrCalls {
fn decode(
data: impl AsRef<[u8]>,
) -> ::core::result::Result<Self, ::ethers_core::abi::AbiError> {
let data = data.as_ref();
if let Ok(decoded) = <QCall as ::ethers_core::abi::AbiDecode>::decode(data) {
return Ok(Self::Q(decoded));
}
if let Ok(decoded) = <VerifyCall as ::ethers_core::abi::AbiDecode>::decode(
data,
) {
return Ok(Self::Verify(decoded));
}
Err(::ethers_core::abi::Error::InvalidData.into())
}
}
impl ::ethers_core::abi::AbiEncode for SchnorrCalls {
fn encode(self) -> Vec<u8> {
match self {
Self::Q(element) => ::ethers_core::abi::AbiEncode::encode(element),
Self::Verify(element) => ::ethers_core::abi::AbiEncode::encode(element),
}
}
}
impl ::core::fmt::Display for SchnorrCalls {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
Self::Q(element) => ::core::fmt::Display::fmt(element, f),
Self::Verify(element) => ::core::fmt::Display::fmt(element, f),
}
}
}
impl ::core::convert::From<QCall> for SchnorrCalls {
fn from(value: QCall) -> Self {
Self::Q(value)
}
}
impl ::core::convert::From<VerifyCall> for SchnorrCalls {
fn from(value: VerifyCall) -> Self {
Self::Verify(value)
}
}
///Container type for all return fields from the `Q` function with signature `Q()` and selector `0xe493ef8c`
#[derive(
Clone,
::ethers_contract::EthAbiType,
::ethers_contract::EthAbiCodec,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
pub struct QReturn(pub ::ethers_core::types::U256);
///Container type for all return fields from the `verify` function with signature `verify(uint8,bytes32,bytes32,bytes32,bytes32)` and selector `0x9186da4c`
#[derive(
Clone,
::ethers_contract::EthAbiType,
::ethers_contract::EthAbiCodec,
Default,
Debug,
PartialEq,
Eq,
Hash
)]
pub struct VerifyReturn(pub bool);
}

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

View File

@@ -0,0 +1,16 @@
pub use serai_emissions_primitives as primitives;
#[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 {
// This call is just a place holder so that abi works as expected.
empty_call,
}
#[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 {
empty_event,
}

View File

@@ -20,6 +20,7 @@ pub mod in_instructions;
pub mod signals;
pub mod genesis_liquidity;
pub mod emissions;
pub mod babe;
pub mod grandpa;
@@ -32,8 +33,9 @@ pub enum Call {
Coins(coins::Call),
LiquidityTokens(liquidity_tokens::Call),
Dex(dex::Call),
GenesisLiquidity(genesis_liquidity::Call),
ValidatorSets(validator_sets::Call),
GenesisLiquidity(genesis_liquidity::Call),
Emissions(emissions::Call),
InInstructions(in_instructions::Call),
Signals(signals::Call),
Babe(babe::Call),
@@ -54,8 +56,9 @@ pub enum Event {
Coins(coins::Event),
LiquidityTokens(liquidity_tokens::Event),
Dex(dex::Event),
GenesisLiquidity(genesis_liquidity::Event),
ValidatorSets(validator_sets::Event),
GenesisLiquidity(genesis_liquidity::Event),
Emissions(emissions::Event),
InInstructions(in_instructions::Event),
Signals(signals::Event),
Babe,

View File

@@ -7,6 +7,8 @@ use crate::{Serai, SeraiError, TemporalSerai};
pub type DexEvent = serai_abi::dex::Event;
const PALLET: &str = "Dex";
#[derive(Clone, Copy)]
pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>);
impl<'a> SeraiDex<'a> {
@@ -75,4 +77,8 @@ impl<'a> SeraiDex<'a> {
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(result.map(|amounts| (Amount(amounts.0), Amount(amounts.1))))
}
pub async fn oracle_value(&self, coin: Coin) -> Result<Option<Amount>, SeraiError> {
self.0.storage(PALLET, "SecurityOracleValue", coin).await
}
}

View File

@@ -0,0 +1,211 @@
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,
};
use crate::common::{in_instructions::provide_batch, tx::publish_tx};
#[allow(dead_code)]
pub 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.
}
#[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 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

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

View File

@@ -0,0 +1,170 @@
use std::{time::Duration, collections::HashMap};
use serai_client::TemporalSerai;
use serai_abi::{
emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY},
primitives::{Coin, COINS, NETWORKS},
};
use serai_client::{
primitives::{Amount, NetworkId, Balance},
Serai,
};
mod common;
use common::genesis_liquidity::test_genesis_liquidity;
serai_test_fast_epoch!(
emissions: (|serai: Serai| async move {
test_emissions(serai).await;
})
);
async fn test_emissions(serai: Serai) {
// provide some genesis liquidity
test_genesis_liquidity(serai.clone()).await;
let mut last_epoch_start = 0;
for i in 1 .. 3 {
let mut current_stake = HashMap::new();
for n in NETWORKS {
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 until we have at least 1 session
wait_for_session(&serai, i).await;
// get distances to ec security
let last_block = serai.latest_finalized_block().await.unwrap();
let serai_latest = serai.as_of(last_block.hash());
let (distances, total_distance) = get_distances(&serai_latest, &current_stake).await;
// calculate how much reward in this session
let block_count = last_block.number() - last_epoch_start;
let reward_this_epoch = if i == 1 {
// last block number should be the block count since we are in the first block of session 1.
block_count * INITIAL_REWARD_PER_BLOCK
} else {
let blocks_until = SECURE_BY - last_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>>();
for (n, reward) in reward_per_network {
let stake = serai_latest
.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
last_epoch_start = last_block.number();
}
}
/// 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(u64::pow(10, 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(serai: &Serai, session: u32) {
// Epoch time is half an hour with the fast epoch feature, so lets wait double that.
tokio::time::timeout(tokio::time::Duration::from_secs(60 * 6), async {
while serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(NetworkId::Serai)
.await
.unwrap()
.unwrap()
.0 <
session
{
tokio::time::sleep(Duration::from_secs(6)).await;
}
})
.await
.unwrap();
}
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 coin in COINS {
if coin == Coin::Serai {
continue;
}
let amount = serai.coins().coin_supply(coin).await.unwrap();
let required = required_stake(serai, Balance { coin, amount }).await;
let mut current = *current_stake.get(&coin.network()).unwrap();
if current > required {
current = required;
}
let distance = required - current;
total_distance += distance;
distances.insert(
coin.network(),
distances.get(&coin.network()).unwrap_or(&0).saturating_add(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)
}

View File

@@ -1,216 +1,10 @@
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,
};
use serai_client::Serai;
mod common;
use common::{in_instructions::provide_batch, tx::publish_tx};
use common::genesis_liquidity::test_genesis_liquidity;
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

@@ -194,6 +194,11 @@ pub mod pallet {
#[pallet::getter(fn security_oracle_value)]
pub type SecurityOracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
/// Current length of the `SpotPrices` map.
#[pallet::storage]
#[pallet::getter(fn swap_volume)]
pub type SwapVolume<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
impl<T: Config> Pallet<T> {
fn restore_median(
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]
pub enum Error<T> {
/// Provided coins are equal.
@@ -510,19 +490,15 @@ pub mod pallet {
///
/// Once a pool is created, someone may [`Pallet::add_liquidity`] to it.
pub(crate) fn create_pool(coin: Coin) -> DispatchResult {
ensure!(coin != Coin::Serai, Error::<T>::EqualCoins);
// prepare pool_id
let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap();
// get pool_id
let pool_id = Self::get_pool_id(coin, Coin::Serai)?;
ensure!(!Pools::<T>::contains_key(pool_id), Error::<T>::PoolExists);
let pool_account = Self::get_pool_account(pool_id);
frame_system::Pallet::<T>::inc_providers(&pool_account);
Pools::<T>::insert(pool_id, ());
Self::deposit_event(Event::PoolCreated { pool_id, pool_account });
Ok(())
}
@@ -561,11 +537,14 @@ pub mod pallet {
) -> DispatchResult {
let sender = ensure_signed(origin)?;
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 sri_reserve = Self::get_balance(&pool_account, Coin::Serai);
@@ -890,6 +869,20 @@ pub mod pallet {
}
i += 1;
}
// update the volume, SRI amount is always at index 1 if the path len is 2 or 3.
// path len 1 is not allowed and 3 is already the maximum.
let swap_volume = amounts.get(1).ok_or(Error::<T>::CorrespondenceError)?;
let existing = SwapVolume::<T>::get(coin1).unwrap_or(0);
let new_volume = existing.saturating_add(*swap_volume);
SwapVolume::<T>::set(coin1, Some(new_volume));
// if we did 2 pools, update the volume for second coin as well.
if u32::try_from(path.len()).unwrap() == T::MaxSwapPathLength::get() {
let existing = SwapVolume::<T>::get(path.last().unwrap()).unwrap_or(0);
let new_volume = existing.saturating_add(*swap_volume);
SwapVolume::<T>::set(path.last().unwrap(), Some(new_volume));
}
Self::deposit_event(Event::SwapExecuted {
who: sender,
send_to,

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",
]
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,384 @@
#![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::*;
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};
use emissions_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,
}
#[pallet::event]
pub enum Event<T: Config> {}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[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,
>;
#[pallet::storage]
#[pallet::getter(fn session_begin_block)]
pub(crate) type SessionBeginBlock<T: Config> = StorageMap<_, Identity, u32, u64, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn session)]
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, u32, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn economic_security_reached)]
pub(crate) type EconomicSecurityReached<T: Config> =
StorageMap<_, Identity, NetworkId, bool, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn last_swap_volume)]
pub(crate) type LastSwapVolume<T: Config> = StorageMap<_, Identity, NetworkId, u64, OptionQuery>;
#[pallet::storage]
pub(crate) type GenesisCompleteBlock<T: Config> = StorageValue<_, 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);
}
SessionBeginBlock::<T>::set(0, 0);
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_finalize(n: BlockNumberFor<T>) {
let genesis_ended = GenesisLiquidity::<T>::genesis_complete().is_some();
if GenesisCompleteBlock::<T>::get().is_none() && genesis_ended {
GenesisCompleteBlock::<T>::set(Some(n.saturated_into::<u64>()));
}
// we accept we reached economic security once we can mint smallest amount of a network's coin
for coin in COINS {
let check = !Self::economic_security_reached(coin.network()) && genesis_ended;
if check && <T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
{
EconomicSecurityReached::<T>::set(coin.network(), true);
}
}
// check wif we got a new session
let mut session_changed = false;
let session = ValidatorSets::<T>::session(NetworkId::Serai).unwrap_or(Session(0)).0;
if session > Self::session(NetworkId::Serai) {
session_changed = true;
CurrentSession::<T>::set(NetworkId::Serai, session);
}
// emissions start only after genesis period and happens once per session.
// so we don't do anything before that time.
if !(genesis_ended && session_changed) {
return;
}
// figure out the amount of blocks in the last session. Session is at least 1
// if we come here.
let current_block = n.saturated_into::<u64>();
let block_count = current_block - Self::session_begin_block(session - 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(10) / (10 - 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
};
// get swap volumes
let mut volume_per_network: BTreeMap<NetworkId, 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);
volume_per_network.insert(
c.network(),
(*volume_per_network.get(&c.network()).unwrap_or(&0)).saturating_add(current_volume),
);
}
// map current volumes to epoch volumes
let mut total_volume = 0u64;
for (n, vol) in &mut volume_per_network {
let last_volume = Self::last_swap_volume(n).unwrap_or(0);
let vol_this_epoch = vol.saturating_sub(last_volume);
// update the current volume
LastSwapVolume::<T>::set(n, Some(*vol));
total_volume = total_volume.saturating_add(vol_this_epoch);
*vol = vol_this_epoch;
}
// map epoch ec-security-distance/volume to rewards
let rewards_per_network = distances
.into_iter()
.map(|(n, distance)| {
let reward = if pre_ec_security {
// calculate how much each network gets based on distance to ec-security
u64::try_from(
u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) /
u128::from(total_distance),
)
.unwrap()
} else {
// 20% of the reward goes to the Serai network and rest is distributed among others
// based on swap-volume.
if n == NetworkId::Serai {
reward_this_epoch / 5
} else {
let reward = reward_this_epoch - (reward_this_epoch / 5);
u64::try_from(
u128::from(reward)
.saturating_mul(u128::from(*volume_per_network.get(&n).unwrap_or(&0))) /
u128::from(total_volume),
)
.unwrap()
}
};
(n, reward)
})
.collect::<BTreeMap<NetworkId, u64>>();
// distribute the rewards within the network
for (n, reward) in rewards_per_network {
let (validators_reward, 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 pool_reward = total - validators_reward;
(validators_reward, pool_reward)
};
// distribute validators rewards
if Self::distribute_to_validators(n, validators_reward).is_err() {
// TODO: log the failure
continue;
}
// send the rest to the pool
let coin_count = u64::try_from(n.coins().len()).unwrap();
for c in n.coins() {
// assumes reward is equally distributed between network coins.
if Coins::<T>::mint(
Dex::<T>::get_pool_account(*c),
Balance { coin: Coin::Serai, amount: Amount(pool_reward / coin_count) },
)
.is_err()
{
// TODO: log the failure
continue;
}
}
}
// set the begin block and participants
SessionBeginBlock::<T>::set(session, current_block);
for n in NETWORKS {
// TODO: `participants_for_latest_decided_set` returns keys with key shares but we
// store keys with actual stake amounts. Pr https://github.com/serai-dex/serai/pull/518
// supposed to change that and so this pr relies and that pr.
let participants = ValidatorSets::<T>::participants_for_latest_decided_set(n)
.unwrap()
.into_iter()
.map(|(key, shares)| {
let amount = match n {
NetworkId::Serai => shares * 50_000 * 10_u64.pow(8),
NetworkId::Bitcoin | NetworkId::Ethereum => shares * 1_000_000 * 10_u64.pow(8),
NetworkId::Monero => shares * 100_000 * 10_u64.pow(8),
};
(key, amount)
})
.collect::<Vec<_>>();
Participants::<T>::set(n, Some(participants.try_into().unwrap()));
}
}
}
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 {
let genesis_complete_block = GenesisCompleteBlock::<T>::get();
genesis_complete_block.is_some() &&
(n.saturated_into::<u64>() < (3 * genesis_complete_block.unwrap()))
}
/// 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
}
fn distribute_to_validators(n: NetworkId, reward: u64) -> DispatchResult {
// distribute among network's set based on
// -> (key shares * stake per share) + ((stake % stake per share) / 2)
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) + (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) })?;
ValidatorSets::<T>::deposit_stake(n, p, Amount(p_reward))?;
}
Ok(())
}
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)?;
}
// calculate how much SRI the balance makes
let value =
Dex::<T>::security_oracle_value(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 & stake the SRI for the network.
Coins::<T>::mint(to, Balance { coin: Coin::Serai, amount: sri_amount })?;
// TODO: deposit_stake lets staking less than per key share. Should we allow that here?
ValidatorSets::<T>::deposit_stake(network, to, sri_amount)?;
Ok(())
}
}
}
pub use pallet::*;

View File

@@ -0,0 +1,22 @@
[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]
[features]
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,24 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
/// Amount of blocks in 30 days for 6s per block.
const BLOCKS_PER_MONTH: u32 = 10 * 60 * 24 * 30;
/// INITIAL_REWARD = 100,000 SRI / BLOCKS_PER_DAY for 60 days
pub const INITIAL_REWARD_PER_BLOCK: u64 = 100_000 * 10u64.pow(8) / ((BLOCKS_PER_MONTH as u64) / 30);
/// REWARD = 20M SRI / BLOCKS_PER_YEAR
pub const REWARD_PER_BLOCK: u64 = 20_000_000 * 10u64.pow(8) / ((BLOCKS_PER_MONTH as u64) * 12);
/// 20% of all stake desired to be for Serai network(2/10)
pub const SERAI_VALIDATORS_DESIRED_PERCENTAGE: u64 = 2;
/// 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 = (BLOCKS_PER_MONTH as u64) * 12;

View File

@@ -15,7 +15,7 @@ pub mod pallet {
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 serai_primitives::*;
use validator_sets_primitives::{ValidatorSet, musig_key};
pub use genesis_liquidity_primitives as primitives;
use primitives::*;
@@ -72,6 +72,7 @@ pub mod pallet {
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn genesis_complete)]
pub(crate) type GenesisComplete<T: Config> = StorageValue<_, (), OptionQuery>;
#[pallet::hooks]

View File

@@ -39,6 +39,7 @@ coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", de
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false }
emissions-pallet = { package = "serai-emissions-pallet", path = "../../emissions/pallet", default-features = false }
[features]
std = [
@@ -62,6 +63,7 @@ std = [
"dex-pallet/std",
"validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
"emissions-pallet/std",
]
default = ["std"]

View File

@@ -35,12 +35,18 @@ pub mod pallet {
};
use genesis_liquidity_pallet::{Pallet as GenesisLiq, Config as GenesisLiqConfig};
use emissions_pallet::{Pallet as Emissions, Config as EmissionsConfig};
use super::*;
#[pallet::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>;
}
@@ -209,6 +215,9 @@ pub mod pallet {
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?;
GenesisLiq::<T>::add_coin_liquidity(address.into(), instruction.balance)?;
}
InInstruction::SwapToStakedSRI(address, network) => {
Emissions::<T>::swap_to_staked_sri(address.into(), network, instruction.balance)?;
}
}
Ok(())
}

View File

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

View File

@@ -7,7 +7,7 @@ use sc_service::ChainType;
use serai_runtime::{
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>;
@@ -46,11 +46,6 @@ fn devnet_genesis(
_ignore: Default::default(),
},
dex: DexConfig {
pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero],
_ignore: Default::default(),
},
validator_sets: ValidatorSetsConfig {
networks: serai_runtime::primitives::NETWORKS
.iter()
@@ -63,6 +58,18 @@ fn devnet_genesis(
.collect(),
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(),
babe: BabeConfig {
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(),
},
dex: DexConfig {
pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero],
_ignore: Default::default(),
},
validator_sets: ValidatorSetsConfig {
networks: serai_runtime::primitives::NETWORKS
.iter()
@@ -114,6 +116,18 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime
.collect(),
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(),
babe: BabeConfig {
authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(),

View File

@@ -15,7 +15,9 @@ use sp_core::{ConstU32, bounded::BoundedVec};
use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec};
/// 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 = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[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 }
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 }
@@ -117,6 +118,7 @@ std = [
"validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
"emissions-pallet/std",
"in-instructions-pallet/std",

View File

@@ -89,17 +89,6 @@ impl From<Call> for RuntimeCall {
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 {
serai_abi::validator_sets::Call::set_keys {
network,
@@ -138,6 +127,18 @@ impl From<Call> for RuntimeCall {
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::Emissions(_) => todo!(), // TODO
Call::InInstructions(ii) => match ii {
serai_abi::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 genesis_liquidity_pallet as genesis_liquidity;
pub use emissions_pallet as emissions;
// Actually used by the runtime
use sp_core::OpaqueMetadata;
@@ -252,6 +253,10 @@ impl genesis_liquidity::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl emissions::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
// for publishing equivocation evidences.
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where
@@ -326,9 +331,10 @@ construct_runtime!(
Coins: coins,
LiquidityTokens: coins::<Instance1>::{Pallet, Call, Storage, Event<T>},
Dex: dex,
GenesisLiquidity: genesis_liquidity,
ValidatorSets: validator_sets,
GenesisLiquidity: genesis_liquidity,
Emissions: emissions,
InInstructions: in_instructions,

View File

@@ -490,11 +490,12 @@ pub mod pallet {
network: NetworkId,
account: T::AccountId,
amount: Amount,
block_reward: bool,
) -> DispatchResult {
let old_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0;
let new_allocation = old_allocation + amount.0;
let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0;
if new_allocation < allocation_per_key_share {
if new_allocation < allocation_per_key_share && !block_reward {
Err(Error::<T>::InsufficientAllocation)?;
}
@@ -819,6 +820,22 @@ pub mod pallet {
total_required
}
// TODO: make the increase_allocation public instead?
pub fn deposit_stake(
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 {
// 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
@@ -966,7 +983,7 @@ pub mod pallet {
Self::account(),
Balance { coin: Coin::Serai, amount },
)?;
Self::increase_allocation(network, validator, amount)
Self::increase_allocation(network, validator, amount, false)
}
#[pallet::call_index(3)]