mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-03 17:40:34 +00:00
Ensure economic security on validator sets (#459)
* add price oracle * tidy up * add todo * bug fixes * fix pr comments * Use spot price, tweak some formulas Also cleans nits. --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
parent
746bf5c6ad
commit
4ebfae0b63
9 changed files with 211 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -7716,6 +7716,7 @@ dependencies = [
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"scale-info",
|
"scale-info",
|
||||||
"serai-coins-pallet",
|
"serai-coins-pallet",
|
||||||
|
"serai-dex-pallet",
|
||||||
"serai-primitives",
|
"serai-primitives",
|
||||||
"serai-validator-sets-primitives",
|
"serai-validator-sets-primitives",
|
||||||
"sp-application-crypto",
|
"sp-application-crypto",
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
use serai_primitives::{Coin, SubstrateAmount, Balance};
|
||||||
|
|
||||||
|
pub trait AllowMint {
|
||||||
|
fn is_allowed(balance: &Balance) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllowMint for () {
|
||||||
|
fn is_allowed(_: &Balance) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[frame_support::pallet]
|
#[frame_support::pallet]
|
||||||
pub mod pallet {
|
pub mod pallet {
|
||||||
|
use super::*;
|
||||||
use sp_std::{vec::Vec, any::TypeId};
|
use sp_std::{vec::Vec, any::TypeId};
|
||||||
use sp_core::sr25519::Public;
|
use sp_core::sr25519::Public;
|
||||||
use sp_runtime::{
|
use sp_runtime::{
|
||||||
|
@ -23,6 +36,7 @@ pub mod pallet {
|
||||||
#[pallet::config]
|
#[pallet::config]
|
||||||
pub trait Config<I: 'static = ()>: frame_system::Config<AccountId = Public> {
|
pub trait Config<I: 'static = ()>: frame_system::Config<AccountId = Public> {
|
||||||
type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
type RuntimeEvent: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||||
|
type AllowMint: AllowMint;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::genesis_config]
|
#[pallet::genesis_config]
|
||||||
|
@ -43,6 +57,7 @@ pub mod pallet {
|
||||||
AmountOverflowed,
|
AmountOverflowed,
|
||||||
NotEnoughCoins,
|
NotEnoughCoins,
|
||||||
BurnWithInstructionNotAllowed,
|
BurnWithInstructionNotAllowed,
|
||||||
|
MintNotAllowed,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::event]
|
#[pallet::event]
|
||||||
|
@ -142,6 +157,10 @@ pub mod pallet {
|
||||||
///
|
///
|
||||||
/// Errors if any amount overflows.
|
/// Errors if any amount overflows.
|
||||||
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
|
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
|
||||||
|
if !T::AllowMint::is_allowed(&balance) {
|
||||||
|
Err(Error::<T, I>::MintNotAllowed)?;
|
||||||
|
}
|
||||||
|
|
||||||
// update the balance
|
// update the balance
|
||||||
Self::increase_balance_internal(to, balance)?;
|
Self::increase_balance_internal(to, balance)?;
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ pub use pallet::*;
|
||||||
|
|
||||||
use sp_runtime::{traits::TrailingZeroInput, DispatchError};
|
use sp_runtime::{traits::TrailingZeroInput, DispatchError};
|
||||||
|
|
||||||
use serai_primitives::{Coin, SubstrateAmount};
|
use serai_primitives::{NetworkId, Coin, SubstrateAmount};
|
||||||
|
|
||||||
use sp_std::prelude::*;
|
use sp_std::prelude::*;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
@ -154,11 +154,42 @@ pub mod pallet {
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfo<Coin>, OptionQuery>;
|
pub type Pools<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, PoolInfo<Coin>, OptionQuery>;
|
||||||
|
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn spot_price_for_block)]
|
||||||
|
pub type SpotPriceForBlock<T: Config> =
|
||||||
|
StorageDoubleMap<_, Identity, BlockNumberFor<T>, Identity, Coin, [u8; 8], ValueQuery>;
|
||||||
|
|
||||||
|
/// Moving window of oracle prices.
|
||||||
|
///
|
||||||
|
/// The second [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions
|
||||||
|
/// in this multi-set.
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn oracle_prices)]
|
||||||
|
pub type OraclePrices<T: Config> =
|
||||||
|
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>;
|
||||||
|
impl<T: Config> Pallet<T> {
|
||||||
|
// TODO: consider an algorithm which removes outliers? This algorithm might work a good bit
|
||||||
|
// better if we remove the bottom n values (so some value sustained over 90% of blocks instead
|
||||||
|
// of all blocks in the window).
|
||||||
|
/// Get the highest sustained value for this window.
|
||||||
|
/// This is actually the lowest price observed during the windows, as it's the price
|
||||||
|
/// all prices are greater than or equal to.
|
||||||
|
pub fn highest_sustained_price(coin: &Coin) -> Option<Amount> {
|
||||||
|
let mut iter = OraclePrices::<T>::iter_key_prefix(coin);
|
||||||
|
// the first key will be the lowest price due to the keys being lexicographically ordered.
|
||||||
|
iter.next().map(|amount| Amount(u64::from_be_bytes(amount)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn oracle_value)]
|
||||||
|
pub type OracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
|
||||||
|
|
||||||
// Pallet's events.
|
// Pallet's events.
|
||||||
#[pallet::event]
|
#[pallet::event]
|
||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
pub enum Event<T: Config> {
|
pub enum Event<T: Config> {
|
||||||
/// A successful call of the `CretaPool` extrinsic will create this event.
|
/// A successful call of the `CreatePool` extrinsic will create this event.
|
||||||
PoolCreated {
|
PoolCreated {
|
||||||
/// The pool id associated with the pool. Note that the order of the coins may not be
|
/// The pool id associated with the pool. Note that the order of the coins may not be
|
||||||
/// the same as the order specified in the create pool extrinsic.
|
/// the same as the order specified in the create pool extrinsic.
|
||||||
|
@ -240,6 +271,13 @@ pub mod pallet {
|
||||||
#[pallet::genesis_build]
|
#[pallet::genesis_build]
|
||||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||||
fn build(&self) {
|
fn build(&self) {
|
||||||
|
// assert that oracle windows size can fit into u16. Otherwise number of observants
|
||||||
|
// for a price in the `OraclePrices` map can overflow
|
||||||
|
// We don't want to make this const directly a u16 because it is used the block number
|
||||||
|
// calculations (which are done as u32s)
|
||||||
|
u16::try_from(ORACLE_WINDOW_SIZE).unwrap();
|
||||||
|
|
||||||
|
// create the pools
|
||||||
for coin in &self.pools {
|
for coin in &self.pools {
|
||||||
Pallet::<T>::create_pool(*coin).unwrap();
|
Pallet::<T>::create_pool(*coin).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -311,8 +349,64 @@ pub mod pallet {
|
||||||
|
|
||||||
#[pallet::hooks]
|
#[pallet::hooks]
|
||||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||||
fn integrity_test() {
|
fn on_finalize(n: BlockNumberFor<T>) {
|
||||||
assert!(T::MaxSwapPathLength::get() > 1, "the `MaxSwapPathLength` should be greater than 1",);
|
// we run this on on_finalize because we want to use the last price of the block for a coin.
|
||||||
|
// This prevents the exploit where a malicious block proposer spikes the price in either
|
||||||
|
// direction, then includes a swap in the other direction (ensuring they don't get arbitraged
|
||||||
|
// against)
|
||||||
|
// Since they'll have to leave the spike present at the end of the block, making the next
|
||||||
|
// block the one to include any arbitrage transactions (which there's no guarantee they'll
|
||||||
|
// produce), this cannot be done in a way without significant risk
|
||||||
|
for coin in Pools::<T>::iter_keys() {
|
||||||
|
// insert the new price to our oracle window
|
||||||
|
// The spot price for 1 coin, in atomic units, to SRI is used
|
||||||
|
let pool_id = Self::get_pool_id(coin, Coin::native()).ok().unwrap();
|
||||||
|
let pool_account = Self::get_pool_account(pool_id);
|
||||||
|
let sri_balance = Self::get_balance(&pool_account, Coin::native());
|
||||||
|
let coin_balance = Self::get_balance(&pool_account, coin);
|
||||||
|
// We use 1 coin to handle rounding errors which may occur with atomic units
|
||||||
|
// If we used atomic units, any coin whose atomic unit is worth less than SRI's atomic unit
|
||||||
|
// would cause a 'price' of 0
|
||||||
|
// If the decimals aren't large enough to provide sufficient buffer, use 10,000
|
||||||
|
let coin_decimals = coin.decimals().max(5);
|
||||||
|
let accuracy_increase =
|
||||||
|
HigherPrecisionBalance::from(SubstrateAmount::pow(10, coin_decimals));
|
||||||
|
let sri_per_coin = u64::try_from(
|
||||||
|
accuracy_increase * HigherPrecisionBalance::from(sri_balance) /
|
||||||
|
HigherPrecisionBalance::from(coin_balance),
|
||||||
|
)
|
||||||
|
.unwrap_or(u64::MAX);
|
||||||
|
let sri_per_coin = sri_per_coin.to_be_bytes();
|
||||||
|
|
||||||
|
SpotPriceForBlock::<T>::set(n, coin, sri_per_coin);
|
||||||
|
|
||||||
|
// Include this spot price into the multiset
|
||||||
|
{
|
||||||
|
let observed = OraclePrices::<T>::get(coin, sri_per_coin).unwrap_or(0);
|
||||||
|
OraclePrices::<T>::set(coin, sri_per_coin, Some(observed + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// pop the earliest key from the window once we reach its full size.
|
||||||
|
if n >= ORACLE_WINDOW_SIZE.into() {
|
||||||
|
let start_of_window = n - ORACLE_WINDOW_SIZE.into();
|
||||||
|
let start_spot_price = Self::spot_price_for_block(start_of_window, coin);
|
||||||
|
SpotPriceForBlock::<T>::remove(start_of_window, coin);
|
||||||
|
// Remove this price from the multiset
|
||||||
|
OraclePrices::<T>::mutate_exists(coin, start_spot_price, |v| {
|
||||||
|
*v = Some(v.unwrap() - 1);
|
||||||
|
if *v == Some(0) {
|
||||||
|
*v = None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the oracle value
|
||||||
|
let highest_sustained = Self::highest_sustained_price(&coin).unwrap_or(Amount(0));
|
||||||
|
let oracle_value = Self::oracle_value(coin).unwrap_or(Amount(0));
|
||||||
|
if highest_sustained > oracle_value {
|
||||||
|
OracleValue::<T>::set(coin, Some(highest_sustained));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,6 +432,14 @@ pub mod pallet {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A hook to be called whenever a network's session is rotated.
|
||||||
|
pub fn on_new_session(network: NetworkId) {
|
||||||
|
// reset the oracle value
|
||||||
|
for coin in network.coins() {
|
||||||
|
OracleValue::<T>::set(*coin, Self::highest_sustained_price(coin));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pallet's callable functions.
|
/// Pallet's callable functions.
|
||||||
|
|
|
@ -78,10 +78,12 @@ impl frame_system::Config for Test {
|
||||||
|
|
||||||
impl coins::Config for Test {
|
impl coins::Config for Test {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
|
type AllowMint = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl coins::Config<coins::Instance1> for Test {
|
impl coins::Config<coins::Instance1> for Test {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
|
type AllowMint = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config for Test {
|
impl Config for Test {
|
||||||
|
|
|
@ -23,6 +23,10 @@ use super::*;
|
||||||
use codec::{Decode, Encode, MaxEncodedLen};
|
use codec::{Decode, Encode, MaxEncodedLen};
|
||||||
use scale_info::TypeInfo;
|
use scale_info::TypeInfo;
|
||||||
|
|
||||||
|
/// This needs to be long enough for arbitrage to occur and make holding
|
||||||
|
/// any fake price up sufficiently unprofitable.
|
||||||
|
pub const ORACLE_WINDOW_SIZE: u32 = 1000;
|
||||||
|
|
||||||
/// Stores the lp_token coin id a particular pool has been assigned.
|
/// Stores the lp_token coin id a particular pool has been assigned.
|
||||||
#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
|
#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
|
||||||
pub struct PoolInfo<PoolCoinId> {
|
pub struct PoolInfo<PoolCoinId> {
|
||||||
|
|
|
@ -25,6 +25,16 @@ pub enum NetworkId {
|
||||||
Ethereum,
|
Ethereum,
|
||||||
Monero,
|
Monero,
|
||||||
}
|
}
|
||||||
|
impl NetworkId {
|
||||||
|
pub fn coins(&self) -> &'static [Coin] {
|
||||||
|
match self {
|
||||||
|
Self::Serai => &[Coin::Serai],
|
||||||
|
Self::Bitcoin => &[Coin::Bitcoin],
|
||||||
|
Self::Ethereum => &[Coin::Ether, Coin::Dai],
|
||||||
|
Self::Monero => &[Coin::Monero],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const NETWORKS: [NetworkId; 4] =
|
pub const NETWORKS: [NetworkId; 4] =
|
||||||
[NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero];
|
[NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero];
|
||||||
|
|
|
@ -224,10 +224,12 @@ impl transaction_payment::Config for Runtime {
|
||||||
|
|
||||||
impl coins::Config for Runtime {
|
impl coins::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
|
type AllowMint = ValidatorSets;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl coins::Config<coins::Instance1> for Runtime {
|
impl coins::Config<coins::Instance1> for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
|
type AllowMint = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl dex::Config for Runtime {
|
impl dex::Config for Runtime {
|
||||||
|
|
|
@ -33,6 +33,7 @@ serai-primitives = { path = "../../primitives", default-features = false }
|
||||||
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../primitives", default-features = false }
|
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../primitives", default-features = false }
|
||||||
|
|
||||||
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
|
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
|
||||||
|
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
std = [
|
||||||
|
@ -56,6 +57,7 @@ std = [
|
||||||
"validator-sets-primitives/std",
|
"validator-sets-primitives/std",
|
||||||
|
|
||||||
"coins-pallet/std",
|
"coins-pallet/std",
|
||||||
|
"dex-pallet/std",
|
||||||
]
|
]
|
||||||
|
|
||||||
runtime-benchmarks = [
|
runtime-benchmarks = [
|
||||||
|
|
|
@ -22,7 +22,8 @@ pub mod pallet {
|
||||||
pub use validator_sets_primitives as primitives;
|
pub use validator_sets_primitives as primitives;
|
||||||
use primitives::*;
|
use primitives::*;
|
||||||
|
|
||||||
use coins_pallet::Pallet as Coins;
|
use coins_pallet::{Pallet as Coins, AllowMint};
|
||||||
|
use dex_pallet::Pallet as Dex;
|
||||||
|
|
||||||
use pallet_babe::{Pallet as Babe, AuthorityId as BabeAuthorityId};
|
use pallet_babe::{Pallet as Babe, AuthorityId as BabeAuthorityId};
|
||||||
use pallet_grandpa::{Pallet as Grandpa, AuthorityId as GrandpaAuthorityId};
|
use pallet_grandpa::{Pallet as Grandpa, AuthorityId as GrandpaAuthorityId};
|
||||||
|
@ -31,6 +32,7 @@ pub mod pallet {
|
||||||
pub trait Config:
|
pub trait Config:
|
||||||
frame_system::Config<AccountId = Public>
|
frame_system::Config<AccountId = Public>
|
||||||
+ coins_pallet::Config
|
+ coins_pallet::Config
|
||||||
|
+ dex_pallet::Config
|
||||||
+ pallet_babe::Config
|
+ pallet_babe::Config
|
||||||
+ pallet_grandpa::Config
|
+ pallet_grandpa::Config
|
||||||
+ TypeInfo
|
+ TypeInfo
|
||||||
|
@ -333,6 +335,8 @@ pub mod pallet {
|
||||||
|
|
||||||
impl<T: Config> Pallet<T> {
|
impl<T: Config> Pallet<T> {
|
||||||
fn new_set(network: NetworkId) {
|
fn new_set(network: NetworkId) {
|
||||||
|
// TODO: prevent new set if it doesn't have enough stake for economic security.
|
||||||
|
|
||||||
// Update CurrentSession
|
// Update CurrentSession
|
||||||
let session = {
|
let session = {
|
||||||
let new_session = CurrentSession::<T>::get(network)
|
let new_session = CurrentSession::<T>::get(network)
|
||||||
|
@ -411,6 +415,8 @@ pub mod pallet {
|
||||||
BadSignature,
|
BadSignature,
|
||||||
/// Validator wasn't registered or active.
|
/// Validator wasn't registered or active.
|
||||||
NonExistentValidator,
|
NonExistentValidator,
|
||||||
|
/// Deallocation would take the stake below what is required.
|
||||||
|
DeallocationWouldRemoveEconomicSecurity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::hooks]
|
#[pallet::hooks]
|
||||||
|
@ -560,7 +566,16 @@ pub mod pallet {
|
||||||
account: T::AccountId,
|
account: T::AccountId,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
) -> Result<bool, DispatchError> {
|
) -> Result<bool, DispatchError> {
|
||||||
// TODO: Check it's safe to decrease this set's stake by this amount
|
// Check it's safe to decrease this set's stake by this amount
|
||||||
|
let new_total_staked = Self::total_allocated_stake(network)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.checked_sub(amount.0)
|
||||||
|
.ok_or(Error::<T>::NotEnoughAllocated)?;
|
||||||
|
let required_stake = Self::required_stake_for_network(network);
|
||||||
|
if new_total_staked < required_stake {
|
||||||
|
Err(Error::<T>::DeallocationWouldRemoveEconomicSecurity)?;
|
||||||
|
}
|
||||||
|
|
||||||
let old_allocation =
|
let old_allocation =
|
||||||
Self::allocation((network, account)).ok_or(Error::<T>::NonExistentValidator)?.0;
|
Self::allocation((network, account)).ok_or(Error::<T>::NonExistentValidator)?.0;
|
||||||
|
@ -680,6 +695,8 @@ pub mod pallet {
|
||||||
// - The current set was actually established with a completed handover protocol
|
// - The current set was actually established with a completed handover protocol
|
||||||
if (network == NetworkId::Serai) || Self::handover_completed(network, current_session) {
|
if (network == NetworkId::Serai) || Self::handover_completed(network, current_session) {
|
||||||
Pallet::<T>::new_set(network);
|
Pallet::<T>::new_set(network);
|
||||||
|
// let the Dex know session is rotated.
|
||||||
|
Dex::<T>::on_new_session(network);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -739,6 +756,39 @@ pub mod pallet {
|
||||||
validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(),
|
validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the required stake in terms SRI for a given `Balance`.
|
||||||
|
pub fn required_stake(balance: &Balance) -> SubstrateAmount {
|
||||||
|
use dex_pallet::HigherPrecisionBalance;
|
||||||
|
|
||||||
|
// This is inclusive to an increase in accuracy
|
||||||
|
let sri_per_coin = Dex::<T>::oracle_value(balance.coin).unwrap_or(Amount(0));
|
||||||
|
|
||||||
|
// See dex-pallet for the reasoning on these
|
||||||
|
let coin_decimals = balance.coin.decimals().max(5);
|
||||||
|
let accuracy_increase = HigherPrecisionBalance::from(SubstrateAmount::pow(10, coin_decimals));
|
||||||
|
|
||||||
|
let total_coin_value = u64::try_from(
|
||||||
|
HigherPrecisionBalance::from(balance.amount.0) *
|
||||||
|
HigherPrecisionBalance::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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current total required stake for a given `network`.
|
||||||
|
pub fn required_stake_for_network(network: NetworkId) -> SubstrateAmount {
|
||||||
|
let mut total_required = 0;
|
||||||
|
for coin in network.coins() {
|
||||||
|
let supply = Coins::<T>::supply(coin);
|
||||||
|
total_required += Self::required_stake(&Balance { coin: *coin, amount: Amount(supply) });
|
||||||
|
}
|
||||||
|
total_required
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::call]
|
#[pallet::call]
|
||||||
|
@ -866,6 +916,7 @@ pub mod pallet {
|
||||||
Err(Error::DeallocationWouldRemoveFaultTolerance) |
|
Err(Error::DeallocationWouldRemoveFaultTolerance) |
|
||||||
Err(Error::NonExistentDeallocation) |
|
Err(Error::NonExistentDeallocation) |
|
||||||
Err(Error::NonExistentValidator) |
|
Err(Error::NonExistentValidator) |
|
||||||
|
Err(Error::DeallocationWouldRemoveEconomicSecurity) |
|
||||||
Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
|
Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
|
||||||
Err(Error::__Ignore(_, _)) => unreachable!(),
|
Err(Error::__Ignore(_, _)) => unreachable!(),
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
|
@ -982,6 +1033,18 @@ pub mod pallet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Config> AllowMint for Pallet<T> {
|
||||||
|
fn is_allowed(balance: &Balance) -> bool {
|
||||||
|
// get the required stake
|
||||||
|
let current_required = Self::required_stake_for_network(balance.coin.network());
|
||||||
|
let new_required = current_required + Self::required_stake(balance);
|
||||||
|
|
||||||
|
// get the total stake for the network & compare.
|
||||||
|
let staked = Self::total_allocated_stake(balance.coin.network()).unwrap_or(Amount(0));
|
||||||
|
staked.0 >= new_required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Config> DisabledValidators for Pallet<T> {
|
impl<T: Config> DisabledValidators for Pallet<T> {
|
||||||
fn is_disabled(_: u32) -> bool {
|
fn is_disabled(_: u32) -> bool {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
Loading…
Reference in a new issue