diff --git a/Cargo.lock b/Cargo.lock index 13f334f04a..18ae550c0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6786,6 +6786,7 @@ dependencies = [ "log", "mangata-support", "mangata-types", + "mockall", "orml-tokens", "orml-traits", "pallet-bootstrap", @@ -7381,7 +7382,6 @@ dependencies = [ [[package]] name = "parachain-staking" version = "3.0.0" -source = "git+https://github.com/mangata-finance//moonbeam?branch=mangata-dev#2ac7a339be795ee58603e3cb68e65fedc1b1c295" dependencies = [ "aquamarine", "frame-benchmarking", diff --git a/Cargo.toml b/Cargo.toml index aeae1a639f..c8c03cba34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,8 +55,8 @@ orml-tokens = { git = "https://github.com/mangata-finance//open-runtime-module-l # patch generated by ./scripts/dev_manifest.sh [patch."https://github.com/mangata-finance/moonbeam"] # parachain-staking = { git = "https://github.com/mangata-finance//moonbeam", branch = "feature/update-staking-benchmarks" } -parachain-staking = { git = "https://github.com/mangata-finance//moonbeam", branch = "mangata-dev" } -# parachain-staking = { path = "../moonbeam/pallets/parachain-staking" } +# parachain-staking = { git = "https://github.com/mangata-finance//moonbeam", branch = "mangata-dev" } +parachain-staking = { path = "../moonbeam/pallets/parachain-staking" } [patch."https://github.com/mangata-finance/crowdloan-rewards"] pallet-crowdloan-rewards = { git = "https://github.com/mangata-finance//crowdloan-rewards", branch = "mangata-dev" } diff --git a/pallets/proof-of-stake/Cargo.toml b/pallets/proof-of-stake/Cargo.toml index 68feb0a224..077522c1fd 100644 --- a/pallets/proof-of-stake/Cargo.toml +++ b/pallets/proof-of-stake/Cargo.toml @@ -41,6 +41,7 @@ lazy_static = "1.1.1" env_logger = "0.9.0" serial_test = { version = "0.6.0", default-features = false } test-case = "2.0.2" +mockall = "0.11.0" [features] default = ['std'] diff --git a/pallets/proof-of-stake/src/lib.rs b/pallets/proof-of-stake/src/lib.rs index 2ad99bcd19..bafc0ccd8c 100644 --- a/pallets/proof-of-stake/src/lib.rs +++ b/pallets/proof-of-stake/src/lib.rs @@ -3,10 +3,13 @@ use frame_benchmarking::Zero; use frame_support::{ dispatch::{DispatchError, DispatchResult}, - ensure, + ensure }; use frame_system::ensure_signed; +use frame_support::storage::bounded_btree_map::BoundedBTreeMap; use sp_core::U256; +use sp_runtime::traits::AccountIdConversion; +use mangata_support::traits::Valuate; use frame_support::{ pallet_prelude::*, @@ -54,9 +57,14 @@ pub use weights::WeightInfo; type AccountIdOf = ::AccountId; +const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"rewards!"); + #[frame_support::pallet] pub mod pallet { - use super::*; + use frame_support::traits::Currency; +use mangata_support::traits::PoolCreateApi; + +use super::*; #[pallet::pallet] #[pallet::without_storage_info] @@ -66,7 +74,7 @@ pub mod pallet { impl Hooks for Pallet {} #[cfg(feature = "runtime-benchmarks")] - pub trait PoSBenchmarkingConfig: pallet_issuance::Config {} + pub trait PoSBenchmarkingConrfig: pallet_issuance::Config {} #[cfg(feature = "runtime-benchmarks")] impl PoSBenchmarkingConfig for T {} @@ -90,7 +98,10 @@ pub mod pallet { type LiquidityMiningIssuanceVault: Get; #[pallet::constant] type RewardsDistributionPeriod: Get; + type RewardsSchedulesLimit: Get; + type MinRewardsPerSession: Get; type WeightInfo: WeightInfo; + type ValuationApi: Valuate; } #[pallet::error] @@ -112,6 +123,10 @@ pub mod pallet { CalculateRewardsAllMathError, MissingRewardsInfoError, DeprecatedExtrinsic, + CannotScheduleRewardsInPast, + PoolDoesNotExist, + TooManySchedules, + TooLittleRewardsPerSession, } #[pallet::event] @@ -135,6 +150,17 @@ pub mod pallet { ValueQuery, >; + // #[pallet::storage] + // pub type UserRewards3rdPartyInfo = StorageDoubleMap< + // _, + // Twox64Concat, + // AccountIdOf, + // Twox64Concat, + // (TokenId, TokenId), + // RewardInfo, + // ValueQuery, + // >; + #[pallet::storage] /// Stores information about pool weight and accumulated rewards. The accumulated /// rewards amount is the number of rewards that can be claimed per liquidity @@ -143,6 +169,26 @@ pub mod pallet { pub type PromotedPoolRewards = StorageValue<_, BTreeMap, ValueQuery>; + #[pallet::storage] + pub type PromotedPoolRewards3rdParty = StorageValue<_, BTreeMap, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn schedules)] + pub type RewardsSchedules = + StorageValue<_, BoundedBTreeMap<(T::BlockNumber, TokenId, TokenId, Balance, u64), (), T::RewardsSchedulesLimit>, ValueQuery>; + + #[pallet::storage] + pub type ScheduleId = StorageValue<_, u64, ValueQuery>; + + + // #[pallet::storage] + // pub type Rewards3rdParty = + // StorageValue<_, BTreeMap, ValueQuery>; + // + // #[pallet::storage] + // pub type Rewards3rdParty = + // StorageValue<_, BTreeMap, ValueQuery>; + #[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, Eq, TypeInfo)] /// Information about single token rewards pub struct PromotedPools { @@ -242,10 +288,88 @@ pub mod pallet { amount, ) } + + /// Schedules rewards for selected liquidity token + /// - tokens - pair of tokens + /// - amount - amount of the token + /// - schedule_end - till when the rewards should be distributed, rewards are + /// distributed starting from the *next* session till + #[transactional] + #[pallet::call_index(4)] + #[pallet::weight(<::WeightInfo>::claim_rewards_all())] + pub fn reward_pool( + origin: OriginFor, + pool: (TokenId, TokenId), + token_id: TokenId, + amount: Balance, + schedule_end: T::BlockNumber, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + let rewarded_token = ::ValuationApi::get_liquidity_asset(pool.0, pool.1) + .map_err(|_| Error::::PoolDoesNotExist)?; + + let current_session = Self::session_index(); + ensure!( + schedule_end.saturated_into::() > current_session, + Error::::CannotScheduleRewardsInPast + ); + + let amount_per_session = schedule_end.saturated_into::() + .checked_sub(current_session) + .and_then(|v| amount.checked_div(v.into())) + .ok_or(Error::::MathOverflow)?; + + // TODO: use valuation instead amount directly + ensure!(amount_per_session >= T::MinRewardsPerSession::get(), Error::::TooLittleRewardsPerSession); + + + T::Currency::transfer( + token_id.into(), + &sender, + &Self::pallet_account(), + amount.into(), + ExistenceRequirement::KeepAlive, + )?; + + let current_session = Self::session_index(); + let schedule_id = ScheduleId::::get(); + + RewardsSchedules::::try_mutate(|map| { + + let key: Option<(_,_,_,_,_)> = map + .first_key_value() + .map(|(x,y)| x.clone()); + + + if let Some(val) = key { + if current_session > val.0.saturated_into::() { + map.remove_entry(&val); + } + } + + map.try_insert((schedule_end, rewarded_token, token_id, amount_per_session, schedule_id), ()) + }).or(Err(Error::::TooManySchedules))?; + + ScheduleId::::mutate(|v| *v += 1); + + Ok(()) + } } } impl Pallet { + fn pallet_account() -> T::AccountId { + PALLET_ID.into_account_truncating() + } + + pub fn session_index() -> u32 { + frame_system::Pallet::::block_number() + .saturated_into::() + .checked_div(T::RewardsDistributionPeriod::get()) + .unwrap_or(0) + } + pub fn rewards_period() -> u32 { ::RewardsDistributionPeriod::get() } @@ -285,27 +409,32 @@ impl Pallet { Self::ensure_is_promoted_pool(liquidity_asset_id)?; let current_time: u32 = Self::get_current_rewards_time()?; let pool_ratio_current = Self::get_pool_rewards(liquidity_asset_id)?; - let mut rewards_info = RewardsInfo::::try_get(user.clone(), liquidity_asset_id) - .unwrap_or(RewardInfo { + let default_rewards = RewardInfo { activated_amount: 0_u128, rewards_not_yet_claimed: 0_u128, rewards_already_claimed: 0_u128, last_checkpoint: current_time, pool_ratio_at_last_checkpoint: pool_ratio_current, missing_at_last_checkpoint: U256::from(0u128), - }); + }; - let calc = RewardsCalculator::::new::( - liquidity_asset_id, - rewards_info, - )?; - let rewards_info = calc - .activate_more(liquidity_assets_added) - .map_err(|err| Into::>::into(err))?; - RewardsInfo::::insert(user.clone(), liquidity_asset_id, rewards_info); + // Curved rewards + { + let mut rewards_info = RewardsInfo::::try_get(user.clone(), liquidity_asset_id) + .unwrap_or(default_rewards); + let calc = RewardsCalculator::::new::( + liquidity_asset_id, + rewards_info, + )?; + let rewards_info = calc + .activate_more(liquidity_assets_added) + .map_err(|err| Into::>::into(err))?; + + RewardsInfo::::insert(user.clone(), liquidity_asset_id, rewards_info); + } + - //TODO: refactor storage name TotalActivatedLiquidity::::try_mutate(liquidity_asset_id, |active_amount| { if let Some(val) = active_amount.checked_add(liquidity_assets_added) { *active_amount = val; @@ -513,6 +642,34 @@ impl ProofOfStakeRewardsApi for Pallet { impl LiquidityMiningApi for Pallet { /// Distributs liquidity mining rewards between all the activated tokens based on their weight fn distribute_rewards(liquidity_mining_rewards: Balance) { + + let schedules = RewardsSchedules::::get(); + let mut pools = PromotedPoolRewards3rdParty::::get(); + + let it = schedules + .iter() + .filter_map(|((session, rewarded_token, tokenid, amount, _),())|{ + if (*session).saturated_into::() <= Self::session_index() { + Some((rewarded_token, tokenid, amount)) + } else { + None + } + }); + + for (staked_token, token, amount) in it { + let activated_amount = Self::total_activated_amount(token); + let rewards = pools.get(token).cloned().unwrap_or_default(); + let rewards_for_liquidity = U256::from(*amount) + .checked_mul(U256::from(u128::MAX)) + .and_then(|x| x.checked_div(activated_amount.into())) + .and_then(|x| x.checked_add(rewards)); + + if let Some(val) = rewards_for_liquidity { + pools.insert(*token, val); + } + } + + let _ = PromotedPoolRewards::::try_mutate(|promoted_pools| -> DispatchResult { // benchmark with max of X prom pools let activated_pools: Vec<_> = promoted_pools diff --git a/pallets/proof-of-stake/src/mock.rs b/pallets/proof-of-stake/src/mock.rs index fa95825027..651feb810d 100644 --- a/pallets/proof-of-stake/src/mock.rs +++ b/pallets/proof-of-stake/src/mock.rs @@ -12,7 +12,7 @@ use sp_runtime::{ use crate as pos; use frame_support::{ construct_runtime, parameter_types, - traits::{tokens::currency::MultiTokenCurrency, ConstU32, Contains, Everything}, + traits::{tokens::currency::MultiTokenCurrency, ConstU32, ConstU128, Contains, Everything}, PalletId, }; @@ -202,6 +202,45 @@ lazy_static::lazy_static! { }; } +mockall::mock! { + pub ValuationApi {} + + impl Valuate for ValuationApi { + type Balance = Balance; + type CurrencyId = TokenId; + + fn get_liquidity_asset( + first_asset_id: TokenId, + second_asset_id: TokenId, + ) -> Result; + + fn get_liquidity_token_mga_pool( + liquidity_token_id: TokenId, + ) -> Result<(TokenId, TokenId), DispatchError>; + + fn valuate_liquidity_token( + liquidity_token_id: TokenId, + liquidity_token_amount: Balance, + ) -> Balance; + + fn scale_liquidity_by_mga_valuation( + mga_valuation: Balance, + liquidity_token_amount: Balance, + mga_token_amount: Balance, + ) -> Balance; + + fn get_pool_state(liquidity_token_id: TokenId) -> Option<(Balance, Balance)>; + + fn get_reserves( + first_asset_id: TokenId, + second_asset_id: TokenId, + ) -> Result<(Balance, Balance), DispatchError>; + + + + } +} + impl pos::Config for Test { type RuntimeEvent = RuntimeEvent; type ActivationReservesProvider = TokensActivationPassthrough; @@ -209,7 +248,10 @@ impl pos::Config for Test { type Currency = MultiTokenCurrencyAdapter; type LiquidityMiningIssuanceVault = FakeLiquidityMiningIssuanceVault; type RewardsDistributionPeriod = ConstU32<10>; + type RewardsSchedulesLimit = ConstU32<10>; + type MinRewardsPerSession = ConstU128<10>; type WeightInfo = (); + type ValuationApi = MockValuationApi; } pub struct TokensActivationPassthrough(PhantomData); @@ -327,3 +369,4 @@ macro_rules! assert_event_emitted { } }; } + diff --git a/pallets/proof-of-stake/src/tests.rs b/pallets/proof-of-stake/src/tests.rs index ea311c85d2..c11a2fac4e 100644 --- a/pallets/proof-of-stake/src/tests.rs +++ b/pallets/proof-of-stake/src/tests.rs @@ -3,6 +3,7 @@ #![allow(non_snake_case)] use super::*; +use serial_test::serial; use crate::mock::*; use frame_support::{assert_err, assert_ok}; @@ -34,6 +35,16 @@ fn initialize_liquidity_rewards() { ProofOfStake::activate_liquidity(RuntimeOrigin::signed(2), 4, 10000, None).unwrap(); } + +pub(crate) fn roll_to_session(n: u32) { + let block = n * Pallet::::rewards_period(); + + if block < System::block_number().saturated_into::() { + panic!("cannot roll to past block"); + } + forward_to_block(block); +} + fn forward_to_block(n: u32) { forward_to_block_with_custom_rewards(n, 10000); } @@ -41,7 +52,6 @@ fn forward_to_block(n: u32) { fn forward_to_block_with_custom_rewards(n: u32, rewards: u128) { while System::block_number().saturated_into::() <= n { if System::block_number().saturated_into::() % ProofOfStake::rewards_period() == 0 { - println!("NEW SESSION"); ProofOfStake::distribute_rewards(rewards); } System::set_block_number(System::block_number().saturated_into::() + 1); @@ -966,6 +976,7 @@ fn liquidity_rewards_transfered_liq_tokens_produce_rewards_W() { }); } + pub(crate) fn roll_to_while_minting(n: u64, expected_amount_minted: Option) { let mut session_number: u32; let mut session_issuance: (Balance, Balance); @@ -1090,3 +1101,232 @@ fn claim_rewards_from_pool_that_has_been_disabled() { assert_eq!(ProofOfStake::calculate_rewards_amount(2, 4).unwrap(), 0); }); } + +const MILLION: u128 = 1_000_000; +const ALICE: u128 = 2; +const BOB: u128 = 3; + +#[test] +#[serial] +fn user_can_provide_3rdparty_rewards() { + new_test_ext().execute_with(|| { + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + System::set_block_number(1); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 10u32.into()).unwrap(); + roll_to_session(5); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 6u32.into()).unwrap(); + }); +} + +#[test] +#[serial] +fn cant_schedule_rewards_in_past() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + roll_to_session(5); + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair,token_id, amount, 1u32.into()), + Error::::CannotScheduleRewardsInPast + ); + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair,token_id, amount, 4u32.into()), + Error::::CannotScheduleRewardsInPast + ); + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair,token_id, amount, 5u32.into()), + Error::::CannotScheduleRewardsInPast + ); + }); +} + +#[test] +#[serial] +fn cannot_reward_unexisting_pool() { + new_test_ext().execute_with(|| { + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Err(Error::::PoolDoesNotExist.into())); + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()), + Error::::PoolDoesNotExist + ); + + }); +} + +#[test] +#[serial] +fn rewards_are_stored_in_pallet_account() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + assert_eq!(TokensOf::::free_balance(token_id, &Pallet::::pallet_account()), 0); + assert_eq!(TokensOf::::free_balance(token_id, &ALICE), MILLION); + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()), + ); + + assert_eq!(TokensOf::::free_balance(token_id, &ALICE), MILLION - amount); + assert_eq!(TokensOf::::free_balance(token_id, &Pallet::::pallet_account()), amount); + }); +} + +#[test] +#[serial] +fn rewards_schedule_is_stored() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let liquidity_token_id = 10u32; + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(liquidity_token_id)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()), + ); + + assert_eq!( + ProofOfStake::schedules().into_inner(), + BTreeMap::from([((5u64, liquidity_token_id, token_id, amount/5, 0), ())]) + ); + + }); +} + +#[test] +#[serial] +fn number_of_active_schedules_is_limited() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + + let max_schedules: u32 = <::RewardsSchedulesLimit as sp_core::Get<_>>::get(); + for i in 0..(max_schedules) { + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, (5u32 + i).into()) + ); + } + + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 100u32.into()), + Error::::TooManySchedules + ); + + roll_to_session(10); + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 100u32.into()) + ); + + }); +} + +#[test] +#[serial] +fn duplicated_schedules_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()) + ); + + assert_eq!(1, ProofOfStake::schedules().len()); + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()) + ); + assert_eq!(2, ProofOfStake::schedules().len()); + + }); +} + + +#[test] +#[serial] +fn reject_schedule_with_too_little_rewards_per_session() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + roll_to_session(4); + + let min_rewards = <::MinRewardsPerSession as sp_core::Get>::get(); + + assert_err!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, min_rewards - 1, 5u32.into()), + Error::::TooLittleRewardsPerSession + ); + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, min_rewards, 5u32.into()) + ); + }); +} + + +#[test] +#[serial] +fn user_can_claim_3rdparty_rewards() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(10u32)); + + let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + + assert_ok!( + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()) + ); + + }); +} diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 7eb1bfdb64..69c8dd3170 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -1150,13 +1150,13 @@ where /// Reward payments delay (number of rounds) pub const RewardPaymentDelay: u32 = 2; /// Minimum collators selected per round, default at genesis and minimum forever after - pub const MinSelectedCandidates: u32 = 25; + pub const MinSelectedCandidates: u32 = 50; /// Maximum collator candidates allowed - pub const MaxCollatorCandidates: u32 = 50; + pub const MaxCollatorCandidates: u32 = 100; /// Maximum delegators allowed per candidate - pub const MaxTotalDelegatorsPerCandidate: u32 = 25; + pub const MaxTotalDelegatorsPerCandidate: u32 = 30; /// Maximum delegators counted per candidate - pub const MaxDelegatorsPerCandidate: u32 = 12; + pub const MaxDelegatorsPerCandidate: u32 = 30; /// Maximum delegations per delegator pub const MaxDelegationsPerDelegator: u32 = 30; /// Default fixed percent a collator takes off the top of due rewards