Files
pezkuwi-sdk/bizinikiwi/pezframe/asset-rewards/src/tests.rs
T
pezkuwichain 1c0e57d984 feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
2025-12-14 00:04:10 +03:00

1456 lines
39 KiB
Rust

// This file is part of Bizinikiwi.
// 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 pezframe_support::{
assert_err, assert_noop, assert_ok, hypothetically,
traits::{
fungible,
fungible::NativeOrWithId,
fungibles,
tokens::{Fortitude, Preservation},
},
};
use pezsp_runtime::{traits::BadOrigin, ArithmeticError, TokenError};
const DEFAULT_STAKED_ASSET_ID: NativeOrWithId<u32> = NativeOrWithId::<u32>::WithId(1);
const DEFAULT_REWARD_ASSET_ID: NativeOrWithId<u32> = NativeOrWithId::<u32>::Native;
const DEFAULT_REWARD_RATE_PER_BLOCK: u128 = 100;
const DEFAULT_EXPIRE_AFTER: u64 = 200;
const DEFAULT_ADMIN: u128 = 1;
/// Creates a basic pool with values:
/// - Staking asset: 1
/// - Reward asset: Native
/// - Reward rate per block: 100
/// - Lifetime: 100
/// - Admin: 1
///
/// Useful to reduce boilerplate in tests when it's not important to customise or reuse pool
/// params.
pub fn create_default_pool() {
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(DEFAULT_STAKED_ASSET_ID.clone()),
Box::new(DEFAULT_REWARD_ASSET_ID.clone()),
DEFAULT_REWARD_RATE_PER_BLOCK,
DispatchTime::After(DEFAULT_EXPIRE_AFTER),
Some(DEFAULT_ADMIN)
));
}
/// The same as [`create_default_pool`], but with the admin parameter set to the creator.
pub fn create_default_pool_permissioned_admin() {
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(DEFAULT_STAKED_ASSET_ID.clone()),
Box::new(DEFAULT_REWARD_ASSET_ID.clone()),
DEFAULT_REWARD_RATE_PER_BLOCK,
DispatchTime::After(DEFAULT_EXPIRE_AFTER),
Some(PermissionedAccountId::get()),
));
}
fn assert_hypothetically_earned(
staker: u128,
expected_earned: u128,
pool_id: u32,
reward_asset_id: NativeOrWithId<u32>,
) {
hypothetically!({
// Get the pre-harvest balance.
let balance_before: <MockRuntime as Config>::Balance =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
// Harvest the rewards.
assert_ok!(StakingRewards::harvest_rewards(RuntimeOrigin::signed(staker), pool_id, None),);
// Sanity check: staker rewards are reset to 0 if some `amount` is still staked, otherwise
// the storage item removed.
if let Some(staker_pool) = PoolStakers::<MockRuntime>::get(pool_id, staker) {
assert!(staker_pool.rewards == 0);
assert!(staker_pool.amount > 0);
}
// Check that the staker has earned the expected amount.
let balance_after =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
assert_eq!(balance_after - balance_before, expected_earned);
});
}
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()
}
mod create_pool {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
System::set_block_number(10);
let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
// Create a pool with default values, and no admin override so [`PermissionedAccountId`]
// is admin.
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(DEFAULT_STAKED_ASSET_ID),
Box::new(DEFAULT_REWARD_ASSET_ID),
DEFAULT_REWARD_RATE_PER_BLOCK,
DispatchTime::After(DEFAULT_EXPIRE_AFTER),
Some(PermissionedAccountId::get())
));
// Event is emitted.
assert_eq!(
events(),
[Event::<MockRuntime>::PoolCreated {
creator: PermissionedAccountId::get(),
pool_id: 0,
staked_asset_id: DEFAULT_STAKED_ASSET_ID,
reward_asset_id: DEFAULT_REWARD_ASSET_ID,
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
expiry_block: expected_expiry_block,
admin: PermissionedAccountId::get(),
}]
);
// State is updated correctly.
assert_eq!(NextPoolId::<MockRuntime>::get(), 1);
assert_eq!(
pools(),
vec![(
0,
PoolInfo {
staked_asset_id: DEFAULT_STAKED_ASSET_ID,
reward_asset_id: DEFAULT_REWARD_ASSET_ID,
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
expiry_block: expected_expiry_block,
admin: PermissionedAccountId::get(),
total_tokens_staked: 0,
reward_per_token_stored: 0,
last_update_block: 0,
account: StakingRewards::pool_account_id(&0),
}
)]
);
// Create another pool with explicit admin and other overrides.
let admin = 2;
let staked_asset_id = NativeOrWithId::<u32>::WithId(10);
let reward_asset_id = NativeOrWithId::<u32>::WithId(20);
let reward_rate_per_block = 250;
let expiry_block = 500;
let expected_expiry_block = expiry_block + 10;
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(staked_asset_id.clone()),
Box::new(reward_asset_id.clone()),
reward_rate_per_block,
DispatchTime::After(expiry_block),
Some(admin)
));
// Event is emitted.
assert_eq!(
events(),
[Event::<MockRuntime>::PoolCreated {
creator: PermissionedAccountId::get(),
pool_id: 1,
staked_asset_id: staked_asset_id.clone(),
reward_asset_id: reward_asset_id.clone(),
reward_rate_per_block,
admin,
expiry_block: expected_expiry_block,
}]
);
// State is updated correctly.
assert_eq!(NextPoolId::<MockRuntime>::get(), 2);
assert_eq!(
pools(),
vec![
(
0,
PoolInfo {
staked_asset_id: DEFAULT_STAKED_ASSET_ID,
reward_asset_id: DEFAULT_REWARD_ASSET_ID,
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
admin: PermissionedAccountId::get(),
expiry_block: DEFAULT_EXPIRE_AFTER + 10,
total_tokens_staked: 0,
reward_per_token_stored: 0,
last_update_block: 0,
account: StakingRewards::pool_account_id(&0),
}
),
(
1,
PoolInfo {
staked_asset_id,
reward_asset_id,
reward_rate_per_block,
admin,
total_tokens_staked: 0,
expiry_block: expected_expiry_block,
reward_per_token_stored: 0,
last_update_block: 0,
account: StakingRewards::pool_account_id(&1),
}
)
]
);
});
}
#[test]
fn success_same_assets() {
new_test_ext().execute_with(|| {
assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
System::set_block_number(10);
let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
// Create a pool with the same staking and reward asset.
let asset = NativeOrWithId::<u32>::Native;
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(asset.clone()),
Box::new(asset.clone()),
DEFAULT_REWARD_RATE_PER_BLOCK,
DispatchTime::After(DEFAULT_EXPIRE_AFTER),
Some(PermissionedAccountId::get())
));
// Event is emitted.
assert_eq!(
events(),
[Event::<MockRuntime>::PoolCreated {
creator: PermissionedAccountId::get(),
pool_id: 0,
staked_asset_id: asset.clone(),
reward_asset_id: asset.clone(),
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
expiry_block: expected_expiry_block,
admin: PermissionedAccountId::get(),
}]
);
// State is updated correctly.
assert_eq!(NextPoolId::<MockRuntime>::get(), 1);
assert_eq!(
pools(),
vec![(
0,
PoolInfo {
staked_asset_id: asset.clone(),
reward_asset_id: asset,
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
expiry_block: expected_expiry_block,
admin: PermissionedAccountId::get(),
total_tokens_staked: 0,
reward_per_token_stored: 0,
last_update_block: 0,
account: StakingRewards::pool_account_id(&0),
}
)]
);
})
}
#[test]
fn fails_for_non_existent_asset() {
new_test_ext().execute_with(|| {
let valid_asset = NativeOrWithId::<u32>::WithId(1);
let invalid_asset = NativeOrWithId::<u32>::WithId(200);
assert_err!(
StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(valid_asset.clone()),
Box::new(invalid_asset.clone()),
10,
DispatchTime::After(10u64),
None
),
Error::<MockRuntime>::NonExistentAsset
);
assert_err!(
StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(invalid_asset.clone()),
Box::new(valid_asset.clone()),
10,
DispatchTime::After(10u64),
None
),
Error::<MockRuntime>::NonExistentAsset
);
assert_err!(
StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(invalid_asset.clone()),
Box::new(invalid_asset.clone()),
10,
DispatchTime::After(10u64),
None
),
Error::<MockRuntime>::NonExistentAsset
);
})
}
#[test]
fn fails_for_not_permissioned() {
new_test_ext().execute_with(|| {
let user = 100;
let staked_asset_id = NativeOrWithId::<u32>::Native;
let reward_asset_id = NativeOrWithId::<u32>::WithId(1);
let reward_rate_per_block = 100;
let expiry_block = 100u64;
assert_err!(
StakingRewards::create_pool(
RuntimeOrigin::signed(user),
Box::new(staked_asset_id.clone()),
Box::new(reward_asset_id.clone()),
reward_rate_per_block,
DispatchTime::After(expiry_block),
None
),
BadOrigin
);
});
}
#[test]
fn create_pool_with_caller_admin() {
new_test_ext().execute_with(|| {
assert_eq!(NextPoolId::<MockRuntime>::get(), 0);
System::set_block_number(10);
let expected_expiry_block = DEFAULT_EXPIRE_AFTER + 10;
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(DEFAULT_STAKED_ASSET_ID),
Box::new(DEFAULT_REWARD_ASSET_ID),
DEFAULT_REWARD_RATE_PER_BLOCK,
DispatchTime::After(DEFAULT_EXPIRE_AFTER),
None,
));
assert_eq!(
events(),
[Event::<MockRuntime>::PoolCreated {
creator: PermissionedAccountId::get(),
pool_id: 0,
staked_asset_id: DEFAULT_STAKED_ASSET_ID,
reward_asset_id: DEFAULT_REWARD_ASSET_ID,
reward_rate_per_block: DEFAULT_REWARD_RATE_PER_BLOCK,
expiry_block: expected_expiry_block,
admin: PermissionedAccountId::get(),
}]
);
assert_eq!(Pools::<MockRuntime>::get(0).unwrap().admin, PermissionedAccountId::get());
});
}
}
mod stake {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
let user = 1;
create_default_pool();
let pool_id = 0;
let initial_balance = <Assets as fungibles::Inspect<u128>>::reducible_balance(
1,
&user,
Preservation::Expendable,
Fortitude::Force,
);
// User stakes tokens
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
// Check that the user's staked amount is updated
assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 1000);
// Event is emitted.
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::Staked { staker: user, amount: 1000, pool_id: 0 }
);
// Check that the pool's total tokens staked is updated
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 1000);
// Check user's frozen balance is updated
assert_eq!(
<Assets as fungibles::Inspect<u128>>::reducible_balance(
1,
&user,
Preservation::Expendable,
Fortitude::Force,
),
initial_balance - 1000
);
// User stakes more tokens
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 500));
// Event is emitted.
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::Staked { staker: user, amount: 500, pool_id: 0 }
);
// Check that the user's staked amount is updated
assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 1000 + 500);
// Check that the pool's total tokens staked is updated
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 1000 + 500);
assert_eq!(
<Assets as fungibles::Inspect<u128>>::reducible_balance(
1,
&user,
Preservation::Expendable,
Fortitude::Force,
),
initial_balance - 1500
);
// Event is emitted.
assert_eq!(events(), []);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let user = 1;
assert_err!(
StakingRewards::stake(RuntimeOrigin::signed(user), 999, 1000),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_insufficient_balance() {
new_test_ext().execute_with(|| {
let user = 1;
create_default_pool();
let pool_id = 0;
let initial_balance = <Assets as fungibles::Inspect<u128>>::reducible_balance(
1,
&user,
Preservation::Expendable,
Fortitude::Force,
);
assert_err!(
StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, initial_balance + 1),
TokenError::FundsUnavailable,
);
})
}
}
mod unstake {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
let user = 1;
create_default_pool();
let pool_id = 0;
// User stakes tokens
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
// User unstakes tokens
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 500, None));
// Event is emitted.
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::Unstaked {
caller: user,
staker: user,
amount: 500,
pool_id: 0
}
);
// Check that the user's staked amount is updated
assert_eq!(PoolStakers::<MockRuntime>::get(pool_id, user).unwrap().amount, 500);
// Check that the pool's total tokens staked is updated
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 500);
// User unstakes remaining tokens
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 500, None));
// Check that the storage items is removed since stake amount and rewards are zero.
assert!(PoolStakers::<MockRuntime>::get(pool_id, user).is_none());
// Check that the pool's total tokens staked is zero
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().total_tokens_staked, 0);
});
}
#[test]
fn unstake_for_other() {
new_test_ext().execute_with(|| {
let staker = 1;
let caller = 2;
let pool_id = 0;
let init_block = System::block_number();
create_default_pool();
// User stakes tokens
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
// Fails to unstake for other since pool is still active
assert_noop!(
StakingRewards::unstake(RuntimeOrigin::signed(caller), pool_id, 500, Some(staker)),
BadOrigin,
);
System::set_block_number(init_block + DEFAULT_EXPIRE_AFTER + 1);
assert_ok!(StakingRewards::unstake(
RuntimeOrigin::signed(caller),
pool_id,
500,
Some(staker)
));
// Event is emitted.
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::Unstaked { caller, staker, amount: 500, pool_id: 0 }
);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let user = 1;
let non_existent_pool_id = 999;
// User tries to unstake tokens from a non-existent pool
assert_err!(
StakingRewards::unstake(
RuntimeOrigin::signed(user),
non_existent_pool_id,
500,
None
),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_insufficient_staked_amount() {
new_test_ext().execute_with(|| {
let user = 1;
create_default_pool();
let pool_id = 0;
// User stakes tokens
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(user), pool_id, 1000));
// User tries to unstake more tokens than they have staked
assert_err!(
StakingRewards::unstake(RuntimeOrigin::signed(user), pool_id, 1500, None),
Error::<MockRuntime>::NotEnoughTokens
);
});
}
}
mod harvest_rewards {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
let staker = 1;
let pool_id = 0;
let reward_asset_id = NativeOrWithId::<u32>::Native;
create_default_pool();
// Stake
System::set_block_number(10);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
// Harvest
System::set_block_number(20);
let balance_before: <MockRuntime as Config>::Balance =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
assert_ok!(StakingRewards::harvest_rewards(
RuntimeOrigin::signed(staker),
pool_id,
None
));
let balance_after =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker);
// Assert
assert_eq!(
balance_after - balance_before,
10 * Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block
);
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::RewardsHarvested {
caller: staker,
staker,
pool_id,
amount: 10 * Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block
}
);
});
}
#[test]
fn harvest_for_other() {
new_test_ext().execute_with(|| {
let caller = 2;
let staker = 1;
let pool_id = 0;
let init_block = System::block_number();
create_default_pool();
// Stake
System::set_block_number(10);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
System::set_block_number(20);
// Fails to harvest for staker since pool is still active
assert_noop!(
StakingRewards::harvest_rewards(
RuntimeOrigin::signed(caller),
pool_id,
Some(staker)
),
BadOrigin
);
System::set_block_number(init_block + DEFAULT_EXPIRE_AFTER + 1);
// Harvest for staker
assert_ok!(StakingRewards::harvest_rewards(
RuntimeOrigin::signed(caller),
pool_id,
Some(staker),
));
assert!(matches!(
events().last().unwrap(),
Event::<MockRuntime>::RewardsHarvested {
caller,
staker,
pool_id,
..
} if caller == caller && staker == staker && pool_id == pool_id
));
});
}
#[test]
fn fails_for_non_existent_staker() {
new_test_ext().execute_with(|| {
let non_existent_staker = 999;
create_default_pool();
assert_err!(
StakingRewards::harvest_rewards(
RuntimeOrigin::signed(non_existent_staker),
0,
None
),
Error::<MockRuntime>::NonExistentStaker
);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let staker = 1;
let non_existent_pool_id = 999;
assert_err!(
StakingRewards::harvest_rewards(
RuntimeOrigin::signed(staker),
non_existent_pool_id,
None,
),
Error::<MockRuntime>::NonExistentPool
);
});
}
}
mod set_pool_admin {
use super::*;
#[test]
fn success_signed_admin() {
new_test_ext().execute_with(|| {
let admin = 1;
let new_admin = 2;
let pool_id = 0;
create_default_pool();
// Modify the pool admin
assert_ok!(StakingRewards::set_pool_admin(
RuntimeOrigin::signed(admin),
pool_id,
new_admin,
));
// Check state
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolAdminModified { pool_id, new_admin }
);
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().admin, new_admin);
});
}
#[test]
fn success_permissioned_admin() {
new_test_ext().execute_with(|| {
let new_admin = 2;
let pool_id = 0;
create_default_pool_permissioned_admin();
// Modify the pool admin
assert_ok!(StakingRewards::set_pool_admin(RuntimeOrigin::root(), pool_id, new_admin));
// Check state
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolAdminModified { pool_id, new_admin }
);
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().admin, new_admin);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let admin = 1;
let new_admin = 2;
let non_existent_pool_id = 999;
assert_err!(
StakingRewards::set_pool_admin(
RuntimeOrigin::signed(admin),
non_existent_pool_id,
new_admin
),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_non_admin() {
new_test_ext().execute_with(|| {
let new_admin = 2;
let non_admin = 3;
let pool_id = 0;
create_default_pool();
assert_err!(
StakingRewards::set_pool_admin(
RuntimeOrigin::signed(non_admin),
pool_id,
new_admin
),
BadOrigin
);
});
}
}
mod set_pool_expiry_block {
use super::*;
#[test]
fn success_permissioned_admin() {
new_test_ext().execute_with(|| {
let pool_id = 0;
let new_expiry_block = System::block_number() + DEFAULT_EXPIRE_AFTER + 1u64;
create_default_pool_permissioned_admin();
assert_ok!(StakingRewards::set_pool_expiry_block(
RuntimeOrigin::root(),
pool_id,
DispatchTime::At(new_expiry_block),
));
// Check state
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().expiry_block, new_expiry_block);
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolExpiryBlockModified { pool_id, new_expiry_block }
);
});
}
#[test]
fn success_signed_admin() {
new_test_ext().execute_with(|| {
let admin = 1;
let pool_id = 0;
let new_expiry_block = System::block_number() + DEFAULT_EXPIRE_AFTER + 1u64;
create_default_pool();
assert_ok!(StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
pool_id,
DispatchTime::At(new_expiry_block)
));
// Check state
assert_eq!(Pools::<MockRuntime>::get(pool_id).unwrap().expiry_block, new_expiry_block);
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolExpiryBlockModified { pool_id, new_expiry_block }
);
});
}
#[test]
fn extends_reward_accumulation() {
new_test_ext().execute_with(|| {
let admin = 1;
let staker = 2;
let pool_id = 0;
let new_expiry_block = 300u64;
System::set_block_number(10);
create_default_pool();
// Regular reward accumulation
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
System::set_block_number(20);
assert_hypothetically_earned(
staker,
DEFAULT_REWARD_RATE_PER_BLOCK * 10,
pool_id,
NativeOrWithId::<u32>::Native,
);
// Expiry was block 210, so earned 200 at block 250
System::set_block_number(250);
assert_hypothetically_earned(
staker,
DEFAULT_REWARD_RATE_PER_BLOCK * 200,
pool_id,
NativeOrWithId::<u32>::Native,
);
// Extend expiry 50 more blocks
assert_ok!(StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
pool_id,
DispatchTime::At(new_expiry_block)
));
System::set_block_number(350);
// Staker has been in pool with rewards active for 250 blocks total
assert_hypothetically_earned(
staker,
DEFAULT_REWARD_RATE_PER_BLOCK * 250,
pool_id,
NativeOrWithId::<u32>::Native,
);
});
}
#[test]
fn fails_to_cutback_expiration() {
new_test_ext().execute_with(|| {
let admin = 1;
let pool_id = 0;
create_default_pool();
assert_noop!(
StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
pool_id,
DispatchTime::After(30)
),
Error::<MockRuntime>::ExpiryCut
);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let admin = 1;
let non_existent_pool_id = 999;
let new_expiry_block = 200u64;
assert_err!(
StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
non_existent_pool_id,
DispatchTime::After(new_expiry_block)
),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_non_admin() {
new_test_ext().execute_with(|| {
let non_admin = 2;
let pool_id = 0;
let new_expiry_block = 200u64;
create_default_pool();
assert_err!(
StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(non_admin),
pool_id,
DispatchTime::After(new_expiry_block)
),
BadOrigin
);
});
}
#[test]
fn fails_for_expiry_block_in_the_past() {
new_test_ext().execute_with(|| {
let admin = 1;
let pool_id = 0;
create_default_pool();
System::set_block_number(50);
assert_err!(
StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
pool_id,
DispatchTime::At(40u64)
),
Error::<MockRuntime>::ExpiryBlockMustBeInTheFuture
);
});
}
}
mod set_pool_reward_rate_per_block {
use super::*;
#[test]
fn success_signed_admin() {
new_test_ext().execute_with(|| {
let pool_id = 0;
let new_reward_rate = 200;
create_default_pool();
// Pool Admin can modify
assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::signed(DEFAULT_ADMIN),
pool_id,
new_reward_rate
));
// Check state
assert_eq!(
Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block,
new_reward_rate
);
// Check event
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolRewardRateModified {
pool_id,
new_reward_rate_per_block: new_reward_rate
}
);
});
}
#[test]
fn success_permissioned_admin() {
new_test_ext().execute_with(|| {
let pool_id = 0;
let new_reward_rate = 200;
create_default_pool_permissioned_admin();
// Root can modify
assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::root(),
pool_id,
new_reward_rate
));
// Check state
assert_eq!(
Pools::<MockRuntime>::get(pool_id).unwrap().reward_rate_per_block,
new_reward_rate
);
// Check event
assert_eq!(
*events().last().unwrap(),
Event::<MockRuntime>::PoolRewardRateModified {
pool_id,
new_reward_rate_per_block: new_reward_rate
}
);
});
}
#[test]
fn staker_rewards_are_affected_correctly() {
new_test_ext().execute_with(|| {
let admin = 1;
let staker = 2;
let pool_id = 0;
let new_reward_rate = 150;
create_default_pool();
// Stake some tokens, and accumulate 10 blocks of rewards at the default pool rate (100)
System::set_block_number(10);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 1000));
System::set_block_number(20);
// Increase the reward rate
assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::signed(admin),
pool_id,
new_reward_rate
));
// Accumulate 10 blocks of rewards at the new rate
System::set_block_number(30);
// Check that rewards are calculated correctly with the updated rate
assert_hypothetically_earned(
staker,
10 * 100 + 10 * new_reward_rate,
pool_id,
NativeOrWithId::<u32>::Native,
);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
let admin = 1;
let non_existent_pool_id = 999;
let new_reward_rate = 200;
assert_err!(
StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::signed(admin),
non_existent_pool_id,
new_reward_rate
),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_non_admin() {
new_test_ext().execute_with(|| {
let non_admin = 2;
let pool_id = 0;
let new_reward_rate = 200;
create_default_pool();
assert_err!(
StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::signed(non_admin),
pool_id,
new_reward_rate
),
BadOrigin
);
});
}
#[test]
fn fails_to_decrease() {
new_test_ext().execute_with(|| {
create_default_pool_permissioned_admin();
assert_noop!(
StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::root(),
0,
DEFAULT_REWARD_RATE_PER_BLOCK - 1
),
Error::<MockRuntime>::RewardRateCut
);
});
}
}
mod deposit_reward_tokens {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
let depositor = 1;
let pool_id = 0;
let amount = 1000;
let reward_asset_id = NativeOrWithId::<u32>::Native;
create_default_pool();
let pool_account_id = StakingRewards::pool_account_id(&pool_id);
let depositor_balance_before =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &depositor);
let pool_balance_before = <<MockRuntime as Config>::Assets>::balance(
reward_asset_id.clone(),
&pool_account_id,
);
assert_ok!(StakingRewards::deposit_reward_tokens(
RuntimeOrigin::signed(depositor),
pool_id,
amount
));
let depositor_balance_after =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &depositor);
let pool_balance_after =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id, &pool_account_id);
assert_eq!(pool_balance_after - pool_balance_before, amount);
assert_eq!(depositor_balance_before - depositor_balance_after, amount);
});
}
#[test]
fn fails_for_non_existent_pool() {
new_test_ext().execute_with(|| {
assert_err!(
StakingRewards::deposit_reward_tokens(RuntimeOrigin::signed(1), 999, 100),
Error::<MockRuntime>::NonExistentPool
);
});
}
#[test]
fn fails_for_insufficient_balance() {
new_test_ext().execute_with(|| {
create_default_pool();
assert_err!(
StakingRewards::deposit_reward_tokens(RuntimeOrigin::signed(1), 0, 100_000_000),
ArithmeticError::Underflow
);
});
}
}
mod cleanup_pool {
use super::*;
#[test]
fn success() {
new_test_ext().execute_with(|| {
let pool_id = 0;
let admin = DEFAULT_ADMIN;
let admin_balance_before = <Balances as fungible::Inspect<u128>>::balance(&admin);
create_default_pool();
assert!(Pools::<MockRuntime>::get(pool_id).is_some());
assert_ok!(StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id));
assert_eq!(
<Balances as fungible::Inspect<u128>>::balance(&admin),
// `100_000` initial pool account balance from Genesis config
admin_balance_before + 100_000,
);
assert_eq!(Pools::<MockRuntime>::get(pool_id), None);
assert_eq!(PoolStakers::<MockRuntime>::iter_prefix_values(pool_id).count(), 0);
assert_eq!(PoolCost::<MockRuntime>::get(pool_id), None);
});
}
#[test]
fn success_only_when_pool_empty() {
new_test_ext().execute_with(|| {
let pool_id = 0;
let staker = 20;
let admin = DEFAULT_ADMIN;
create_default_pool();
// stake to prevent pool cleanup
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker), pool_id, 100));
assert_noop!(
StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),
Error::<MockRuntime>::NonEmptyPool
);
// unstake partially
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker), pool_id, 50, None));
assert_noop!(
StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),
Error::<MockRuntime>::NonEmptyPool
);
// unstake all
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker), pool_id, 50, None));
assert_ok!(StakingRewards::cleanup_pool(RuntimeOrigin::signed(admin), pool_id),);
assert_eq!(Pools::<MockRuntime>::get(pool_id), None);
assert_eq!(PoolStakers::<MockRuntime>::iter_prefix_values(pool_id).count(), 0);
assert_eq!(PoolCost::<MockRuntime>::get(pool_id), None);
});
}
#[test]
fn fails_on_wrong_origin() {
new_test_ext().execute_with(|| {
let caller = 888;
let pool_id = 0;
create_default_pool();
assert_noop!(
StakingRewards::cleanup_pool(RuntimeOrigin::signed(caller), pool_id),
BadOrigin
);
});
}
}
/// This integration test
/// 1. Considers 2 stakers each staking and unstaking at different intervals, asserts their
/// claimable rewards are adjusted as expected, and that harvesting works.
/// 2. Checks that rewards are correctly halted after the pool's expiry block, and resume when the
/// pool is extended.
/// 3. Checks that reward rates adjustment works correctly.
///
/// Note: There are occasionally off by 1 errors due to rounding. In practice this is
/// insignificant.
#[test]
fn integration() {
new_test_ext().execute_with(|| {
let admin = 1;
let staker1 = 10u128;
let staker2 = 20;
let staked_asset_id = NativeOrWithId::<u32>::WithId(1);
let reward_asset_id = NativeOrWithId::<u32>::Native;
let reward_rate_per_block = 100;
let lifetime = 24u64.into();
System::set_block_number(1);
assert_ok!(StakingRewards::create_pool(
RuntimeOrigin::root(),
Box::new(staked_asset_id.clone()),
Box::new(reward_asset_id.clone()),
reward_rate_per_block,
DispatchTime::After(lifetime),
Some(admin)
));
let pool_id = 0;
// Block 7: Staker 1 stakes 100 tokens.
System::set_block_number(7);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100));
// At this point
// - Staker 1 has earned 0 tokens.
// - Staker 1 is earning 100 tokens per block.
// Check that Staker 1 has earned 0 tokens.
assert_hypothetically_earned(staker1, 0, pool_id, reward_asset_id.clone());
// Block 9: Staker 2 stakes 100 tokens.
System::set_block_number(9);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker2), pool_id, 100));
// At this point
// - Staker 1 has earned 200 (100*2) tokens.
// - Staker 2 has earned 0 tokens.
// - Staker 1 is earning 50 tokens per block.
// - Staker 2 is earning 50 tokens per block.
// Check that Staker 1 has earned 200 tokens and Staker 2 has earned 0 tokens.
assert_hypothetically_earned(staker1, 200, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 0, pool_id, reward_asset_id.clone());
// Block 12: Staker 1 stakes an additional 100 tokens.
System::set_block_number(12);
assert_ok!(StakingRewards::stake(RuntimeOrigin::signed(staker1), pool_id, 100));
// At this point
// - Staker 1 has earned 350 (200 + (50 * 3)) tokens.
// - Staker 2 has earned 150 (50 * 3) tokens.
// - Staker 1 is earning 66.66 tokens per block.
// - Staker 2 is earning 33.33 tokens per block.
// Check that Staker 1 has earned 350 tokens and Staker 2 has earned 150 tokens.
assert_hypothetically_earned(staker1, 350, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 150, pool_id, reward_asset_id.clone());
// Block 22: Staker 1 unstakes 100 tokens.
System::set_block_number(22);
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100, None));
// - Staker 1 has earned 1016 (350 + 66.66 * 10) tokens.
// - Staker 2 has earned 483 (150 + 33.33 * 10) tokens.
// - Staker 1 is earning 50 tokens per block.
// - Staker 2 is earning 50 tokens per block.
assert_hypothetically_earned(staker1, 1016, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 483, pool_id, reward_asset_id.clone());
// Block 23: Staker 1 unstakes 100 tokens.
System::set_block_number(23);
assert_ok!(StakingRewards::unstake(RuntimeOrigin::signed(staker1), pool_id, 100, None));
// - Staker 1 has earned 1065 (1015 + 50) tokens.
// - Staker 2 has earned 533 (483 + 50) tokens.
// - Staker 1 is earning 0 tokens per block.
// - Staker 2 is earning 100 tokens per block.
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 533, pool_id, reward_asset_id.clone());
// Block 50: Stakers should only have earned 2 blocks worth of tokens (expiry is 25).
System::set_block_number(50);
// - Staker 1 has earned 1065 tokens.
// - Staker 2 has earned 733 (533 + 2 * 100) tokens.
// - Staker 1 is earning 0 tokens per block.
// - Staker 2 is earning 0 tokens per block.
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone());
// Block 51: Extend the pool expiry block to 60.
System::set_block_number(51);
// - Staker 1 is earning 0 tokens per block.
// - Staker 2 is earning 100 tokens per block.
assert_ok!(StakingRewards::set_pool_expiry_block(
RuntimeOrigin::signed(admin),
pool_id,
DispatchTime::At(60u64),
));
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 733, pool_id, reward_asset_id.clone());
// Block 53: Check rewards are resumed.
// - Staker 1 has earned 1065 tokens.
// - Staker 2 has earned 933 (733 + 2 * 100) tokens.
// - Staker 2 is earning 100 tokens per block.
System::set_block_number(53);
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 933, pool_id, reward_asset_id.clone());
// Block 55: Increase the block reward.
// - Staker 1 has earned 1065 tokens.
// - Staker 2 has earned 1133 (933 + 2 * 100) tokens.
// - Staker 2 is earning 50 tokens per block.
System::set_block_number(55);
assert_ok!(StakingRewards::set_pool_reward_rate_per_block(
RuntimeOrigin::signed(admin),
pool_id,
150
));
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 1133, pool_id, reward_asset_id.clone());
// Block 57: Staker2 harvests their rewards.
System::set_block_number(57);
// - Staker 2 has earned 1433 (1133 + 2 * 150) tokens.
assert_hypothetically_earned(staker2, 1433, pool_id, reward_asset_id.clone());
// Get the pre-harvest balance.
let balance_before: <MockRuntime as Config>::Balance =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker2);
assert_ok!(StakingRewards::harvest_rewards(RuntimeOrigin::signed(staker2), pool_id, None));
let balance_after =
<<MockRuntime as Config>::Assets>::balance(reward_asset_id.clone(), &staker2);
assert_eq!(balance_after - balance_before, 1433u128);
// Block 60: Check rewards were adjusted correctly.
// - Staker 1 has earned 1065 tokens.
// - Staker 2 has earned 450 (3 * 150) tokens.
System::set_block_number(60);
assert_hypothetically_earned(staker1, 1066, pool_id, reward_asset_id.clone());
assert_hypothetically_earned(staker2, 450, pool_id, reward_asset_id.clone());
// Finally, check events.
assert_eq!(
events(),
[
Event::PoolCreated {
creator: PermissionedAccountId::get(),
pool_id,
staked_asset_id,
reward_asset_id,
reward_rate_per_block: 100,
expiry_block: 25,
admin,
},
Event::Staked { staker: staker1, pool_id, amount: 100 },
Event::Staked { staker: staker2, pool_id, amount: 100 },
Event::Staked { staker: staker1, pool_id, amount: 100 },
Event::Unstaked { caller: staker1, staker: staker1, pool_id, amount: 100 },
Event::Unstaked { caller: staker1, staker: staker1, pool_id, amount: 100 },
Event::PoolExpiryBlockModified { pool_id, new_expiry_block: 60 },
Event::PoolRewardRateModified { pool_id, new_reward_rate_per_block: 150 },
Event::RewardsHarvested { caller: staker2, staker: staker2, pool_id, amount: 1433 }
]
);
});
}