14 Commits

Author SHA1 Message Date
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
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
26 changed files with 2758 additions and 22 deletions

35
Cargo.lock generated
View File

@@ -7652,6 +7652,7 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serai-coins-primitives",
"serai-genesis-liquidity-primitives",
"serai-in-instructions-primitives",
"serai-primitives",
"serai-signals-primitives",
@@ -7838,6 +7839,38 @@ dependencies = [
"zeroize",
]
[[package]]
name = "serai-genesis-liquidity-pallet"
version = "0.1.0"
dependencies = [
"frame-support",
"frame-system",
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-genesis-liquidity-primitives",
"serai-primitives",
"serai-validator-sets-primitives",
"sp-application-crypto",
"sp-core",
"sp-std",
]
[[package]]
name = "serai-genesis-liquidity-primitives"
version = "0.1.0"
dependencies = [
"borsh",
"parity-scale-codec",
"scale-info",
"serai-primitives",
"serai-validator-sets-primitives",
"serde",
"sp-std",
"zeroize",
]
[[package]]
name = "serai-in-instructions-pallet"
version = "0.1.0"
@@ -7848,6 +7881,7 @@ dependencies = [
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-in-instructions-primitives",
"serai-primitives",
"serai-validator-sets-pallet",
@@ -8112,6 +8146,7 @@ dependencies = [
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-in-instructions-pallet",
"serai-primitives",
"serai-signals-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

@@ -55,6 +55,8 @@ 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-in-instructions-pallet" },

View File

@@ -31,6 +31,7 @@ sp-consensus-grandpa = { git = "https://github.com/serai-dex/substrate" }
serai-primitives = { path = "../primitives", version = "0.1" }
serai-coins-primitives = { path = "../coins/primitives", version = "0.1" }
serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1" }
serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1" }
serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1" }
serai-signals-primitives = { path = "../signals/primitives", version = "0.1" }
@@ -42,6 +43,7 @@ borsh = [
"serai-primitives/borsh",
"serai-coins-primitives/borsh",
"serai-validator-sets-primitives/borsh",
"serai-genesis-liquidity-primitives/borsh",
"serai-in-instructions-primitives/borsh",
"serai-signals-primitives/borsh",
]
@@ -50,6 +52,7 @@ serde = [
"serai-primitives/serde",
"serai-coins-primitives/serde",
"serai-validator-sets-primitives/serde",
"serai-genesis-liquidity-primitives/serde",
"serai-in-instructions-primitives/serde",
"serai-signals-primitives/serde",
]

View File

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

View File

@@ -11,6 +11,8 @@ pub mod validator_sets;
pub mod in_instructions;
pub mod signals;
pub mod genesis_liquidity;
pub mod babe;
pub mod grandpa;
@@ -24,6 +26,7 @@ pub enum Call {
Coins(coins::Call),
LiquidityTokens(coins::Call),
Dex(dex::Call),
GenesisLiquidity(genesis_liquidity::Call),
ValidatorSets(validator_sets::Call),
InInstructions(in_instructions::Call),
Signals(signals::Call),
@@ -45,6 +48,7 @@ pub enum Event {
Coins(coins::Event),
LiquidityTokens(coins::Event),
Dex(dex::Event),
GenesisLiquidity(genesis_liquidity::Event),
ValidatorSets(validator_sets::Event),
InInstructions(in_instructions::Event),
Signals(signals::Event),

View File

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

View File

@@ -1,7 +1,9 @@
use sp_core::bounded_vec::BoundedVec;
use serai_abi::primitives::{SeraiAddress, Amount, Coin};
use crate::{SeraiError, TemporalSerai};
use scale::{decode_from_bytes, Encode};
use crate::{SeraiError, hex_decode, TemporalSerai};
pub type DexEvent = serai_abi::dex::Event;
@@ -57,4 +59,21 @@ impl<'a> SeraiDex<'a> {
send_to: address,
})
}
pub async fn get_reserves(
&self,
coin1: Coin,
coin2: Coin,
) -> Result<Option<(Amount, Amount)>, SeraiError> {
let hash = self
.0
.serai
.call("state_call", ["DexApi_get_reserves".to_string(), hex::encode((coin1, coin2).encode())])
.await?;
let bytes = hex_decode(hash)
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?;
let resut = decode_from_bytes::<Option<(u64, u64)>>(bytes.into())
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(resut.map(|amounts| (Amount(amounts.0), Amount(amounts.1))))
}
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
use hex::FromHexError;
use thiserror::Error;
use async_lock::RwLock;
@@ -26,6 +27,10 @@ pub mod in_instructions;
pub use in_instructions::SeraiInInstructions;
pub mod validator_sets;
pub use validator_sets::SeraiValidatorSets;
pub mod genesis_liquidity;
pub use genesis_liquidity::SeraiGenesisLiquidity;
pub mod liquidity_tokens;
pub use liquidity_tokens::SeraiLiquidityTokens;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)]
pub struct Block {
@@ -82,6 +87,14 @@ impl<'a> Clone for TemporalSerai<'a> {
}
}
pub fn hex_decode(str: String) -> Result<Vec<u8>, FromHexError> {
if let Some(stripped) = str.strip_prefix("0x") {
hex::decode(stripped)
} else {
hex::decode(str)
}
}
impl Serai {
pub async fn call<Req: Serialize, Res: DeserializeOwned>(
&self,
@@ -134,19 +147,11 @@ impl Serai {
}
}
fn hex_decode(str: String) -> Result<Vec<u8>, SeraiError> {
(if let Some(stripped) = str.strip_prefix("0x") {
hex::decode(stripped)
} else {
hex::decode(str)
})
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))
}
pub async fn block_hash(&self, number: u64) -> Result<Option<[u8; 32]>, SeraiError> {
let hash: Option<String> = self.call("chain_getBlockHash", [number]).await?;
let Some(hash) = hash else { return Ok(None) };
Self::hex_decode(hash)?
hex_decode(hash)
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?
.try_into()
.map_err(|_| SeraiError::InvalidNode("didn't respond to getBlockHash with hash".to_string()))
.map(Some)
@@ -195,11 +200,13 @@ impl Serai {
Ok(())
}
// TODO: move this into substrate/client/src/validator_sets.rs
async fn active_network_validators(&self, network: NetworkId) -> Result<Vec<Public>, SeraiError> {
let hash: String = self
.call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())])
.await?;
let bytes = Self::hex_decode(hash)?;
let bytes = hex_decode(hash)
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?;
let r = Vec::<Public>::decode(&mut bytes.as_slice())
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(r)
@@ -207,9 +214,12 @@ impl Serai {
pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> {
let hash: String = self.call("chain_getFinalizedHead", ()).await?;
Self::hex_decode(hash)?.try_into().map_err(|_| {
SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string())
})
hex_decode(hash)
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?
.try_into()
.map_err(|_| {
SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string())
})
}
pub async fn header(&self, hash: [u8; 32]) -> Result<Option<Header>, SeraiError> {
@@ -219,7 +229,7 @@ impl Serai {
pub async fn block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
let block: Option<String> = self.call("chain_getBlockBin", [hex::encode(hash)]).await?;
let Some(block) = block else { return Ok(None) };
let Ok(bytes) = Self::hex_decode(block) else {
let Ok(bytes) = hex_decode(block) else {
Err(SeraiError::InvalidNode("didn't return a hex-encoded block".to_string()))?
};
let Ok(block) = Block::decode(&mut bytes.as_slice()) else {
@@ -365,7 +375,8 @@ impl<'a> TemporalSerai<'a> {
let res: Option<String> =
self.serai.call("state_getStorage", [hex::encode(full_key), hex::encode(self.block)]).await?;
let Some(res) = res else { return Ok(None) };
let res = Serai::hex_decode(res)?;
let res = hex_decode(res)
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?;
Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| {
SeraiError::InvalidRuntime("different type present at storage location".to_string())
})?))
@@ -386,4 +397,12 @@ impl<'a> TemporalSerai<'a> {
pub fn validator_sets(&'a self) -> SeraiValidatorSets<'a> {
SeraiValidatorSets(self)
}
pub fn genesis_liquidity(&'a self) -> SeraiGenesisLiquidity {
SeraiGenesisLiquidity(self)
}
pub fn liquidity_tokens(&'a self) -> SeraiLiquidityTokens {
SeraiLiquidityTokens(self)
}
}

View File

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

View File

@@ -0,0 +1,232 @@
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, GENESIS_SRI},
SeraiGenesisLiquidity,
},
validator_sets::primitives::{musig_context, Session, ValidatorSet},
};
use serai_abi::{
genesis_liquidity::primitives::{set_initial_price_message, Prices},
primitives::COINS,
};
use sp_core::{sr25519::Signature, Pair as PairTrait};
use serai_client::{
primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name,
},
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
Serai,
};
mod common;
use common::{in_instructions::provide_batch, tx::publish_tx};
serai_test_fast_epoch!(
genesis_liquidity: (|serai: Serai| async move {
test_genesis_liquidity(serai).await;
})
);
async fn test_genesis_liquidity(serai: Serai) {
// amounts
let amounts = vec![
Amount(5_53246991),
Amount(3_14562819),
Amount(9_33648912),
Amount(150_873639000000),
Amount(248_665228000000),
Amount(451_765529000000),
];
// addresses
let mut btc_addresses = vec![];
let mut xmr_addresses = vec![];
let addr_count = amounts.len();
for (i, amount) in amounts.into_iter().enumerate() {
let mut address = SeraiAddress::new([0; 32]);
OsRng.fill_bytes(&mut address.0);
if i < addr_count / 2 {
btc_addresses.push((address, amount));
} else {
xmr_addresses.push((address, amount));
}
}
btc_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0));
xmr_addresses.sort_by(|a1, a2| a1.0.cmp(&a2.0));
// btc batch
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let btc_ins = btc_addresses
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: Balance { coin: Coin::Bitcoin, amount: *amount },
})
.collect::<Vec<_>>();
let batch =
Batch { network: NetworkId::Bitcoin, id: 0, block: block_hash, instructions: btc_ins };
provide_batch(&serai, batch).await;
// xmr batch
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let xmr_ins = xmr_addresses
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: Balance { coin: Coin::Monero, amount: *amount },
})
.collect::<Vec<_>>();
let batch = Batch { network: NetworkId::Monero, id: 0, block: block_hash, instructions: xmr_ins };
provide_batch(&serai, batch).await;
// set prices
let prices = Prices { bitcoin: 10u64.pow(8), monero: 184100, ethereum: 4785000, dai: 1500 };
set_prices(&serai, &prices).await;
// wait until genesis ends..
tokio::time::timeout(tokio::time::Duration::from_secs(300), async {
while serai.latest_finalized_block().await.unwrap().number() < 25 {
tokio::time::sleep(Duration::from_secs(6)).await;
}
})
.await
.unwrap();
// check total SRI supply is +100M
let last_block = serai.latest_finalized_block().await.unwrap().hash();
let serai = serai.as_of(last_block);
// Check balance instead of supply
let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap();
// there are 6 endowed accounts in dev-net. Take this into consideration when checking
// for the total sri minted at this time.
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 pool_btc = btc_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let pool_xmr = xmr_addresses.iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let pool_btc_value = (pool_btc * u128::from(prices.bitcoin)) / 10u128.pow(8);
let pool_xmr_value = (pool_xmr * u128::from(prices.monero)) / 10u128.pow(12);
let total_value = pool_btc_value + pool_xmr_value;
// calculated distributed SRI. We know that xmr is at the end of COINS array
// so it will be approximated to roof instead of floor after integer division.
let btc_sri = (pool_btc_value * u128::from(GENESIS_SRI)) / total_value;
let xmr_sri = ((pool_xmr_value * u128::from(GENESIS_SRI)) / total_value) + 1;
let btc_reserves = serai.dex().get_reserves(Coin::Bitcoin, Coin::Serai).await.unwrap().unwrap();
assert_eq!(u128::from(btc_reserves.0 .0), pool_btc);
assert_eq!(u128::from(btc_reserves.1 .0), btc_sri);
let xmr_reserves = serai.dex().get_reserves(Coin::Monero, Coin::Serai).await.unwrap().unwrap();
assert_eq!(u128::from(xmr_reserves.0 .0), pool_xmr);
assert_eq!(u128::from(xmr_reserves.1 .0), xmr_sri);
// check each btc liq provider got liq tokens proportional to their value
let btc_liq_token_supply = u128::from(
serai
.liquidity_tokens()
.token_balance(Coin::Bitcoin, GENESIS_LIQUIDITY_ACCOUNT)
.await
.unwrap()
.0,
);
let mut total_tokens_this_coin: u128 = 0;
for (i, (addr, amount)) in btc_addresses.iter().enumerate() {
let addr_value = (u128::from(amount.0) * u128::from(prices.bitcoin)) / 10u128.pow(8);
let addr_liq_tokens = if i == btc_addresses.len() - 1 {
btc_liq_token_supply - total_tokens_this_coin
} else {
(addr_value * btc_liq_token_supply) / pool_btc_value
};
let addr_actual_token_amount =
serai.genesis_liquidity().liquidity_tokens(addr, Coin::Bitcoin).await.unwrap();
assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into());
total_tokens_this_coin += addr_liq_tokens;
}
// check each xmr liq provider got liq tokens proportional to their value
let xmr_liq_token_supply = u128::from(
serai
.liquidity_tokens()
.token_balance(Coin::Monero, GENESIS_LIQUIDITY_ACCOUNT)
.await
.unwrap()
.0,
);
total_tokens_this_coin = 0;
for (i, (addr, amount)) in xmr_addresses.iter().enumerate() {
let addr_value = (u128::from(amount.0) * u128::from(prices.monero)) / 10u128.pow(12);
let addr_liq_tokens = if i == xmr_addresses.len() - 1 {
xmr_liq_token_supply - total_tokens_this_coin
} else {
(addr_value * xmr_liq_token_supply) / pool_xmr_value
};
let addr_actual_token_amount =
serai.genesis_liquidity().liquidity_tokens(addr, Coin::Monero).await.unwrap();
assert_eq!(addr_liq_tokens, addr_actual_token_amount.0.into());
total_tokens_this_coin += addr_liq_tokens;
}
// TODO: remove the liq before/after genesis ended.
}
async fn set_prices(serai: &Serai, prices: &Prices) {
// prepare a Musig tx to set the initial prices
let pair = insecure_pair_from_name("Alice");
let public = pair.public();
let set = ValidatorSet { session: Session(0), 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())]),
),
&set_initial_price_message(&set, prices),
);
// set initial prices
let _ = publish_tx(
serai,
&SeraiGenesisLiquidity::set_initial_price(*prices, Signature(sig.to_bytes())),
)
.await;
}

View File

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

View File

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

View File

@@ -0,0 +1,394 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[allow(clippy::cast_possible_truncation, clippy::no_effect_underscore_binding)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_system::{pallet_prelude::*, RawOrigin};
use frame_support::{
pallet_prelude::*,
sp_runtime::{self, SaturatedConversion},
};
use sp_std::{vec, vec::Vec, collections::btree_map::BTreeMap};
use sp_core::sr25519::Signature;
use sp_application_crypto::RuntimePublic;
use dex_pallet::{Pallet as Dex, Config as DexConfig};
use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint};
use serai_primitives::*;
use validator_sets_primitives::{ValidatorSet, Session, musig_key};
pub use genesis_liquidity_primitives as primitives;
use primitives::*;
/// LiquidityTokens Pallet as an instance of coins pallet.
pub type LiquidityTokens<T> = coins_pallet::Pallet<T, coins_pallet::Instance1>;
#[pallet::config]
pub trait Config:
frame_system::Config + DexConfig + CoinsConfig + coins_pallet::Config<coins_pallet::Instance1>
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
/// 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 { participants: Default::default() }
}
}
#[pallet::error]
pub enum Error<T> {
GenesisPeriodEnded,
AmountOverflowed,
NotEnoughLiquidity,
CanOnlyRemoveFullAmount,
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
GenesisLiquidityAdded { by: SeraiAddress, balance: Balance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance },
GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance },
EconomicSecurityReached { network: NetworkId },
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::storage]
pub(crate) type Liquidity<T: Config> =
StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type LiquidityTokensPerAddress<T: Config> =
StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type EconomicSecurityReached<T: Config> =
StorageMap<_, Identity, NetworkId, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
pub(crate) type Participants<T: Config> =
StorageMap<_, Identity, NetworkId, BoundedVec<PublicKey, ConstU32<150>>, ValueQuery>;
#[pallet::storage]
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, ValueQuery>;
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
Participants::<T>::set(NetworkId::Serai, self.participants.clone().try_into().unwrap());
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_finalize(n: BlockNumberFor<T>) {
#[cfg(feature = "fast-epoch")]
let final_block = 25u32;
#[cfg(not(feature = "fast-epoch"))]
let final_block = BLOCKS_PER_MONTH;
// Distribute the genesis sri to pools after a month
if n == final_block.into() {
// mint the SRI
Coins::<T>::mint(
GENESIS_LIQUIDITY_ACCOUNT.into(),
Balance { coin: Coin::Serai, amount: Amount(GENESIS_SRI) },
)
.unwrap();
// get coin values & total
let mut account_values = BTreeMap::new();
let mut pool_values = vec![];
let mut total_value: u128 = 0;
for coin in COINS {
if coin == Coin::Serai {
continue;
}
// initial coin value in terms of btc
let value = Oracle::<T>::get(coin);
// get the pool & individual address values
account_values.insert(coin, vec![]);
let mut pool_amount: u128 = 0;
for (account, amount) in Liquidity::<T>::iter_prefix(coin) {
pool_amount = pool_amount.saturating_add(amount.into());
let value_this_addr =
u128::from(amount).saturating_mul(value.into()) / 10u128.pow(coin.decimals());
account_values.get_mut(&coin).unwrap().push((account, value_this_addr))
}
// sort, so that everyone has a consistent accounts vector per coin
account_values.get_mut(&coin).unwrap().sort();
let pool_value = pool_amount.saturating_mul(value.into()) / 10u128.pow(coin.decimals());
total_value = total_value.saturating_add(pool_value);
pool_values.push((coin, pool_amount, pool_value));
}
// add the liquidity per pool
let mut total_sri_distributed = 0;
let pool_values_len = pool_values.len();
for (i, (coin, pool_amount, pool_value)) in pool_values.into_iter().enumerate() {
// whatever sri left for the last coin should be ~= it's ratio
let sri_amount = if i == (pool_values_len - 1) {
GENESIS_SRI - total_sri_distributed
} else {
u64::try_from(u128::from(GENESIS_SRI).saturating_mul(pool_value) / total_value).unwrap()
};
total_sri_distributed += sri_amount;
// we can't add 0 liquidity
if !(pool_amount > 0 && sri_amount > 0) {
continue;
}
// actually add the liquidity to dex
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
Dex::<T>::add_liquidity(
origin.into(),
coin,
u64::try_from(pool_amount).unwrap(),
sri_amount,
u64::try_from(pool_amount).unwrap(),
sri_amount,
GENESIS_LIQUIDITY_ACCOUNT.into(),
)
.unwrap();
// let everyone know about the event
Self::deposit_event(Event::GenesisLiquidityAddedToPool {
coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) },
coin2: Balance { coin: Coin::Serai, amount: Amount(sri_amount) },
});
// set liquidity tokens per account
let tokens =
u128::from(LiquidityTokens::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin).0);
let mut total_tokens_this_coin: u128 = 0;
let accounts = account_values.get(&coin).unwrap();
for (i, (acc, acc_value)) in accounts.iter().enumerate() {
// give whatever left to the last account not to have rounding errors.
let liq_tokens_this_acc = if i == accounts.len() - 1 {
tokens - total_tokens_this_coin
} else {
tokens.saturating_mul(*acc_value) / pool_value
};
total_tokens_this_coin = total_tokens_this_coin.saturating_add(liq_tokens_this_acc);
LiquidityTokensPerAddress::<T>::set(
coin,
acc,
Some(u64::try_from(liq_tokens_this_acc).unwrap()),
);
}
assert_eq!(tokens, total_tokens_this_coin);
}
assert_eq!(total_sri_distributed, GENESIS_SRI);
// we shouldn't have left any coin in genesis account at this moment, including SRI.
// All transferred to the pools.
for coin in COINS {
assert_eq!(Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0));
}
}
// we accept we reached economic security once we can mint smallest amount of a network's coin
for coin in COINS {
let existing = EconomicSecurityReached::<T>::get(coin.network());
if existing == 0u32.into() &&
<T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
{
EconomicSecurityReached::<T>::set(coin.network(), n);
Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() });
}
}
}
}
impl<T: Config> Pallet<T> {
/// Add genesis liquidity for the given account. All accounts that provide liquidity
/// will receive the genesis SRI according to their liquidity ratio.
pub fn add_coin_liquidity(account: PublicKey, balance: Balance) -> DispatchResult {
// check we are still in genesis period
if Self::genesis_ended() {
Err(Error::<T>::GenesisPeriodEnded)?;
}
// mint the coins
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance)?;
// save
let existing = Liquidity::<T>::get(balance.coin, account).unwrap_or(0);
let new = existing.checked_add(balance.amount.0).ok_or(Error::<T>::AmountOverflowed)?;
Liquidity::<T>::set(balance.coin, account, Some(new));
Self::deposit_event(Event::GenesisLiquidityAdded { by: account.into(), balance });
Ok(())
}
/// Returns the number of blocks since the coin's network reached economic security first time.
/// If the network is yet to be reached that threshold, 0 is returned. And maximum of
/// `GENESIS_SRI_TRICKLE_FEED` returned.
fn blocks_since_ec_security(coin: &Coin) -> u64 {
let ec_security_block =
EconomicSecurityReached::<T>::get(coin.network()).saturated_into::<u64>();
let current = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
if ec_security_block > 0 {
let diff = current - ec_security_block;
if diff > GENESIS_SRI_TRICKLE_FEED {
return GENESIS_SRI_TRICKLE_FEED;
}
return diff;
}
0
}
fn genesis_ended() -> bool {
<frame_system::Pallet<T>>::block_number() >= BLOCKS_PER_MONTH.into()
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Remove the provided genesis liquidity for an account.
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn remove_coin_liquidity(origin: OriginFor<T>, balance: Balance) -> DispatchResult {
let account = ensure_signed(origin)?;
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
// check we are still in genesis period
if Self::genesis_ended() {
// check user have enough to remove
let existing = LiquidityTokensPerAddress::<T>::get(balance.coin, account).unwrap_or(0);
if balance.amount.0 > existing {
Err(Error::<T>::NotEnoughLiquidity)?;
}
// remove liquidity from pool
let prev_sri = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let prev_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin);
Dex::<T>::remove_liquidity(
origin.clone().into(),
balance.coin,
balance.amount.0,
1,
1,
GENESIS_LIQUIDITY_ACCOUNT.into(),
)?;
let current_sri = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let current_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin);
// burn the SRI if necessary
let mut sri = current_sri.0.saturating_sub(prev_sri.0);
let distance_to_full_pay =
GENESIS_SRI_TRICKLE_FEED - Self::blocks_since_ec_security(&balance.coin);
let burn_sri_amount = sri.saturating_mul(distance_to_full_pay) / GENESIS_SRI_TRICKLE_FEED;
Coins::<T>::burn(
origin.clone().into(),
Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) },
)?;
sri -= burn_sri_amount;
// transfer to owner
let coin_out = current_coin.0 - prev_coin.0;
Coins::<T>::transfer(
origin.clone().into(),
account,
Balance { coin: balance.coin, amount: Amount(coin_out) },
)?;
Coins::<T>::transfer(
origin.into(),
account,
Balance { coin: Coin::Serai, amount: Amount(sri) },
)?;
// save
let existing = LiquidityTokensPerAddress::<T>::get(balance.coin, account).unwrap_or(0);
let new = existing.checked_sub(balance.amount.0).ok_or(Error::<T>::AmountOverflowed)?;
LiquidityTokensPerAddress::<T>::set(balance.coin, account, Some(new));
} else {
let existing = Liquidity::<T>::get(balance.coin, account).unwrap_or(0);
if balance.amount.0 > existing || balance.amount.0 == 0 {
Err(Error::<T>::NotEnoughLiquidity)?;
}
if balance.amount.0 < existing {
Err(Error::<T>::CanOnlyRemoveFullAmount)?;
}
// TODO: do external transfer instead for making it easier for the user?
// or do we even want to make it easier?
Coins::<T>::transfer(origin.into(), account, balance)?;
// save
Liquidity::<T>::set(balance.coin, account, None);
}
Self::deposit_event(Event::GenesisLiquidityRemoved { by: account.into(), balance });
Ok(())
}
/// A call to submit the initial coi values.
#[pallet::call_index(1)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn set_initial_price(
origin: OriginFor<T>,
prices: Prices,
_signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
// set the prices
Oracle::<T>::set(Coin::Bitcoin, prices.bitcoin);
Oracle::<T>::set(Coin::Monero, prices.monero);
Oracle::<T>::set(Coin::Ether, prices.ethereum);
Oracle::<T>::set(Coin::Dai, prices.dai);
Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
match call {
Call::set_initial_price { ref prices, ref signature } => {
let set = ValidatorSet { network: NetworkId::Serai, session: Session(0) };
let signers = Participants::<T>::get(NetworkId::Serai);
if !musig_key(set, &signers).verify(&set_initial_price_message(&set, prices), signature) {
Err(InvalidTransaction::BadProof)?;
}
ValidTransaction::with_tag_prefix("GenesisLiquidity")
.and_provides((0, set))
.longevity(u64::MAX)
.propagate(true)
.build()
}
Call::remove_coin_liquidity { .. } => Err(InvalidTransaction::Call)?,
Call::__Ignore(_, _) => unreachable!(),
}
}
}
}
pub use pallet::*;

View File

@@ -0,0 +1,45 @@
[package]
name = "serai-genesis-liquidity-primitives"
version = "0.1.0"
description = "Serai genesis liquidity primitives"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity/primitives"
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
rust-version = "1.77"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
zeroize = { version = "^1.5", features = ["derive"], optional = true }
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../validator-sets/primitives", default-features = false }
[features]
std = [
"zeroize",
"scale/std",
"borsh?/std",
"serde?/std",
"scale-info/std",
"serai-primitives/std",
"validator-sets-primitives/std",
"sp-std/std"
]
default = ["std"]

View File

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

View File

@@ -0,0 +1,47 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
use zeroize::Zeroize;
#[cfg(feature = "borsh")]
use borsh::{BorshSerialize, BorshDeserialize};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use sp_std::vec::Vec;
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
use serai_primitives::*;
use validator_sets_primitives::ValidatorSet;
// amount of blocks in 30 days for 6s per block.
pub const BLOCKS_PER_MONTH: u32 = 10 * 60 * 24 * 30;
/// 180 days of blocks
pub const GENESIS_SRI_TRICKLE_FEED: u64 = 10 * 60 * 24 * 180;
// 100 Million SRI
pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8);
// This is the account to hold and manage the genesis liquidity.
pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"Genesis-liquidity-account");
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Zeroize))]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Prices {
pub bitcoin: u64,
pub monero: u64,
pub ethereum: u64,
pub dai: u64,
}
/// The message for the set_initial_price signature.
pub fn set_initial_price_message(set: &ValidatorSet, prices: &Prices) -> Vec<u8> {
(b"GenesisLiquidity-set_initial_price", set, prices).encode()
}

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ use sc_service::ChainType;
use serai_runtime::{
primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig,
CoinsConfig, DexConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig,
GenesisLiquidityConfig,
};
pub type ChainSpec = sc_service::GenericChainSpec<RuntimeGenesisConfig>;
@@ -60,6 +61,7 @@ fn devnet_genesis(
.collect(),
participants: validators.clone(),
},
genesis_liquidity: GenesisLiquidityConfig { participants: validators.clone() },
signals: SignalsConfig::default(),
babe: BabeConfig {
authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(),
@@ -111,6 +113,7 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime
.collect(),
participants: validators.clone(),
},
genesis_liquidity: GenesisLiquidityConfig { participants: validators.clone() },
signals: SignalsConfig::default(),
babe: BabeConfig {
authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(),

View File

@@ -59,6 +59,7 @@ coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", defau
dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false }
genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity/pallet", default-features = false }
in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false }
@@ -112,6 +113,7 @@ std = [
"dex-pallet/std",
"validator-sets-pallet/std",
"genesis-liquidity-pallet/std",
"in-instructions-pallet/std",
@@ -124,7 +126,7 @@ std = [
"pallet-transaction-payment-rpc-runtime-api/std",
]
fast-epoch = []
fast-epoch = ["genesis-liquidity-pallet/fast-epoch"]
runtime-benchmarks = [
"sp-runtime/runtime-benchmarks",

View File

@@ -11,7 +11,6 @@ use core::marker::PhantomData;
// Re-export all components
pub use serai_primitives as primitives;
pub use primitives::{BlockNumber, Header};
use primitives::{NetworkId, NETWORKS};
pub use frame_system as system;
pub use frame_support as support;
@@ -32,6 +31,8 @@ pub use signals_pallet as signals;
pub use pallet_babe as babe;
pub use pallet_grandpa as grandpa;
pub use genesis_liquidity_pallet as genesis_liquidity;
// Actually used by the runtime
use sp_core::OpaqueMetadata;
use sp_std::prelude::*;
@@ -47,7 +48,7 @@ use sp_runtime::{
BoundedVec, Perbill, ApplyExtrinsicResult,
};
use primitives::{PublicKey, AccountLookup, SubstrateAmount};
use primitives::{NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS};
use support::{
traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains},
@@ -175,6 +176,9 @@ impl Contains<RuntimeCall> for CallFilter {
},
RuntimeCall::Dex(call) => !matches!(call, dex::Call::__Ignore(_, _)),
RuntimeCall::ValidatorSets(call) => !matches!(call, validator_sets::Call::__Ignore(_, _)),
RuntimeCall::GenesisLiquidity(call) => {
!matches!(call, genesis_liquidity::Call::__Ignore(_, _))
}
RuntimeCall::InInstructions(call) => !matches!(call, in_instructions::Call::__Ignore(_, _)),
RuntimeCall::Signals(call) => !matches!(call, signals::Call::__Ignore(_, _)),
@@ -288,6 +292,10 @@ impl in_instructions::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl genesis_liquidity::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
// for publishing equivocation evidences.
impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
where
@@ -314,7 +322,7 @@ pub type ReportLongevity = <Runtime as pallet_babe::Config>::EpochDuration;
impl babe::Config for Runtime {
#[cfg(feature = "fast-epoch")]
type EpochDuration = ConstU64<{ MINUTES / 2 }>; // 30 seconds
type EpochDuration = ConstU64<{ HOURS / 2 }>; // 30 minutes
#[cfg(not(feature = "fast-epoch"))]
type EpochDuration = ConstU64<{ 4 * 7 * DAYS }>;
@@ -362,6 +370,7 @@ construct_runtime!(
Coins: coins,
LiquidityTokens: coins::<Instance1>::{Pallet, Call, Storage, Event<T>},
Dex: dex,
GenesisLiquidity: genesis_liquidity,
ValidatorSets: validator_sets,
@@ -628,4 +637,28 @@ sp_api::impl_runtime_apis! {
}
}
}
impl dex::DexApi<Block> for Runtime {
fn quote_price_exact_tokens_for_tokens(
asset1: Coin,
asset2: Coin,
amount: SubstrateAmount,
include_fee: bool
) -> Option<SubstrateAmount> {
Dex::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee)
}
fn quote_price_tokens_for_exact_tokens(
asset1: Coin,
asset2: Coin,
amount: SubstrateAmount,
include_fee: bool
) -> Option<SubstrateAmount> {
Dex::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee)
}
fn get_reserves(asset1: Coin, asset2: Coin) -> Option<(SubstrateAmount, SubstrateAmount)> {
Dex::get_reserves(&asset1, &asset2).ok()
}
}
}