diff --git a/substrate/frame/staking-rewards/src/lib.rs b/substrate/frame/staking-rewards/src/lib.rs index 091de5a3f6351049b2ae13e12a38fd0773efdbf8..7e1fbfa4644173f0114b9f05b9eb7afc8c181f68 100644 --- a/substrate/frame/staking-rewards/src/lib.rs +++ b/substrate/frame/staking-rewards/src/lib.rs @@ -68,6 +68,11 @@ use sp_core::Get; use sp_runtime::DispatchError; use sp_std::boxed::Box; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + /// The type of the unique id for each pool. pub type PoolId = u32; @@ -80,7 +85,7 @@ pub struct PoolStakerInfo<Balance> { } /// A staking pool. -#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[derive(Debug, Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] pub struct PoolInfo<AccountId, AssetId, Balance, BlockNumber> { /// The asset that is staked in this pool. staking_asset_id: AssetId, @@ -103,7 +108,7 @@ pub mod pallet { use super::*; use frame_support::{pallet_prelude::*, traits::tokens::AssetId}; use frame_system::pallet_prelude::*; - use sp_runtime::traits::AccountIdConversion; + use sp_runtime::traits::{AccountIdConversion, Saturating}; #[pallet::pallet] pub struct Pallet<T>(_); @@ -158,7 +163,7 @@ pub mod pallet { /// /// Incremented when a new pool is created. #[pallet::storage] - pub type NextPoolId<T: Config> = StorageValue<_, PoolId>; + pub type NextPoolId<T: Config> = StorageValue<_, PoolId, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -166,7 +171,7 @@ pub mod pallet { /// An account staked some tokens in a pool. Staked { /// The account that staked assets. - staker: T::AccountId, + who: T::AccountId, /// The pool. pool_id: PoolId, /// The staked asset amount. @@ -175,7 +180,7 @@ pub mod pallet { /// An account unstaked some tokens from a pool. Unstaked { /// The account that unstaked assets. - staker: T::AccountId, + who: T::AccountId, /// The pool. pool_id: PoolId, /// The unstaked asset amount. @@ -183,6 +188,8 @@ pub mod pallet { }, /// An account harvested some rewards. RewardsHarvested { + /// The extrinsic caller. + who: T::AccountId, /// The staker whos rewards were harvested. staker: T::AccountId, /// The pool. @@ -192,6 +199,8 @@ pub mod pallet { }, /// A new reward pool was created. PoolCreated { + /// The account that created the pool. + creator: T::AccountId, /// Unique ID for the new pool. pool_id: PoolId, /// The staking asset. @@ -200,26 +209,21 @@ pub mod pallet { reward_asset_id: T::AssetId, /// The initial reward rate per block. reward_rate_per_block: T::Balance, + /// The account allowed to modify the pool. + admin: T::AccountId, }, - /// A reward pool was deleted. + /// A reward pool was deleted by the admin. PoolDeleted { /// The deleted pool id. pool_id: PoolId, }, - /// A pool was modified. + /// A pool was modified by the admin. PoolModifed { /// The modified pool. pool_id: PoolId, /// The new reward rate. new_reward_rate_per_block: T::Balance, }, - /// Reward assets were withdrawn from a pool. - RewardPoolWithdrawal { - /// The affected pool. - pool_id: PoolId, - /// The acount of reward asset withdrawn. - amount: T::Balance, - }, } #[pallet::error] @@ -231,7 +235,7 @@ pub mod pallet { #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { fn integrity_test() { - todo!() + // TODO: Proper implementation } } @@ -243,12 +247,49 @@ pub mod pallet { impl<T: Config> Pallet<T> { /// Create a new reward pool. pub fn create_pool( - _origin: OriginFor<T>, - _staked_asset_id: Box<T::AssetId>, - _reward_asset_id: Box<T::AssetId>, - _admin: Option<T::AccountId>, + origin: OriginFor<T>, + staked_asset_id: Box<T::AssetId>, + reward_asset_id: Box<T::AssetId>, + reward_rate_per_block: T::Balance, + admin: Option<T::AccountId>, ) -> DispatchResult { - todo!() + // Ensure Origin is allowed to create pools. + T::PermissionedPoolCreator::ensure_origin(origin.clone())?; + + // Get the admin, or try to use the origin as admin. + let origin_acc_id = ensure_signed(origin)?; + let admin = match admin { + Some(admin) => admin, + None => origin_acc_id, + }; + + // Create the pool. + let pool = PoolInfo::<T::AccountId, T::AssetId, T::Balance, BlockNumberFor<T>> { + staking_asset_id: *staked_asset_id.clone(), + reward_asset_id: *reward_asset_id.clone(), + reward_rate_per_block, + total_tokens_staked: 0u32.into(), + accumulated_rewards_per_share: 0u32.into(), + last_rewarded_block: 0u32.into(), + admin: admin.clone(), + }; + + // Insert the pool into storage. + let pool_id = NextPoolId::<T>::get(); + Pools::<T>::insert(pool_id, pool); + NextPoolId::<T>::put(pool_id.saturating_add(1)); + + // Emit the event. + Self::deposit_event(Event::PoolCreated { + creator: origin_acc_id, + pool_id, + staking_asset_id: *staked_asset_id, + reward_asset_id: *reward_asset_id, + reward_rate_per_block, + admin, + }); + + Ok(()) } /// Removes an existing reward pool. diff --git a/substrate/frame/staking-rewards/src/mock.rs b/substrate/frame/staking-rewards/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..658d1bb68426de33129ace5e13f86beb2b97af81 --- /dev/null +++ b/substrate/frame/staking-rewards/src/mock.rs @@ -0,0 +1,168 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Staking Rewards pallet. + +use super::*; +use crate as pallet_staking_rewards; +use core::default::Default; +use frame_support::{ + construct_runtime, derive_impl, + instances::Instance1, + ord_parameter_types, parameter_types, + traits::{ + tokens::fungible::{NativeFromLeft, NativeOrWithId, UnionOf}, + AsEnsureOriginWithArg, ConstU128, ConstU32, EnsureOrigin, + }, + PalletId, +}; +use frame_system::{ensure_signed, EnsureSigned}; +use sp_runtime::{ + traits::{AccountIdConversion, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlock<MockRuntime>; + +construct_runtime!( + pub enum MockRuntime + { + System: frame_system, + Balances: pallet_balances, + Assets: pallet_assets::<Instance1>, + StakingRewards: pallet_staking_rewards, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for MockRuntime { + type AccountId = u128; + type Lookup = IdentityLookup<Self::AccountId>; + type Block = Block; + type AccountData = pallet_balances::AccountData<u128>; +} + +impl pallet_balances::Config for MockRuntime { + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<100>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); +} + +impl pallet_assets::Config<Instance1> for MockRuntime { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg<EnsureSigned<Self::AccountId>>; + type ForceOrigin = frame_system::EnsureRoot<Self::AccountId>; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +parameter_types! { + pub const StakingRewardsPalletId: PalletId = PalletId(*b"py/stkrd"); + pub const Native: NativeOrWithId<u32> = NativeOrWithId::Native; + pub const PermissionedAccountId: u128 = 1; +} +ord_parameter_types! { + pub const AssetConversionOrigin: u128 = AccountIdConversion::<u128>::into_account_truncating(&StakingRewardsPalletId::get()); +} + +pub struct MockPermissionedPoolCreator; +impl EnsureOrigin<RuntimeOrigin> for MockPermissionedPoolCreator { + type Success = (); + + fn try_origin( + origin: RuntimeOrigin, + // key: &RuntimeParametersKey, + ) -> Result<Self::Success, RuntimeOrigin> { + // Set account 1 to admin in tests + if ensure_signed(origin.clone()).map_or(false, |acc| acc == 1) { + return Ok(()); + } + + return Err(origin); + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result<O, ()> { + todo!() + } +} + +pub type NativeAndAssets = UnionOf<Balances, Assets, NativeFromLeft, NativeOrWithId<u32>, u128>; + +impl Config for MockRuntime { + type RuntimeEvent = RuntimeEvent; + type AssetId = NativeOrWithId<u32>; + type Balance = <Self as pallet_balances::Config>::Balance; + type Assets = NativeAndAssets; + type PalletId = StakingRewardsPalletId; + // allow account id 1 to be permissioned creator + type PermissionedPoolCreator = MockPermissionedPoolCreator; +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::<MockRuntime>::default().build_storage().unwrap(); + + // pallet_assets::GenesisConfig::<MockRuntime, Instance1> { + // // Genesis assets: id, owner, is_sufficient, min_balance + // // pub assets: Vec<(T::AssetId, T::AccountId, bool, T::Balance)>, + // assets: vec![(1, 1, true, 10000)], + // // Genesis metadata: id, name, symbol, decimals + // // pub metadata: Vec<(T::AssetId, Vec<u8>, Vec<u8>, u8)>, + // metadata: vec![(1, b"test".to_vec(), b"TST".to_vec(), 18)], + // // Genesis accounts: id, account_id, balance + // // pub accounts: Vec<(T::AssetId, T::AccountId, T::Balance)>, + // accounts: vec![(1, 1, 10000)], + // } + // .assimilate_storage(&mut t) + // .unwrap(); + + pallet_balances::GenesisConfig::<MockRuntime> { + balances: vec![(1, 10000), (2, 20000), (3, 30000), (4, 40000)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/substrate/frame/staking-rewards/src/tests.rs b/substrate/frame/staking-rewards/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..18e1f86b516b07d203b47271d30e4e83f4f458e7 --- /dev/null +++ b/substrate/frame/staking-rewards/src/tests.rs @@ -0,0 +1,163 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{mock::*, *}; +use frame_support::{assert_ok, traits::fungible::NativeOrWithId}; + +fn create_tokens(owner: u128, tokens: Vec<NativeOrWithId<u32>>) { + create_tokens_with_ed(owner, tokens, 1) +} + +fn create_tokens_with_ed(owner: u128, tokens: Vec<NativeOrWithId<u32>>, ed: u128) { + for token_id in tokens { + let asset_id = match token_id { + NativeOrWithId::WithId(id) => id, + _ => unreachable!("invalid token"), + }; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, false, ed)); + } +} + +fn events() -> Vec<Event<MockRuntime>> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let mock::RuntimeEvent::StakingRewards(inner) = e { + Some(inner) + } else { + None + } + }) + .collect(); + + System::reset_events(); + + result +} + +fn pools() -> Vec<(u32, PoolInfo<u128, NativeOrWithId<u32>, u128, u64>)> { + Pools::<MockRuntime>::iter().collect() +} + +#[test] +fn create_pool_works() { + new_test_ext().execute_with(|| { + // Setup + let user = 1; + let staking_asset_id = NativeOrWithId::<u32>::Native; + let reward_asset_id = NativeOrWithId::<u32>::WithId(1); + let reward_rate_per_block = 100; + + create_tokens(user, vec![reward_asset_id.clone()]); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 1000)); + + // Create a pool with default admin. + assert_eq!(NextPoolId::<MockRuntime>::get(), 0); + assert_ok!(StakingRewards::create_pool( + RuntimeOrigin::signed(user), + Box::new(staking_asset_id.clone()), + Box::new(reward_asset_id.clone()), + reward_rate_per_block, + None + )); + + // Event is emitted. + assert_eq!( + events(), + [Event::<MockRuntime>::PoolCreated { + pool_id: 0, + staking_asset_id: staking_asset_id.clone(), + reward_asset_id: reward_asset_id.clone(), + reward_rate_per_block, + admin: user, + }] + ); + + // State is updated correctly. + assert_eq!(NextPoolId::<MockRuntime>::get(), 1); + assert_eq!( + pools(), + vec![( + 0, + PoolInfo { + staking_asset_id: staking_asset_id.clone(), + reward_asset_id: reward_asset_id.clone(), + reward_rate_per_block, + admin: user, + total_tokens_staked: 0, + accumulated_rewards_per_share: 0, + last_rewarded_block: 0 + } + )] + ); + + // Create another pool with explicit admin. + let admin = 2; + assert_ok!(StakingRewards::create_pool( + RuntimeOrigin::signed(user), + Box::new(staking_asset_id.clone()), + Box::new(reward_asset_id.clone()), + reward_rate_per_block, + Some(admin) + )); + + // Event is emitted. + assert_eq!( + events(), + [Event::<MockRuntime>::PoolCreated { + pool_id: 1, + staking_asset_id: staking_asset_id.clone(), + reward_asset_id: reward_asset_id.clone(), + reward_rate_per_block, + admin, + }] + ); + + // State is updated correctly. + assert_eq!(NextPoolId::<MockRuntime>::get(), 2); + assert_eq!( + pools(), + vec![ + ( + 0, + PoolInfo { + staking_asset_id: staking_asset_id.clone(), + reward_asset_id: reward_asset_id.clone(), + reward_rate_per_block, + admin: user, + total_tokens_staked: 0, + accumulated_rewards_per_share: 0, + last_rewarded_block: 0 + } + ), + ( + 1, + PoolInfo { + staking_asset_id, + reward_asset_id, + reward_rate_per_block, + admin, + total_tokens_staked: 0, + accumulated_rewards_per_share: 0, + last_rewarded_block: 0 + } + ) + ] + ); + }); +}