diff --git a/pallets/phala/Cargo.toml b/pallets/phala/Cargo.toml index 0dba9e4e64..bc51b71a11 100644 --- a/pallets/phala/Cargo.toml +++ b/pallets/phala/Cargo.toml @@ -91,7 +91,7 @@ std = [ "pallet-preimage/std", ] runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks" + "frame-benchmarking/runtime-benchmarks", ] try-runtime = ["frame-support/try-runtime"] native = [ diff --git a/pallets/phala/src/compute/base_pool.rs b/pallets/phala/src/compute/base_pool.rs index 94fa1ba723..8287b65269 100644 --- a/pallets/phala/src/compute/base_pool.rs +++ b/pallets/phala/src/compute/base_pool.rs @@ -293,7 +293,7 @@ pub mod pallet { BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_assets::Config>, - T: Config + wrapped_balances::Config + vault::Config, + T: wrapped_balances::Config + vault::Config, { Pallet::::set_nft_attr(self.cid, self.nftid, &self.attr)?; Ok(()) @@ -336,7 +336,7 @@ pub mod pallet { where T: pallet_assets::Config, T: Config, - T: Config + wrapped_balances::Config + vault::Config, + T: wrapped_balances::Config + vault::Config, { pallet_assets::Pallet::::balance( ::WPhaAssetId::get(), @@ -359,7 +359,7 @@ pub mod pallet { sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_assets::Config>, T: Config, - T: Config + wrapped_balances::Config + vault::Config, + T: wrapped_balances::Config + vault::Config, { self.total_value += rewards; for vault_staker in &self.value_subscribers { @@ -422,7 +422,7 @@ pub mod pallet { BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { /// Adds a staker accountid to contribution whitelist. /// @@ -588,7 +588,7 @@ pub mod pallet { BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + wrapped_balances::Config + vault::Config, + T: wrapped_balances::Config + vault::Config, { /// Returns a [`NftGuard`] object that can read or write to the nft attributes /// diff --git a/pallets/phala/src/compute/stake_pool_v2.rs b/pallets/phala/src/compute/stake_pool_v2.rs index da949e8aa8..ccf8a983b4 100644 --- a/pallets/phala/src/compute/stake_pool_v2.rs +++ b/pallets/phala/src/compute/stake_pool_v2.rs @@ -319,7 +319,7 @@ pub mod pallet { BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { /// Creates a new stake pool #[pallet::call_index(0)] @@ -994,7 +994,7 @@ pub mod pallet { BalanceOf: FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { pub fn do_start_computing( owner: &T::AccountId, @@ -1206,7 +1206,7 @@ pub mod pallet { BalanceOf: FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { /// Called when gk send new payout information. /// Append specific worker's reward balance of current round, @@ -1239,7 +1239,7 @@ pub mod pallet { BalanceOf: FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { fn on_unbound(worker: &WorkerPublicKey, _force: bool) { // Usually called on worker force unbinding (force == true), but it's also possible @@ -1261,7 +1261,7 @@ pub mod pallet { BalanceOf: FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { fn on_stopped( _worker: &WorkerPublicKey, diff --git a/pallets/phala/src/compute/wrapped_balances.rs b/pallets/phala/src/compute/wrapped_balances.rs index a2d551d456..508120c5c2 100644 --- a/pallets/phala/src/compute/wrapped_balances.rs +++ b/pallets/phala/src/compute/wrapped_balances.rs @@ -8,16 +8,17 @@ pub mod pallet { use crate::pool_proxy::PoolProxy; use crate::registry; use crate::vault; - use crate::{BalanceOf, NegativeImbalanceOf, PhalaConfig}; - use frame_support::traits::tokens::{Fortitude, Precision}; + use crate::{BalanceOf, NegativeImbalanceOf, PhalaConfig, PositiveImbalanceOf}; use frame_support::{ pallet_prelude::*, traits::{ - tokens::fungibles::{Inspect, Mutate}, tokens::nonfungibles::InspectEnumerable, - Currency, - ExistenceRequirement::{AllowDeath, KeepAlive}, - OnUnbalanced, StorageVersion, + tokens::{ + fungibles::{Inspect, Mutate}, + BalanceStatus, Fortitude, Precision, + }, + Currency, ExistenceRequirement, Imbalance, LockIdentifier, LockableCurrency, + OnUnbalanced, ReservableCurrency, SignedImbalance, StorageVersion, WithdrawReasons, }, }; use frame_system::{pallet_prelude::*, RawOrigin}; @@ -26,6 +27,7 @@ pub mod pallet { use scale_info::TypeInfo; use sp_runtime::traits::Zero; use sp_std::{fmt::Display, prelude::*, result::Result}; + #[pallet::config] pub trait Config: frame_system::Config @@ -47,6 +49,8 @@ pub mod pallet { type WrappedBalancesAccountId: Get; /// The handler to absorb the slashed amount. type OnSlashed: OnUnbalanced>; + /// Council election call origin + type ElectionPalletId: Get; } /// User's asset status proxy @@ -88,6 +92,21 @@ pub mod pallet { pub type StakerAccounts = StorageMap<_, Twox64Concat, T::AccountId, FinanceAccount>>; + #[pallet::storage] + #[pallet::getter(fn election_locks)] + pub type ElectionLocks = + StorageMap<_, Twox64Concat, T::AccountId, BalanceOf, ValueQuery>; + + /// The WPHA a user owes the system because of a lack of liquid token. Wills be settled by `slash_reserved()` in the future. + #[pallet::storage] + #[pallet::getter(fn slash_debts)] + pub type SlashDebts = + StorageMap<_, Twox64Concat, T::AccountId, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn election_reserves)] + pub type ElectionReserves = + StorageMap<_, Twox64Concat, T::AccountId, BalanceOf, ValueQuery>; /// Collect the unmintable dust // TODO: since this is the imbalance, consider to mint it in the future. #[pallet::storage] @@ -121,6 +140,11 @@ pub mod pallet { aye_amount: BalanceOf, nay_amount: BalanceOf, }, + ReserveSlashed { + user: T::AccountId, + slash: BalanceOf, + new_debt: BalanceOf, + }, } #[pallet::error] @@ -137,6 +161,238 @@ pub mod pallet { ReferendumOngoing, /// The Iteration exceed the max limitaion IterationsIsNotVaild, + /// The amount of lock is larger than the total balances you owned + LiquidityRestrictions, + /// There is a debt to settle and before it is done you can not unwrap WPha to Pha + WphaNotSettled, + } + + impl Currency for Pallet + where + BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, + T: pallet_assets::Config>, + T: pallet_balances::Config> + vault::Config, + { + type Balance = BalanceOf; + type PositiveImbalance = PositiveImbalanceOf; + type NegativeImbalance = NegativeImbalanceOf; + fn total_balance(who: &T::AccountId) -> Self::Balance { + Self::get_net_value(who.clone()).expect("Get net value should success; qed.") + } + + fn free_balance(who: &T::AccountId) -> Self::Balance { + Self::total_balance(who) + } + + fn can_slash(who: &T::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(who) >= value + } + + fn total_issuance() -> Self::Balance { + as Inspect>::total_issuance( + T::WPhaAssetId::get(), + ) + } + + fn active_issuance() -> Self::Balance { + as Inspect>::total_issuance( + T::WPhaAssetId::get(), + ) + } + + fn deactivate(_amount: Self::Balance) { + unimplemented!() + } + + fn reactivate(_amount: Self::Balance) { + unimplemented!() + } + + fn minimum_balance() -> Self::Balance { + as Inspect>::minimum_balance( + T::WPhaAssetId::get(), + ) + } + + fn burn(mut _amount: Self::Balance) -> Self::PositiveImbalance { + unimplemented!() + } + + fn issue(mut _amount: Self::Balance) -> Self::NegativeImbalance { + unimplemented!() + } + + fn ensure_can_withdraw( + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + new_balance: Self::Balance, + ) -> DispatchResult { + if amount.is_zero() { + return Ok(()); + } + let lock = Self::get_lock(who); + ensure!(new_balance >= lock, Error::::LiquidityRestrictions); + Ok(()) + } + + fn transfer( + _transactor: &T::AccountId, + _dest: &T::AccountId, + _value: Self::Balance, + _existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + unimplemented!() + } + + fn slash( + _who: &T::AccountId, + _value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + unimplemented!() + } + + fn deposit_into_existing( + _who: &T::AccountId, + _value: Self::Balance, + ) -> Result { + unimplemented!() + } + + fn deposit_creating(_who: &T::AccountId, _value: Self::Balance) -> Self::PositiveImbalance { + unimplemented!() + } + + fn withdraw( + _who: &T::AccountId, + _value: Self::Balance, + _reasons: WithdrawReasons, + _liveness: ExistenceRequirement, + ) -> Result { + unimplemented!() + } + + fn make_free_balance_be( + _who: &T::AccountId, + _value: Self::Balance, + ) -> SignedImbalance { + unimplemented!() + } + } + + impl ReservableCurrency for Pallet + where + BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, + T: pallet_assets::Config>, + T: pallet_balances::Config> + vault::Config, + { + fn can_reserve(who: &T::AccountId, value: Self::Balance) -> bool { + Self::total_balance(who) >= value + } + fn reserved_balance(who: &T::AccountId) -> Self::Balance { + ElectionReserves::::get(who) + } + fn reserve(who: &T::AccountId, value: Self::Balance) -> DispatchResult { + let actual = value.min(Self::total_balance(who)); + ElectionReserves::::mutate(who, |reserve| { + *reserve += actual; + }); + Ok(()) + } + fn unreserve(who: &T::AccountId, value: Self::Balance) -> Self::Balance { + let actual = value.min(ElectionReserves::::get(who)); + ElectionReserves::::mutate(who, |reserve| { + *reserve -= actual; + }); + value - actual + } + fn slash_reserved( + who: &T::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value == Zero::zero() { + return (NegativeImbalanceOf::::zero(), Zero::zero()); + } + let mut actual = value.min(ElectionReserves::::get(who)); + let free_wpha: BalanceOf = as Inspect< + T::AccountId, + >>::balance(T::WPhaAssetId::get(), who); + let mut new_debt = Zero::zero(); + if free_wpha < actual { + new_debt = actual - free_wpha; + actual = free_wpha; + SlashDebts::::mutate(who, |debt| { + *debt += new_debt; + }); + } + ElectionReserves::::mutate(who, |reserve| { + *reserve -= actual; + }); + Self::burn_from(who, actual).expect("there are enough WPHA to burn; qed."); + let imbalance = ::Currency::withdraw( + &T::WrappedBalancesAccountId::get(), + actual, + WithdrawReasons::all(), + ExistenceRequirement::AllowDeath, + ) + .expect("slash imbalance should success; qed."); + Self::deposit_event(Event::::ReserveSlashed { + user: who.clone(), + slash: actual, + new_debt, + }); + (imbalance, value - actual) + } + fn repatriate_reserved( + _slashed: &T::AccountId, + _beneficiary: &T::AccountId, + _value: Self::Balance, + _status: BalanceStatus, + ) -> Result { + unimplemented!() + } + } + impl LockableCurrency for Pallet + where + BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, + T: pallet_assets::Config>, + T: pallet_balances::Config> + vault::Config, + { + type Moment = T::BlockNumber; + type MaxLocks = T::MaxLocks; + fn set_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + ) { + Self::ensure_lock_id_supported(id); + if amount == Zero::zero() { + return; + } + ElectionLocks::::insert(who, amount); + } + fn extend_lock( + id: LockIdentifier, + who: &T::AccountId, + amount: Self::Balance, + _reasons: WithdrawReasons, + ) { + Self::ensure_lock_id_supported(id); + if amount == Zero::zero() { + return; + } + ElectionLocks::::mutate(who, |locks| { + *locks += amount; + }); + } + fn remove_lock(id: LockIdentifier, who: &T::AccountId) { + Self::ensure_lock_id_supported(id); + ElectionLocks::::remove(who); + } } impl rmrk_traits::TransferHooks for Pallet @@ -145,7 +401,7 @@ pub mod pallet { T: pallet_uniques::Config, T: pallet_assets::Config>, T: pallet_democracy::Config::Currency>, - T: Config + vault::Config, + T: vault::Config, { fn pre_check( _sender: &T::AccountId, @@ -186,7 +442,7 @@ pub mod pallet { T: pallet_uniques::Config, T: pallet_assets::Config>, T: pallet_democracy::Config::Currency>, - T: Config + vault::Config, + T: pallet_balances::Config> + vault::Config, { /// Wraps some pha and gain equal amount of W-PHA /// @@ -200,7 +456,7 @@ pub mod pallet { &user, &T::WrappedBalancesAccountId::get(), amount, - KeepAlive, + ExistenceRequirement::KeepAlive, )?; Self::mint_into(&user, amount)?; if !StakerAccounts::::contains_key(&user) { @@ -224,18 +480,21 @@ pub mod pallet { #[frame_support::transactional] pub fn unwrap_all(origin: OriginFor) -> DispatchResult { let user = ensure_signed(origin)?; + ensure!( + SlashDebts::::get(&user) > Zero::zero(), + Error::::WphaNotSettled, + ); let active_stakes = Self::get_net_value(user.clone())?; let free_stakes: BalanceOf = as Inspect< T::AccountId, >>::balance(T::WPhaAssetId::get(), &user); - let locked = - StakerAccounts::::get(&user).map_or(Zero::zero(), |status| status.locked); + let locked = Self::get_lock(&user); let withdraw_amount = (active_stakes - locked).min(free_stakes); ::Currency::transfer( &T::WrappedBalancesAccountId::get(), &user, withdraw_amount, - AllowDeath, + ExistenceRequirement::AllowDeath, )?; Self::burn_from(&user, withdraw_amount)?; Ok(()) @@ -249,6 +508,10 @@ pub mod pallet { #[frame_support::transactional] pub fn unwrap(origin: OriginFor, amount: BalanceOf) -> DispatchResult { let user = ensure_signed(origin)?; + ensure!( + SlashDebts::::get(&user) > Zero::zero(), + Error::::WphaNotSettled, + ); let free_stakes: BalanceOf = as Inspect< T::AccountId, >>::balance(T::WPhaAssetId::get(), &user); @@ -257,8 +520,7 @@ pub mod pallet { Error::::UnwrapAmountExceedsAvaliableStake ); let active_stakes = Self::get_net_value(user.clone())?; - let locked = - StakerAccounts::::get(&user).map_or(Zero::zero(), |status| status.locked); + let locked = Self::get_lock(&user); ensure!( amount + locked <= active_stakes, Error::::UnwrapAmountExceedsAvaliableStake, @@ -267,7 +529,7 @@ pub mod pallet { &T::WrappedBalancesAccountId::get(), &user, amount, - AllowDeath, + ExistenceRequirement::AllowDeath, )?; Self::burn_from(&user, amount)?; Self::deposit_event(Event::::Unwrapped { user, amount }); @@ -366,13 +628,40 @@ pub mod pallet { } Ok(()) } + #[pallet::call_index(6)] + #[pallet::weight({0})] + #[frame_support::transactional] + pub fn settle_balance_debt(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let debt = SlashDebts::::get(&who); + if debt == Zero::zero() { + return Ok(()); + } + let free_wpha = as Inspect>::balance( + T::WPhaAssetId::get(), + &who, + ); + let slash = debt.min(free_wpha); + let mut new_debt = Zero::zero(); + if debt > free_wpha { + new_debt = debt - free_wpha; + } + let (imbalance, _) = Self::slash_reserved(&who, slash); + T::OnSlashed::on_unbalanced(imbalance); + if new_debt != Zero::zero() { + SlashDebts::::insert(&who, new_debt); + } else { + SlashDebts::::remove(&who); + } + Ok(()) + } } impl Pallet where BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, T: pallet_uniques::Config, T: pallet_assets::Config>, - T: Config + vault::Config, + T: vault::Config, { /// Gets W-PHA's asset id pub fn get_asset_id() -> u32 { @@ -518,6 +807,18 @@ pub mod pallet { matches!(vote_info, Some(ReferendumInfo::Ongoing(_))) } + fn get_lock(who: &T::AccountId) -> BalanceOf { + let lock = StakerAccounts::::get(who).map_or(Zero::zero(), |status| status.locked); + let election_lock = ElectionLocks::::get(who); + let election_reserve = ElectionReserves::::get(who); + lock.max(election_lock).max(election_reserve) + } + + fn ensure_lock_id_supported(id: LockIdentifier) { + if id != T::ElectionPalletId::get() { + panic!("LockIdentifier is not supported."); + } + } /// Returns the minimum balance of WPHA pub fn min_balance() -> BalanceOf { if ! as Inspect>::asset_exists( diff --git a/pallets/phala/src/lib.rs b/pallets/phala/src/lib.rs index cff335a980..5d379ad6da 100644 --- a/pallets/phala/src/lib.rs +++ b/pallets/phala/src/lib.rs @@ -39,6 +39,9 @@ type NegativeImbalanceOf = <::Currency as frame_support::tr ::AccountId, >>::NegativeImbalance; +type PositiveImbalanceOf = <::Currency as frame_support::traits::Currency< + ::AccountId, +>>::PositiveImbalance; // Alias pub use compute::base_pool as pallet_base_pool; pub use compute::computation as pallet_computation; diff --git a/pallets/phala/src/mock.rs b/pallets/phala/src/mock.rs index e2fbde8c1d..592c7ff3b6 100644 --- a/pallets/phala/src/mock.rs +++ b/pallets/phala/src/mock.rs @@ -263,6 +263,7 @@ impl computation::Config for Test { parameter_types! { pub const WPhaAssetId: u32 = 1; + pub const ElectionPalletId: LockIdentifier = b"phrelect"; } impl wrapped_balances::Config for Test { @@ -270,6 +271,7 @@ impl wrapped_balances::Config for Test { type WPhaAssetId = WPhaAssetId; type WrappedBalancesAccountId = ConstU64<1234>; type OnSlashed = (); + type ElectionPalletId = ElectionPalletId; } parameter_types! { diff --git a/standalone/runtime/src/lib.rs b/standalone/runtime/src/lib.rs index a383cbdfa4..f9efb1976c 100644 --- a/standalone/runtime/src/lib.rs +++ b/standalone/runtime/src/lib.rs @@ -931,7 +931,7 @@ const_assert!(DesiredMembers::get() <= CouncilMaxMembers::get()); impl pallet_elections_phragmen::Config for Runtime { type RuntimeEvent = RuntimeEvent; type PalletId = ElectionsPhragmenPalletId; - type Currency = Balances; + type Currency = PhalaWrappedBalances; type ChangeMembers = Council; // NOTE: this implies that council's genesis members cannot be set directly and must come from // this module. @@ -1454,6 +1454,7 @@ impl pallet_wrapped_balances::Config for Runtime { type WPhaAssetId = ConstU32<10000>; type WrappedBalancesAccountId = WrappedBalancesPalletAccount; type OnSlashed = Treasury; + type ElectionPalletId = ElectionsPhragmenPalletId; } pub struct MigrationAccount;