diff --git a/Cargo.lock b/Cargo.lock index e3091540f1135..c3dbc506f0e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,7 @@ dependencies = [ "xpallet-contracts", "xpallet-contracts-primitives", "xpallet-contracts-rpc-runtime-api", + "xpallet-mining-staking", "xpallet-protocol", "xpallet-system", "xpallet-transaction-payment", @@ -6868,6 +6869,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xp-staking" +version = "0.1.0" +dependencies = [ + "chainx-primitives", + "sp-runtime", + "sp-std", +] + [[package]] name = "xpallet-assets" version = "0.1.0" @@ -7009,6 +7019,27 @@ dependencies = [ "xpallet-contracts-primitives", ] +[[package]] +name = "xpallet-mining-staking" +version = "0.1.0" +dependencies = [ + "chainx-primitives", + "env_logger", + "frame-support", + "frame-system", + "parity-scale-codec", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "xp-staking", + "xpallet-assets", + "xpallet-protocol", + "xpallet-support", +] + [[package]] name = "xpallet-protocol" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1f7705f2cea4f..8b32040f337d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "cli", "runtime", "primitives", + "primitives/staking", "rpc", "xpallets/assets", @@ -23,6 +24,7 @@ members = [ "xpallets/contracts/common", "xpallets/contracts/rpc", "xpallets/contracts/rpc/runtime-api", + "xpallets/mining/staking", "xpallets/protocol", "xpallets/support", "xpallets/transaction-payment", diff --git a/cli/src/chain_spec.rs b/cli/src/chain_spec.rs index 4fb50f878b5ff..9c52dd11b4832 100644 --- a/cli/src/chain_spec.rs +++ b/cli/src/chain_spec.rs @@ -7,7 +7,7 @@ use chainx_runtime::{ use chainx_runtime::{AccountId, AssetId, Balance, Runtime, Signature, WASM_BINARY}; use chainx_runtime::{ AuraConfig, GenesisConfig, GrandpaConfig, SudoConfig, SystemConfig, XAssetsConfig, - XBridgeBitcoinConfig, XContractsConfig, XSystemConfig, + XBridgeBitcoinConfig, XContractsConfig, XMiningStakingConfig, XSystemConfig, }; use sc_service::ChainType; @@ -146,7 +146,8 @@ fn pcx() -> (AssetId, AssetInfo, AssetRestrictions) { AssetRestriction::Deposit | AssetRestriction::Withdraw | AssetRestriction::DestroyWithdrawal - | AssetRestriction::DestroyFree, + | AssetRestriction::DestroyFree + | AssetRestriction::Move, ) } @@ -220,6 +221,7 @@ fn testnet_genesis( btc_withdrawal_fee: 500000, max_withdrawal_count: 100, }), + xpallet_mining_staking: Some(Default::default()), xpallet_contracts: Some(XContractsConfig { current_schedule: ContractsSchedule { enable_println, // this should only be enabled on development chains diff --git a/primitives/staking/Cargo.toml b/primitives/staking/Cargo.toml new file mode 100644 index 0000000000000..16bf71510445e --- /dev/null +++ b/primitives/staking/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "xp-staking" +version = "0.1.0" +authors = ["ChainX community "] +edition = "2018" + +[dependencies] +# Substrate primitives +sp-std = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } + +# ChainX primitives +chainx-primitives = { path = "../../primitives", default-features = false } + +[features] +default = ["std"] +std = [ + "sp-std/std", + "sp-runtime/std", + + "chainx-primitives/std", +] diff --git a/primitives/staking/src/lib.rs b/primitives/staking/src/lib.rs new file mode 100644 index 0000000000000..3a80f85dffde6 --- /dev/null +++ b/primitives/staking/src/lib.rs @@ -0,0 +1,52 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! A crate which contains primitives that are useful for implementation that uses staking +//! approaches in general. Definitions related to sessions, slashing, etc go here. + +mod vote_weight; + +pub use vote_weight::*; + +use chainx_primitives::AssetId; +use sp_std::prelude::Vec; + +/// Simple index type with which we can count sessions. +pub type SessionIndex = u32; + +/// Simple index type with which we can count unbonded entries. +pub type UnbondedIndex = u32; + +/// Type for calculating staker's vote weight. +pub type VoteWeight = u128; + +/// Type for measuring the non-validator entity's mining power. +pub type MiningPower = u128; + +/// +pub trait CollectAssetMiningInfo { + /// + fn collect_asset_mining_info() -> Vec<(AssetId, MiningPower)>; + + /// + fn total_mining_power() -> MiningPower { + Self::collect_asset_mining_info() + .iter() + .map(|(_, power)| power) + .sum() + } +} + +impl CollectAssetMiningInfo for () { + fn collect_asset_mining_info() -> Vec<(AssetId, MiningPower)> { + Vec::new() + } +} + +/// Issue the fresh PCX to the non-validator mining entities. +pub trait OnMinting { + fn mint(_: &MiningEntity, _: Balance); +} + +impl OnMinting for () { + fn mint(_: &MiningEntity, _: Balance) {} +} diff --git a/primitives/staking/src/vote_weight.rs b/primitives/staking/src/vote_weight.rs new file mode 100644 index 0000000000000..054de99408a88 --- /dev/null +++ b/primitives/staking/src/vote_weight.rs @@ -0,0 +1,109 @@ +use crate::VoteWeight; +use sp_std::result::Result; + +/// The getter and setter methods for the further vote weight processing. +pub trait BaseVoteWeight { + fn amount(&self) -> u128; + fn set_amount(&mut self, new: u128); + + fn last_acum_weight(&self) -> VoteWeight; + fn set_last_acum_weight(&mut self, s: VoteWeight); + + fn last_acum_weight_update(&self) -> u32; + fn set_last_acum_weight_update(&mut self, num: BlockNumber); +} + +#[derive(Clone, Copy, sp_runtime::RuntimeDebug)] +pub enum Delta { + Add(u128), + Sub(u128), + Zero, +} + +/// General logic for stage changes of the vote weight operations. +pub trait VoteWightTrait: BaseVoteWeight { + /// Set the new amount after settling the change of nomination. + fn settle_and_set_amount(&mut self, delta: &Delta) { + let new = match *delta { + Delta::Add(x) => self.amount() + x, + Delta::Sub(x) => self.amount() - x, + Delta::Zero => return, + }; + self.set_amount(new); + } + + /// This action doesn't involve in a change of amount, used for tokens module only. + fn set_state_weight(&mut self, latest_acum_weight: VoteWeight, current_block: BlockNumber) { + self.set_last_acum_weight(latest_acum_weight); + self.set_last_acum_weight_update(current_block); + } + + /// Set new state on nominate, unnominate and renominate. + /// + /// This is similar to set_state_on_claim with the settlement of amount added. + fn set_state( + &mut self, + latest_acum_weight: VoteWeight, + current_block: BlockNumber, + delta: &Delta, + ) { + self.set_state_weight(latest_acum_weight, current_block); + self.settle_and_set_amount(delta); + } +} + +impl> VoteWightTrait for T {} + +/// Formula: Latest Vote Weight = last_acum_weight(VoteWeight) + amount(u64) * duration(u64) +pub type WeightFactors = (VoteWeight, u128, u32); + +pub struct ZeroVoteWeightError; + +pub trait ComputeVoteWeight { + /// The entity that holds the funds of claimers. + type Claimee; + type Error: From; + + fn claimer_weight_factors(_: &AccountId, _: &Self::Claimee, _: u32) -> WeightFactors; + fn claimee_weight_factors(_: &Self::Claimee, _: u32) -> WeightFactors; + + fn settle_claimer_weight( + who: &AccountId, + target: &Self::Claimee, + current_block: u32, + ) -> VoteWeight { + Self::calc_latest_vote_weight(Self::claimer_weight_factors(who, target, current_block)) + } + + fn settle_claimee_weight(target: &Self::Claimee, current_block: u32) -> VoteWeight { + Self::calc_latest_vote_weight(Self::claimee_weight_factors(target, current_block)) + } + + fn settle_weight_on_claim( + who: &AccountId, + target: &Self::Claimee, + current_block: u32, + ) -> Result<(VoteWeight, VoteWeight), Self::Error> { + let claimer_weight = Self::settle_claimer_weight(who, target, current_block); + + if claimer_weight == 0 { + return Err(ZeroVoteWeightError.into()); + } + + let claimee_weight = Self::settle_claimee_weight(target, current_block); + + Ok((claimer_weight, claimee_weight)) + } + + fn calc_latest_vote_weight(weight_factors: WeightFactors) -> VoteWeight { + let (last_acum_weight, amount, duration) = weight_factors; + last_acum_weight + VoteWeight::from(amount) * VoteWeight::from(duration) + } +} + +pub trait Claim { + type Claimee; + type Error; + + fn claim(claimer: &AccountId, claimee: &Self::Claimee) -> Result<(), Self::Error>; +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9266c4de744f4..c848509e14ea1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -50,6 +50,7 @@ xpallet-bridge-bitcoin = { path = "../xpallets/bridge/bitcoin", default-features xpallet-contracts = { path = "../xpallets/contracts", default-features = false } xpallet-contracts-primitives = { path = "../xpallets/contracts/common", default-features = false } xpallet-contracts-rpc-runtime-api = { path = "../xpallets/contracts/rpc/runtime-api", default-features = false } +xpallet-mining-staking = { path = "../xpallets/mining/staking", default-features = false } xpallet-transaction-payment = { path = "../xpallets/transaction-payment", default-features = false } xpallet-transaction-payment-rpc-runtime-api = { path = "../xpallets/transaction-payment/rpc/runtime-api", default-features = false } @@ -97,6 +98,7 @@ std = [ "xpallet-contracts/std", "xpallet-contracts-primitives/std", "xpallet-contracts-rpc-runtime-api/std", + "xpallet-mining-staking/std", "xpallet-transaction-payment/std", "xpallet-transaction-payment-rpc-runtime-api/std", ] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 409869b7c85a2..ee8728b5161b3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -300,6 +300,12 @@ impl xpallet_contracts::Trait for Runtime { type WeightPrice = xpallet_transaction_payment::Module; } +impl xpallet_mining_staking::Trait for Runtime { + type Event = Event; + type CollectAssetMiningInfo = (); + type OnMinting = (); +} + parameter_types! { pub const TransactionByteFee: Balance = 1; // TODO change in future pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(25); @@ -332,6 +338,7 @@ construct_runtime!( XAssets: xpallet_assets::{Module, Call, Storage, Event, Config}, XBridgeBitcoin: xpallet_bridge_bitcoin::{Module, Call, Storage, Event, Config}, XContracts: xpallet_contracts::{Module, Call, Config, Storage, Event}, + XMiningStaking: xpallet_mining_staking::{Module, Call, Storage, Event, Config}, XTransactionPayment: xpallet_transaction_payment::{Module, Storage}, } ); diff --git a/xpallets/assets/src/lib.rs b/xpallets/assets/src/lib.rs index ba9fdd3fa5a83..7554fba110e98 100644 --- a/xpallets/assets/src/lib.rs +++ b/xpallets/assets/src/lib.rs @@ -28,11 +28,11 @@ use frame_system::{self as system, ensure_root, ensure_signed}; // ChainX use chainx_primitives::{AssetId, Desc, Memo, Token}; -use xpallet_support::{debug, ensure_with_errorlog, info, str}; +use xpallet_support::{debug, ensure_with_errorlog, info}; -pub use self::traits::{ChainT, OnAssetChanged, OnAssetRegisterOrRevoke, TokenJackpotAccountIdFor}; use self::trigger::AssetTriggerEventAfter; +pub use self::traits::{ChainT, OnAssetChanged, OnAssetRegisterOrRevoke, TokenJackpotAccountIdFor}; pub use self::types::{ is_valid_desc, is_valid_token, AssetErr, AssetInfo, AssetRestriction, AssetRestrictions, AssetType, Chain, NegativeImbalance, PositiveImbalance, SignedBalance, SignedImbalanceT, @@ -104,9 +104,9 @@ decl_error! { /// only allow ASCII alphanumeric character InvalidAsscii, /// - AlreadyExistedToken, + AlreadyExistentToken, /// - NotExistdAsset, + NonexistentAsset, /// InvalidAsset, /// Got an overflow after adding @@ -118,12 +118,12 @@ decl_error! { /// Balance too low to send value TotalAssetInsufficientBalance, - - /// should not be free type here - NotAllowFreeType, - /// should not use chainx token here - NotAllowPcx, - NotAllowAction, + /// Free asset type is not allowed. + FreeTypeNotAllowed, + /// ChainX token is not allowed. + PcxNotAllowed, + /// Action is not allowed. + ActionNotAllowed, } } @@ -202,9 +202,10 @@ decl_module! { let transactor = ensure_signed(origin)?; debug!("[transfer]|from:{:?}|to:{:?}|id:{:}|value:{:?}|memo:{}", transactor, dest, id, value, memo); memo.check_validity()?; - Self::can_transfer(&id)?; - let _ = Self::move_free_balance(&id, &transactor, &dest, value).map_err::, _>(Into::into)?; + + Self::move_free_balance(&id, &transactor, &dest, value).map_err::, _>(Into::into)?; + Ok(()) } @@ -214,9 +215,9 @@ decl_module! { ensure_root(origin)?; debug!("[force_transfer]|from:{:?}|to:{:?}|id:{:}|value:{:?}|memo:{}", transactor, dest, id, value, memo); memo.check_validity()?; - Self::can_transfer(&id)?; - let _ = Self::move_free_balance(&id, &transactor, &dest, value).map_err::, _>(Into::into)?; + + Self::move_free_balance(&id, &transactor, &dest, value).map_err::, _>(Into::into)?; Ok(()) } @@ -304,26 +305,27 @@ impl Module { *is_online, *is_psedu_intention, ) - .expect("genesis for asset must success"); + .expect("asset registeration during the genesis can not fail"); } for (id, endowed) in endowed_accounts.iter() { for (accountid, value) in endowed.iter() { - Self::issue(id, accountid, *value).unwrap(); + Self::issue(id, accountid, *value) + .expect("asset issuance during the genesis can not fail"); } } } pub fn should_not_free_type(type_: AssetType) -> DispatchResult { if type_ == AssetType::Free { - Err(Error::::NotAllowFreeType)?; + Err(Error::::FreeTypeNotAllowed)?; } Ok(()) } pub fn should_not_chainx(id: &AssetId) -> DispatchResult { if *id == ::ASSET_ID { - Err(Error::::NotAllowPcx)?; + Err(Error::::PcxNotAllowed)?; } Ok(()) } @@ -335,12 +337,13 @@ impl Module { fn add_asset(id: AssetId, asset: AssetInfo, restrictions: AssetRestrictions) -> DispatchResult { let chain = asset.chain(); if Self::asset_info_of(&id).is_some() { - Err(Error::::AlreadyExistedToken)?; + Err(Error::::AlreadyExistentToken)?; } AssetInfoOf::insert(&id, asset); AssetRestrictionsOf::insert(&id, restrictions); AssetOnline::insert(&id, ()); + AssetRegisteredBlock::::insert(&id, system::Module::::block_number()); AssetIdsOf::mutate(chain, |v| { @@ -405,19 +408,20 @@ impl Module { Err(Error::::InvalidAsset)? } } else { - Err(Error::::NotExistdAsset)? + Err(Error::::NonexistentAsset)? } } pub fn can_do(id: &AssetId, limit: AssetRestriction) -> bool { Self::asset_restrictions_of(id).contains(limit) } + // can do wrapper #[inline] pub fn can_move(id: &AssetId) -> DispatchResult { ensure_with_errorlog!( Self::can_do(id, AssetRestriction::Move), - Error::::NotAllowAction, + Error::::ActionNotAllowed, "this asset do not allow move|id:{:}|action:{:?}", id, AssetRestriction::Move, @@ -429,7 +433,7 @@ impl Module { pub fn can_transfer(id: &AssetId) -> DispatchResult { ensure_with_errorlog!( Self::can_do(id, AssetRestriction::Transfer), - Error::::NotAllowAction, + Error::::ActionNotAllowed, "this asset do not allow transfer|id:{:}|action:{:?}", id, AssetRestriction::Transfer, @@ -441,7 +445,7 @@ impl Module { pub fn can_destroy_withdrawal(id: &AssetId) -> DispatchResult { ensure_with_errorlog!( Self::can_do(id, AssetRestriction::DestroyWithdrawal), - Error::::NotAllowAction, + Error::::ActionNotAllowed, "this asset do not allow destroy withdrawal|id:{:}|action:{:?}", id, AssetRestriction::DestroyWithdrawal, @@ -453,7 +457,7 @@ impl Module { pub fn can_destroy_free(id: &AssetId) -> DispatchResult { ensure_with_errorlog!( Self::can_do(id, AssetRestriction::DestroyFree), - Error::::NotAllowAction, + Error::::ActionNotAllowed, "this asset do not allow destroy free|id:{:}|action:{:?}", id, AssetRestriction::DestroyFree, @@ -491,39 +495,28 @@ impl Module { } pub fn issue(id: &AssetId, who: &T::AccountId, value: T::Balance) -> DispatchResult { - { - ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); + ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); - // may set storage inner - Self::try_new_account(&who, id); + // may set storage inner + Self::try_new_account(&who, id); - let type_ = AssetType::Free; - let _imbalance = Self::inner_issue(id, who, type_, value)?; - } + let _imbalance = Self::inner_issue(id, who, AssetType::Free, value)?; Ok(()) } pub fn destroy(id: &AssetId, who: &T::AccountId, value: T::Balance) -> DispatchResult { - { - ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); - Self::can_destroy_withdrawal(id)?; - - let type_ = AssetType::ReservedWithdrawal; + ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); + Self::can_destroy_withdrawal(id)?; - let _imbalance = Self::inner_destroy(id, who, type_, value)?; - } + let _imbalance = Self::inner_destroy(id, who, AssetType::ReservedWithdrawal, value)?; Ok(()) } pub fn destroy_free(id: &AssetId, who: &T::AccountId, value: T::Balance) -> DispatchResult { - { - ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); - Self::can_destroy_free(id)?; - - let type_ = AssetType::Free; + ensure!(Self::asset_online(id).is_some(), Error::::InvalidAsset); + Self::can_destroy_free(id)?; - let _imbalance = Self::inner_destroy(id, who, type_, value)?; - } + let _imbalance = Self::inner_destroy(id, who, AssetType::Free, value)?; Ok(()) } @@ -606,10 +599,7 @@ impl Module { id, who, type_, current, value ); // check - let new = match current.checked_add(&value) { - Some(b) => b, - None => Err(Error::::Overflow)?, - }; + let new = current.checked_add(&value).ok_or(Error::::Overflow)?; AssetTriggerEventAfter::::on_issue_before(id, who); @@ -637,10 +627,9 @@ impl Module { debug!("[destroy_directly]|destroy asset for account|id:{:}|who:{:?}|type:{:?}|current:{:?}|destroy:{:?}", id, who, type_, current, value); // check - let new = match current.checked_sub(&value) { - Some(b) => b, - None => Err(Error::::InsufficientBalance)?, - }; + let new = current + .checked_sub(&value) + .ok_or(Error::::InsufficientBalance)?; AssetTriggerEventAfter::::on_destroy_before(id, who); @@ -684,14 +673,10 @@ impl Module { id, from, from_type, from_balance, to, to_type, to_balance, value); // judge balance is enough and test overflow - let new_from_balance = match from_balance.checked_sub(&value) { - Some(b) => b, - None => return Err(AssetErr::NotEnough), - }; - let new_to_balance = match to_balance.checked_add(&value) { - Some(b) => b, - None => return Err(AssetErr::OverFlow), - }; + let new_from_balance = from_balance + .checked_sub(&value) + .ok_or(AssetErr::NotEnough)?; + let new_to_balance = to_balance.checked_add(&value).ok_or(AssetErr::OverFlow)?; // finish basic check, start self check if from == to && from_type == to_type { diff --git a/xpallets/assets/src/types.rs b/xpallets/assets/src/types.rs index 350bb6016349f..208bbd4d39d38 100644 --- a/xpallets/assets/src/types.rs +++ b/xpallets/assets/src/types.rs @@ -249,7 +249,7 @@ impl From for Error { AssetErr::TotalAssetNotEnough => Error::::TotalAssetInsufficientBalance, AssetErr::TotalAssetOverFlow => Error::::TotalAssetOverflow, AssetErr::InvalidAsset => Error::::InvalidAsset, - AssetErr::NotAllow => Error::::NotAllowAction, + AssetErr::NotAllow => Error::::ActionNotAllowed, } } } diff --git a/xpallets/mining/staking/Cargo.toml b/xpallets/mining/staking/Cargo.toml new file mode 100644 index 0000000000000..daa6c8051936b --- /dev/null +++ b/xpallets/mining/staking/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "xpallet-mining-staking" +version = "0.1.0" +authors = ["ChainX community "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.3.1", default-features = false, features = ["derive"] } +serde = { version = "1.0.101", optional = true } + +# Substrate primitives +sp-std = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } + +# Substrate pallets +frame-support = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4", default-features = false } + +# ChainX primitives +chainx-primitives = { path = "../../../primitives", default-features = false } +xp-staking = { path = "../../../primitives/staking", default-features = false } + +# ChainX pallets +xpallet-assets = { path = "../../assets", default-features = false } +xpallet-support = { path = "../../support", default-features = false } + +[dev-dependencies] +xpallet-protocol = { path = "../../protocol" } +sp-core = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4" } +sp-io = { git = "https://github.com/paritytech/substrate.git", tag = "v2.0.0-rc4" } +env_logger = "0.7.1" + +[features] +default = ["std"] +std = [ + "codec/std", + "serde", + + "sp-std/std", + "sp-runtime/std", + "sp-arithmetic/std", + + "frame-support/std", + "frame-system/std", + + "chainx-primitives/std", + "xp-staking/std", + + "xpallet-assets/std", + "xpallet-support/std", +] diff --git a/xpallets/mining/staking/src/impls.rs b/xpallets/mining/staking/src/impls.rs new file mode 100644 index 0000000000000..bf8e3c492330c --- /dev/null +++ b/xpallets/mining/staking/src/impls.rs @@ -0,0 +1,204 @@ +use super::*; +use sp_arithmetic::traits::BaseArithmetic; +use xp_staking::{BaseVoteWeight, Claim, ComputeVoteWeight, Delta, VoteWeight, WeightFactors}; + +impl BaseVoteWeight for ValidatorLedger +where + Balance: Default + BaseArithmetic + Copy, + BlockNumber: Default + BaseArithmetic + Copy, +{ + fn amount(&self) -> u128 { + self.total.saturated_into() + } + + fn set_amount(&mut self, new: u128) { + self.total = new.saturated_into(); + } + + fn last_acum_weight(&self) -> VoteWeight { + self.last_total_vote_weight + } + + fn set_last_acum_weight(&mut self, latest_vote_weight: VoteWeight) { + self.last_total_vote_weight = latest_vote_weight; + } + + fn last_acum_weight_update(&self) -> u32 { + self.last_total_vote_weight_update.saturated_into::() + } + + fn set_last_acum_weight_update(&mut self, current_block: BlockNumber) { + self.last_total_vote_weight_update = current_block; + } +} + +impl BaseVoteWeight for NominatorLedger +where + Balance: Default + BaseArithmetic + Copy, + BlockNumber: Default + BaseArithmetic + Copy, +{ + fn amount(&self) -> u128 { + self.nomination.saturated_into() + } + + fn set_amount(&mut self, new: u128) { + self.nomination = new.saturated_into(); + } + + fn last_acum_weight(&self) -> VoteWeight { + self.last_vote_weight + } + + fn set_last_acum_weight(&mut self, latest_vote_weight: VoteWeight) { + self.last_vote_weight = latest_vote_weight; + } + + fn last_acum_weight_update(&self) -> u32 { + self.last_vote_weight_update.saturated_into::() + } + + fn set_last_acum_weight_update(&mut self, current_block: BlockNumber) { + self.last_vote_weight_update = current_block; + } +} + +impl ComputeVoteWeight for Module { + type Claimee = T::AccountId; + type Error = Error; + + fn claimer_weight_factors( + who: &T::AccountId, + target: &Self::Claimee, + current_block: u32, + ) -> WeightFactors { + let claimer_ledger = Nominations::::get(who, target); + ( + claimer_ledger.last_vote_weight, + claimer_ledger.amount(), + current_block - claimer_ledger.last_acum_weight_update(), + ) + } + + fn claimee_weight_factors(target: &Self::Claimee, current_block: u32) -> WeightFactors { + let claimee_ledger = ValidatorLedgers::::get(target); + ( + claimee_ledger.last_total_vote_weight, + claimee_ledger.amount(), + current_block - claimee_ledger.last_acum_weight_update(), + ) + } +} + +/// Computes the dividend according to the ratio of source_vote_weight/target_vote_weight. +/// +/// dividend = source_vote_weight/target_vote_weight * balance_of(claimee_jackpot) +pub fn compute_dividend( + source_vote_weight: VoteWeight, + target_vote_weight: VoteWeight, + claimee_jackpot: &T::AccountId, +) -> T::Balance { + let total_jackpot = xpallet_assets::Module::::pcx_free_balance(&claimee_jackpot); + match source_vote_weight.checked_mul(total_jackpot.saturated_into()) { + Some(x) => ((x / target_vote_weight) as u64).saturated_into(), + None => panic!("source_vote_weight * total_jackpot overflow, this should not happen"), + } +} + +impl Module { + fn jackpot_account_for(validator: &T::AccountId) -> T::AccountId { + todo!() + } + + fn allocate_dividend( + claimer: &T::AccountId, + pot_account: &T::AccountId, + dividend: T::Balance, + ) -> Result<(), AssetErr> { + xpallet_assets::Module::::pcx_move_free_balance(pot_account, claimer, dividend) + } + + /// Calculates the new amount given the origin amount and delta + fn apply_delta(origin: T::Balance, delta: Delta) -> T::Balance { + match delta { + Delta::Add(v) => origin + v.saturated_into(), + Delta::Sub(v) => origin - v.saturated_into(), + Delta::Zero => origin, + } + } + + /// Actually update the nominator vote weight given the new vote weight, block number and amount delta. + pub(crate) fn set_nominator_vote_weight( + nominator: &T::AccountId, + validator: &T::AccountId, + new_weight: VoteWeight, + current_block: T::BlockNumber, + delta: Delta, + ) { + Nominations::::mutate(nominator, validator, |claimer_ledger| { + claimer_ledger.nomination = Self::apply_delta(claimer_ledger.nomination, delta); + claimer_ledger.last_vote_weight = new_weight; + claimer_ledger.last_vote_weight_update = current_block; + }); + } + + /// + pub(crate) fn set_validator_vote_weight( + validator: &T::AccountId, + new_weight: VoteWeight, + current_block: T::BlockNumber, + delta: Delta, + ) { + ValidatorLedgers::::mutate(validator, |validator_ledger| { + validator_ledger.total = Self::apply_delta(validator_ledger.total, delta); + validator_ledger.last_total_vote_weight = new_weight; + validator_ledger.last_total_vote_weight_update = current_block; + }); + } + + fn update_claimer_vote_weight_on_claim( + claimer: &T::AccountId, + target: &T::AccountId, + current_block: T::BlockNumber, + ) { + Self::set_nominator_vote_weight(claimer, target, 0, current_block, Delta::Zero); + } + + fn update_claimee_vote_weight_on_claim( + claimee: &T::AccountId, + new_vote_weight: VoteWeight, + current_block: T::BlockNumber, + ) { + Self::set_validator_vote_weight(claimee, new_vote_weight, current_block, Delta::Zero); + } +} + +impl Claim for Module { + type Claimee = T::AccountId; + type Error = Error; + + fn claim(claimer: &T::AccountId, claimee: &Self::Claimee) -> Result<(), Self::Error> { + let current_block = >::block_number(); + + let (source_weight, target_weight) = + >::settle_weight_on_claim( + claimer, + claimee, + current_block.saturated_into::(), + )?; + + let claimee_pot = Self::jackpot_account_for(claimee); + + let dividend = compute_dividend::(source_weight, target_weight, &claimee_pot); + + Self::allocate_dividend(claimer, &claimee_pot, dividend)?; + + Self::deposit_event(RawEvent::Claim(claimer.clone(), claimee.clone(), dividend)); + + let new_target_weight = target_weight - source_weight; + + Self::update_claimer_vote_weight_on_claim(claimer, claimee, current_block); + Self::update_claimee_vote_weight_on_claim(claimee, new_target_weight, current_block); + + Ok(()) + } +} diff --git a/xpallets/mining/staking/src/lib.rs b/xpallets/mining/staking/src/lib.rs new file mode 100644 index 0000000000000..692f6af735843 --- /dev/null +++ b/xpallets/mining/staking/src/lib.rs @@ -0,0 +1,572 @@ +//! # Staking Module + +#![cfg_attr(not(feature = "std"), no_std)] + +mod impls; +mod types; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +use chainx_primitives::AssetId; +use chainx_primitives::Memo; +use frame_support::{ + decl_error, decl_event, decl_module, decl_storage, + dispatch::DispatchResult, + ensure, + storage::IterableStorageMap, + traits::Get, + weights::{DispatchInfo, GetDispatchInfo, PostDispatchInfo, Weight}, +}; +use frame_system::{self as system, ensure_signed}; +use sp_runtime::traits::{ + Convert, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SaturatedConversion, Saturating, + SignedExtension, UniqueSaturatedFrom, UniqueSaturatedInto, Zero, +}; +use sp_std::prelude::*; +use types::*; +use xp_staking::{CollectAssetMiningInfo, Delta, OnMinting, UnbondedIndex}; +use xpallet_assets::{AssetErr, AssetType}; +use xpallet_support::debug; + +const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4; +const DEFAULT_MAXIMUM_VALIDATOR_COUNT: u32 = 100; +const DEFAULT_MAXIMUM_UNBONDED_CHUNK_SIZE: u32 = 10; + +/// ChainX 2.0 block time is targeted at 6s, i.e., 5 minute per session by default. +const DEFAULT_BLOCKS_PER_SESSION: u64 = 50; +const DEFAULT_BONDING_DURATION: u64 = DEFAULT_BLOCKS_PER_SESSION * 12 * 24 * 3; +const DEFAULT_VALIDATOR_BONDING_DURATION: u64 = DEFAULT_BONDING_DURATION * 10; + +pub trait Trait: frame_system::Trait + xpallet_assets::Trait { + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// + type CollectAssetMiningInfo: CollectAssetMiningInfo; + + /// + type OnMinting: OnMinting; +} + +decl_storage! { + trait Store for Module as XStaking { + /// The ideal number of staking participants. + pub ValidatorCount get(fn validator_count) config(): u32; + + /// Minimum number of staking participants before emergency conditions are imposed. + pub MinimumValidatorCount get(fn minimum_validator_count) config(): + u32 = DEFAULT_MINIMUM_VALIDATOR_COUNT; + + /// Maximum number of staking participants before emergency conditions are imposed. + pub MaximumValidatorCount get(fn maximum_validator_count) config(): + u32 = DEFAULT_MAXIMUM_VALIDATOR_COUNT; + + /// Minimum value (self_bonded, total_bonded) to be a candidate of validator election. + pub ValidatorCandidateRequirement get(fn minimum_candidate_requirement): + CandidateRequirement; + + /// The length of a session in blocks. + pub BlocksPerSession get(fn blocks_per_session) config(): + T::BlockNumber = T::BlockNumber::saturated_from::(DEFAULT_BLOCKS_PER_SESSION); + + /// The length of a staking era in sessions. + pub SessionsPerEra get(fn sessions_per_era) config(): + T::BlockNumber = T::BlockNumber::saturated_from::(12); + + /// The length of the bonding duration in blocks. + pub BondingDuration get(fn bonding_duration) config(): + T::BlockNumber = T::BlockNumber::saturated_from::(DEFAULT_BONDING_DURATION); + + /// The length of the bonding duration in blocks for validator. + pub ValidatorBondingDuration get(fn validator_bonding_duration) config(): + T::BlockNumber = T::BlockNumber::saturated_from::(DEFAULT_VALIDATOR_BONDING_DURATION); + + /// Maximum number of on-going unbonded chunk. + pub MaximumUnbondedChunkSize get(fn maximum_unbonded_chunk_size) config(): + u32 = DEFAULT_MAXIMUM_UNBONDED_CHUNK_SIZE; + + /// Maximum value of total_bonded/self_bonded. + pub UpperBoundFactorOfAcceptableVotes get(fn upper_bound_factor) config(): + u32 = 10u32; + + /// (Treasury, Staking) + pub GlobalDistributionRatio get(fn globaldistribution_ratio): (u32, u32) = (1u32, 1u32); + + /// (Staker, External Miners) + pub DistributionRatio get(fn distribution_ratio): (u32, u32) = (1u32, 1u32); + + /// The map from (wannabe) validator key to the profile of that validator. + pub Validators get(fn validators): + map hasher(twox_64_concat) T::AccountId => ValidatorProfile; + + /// The map from nominator key to the set of keys of all validators to nominate. + pub Nominators get(fn nominators): + map hasher(twox_64_concat) T::AccountId => NominatorProfile; + + /// The map from validator key to the vote weight ledger of that validator. + pub ValidatorLedgers get(fn validator_ledgers): + map hasher(twox_64_concat) T::AccountId => ValidatorLedger; + + /// The map from nominator to the vote weight ledger of all nominees. + pub Nominations get(fn nominations): + double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId + => NominatorLedger; + } + + add_extra_genesis { + config(validators): + Vec; + // Vec<(T::AccountId, T::Balance)>; + build(|config: &GenesisConfig| { + // for &(ref v, balance) in &config.validators { + for v in &config.validators { + // assert!( + // T::Currency::free_balance(&stash) >= balance, + // "Stash does not have enough balance to bond." + // ); + } + }); + } +} + +decl_event!( + pub enum Event + where + ::AccountId, + ::Balance, + { + /// The staker has been rewarded by this amount. `AccountId` is the stash account. + Reward(AccountId, Balance), + /// One validator (and its nominators) has been slashed by the given amount. + Slash(AccountId, Balance), + /// Nominator has bonded to the validator this amount. + Bond(AccountId, AccountId, Balance), + /// An account has unbonded this amount. + Unbond(AccountId, AccountId, Balance), + /// + Claim(AccountId, AccountId, Balance), + /// An account has called `withdraw_unbonded` and removed unbonding chunks worth `Balance` + /// from the unlocking queue. + WithdrawUnbonded(AccountId, Balance), + } +); + +decl_error! { + /// Error for the staking module. + pub enum Error for Module { + /// Zero amount + ZeroBalance, + /// + ZeroVoteWeight, + /// Invalid validator target. + InvalidValidator, + /// Can not force validator to be chilled. + InsufficientValidators, + /// Free balance can not cover this bond operation. + InsufficientBalance, + /// Can not bond with value less than minimum balance. + InsufficientValue, + /// Invalid rebondable value. + InvalidRebondValue, + /// + InvalidUnbondValue, + /// Can not schedule more unbond chunks. + NoMoreUnbondChunks, + /// Validators can not accept more votes from other voters. + NoMoreAcceptableVotes, + /// Can not rebond the validator self-bonded. + /// + /// Due to the validator and regular nominator have different bonding duration. + RebondSelfBondedNotAllowed, + /// Nominator did not nominate that validator before. + NonexistentNomination, + /// + RegisteredAlready, + /// + NoUnbondedChunk, + /// + InvalidUnbondedIndex, + /// + UnbondRequestNotYetDue, + /// Can not rebond due to the restriction of rebond frequency limit. + NoMoreRebond, + /// The call is not allowed at the given time due to restrictions of election period. + CallNotAllowed, + /// + AssetError, + } +} + +impl From for Error { + fn from(asset_err: AssetErr) -> Self { + Self::AssetError + } +} + +impl From for Error { + fn from(e: xp_staking::ZeroVoteWeightError) -> Self { + Self::ZeroVoteWeight + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + + type Error = Error; + + fn deposit_event() = default; + + fn on_finalize() { + } + + /// Nominates the `target` with `value` of the origin account's balance locked. + #[weight = 10] + fn bond(origin, target: T::AccountId, value: T::Balance, memo: Memo) { + let sender = ensure_signed(origin)?; + memo.check_validity()?; + + ensure!(!value.is_zero(), Error::::ZeroBalance); + ensure!(Self::is_validator(&target), Error::::InvalidValidator); + ensure!(value <= Self::free_balance_of(&sender), Error::::InsufficientBalance); + if !Self::is_validator_self_bonding(&sender, &target) { + Self::check_validator_acceptable_votes_limit(&sender, value)?; + } + + Self::apply_bond(&sender, &target, value)?; + } + + /// Switchs the nomination of `value` from one validator to another. + #[weight = 10] + fn rebond(origin, from: T::AccountId, to: T::AccountId, value: T::Balance, memo: Memo) { + let sender = ensure_signed(origin)?; + memo.check_validity()?; + + ensure!(!value.is_zero(), Error::::ZeroBalance); + ensure!(Self::is_validator(&from) && Self::is_validator(&to), Error::::InvalidValidator); + ensure!(sender != from, Error::::RebondSelfBondedNotAllowed); + + ensure!(value <= Self::bonded_to(&sender, &from), Error::::InvalidRebondValue); + + if !Self::is_validator_self_bonding(&sender, &to) { + Self::check_validator_acceptable_votes_limit(&to, value)?; + } + + let current_block = >::block_number(); + if let Some(last_rebond) = Self::last_rebond_of(&sender) { + ensure!( + current_block > last_rebond + Self::bonding_duration(), + Error::::NoMoreRebond + ); + } + + Self::apply_rebond(&sender, &from, &to, value, current_block); + } + + /// + #[weight = 10] + fn unbond(origin, target: T::AccountId, value: T::Balance, memo: Memo) { + let sender = ensure_signed(origin)?; + memo.check_validity()?; + + ensure!(!value.is_zero(), Error::::ZeroBalance); + ensure!(Self::is_validator(&target), Error::::InvalidValidator); + // TODO: is this unneccessary? + // ensure!(Self::nomination_exists(&sender, &target), Error::::NonexistentNomination); + ensure!(value <= Self::bonded_to(&sender, &target), Error::::InvalidUnbondValue); + ensure!( + Self::unbonded_chunks_of(&sender).len() < Self::maximum_unbonded_chunk_size() as usize, + Error::::NoMoreUnbondChunks + ); + + Self::apply_unbond(&sender, &target, value)?; + } + + /// Frees up the unbonded balances that are due. + #[weight = 10] + fn withdraw_unbonded(origin, unbonded_index: UnbondedIndex) { + let sender = ensure_signed(origin)?; + + let mut unbonded_chunks = Self::unbonded_chunks_of(&sender); + ensure!(!unbonded_chunks.is_empty(), Error::::NoUnbondedChunk); + ensure!(unbonded_index < unbonded_chunks.len() as u32, Error::::InvalidUnbondedIndex); + + let Unbonded { value, locked_until } = unbonded_chunks[unbonded_index as usize]; + let current_block = >::block_number(); + + ensure!(current_block > locked_until, Error::::UnbondRequestNotYetDue); + + // apply withdraw_unbonded + Self::unlock_unbonded_reservation(&sender, value).map_err(|_| Error::::AssetError)?; + unbonded_chunks.swap_remove(unbonded_index as usize); + + Nominators::::mutate(&sender, |nominator_profile| { + nominator_profile.unbonded_chunks = unbonded_chunks; + }); + + Self::deposit_event(RawEvent::WithdrawUnbonded(sender, value)); + } + + /// Claims the staking reward given the `target` validator. + #[weight = 10] + fn claim(origin, target: T::AccountId) { + let sender = ensure_signed(origin)?; + + ensure!(Self::is_validator(&target), Error::::InvalidValidator); + todo!("ensure nominator record exists"); + + >::claim(&sender, &target)?; + } + + /// Declare the desire to validate for the origin account. + #[weight = 10] + fn validate(origin) { + let sender = ensure_signed(origin)?; + } + + /// Declare no desire to validate for the origin account. + #[weight = 10] + fn chill(origin) { + let sender = ensure_signed(origin)?; + + // for validator in Validators::::iter(){} + } + + /// TODO: figure out whether this should be kept. + #[weight = 100_000] + fn register(origin) { + let sender = ensure_signed(origin)?; + ensure!(!Self::is_validator(&sender), Error::::RegisteredAlready); + let current_block = >::block_number(); + Validators::::insert(sender, ValidatorProfile { + registered_at: current_block, + ..Default::default() + }); + } + } +} + +impl Module { + #[inline] + pub fn is_validator(who: &T::AccountId) -> bool { + Validators::::contains_key(who) + } + + #[inline] + pub fn is_chilled(who: &T::AccountId) -> bool { + Validators::::get(who).is_chilled + } + + #[inline] + pub fn is_active(who: &T::AccountId) -> bool { + !Self::is_chilled(who) + } + + #[inline] + fn unbonded_chunks_of(nominator: &T::AccountId) -> Vec> { + Nominators::::get(nominator).unbonded_chunks + } + + #[inline] + fn last_rebond_of(nominator: &T::AccountId) -> Option { + Nominators::::get(nominator).last_rebond + } + + #[inline] + fn free_balance_of(who: &T::AccountId) -> T::Balance { + >::pcx_free_balance(who) + } + + fn is_validator_self_bonding(nominator: &T::AccountId, nominee: &T::AccountId) -> bool { + Self::is_validator(nominator) && *nominator == *nominee + } + + fn nomination_exists(nominator: &T::AccountId, nominee: &T::AccountId) -> bool { + Nominations::::contains_key(nominator, nominee) + } + + pub fn validator_set() -> Vec { + Validators::::iter() + .map(|(v, _)| v) + .filter(Self::is_active) + .collect() + } + + fn can_force_chilled() -> bool { + // TODO: optimize using try_for_each? + let active = Validators::::iter() + .map(|(v, _)| v) + .filter(Self::is_active) + .collect::>(); + active.len() > Self::minimum_validator_count() as usize + } + + fn try_force_chilled(who: &T::AccountId) -> Result<(), Error> { + if !Self::can_force_chilled() { + return Err(Error::::InsufficientValidators); + } + // TODO: apply_force_chilled() + Ok(()) + } + + fn total_votes_of(validator: &T::AccountId) -> T::Balance { + ValidatorLedgers::::get(validator).total + } + + fn validator_self_bonded(validator: &T::AccountId) -> T::Balance { + Self::bonded_to(validator, validator) + } + + #[inline] + fn bonded_to(nominator: &T::AccountId, nominee: &T::AccountId) -> T::Balance { + Nominations::::get(nominator, nominee).nomination + } + + fn acceptable_votes_limit_of(validator: &T::AccountId) -> T::Balance { + Self::validator_self_bonded(validator) * T::Balance::from(Self::upper_bound_factor()) + } + + fn check_validator_acceptable_votes_limit( + validator: &T::AccountId, + value: T::Balance, + ) -> Result<(), Error> { + let cur_total = Self::total_votes_of(validator); + let upper_limit = Self::acceptable_votes_limit_of(validator); + if cur_total + value <= upper_limit { + Ok(()) + } else { + Err(Error::::NoMoreAcceptableVotes) + } + } + + // Staking specific assets operation + // + fn bond_reserve(who: &T::AccountId, value: T::Balance) -> Result<(), AssetErr> { + >::pcx_move_balance( + who, + AssetType::Free, + who, + AssetType::ReservedStaking, + value, + ) + } + + fn unbond_reserve(who: &T::AccountId, value: T::Balance) -> Result<(), AssetErr> { + >::pcx_move_balance( + who, + AssetType::ReservedStaking, + who, + AssetType::ReservedStakingRevocation, + value, + ) + } + + fn unlock_unbonded_reservation(who: &T::AccountId, value: T::Balance) -> Result<(), AssetErr> { + >::pcx_move_balance( + who, + AssetType::ReservedStakingRevocation, + who, + AssetType::Free, + value, + ) + } + + /// Settles and update the vote weight state of the nominator `source` and validator `target` given the delta amount. + fn update_vote_weight(source: &T::AccountId, target: &T::AccountId, delta: Delta) { + let current_block = >::block_number(); + let saturated_current_block = current_block.saturated_into::(); + + let source_weight = + >::settle_claimer_weight( + source, + target, + saturated_current_block, + ); + + let target_weight = + >::settle_claimee_weight( + target, + saturated_current_block, + ); + + Self::set_nominator_vote_weight(source, target, source_weight, current_block, delta); + Self::set_validator_vote_weight(target, target_weight, current_block, delta); + } + + fn apply_bond( + nominator: &T::AccountId, + nominee: &T::AccountId, + value: T::Balance, + ) -> Result<(), Error> { + Self::bond_reserve(nominator, value)?; + Self::update_vote_weight( + nominator, + nominee, + Delta::Add(value.saturated_into::()), + ); + Self::deposit_event(RawEvent::Bond(nominator.clone(), nominee.clone(), value)); + Ok(()) + } + + fn apply_rebond( + who: &T::AccountId, + from: &T::AccountId, + to: &T::AccountId, + value: T::Balance, + current_block: T::BlockNumber, + ) { + let v = value.saturated_into::(); + // TODO: reduce one block_number read? + Self::update_vote_weight(who, from, Delta::Sub(v)); + Self::update_vote_weight(who, to, Delta::Add(v)); + Nominators::::mutate(who, |nominator_profile| { + nominator_profile.last_rebond = Some(current_block); + }); + } + + fn apply_unbond( + who: &T::AccountId, + target: &T::AccountId, + value: T::Balance, + ) -> Result<(), Error> { + debug!( + "[apply_unbond] who:{:?}, target: {:?}, value: {:?}", + who, target, value + ); + Self::unbond_reserve(who, value)?; + + let bonding_duration = if Self::is_validator(who) && *who == *target { + Self::validator_bonding_duration() + } else { + Self::bonding_duration() + }; + + let locked_until = >::block_number() + bonding_duration; + + let mut unbonded_chunks = Self::unbonded_chunks_of(who); + + if let Some(idx) = unbonded_chunks + .iter() + .position(|x| x.locked_until == locked_until) + { + unbonded_chunks[idx].value += value; + } else { + unbonded_chunks.push(Unbonded { + value, + locked_until, + }); + } + + Nominators::::mutate(who, |nominator_profile| { + nominator_profile.unbonded_chunks = unbonded_chunks; + }); + + Self::update_vote_weight(who, target, Delta::Sub(value.saturated_into::())); + + Self::deposit_event(RawEvent::Unbond(who.clone(), target.clone(), value)); + + Ok(()) + } +} diff --git a/xpallets/mining/staking/src/mock.rs b/xpallets/mining/staking/src/mock.rs new file mode 100644 index 0000000000000..935d5356d9de8 --- /dev/null +++ b/xpallets/mining/staking/src/mock.rs @@ -0,0 +1,316 @@ +use crate::*; +use crate::{Module, Trait}; +use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, +}; +use xp_staking::SessionIndex; + +/// The AccountId alias in this test module. +pub(crate) type AccountId = u64; +pub(crate) type AccountIndex = u64; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; + +impl_outer_origin! { + pub enum Origin for Test {} +} + +// For testing the pallet, we construct most of a mock runtime. This means +// first constructing a configuration type (`Test`) which `impl`s each of the +// configuration traits of pallets we want to use. +#[derive(Clone, Eq, PartialEq)] +pub struct Test; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75); +} + +impl system::Trait for Test { + type BaseCallFilter = (); + type Origin = Origin; + type Call = (); + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl Trait for Test { + type Event = (); + type OnMinting = (); + type CollectAssetMiningInfo = (); +} + +impl xpallet_assets::Trait for Test { + type Balance = Balance; + type Event = (); + type OnAssetChanged = (); + type OnAssetRegisterOrRevoke = (); + type DetermineTokenJackpotAccountId = (); +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::default() + .build_storage::() + .unwrap() + .into() +} + +thread_local! { + static SESSION: RefCell<(Vec, HashSet)> = RefCell::new(Default::default()); + static SESSION_PER_ERA: RefCell = RefCell::new(3); + static EXISTENTIAL_DEPOSIT: RefCell = RefCell::new(0); + static ELECTION_LOOKAHEAD: RefCell = RefCell::new(0); + static PERIOD: RefCell = RefCell::new(1); + static MAX_ITERATIONS: RefCell = RefCell::new(0); +} + +pub struct ExtBuilder { + session_length: BlockNumber, + election_lookahead: BlockNumber, + session_per_era: SessionIndex, + existential_deposit: Balance, + validator_pool: bool, + nominate: bool, + validator_count: u32, + minimum_validator_count: u32, + fair: bool, + num_validators: Option, + has_stakers: bool, + max_offchain_iterations: u32, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + session_length: 1, + election_lookahead: 0, + session_per_era: 3, + existential_deposit: 1, + validator_pool: false, + nominate: true, + validator_count: 2, + minimum_validator_count: 0, + fair: true, + num_validators: None, + has_stakers: true, + max_offchain_iterations: 0, + } + } +} + +use chainx_primitives::AssetId; +use xpallet_assets::{AssetInfo, AssetRestriction, AssetRestrictions, Chain}; + +const PCX_PRECISION: u8 = 8; +fn pcx() -> (AssetId, AssetInfo, AssetRestrictions) { + ( + xpallet_protocol::PCX, + AssetInfo::new::( + b"PCX".to_vec(), + b"Polkadot ChainX".to_vec(), + Chain::ChainX, + PCX_PRECISION, + b"ChainX's crypto currency in Polkadot ecology".to_vec(), + ) + .unwrap(), + AssetRestriction::Deposit + | AssetRestriction::Withdraw + | AssetRestriction::DestroyWithdrawal + | AssetRestriction::DestroyFree + | AssetRestriction::Move, + ) +} + +fn testnet_assets() -> Vec<(AssetId, AssetInfo, AssetRestrictions, bool, bool)> { + let pcx = pcx(); + let assets = vec![(pcx.0, pcx.1, pcx.2, true, true)]; + assets +} + +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: Balance) -> Self { + self.existential_deposit = existential_deposit; + self + } + pub fn validator_pool(mut self, validator_pool: bool) -> Self { + self.validator_pool = validator_pool; + self + } + pub fn nominate(mut self, nominate: bool) -> Self { + self.nominate = nominate; + self + } + pub fn validator_count(mut self, count: u32) -> Self { + self.validator_count = count; + self + } + pub fn minimum_validator_count(mut self, count: u32) -> Self { + self.minimum_validator_count = count; + self + } + pub fn fair(mut self, is_fair: bool) -> Self { + self.fair = is_fair; + self + } + pub fn num_validators(mut self, num_validators: u32) -> Self { + self.num_validators = Some(num_validators); + self + } + pub fn session_per_era(mut self, length: SessionIndex) -> Self { + self.session_per_era = length; + self + } + pub fn election_lookahead(mut self, look: BlockNumber) -> Self { + self.election_lookahead = look; + self + } + pub fn session_length(mut self, length: BlockNumber) -> Self { + self.session_length = length; + self + } + pub fn has_stakers(mut self, has: bool) -> Self { + self.has_stakers = has; + self + } + pub fn max_offchain_iterations(mut self, iterations: u32) -> Self { + self.max_offchain_iterations = iterations; + self + } + pub fn offchain_phragmen_ext(self) -> Self { + self.session_per_era(4) + .session_length(5) + .election_lookahead(3) + } + pub fn set_associated_constants(&self) { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); + SESSION_PER_ERA.with(|v| *v.borrow_mut() = self.session_per_era); + ELECTION_LOOKAHEAD.with(|v| *v.borrow_mut() = self.election_lookahead); + PERIOD.with(|v| *v.borrow_mut() = self.session_length); + MAX_ITERATIONS.with(|v| *v.borrow_mut() = self.max_offchain_iterations); + } + pub fn build(self) -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + self.set_associated_constants(); + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let balance_factor = if self.existential_deposit > 1 { 256 } else { 1 }; + + let num_validators = self.num_validators.unwrap_or(self.validator_count); + let validators = (0..num_validators) + .map(|x| ((x + 1) * 10 + 1) as AccountId) + .collect::>(); + + let pcx_asset = pcx(); + let assets = vec![(pcx_asset.0, pcx_asset.1, pcx_asset.2, true, true)]; + + let mut endowed = BTreeMap::new(); + let pcx_id = pcx().0; + let endowed_info = vec![(1, 100), (2, 200), (3, 300), (4, 400)]; + endowed.insert(pcx_id, endowed_info); + let _ = xpallet_assets::GenesisConfig:: { + assets, + endowed, + memo_len: 128, + } + .assimilate_storage(&mut storage); + + if self.has_stakers { + let stake_21 = if self.fair { 1000 } else { 2000 }; + let stake_31 = if self.validator_pool { + balance_factor * 1000 + } else { + 1 + }; + let nominated = if self.nominate { vec![11, 21] } else { vec![] }; + } + let _ = GenesisConfig:: { + validators: vec![1, 2, 3, 4], + ..Default::default() + } + .assimilate_storage(&mut storage); + + // let _ = pallet_session::GenesisConfig:: { + // keys: validators + // .iter() + // .map(|x| { + // ( + // *x, + // *x, + // SessionKeys { + // other: UintAuthorityId(*x as u64), + // }, + // ) + // }) + // .collect(), + // } + // .assimilate_storage(&mut storage); + + let mut ext = sp_io::TestExternalities::from(storage); + // ext.execute_with(|| { + // let validators = Session::validators(); + // SESSION.with(|x| *x.borrow_mut() = (validators.clone(), HashSet::new())); + // }); + + // We consider all test to start after timestamp is initialized + // This must be ensured by having `timestamp::on_initialize` called before + // `staking::on_initialize` + ext.execute_with(|| { + System::set_block_number(1); + // Timestamp::set_timestamp(INIT_TIMESTAMP); + XStaking::register(Origin::signed(1)).unwrap(); + XStaking::register(Origin::signed(2)).unwrap(); + XStaking::register(Origin::signed(3)).unwrap(); + XStaking::register(Origin::signed(4)).unwrap(); + XStaking::bond(Origin::signed(1), 1, 10, b"memo".to_vec().into()).unwrap(); + XStaking::bond(Origin::signed(2), 2, 20, b"memo".to_vec().into()).unwrap(); + XStaking::bond(Origin::signed(3), 3, 30, b"memo".to_vec().into()).unwrap(); + XStaking::bond(Origin::signed(4), 4, 40, b"memo".to_vec().into()).unwrap(); + }); + + ext + } + pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + let mut ext = self.build(); + ext.execute_with(test); + // ext.execute_with(post_conditions); + } +} + +pub type System = frame_system::Module; +pub type XAssets = xpallet_assets::Module; +// pub type Session = pallet_session::Module; +// pub type Timestamp = pallet_timestamp::Module; +pub type XStaking = Module; diff --git a/xpallets/mining/staking/src/tests.rs b/xpallets/mining/staking/src/tests.rs new file mode 100644 index 0000000000000..de656f4c5d165 --- /dev/null +++ b/xpallets/mining/staking/src/tests.rs @@ -0,0 +1,207 @@ +use super::*; +use crate::mock::*; +use frame_support::{assert_err, assert_noop, assert_ok}; + +fn t_bond(who: AccountId, target: AccountId, value: Balance) -> DispatchResult { + XStaking::bond(Origin::signed(who), target, value, b"memo".as_ref().into()) +} + +fn t_rebond(who: AccountId, from: AccountId, to: AccountId, value: Balance) -> DispatchResult { + XStaking::rebond( + Origin::signed(who), + from, + to, + value, + b"memo".as_ref().into(), + ) +} + +fn t_unbond(who: AccountId, target: AccountId, value: Balance) -> DispatchResult { + XStaking::unbond(Origin::signed(who), target, value, b"memo".as_ref().into()) +} + +fn t_withdraw_unbonded(who: AccountId, unbonded_index: UnbondedIndex) -> DispatchResult { + XStaking::withdraw_unbonded(Origin::signed(who), unbonded_index) +} + +fn t_system_block_number_inc(number: BlockNumber) { + System::set_block_number((System::block_number() + number).into()); +} + +#[test] +fn bond_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(XAssets::pcx_free_balance(&1), 90); + assert_eq!( + >::get(2), + ValidatorLedger { + total: 20, + last_total_vote_weight: 0, + last_total_vote_weight_update: 1, + } + ); + t_system_block_number_inc(1); + assert_ok!(t_bond(1, 2, 10)); + + assert_eq!(XAssets::pcx_free_balance(&1), 80); + assert_eq!( + >::get(2), + ValidatorLedger { + total: 30, + last_total_vote_weight: 20, + last_total_vote_weight_update: 2, + } + ); + assert_eq!( + >::get(1, 2), + NominatorLedger { + nomination: 10, + last_vote_weight: 0, + last_vote_weight_update: 2, + } + ); + }); +} + +#[test] +fn unbond_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_err!(t_unbond(1, 2, 50), Error::::InvalidUnbondValue); + + t_system_block_number_inc(1); + + assert_ok!(t_bond(1, 2, 10)); + + t_system_block_number_inc(1); + + assert_ok!(t_unbond(1, 2, 5)); + + assert_eq!( + >::get(2), + ValidatorLedger { + total: 25, + last_total_vote_weight: 30 + 20, + last_total_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(1, 2), + NominatorLedger { + nomination: 5, + last_vote_weight: 10, + last_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(1), + NominatorProfile { + last_rebond: None, + unbonded_chunks: vec![Unbonded { + value: 5, + locked_until: 50 * 12 * 24 * 3 + 3 + }], + } + ); + }); +} + +#[test] +fn rebond_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_err!( + XStaking::unbond(Origin::signed(1), 2, 50, b"memo".as_ref().into()), + Error::::InvalidUnbondValue + ); + + t_system_block_number_inc(1); + + assert_ok!(t_bond(1, 2, 10)); + + t_system_block_number_inc(1); + + assert_ok!(t_rebond(1, 2, 3, 5)); + + assert_eq!( + >::get(2), + ValidatorLedger { + total: 25, + last_total_vote_weight: 30 + 20, + last_total_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(3), + ValidatorLedger { + total: 30 + 5, + last_total_vote_weight: 30 * 2, + last_total_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(1, 2), + NominatorLedger { + nomination: 5, + last_vote_weight: 10, + last_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(1, 3), + NominatorLedger { + nomination: 5, + last_vote_weight: 0, + last_vote_weight_update: 3, + } + ); + + assert_eq!( + >::get(1), + NominatorProfile { + last_rebond: Some(3), + unbonded_chunks: vec![] + } + ); + }); +} + +#[test] +fn withdraw_unbond_should_work() { + ExtBuilder::default().build_and_execute(|| { + t_system_block_number_inc(1); + + assert_ok!(t_bond(1, 2, 10)); + assert_eq!(XAssets::pcx_free_balance(&1), 80); + + t_system_block_number_inc(1); + + assert_ok!(t_unbond(1, 2, 5)); + assert_eq!(XAssets::pcx_free_balance(&1), 80); + + assert_eq!( + >::get(1), + NominatorProfile { + last_rebond: None, + unbonded_chunks: vec![Unbonded { + value: 5, + locked_until: DEFAULT_BONDING_DURATION + 3 + }] + } + ); + + t_system_block_number_inc(DEFAULT_BONDING_DURATION); + assert_err!( + t_withdraw_unbonded(1, 0), + Error::::UnbondRequestNotYetDue + ); + + t_system_block_number_inc(1); + + assert_ok!(t_withdraw_unbonded(1, 0),); + assert_eq!(XAssets::pcx_free_balance(&1), 85); + }); +} diff --git a/xpallets/mining/staking/src/types.rs b/xpallets/mining/staking/src/types.rs new file mode 100644 index 0000000000000..c89c2fd10ab4a --- /dev/null +++ b/xpallets/mining/staking/src/types.rs @@ -0,0 +1,124 @@ +use super::*; +use chainx_primitives::AssetId; +use codec::{Decode, Encode}; +use sp_runtime::RuntimeDebug; +#[cfg(feature = "std")] +use sp_runtime::{Deserialize, Serialize}; +use xp_staking::VoteWeight; + +/// Destination for minted fresh PCX on each new session. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] +pub enum MintedDestination { + Validator(AccountId), + Asset(AssetId), +} + +/// The requirement of a qualified staking candidate. +/// +/// If the (potential) validator failed to meet this requirement, force it to be chilled on new election round. +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug)] +pub struct CandidateRequirement { + /// The minimal amount of self-bonded balance to be a qualified validator candidate. + pub self_bonded: Balance, + /// The minimal amount of total-bonded balance to be a qualified validator candidate. + /// + /// total-bonded = self-bonded + all the other nominators' nominations. + pub total: Balance, +} + +/// Type for noting when the unbonded fund can be withdrawn. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct Unbonded { + /// Amount of funds to be unlocked. + pub value: Balance, + /// Block number at which point it'll be unlocked. + pub locked_until: BlockNumber, +} + +/// Vote weight properties of validator. +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct ValidatorLedger { + /// The total amount of all the nominators' vote balances. + pub total: Balance, + /// Last calculated total vote weight of current validator. + pub last_total_vote_weight: VoteWeight, + /// Block number at which point `last_total_vote_weight` just updated. + pub last_total_vote_weight_update: BlockNumber, +} + +/// Vote weight properties of nominator. +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug)] +pub struct NominatorLedger { + /// The amount of + pub nomination: Balance, + /// + pub last_vote_weight: VoteWeight, + /// + pub last_vote_weight_update: BlockNumber, +} + +/// Profile of staking validator. +/// +/// These fields are static or updated less frequently. +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct ValidatorProfile { + /// Block number at which point it's registered on chain. + pub registered_at: BlockNumber, + /// Validator is chilled right now. + pub is_chilled: bool, + /// Block number of last performed `chill` operation. + pub last_chilled: Option, +} + +/// Profile of staking nominator. +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct NominatorProfile { + /// Block number of last `rebond` operation. + pub last_rebond: Option, + /// + pub unbonded_chunks: Vec>, +} + +/// Status of (potential) validator in staking module. +/// +/// For RPC usage. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub enum ValidatorStatus { + /// Declared no desire to be a validator or forced to be chilled due to `MinimumCandidateThreshold`. + Chilled, + /// Declared desire to be a validator but haven't won one place. + Candidate, + /// Being a validator, responsible for authoring the new blocks. + Validating, +} + +impl Default for ValidatorStatus { + fn default() -> Self { + Self::Candidate + } +} + +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", serde(rename_all = "camelCase"))] +pub struct ValidatorInfo { + pub account: AccountId, + #[cfg_attr(feature = "std", serde(flatten))] + pub profile: ValidatorProfile, + #[cfg_attr(feature = "std", serde(flatten))] + pub ledger: ValidatorLedger, + pub jackpot_account: AccountId, + pub jackpot_balance: Balance, + pub self_bonded: Balance, + pub status: bool, +}