From 9f85746fc099b980359267efd610513480b18674 Mon Sep 17 00:00:00 2001 From: martinfridrich Date: Wed, 12 Jul 2023 17:39:20 +0200 Subject: [PATCH] staking: refactor, splited state() to stake and increase_stake and add position_id as param to rest of the extrinsics --- integration-tests/src/staking.rs | 109 +++++-- pallets/staking/src/lib.rs | 140 +++++--- pallets/staking/src/tests/claim.rs | 177 ++++++++-- pallets/staking/src/tests/increase_stake.rs | 338 ++++++++++++++++++++ pallets/staking/src/tests/mock.rs | 20 +- pallets/staking/src/tests/mod.rs | 1 + pallets/staking/src/tests/stake.rs | 331 +++---------------- pallets/staking/src/tests/unstake.rs | 91 ++++-- 8 files changed, 788 insertions(+), 419 deletions(-) create mode 100644 pallets/staking/src/tests/increase_stake.rs diff --git a/integration-tests/src/staking.rs b/integration-tests/src/staking.rs index 799a3e97f..5aa2b1ca3 100644 --- a/integration-tests/src/staking.rs +++ b/integration-tests/src/staking.rs @@ -94,7 +94,6 @@ fn staking_should_transfer_hdx_fees_to_pot_account_when_omnipool_trade_is_execut 0, )); - let staking_account = pallet_staking::Pallet::::pot_account_id(); assert_ok!(Omnipool::sell( hydradx_runtime::RuntimeOrigin::signed(CHARLIE.into()), DAI, @@ -196,23 +195,20 @@ fn staking_action_should_claim_points_for_finished_referendums_when_voted() { )); end_referendum(); - assert_ok!(Staking::stake( - hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), - 10 * UNITS - )); - - let stake_position_id = pallet_staking::Pallet::::get_user_position_id( + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( &sp_runtime::AccountId32::from(ALICE), ) .unwrap() .unwrap(); - println!( - "{:?}", - pallet_staking::Pallet::::positions(stake_position_id) - ); - let stake_voting = pallet_staking::Pallet::::get_position_votes(stake_position_id); + assert_ok!(Staking::increase_stake( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id, + 10 * UNITS + )); + + let stake_voting = pallet_staking::Pallet::::get_position_votes(alice_position_id); let stake_position = - pallet_staking::Pallet::::get_position(stake_position_id).unwrap(); + pallet_staking::Pallet::::get_position(alice_position_id).unwrap(); assert_eq!(stake_position.get_action_points(), 2); assert!(stake_voting.votes.is_empty()); @@ -254,27 +250,31 @@ fn staking_should_transfer_rewards_when_claimed() { )); end_referendum(); - assert_ok!(Staking::stake( + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( + &sp_runtime::AccountId32::from(ALICE), + ) + .unwrap() + .unwrap(); + assert_ok!(Staking::increase_stake( hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id, 10 * UNITS )); let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert!(alice_balance_after_claim > alice_balance); - let stake_position_id = pallet_staking::Pallet::::get_user_position_id( - &sp_runtime::AccountId32::from(ALICE), - ) - .unwrap() - .unwrap(); - let stake_voting = pallet_staking::Pallet::::get_position_votes(stake_position_id); + let stake_voting = pallet_staking::Pallet::::get_position_votes(alice_position_id); let stake_position = - pallet_staking::Pallet::::get_position(stake_position_id).unwrap(); + pallet_staking::Pallet::::get_position(alice_position_id).unwrap(); assert_eq!(stake_position.get_action_points(), 2); assert!(stake_voting.votes.is_empty()); @@ -316,22 +316,34 @@ fn staking_should_not_reward_when_double_claimed() { )); end_referendum(); + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( + &sp_runtime::AccountId32::from(ALICE), + ) + .unwrap() + .unwrap(); + // first claim let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert!(alice_balance_after_claim > alice_balance); // second claim let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert_eq!(alice_balance, alice_balance_after_claim); }); } #[test] -fn staking_should_not_reward_when_stake_again_and_no_vote_activity() { +fn staking_should_not_reward_when_increase_stake_again_and_no_vote_activity() { TestNet::reset(); Hydra::execute_with(|| { System::set_block_number(0); @@ -365,20 +377,32 @@ fn staking_should_not_reward_when_stake_again_and_no_vote_activity() { )); end_referendum(); - assert_ok!(Staking::stake( + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( + &sp_runtime::AccountId32::from(ALICE), + ) + .unwrap() + .unwrap(); + assert_ok!(Staking::increase_stake( hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id, 10 * UNITS )); // first claim let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert!(alice_balance_after_claim > alice_balance); // second claim let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert_eq!(alice_balance, alice_balance_after_claim); }); @@ -419,13 +443,22 @@ fn staking_should_claim_and_unreserve_rewards_when_unstaked() { )); end_referendum(); - assert_ok!(Staking::stake( + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( + &sp_runtime::AccountId32::from(ALICE), + ) + .unwrap() + .unwrap(); + assert_ok!(Staking::increase_stake( hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id, 10 * UNITS )); let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::unstake(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::unstake( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert!(alice_balance_after_claim > alice_balance); @@ -538,7 +571,10 @@ fn staking_should_not_reward_when_refenrendum_is_ongoing() { let stake_voting = pallet_staking::Pallet::::get_position_votes(stake_position_id); assert!(!stake_voting.votes.is_empty()); let alice_balance = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); - assert_ok!(Staking::claim(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()))); + assert_ok!(Staking::claim( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + stake_position_id + )); let alice_balance_after_claim = Currencies::free_balance(HDX, &AccountId32::from(ALICE)); assert_eq!(alice_balance, alice_balance_after_claim); end_referendum(); @@ -817,9 +853,18 @@ fn stake_should_fail_when_tokens_are_already_staked() { 15_000 * UNITS )); + let alice_position_id = pallet_staking::Pallet::::get_user_position_id( + &sp_runtime::AccountId32::from(ALICE), + ) + .unwrap() + .unwrap(); //Act & assert assert_noop!( - Staking::stake(hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), 10_000 * UNITS), + Staking::increase_stake( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + alice_position_id, + 10_000 * UNITS + ), pallet_staking::Error::::InsufficientBalance ); }); diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 406a00609..8b6cf31bf 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -28,7 +28,7 @@ use frame_support::{ defensive, pallet_prelude::DispatchResult, pallet_prelude::*, - traits::nonfungibles::{Create, InspectEnumerable, Mutate}, + traits::nonfungibles::{Create, Inspect, InspectEnumerable, Mutate}, traits::{DefensiveOption, LockIdentifier}, }; use hydra_dx_math::staking as math; @@ -155,6 +155,7 @@ pub mod pallet { /// Non fungible handling - mint,burn, check owner type NFTHandler: Mutate + Create + + Inspect + InspectEnumerable; #[pallet::constant] @@ -194,6 +195,12 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { + PositionCreated { + who: T::AccountId, + position_id: T::PositionItemId, + stake: Balance, + }, + StakeAdded { who: T::AccountId, position_id: T::PositionItemId, @@ -249,6 +256,12 @@ pub mod pallet { /// Pot's balance is zero. MissingPotBalance, + /// Account's position already exits. + PositionAlreadyExits, + + /// + Forbidden, + /// Action cannot be completed because unexpected error has occurred. This should be reported /// to protocol maintainers. InconsistentState(InconsistentStateError), @@ -305,51 +318,26 @@ pub mod pallet { ensure!(Self::is_initialized(), Error::::NotInitialized); + ensure!( + Self::get_user_position_id(&who)?.is_none(), + Error::::PositionAlreadyExits + ); + Staking::::try_mutate(|staking| { Self::update_rewards(staking)?; - let (position_id, position_new_total_stake, amount_to_lock, locked_rewards, slashed_points) = - if let Some(position_id) = Self::get_user_position_id(&who)? { - Positions::::try_mutate( - position_id, - |maybe_position| -> Result<(T::PositionItemId, Balance, Balance, Balance, Point), DispatchError> { - let position = maybe_position.as_mut().defensive_ok_or::>(InconsistentStateError::PositionNotFound.into())?; + Self::ensure_stakable_balance(&who, amount, None)?; + let position_id = + Self::create_position_and_mint_nft(&who, amount, staking.accumulated_reward_per_stake)?; - Self::ensure_stakable_balance(&who, amount, Some(&position))?; - - Self::process_votes(position_id, position)?; - - let current_period = Self::get_current_period().ok_or(Error::::Arithmetic)?; - let created_at = Self::get_period_number(position.created_at).ok_or(Error::::NotInitialized)?; //TOOD: better error - - let (rewards, slashed_points) = Self::do_increase_stake(position, staking, amount, current_period, created_at).ok_or(Error::::Arithmetic)?; - - let pot = Self::pot_account_id(); - T::Currency::transfer(T::HdxAssetId::get(), &pot, &who, rewards)?; - staking.accumulated_claimable_rewards = staking.accumulated_claimable_rewards.checked_sub(rewards).ok_or(Error::::Arithmetic)?; - - Ok((position_id, position.stake, position.get_total_locked()?, rewards, slashed_points)) - }, - )? - } else { - Self::ensure_stakable_balance(&who, amount, None)?; - let position_id = - Self::create_position_and_mint_nft(&who, amount, staking.accumulated_reward_per_stake)?; - - (position_id, amount, amount, 0, 0) - }; - - T::Currency::set_lock(STAKING_LOCK_ID, T::HdxAssetId::get(), &who, amount_to_lock)?; + T::Currency::set_lock(STAKING_LOCK_ID, T::HdxAssetId::get(), &who, amount)?; staking.add_stake(amount)?; - Self::deposit_event(Event::StakeAdded { + Self::deposit_event(Event::PositionCreated { who, position_id, stake: amount, - total_stake: position_new_total_stake, - locked_rewards, - slashed_points, }); Ok(()) @@ -358,12 +346,74 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight(1_000)] - pub fn claim(origin: OriginFor) -> DispatchResult { + pub fn increase_stake(origin: OriginFor, position_id: T::PositionItemId, amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; + ensure!(amount >= T::MinStake::get(), Error::::InsufficientStake); + ensure!(Self::is_initialized(), Error::::NotInitialized); - let position_id = Self::get_user_position_id(&who)?.ok_or(Error::::PositionNotFound)?; + ensure!(Self::is_owner(&who, position_id), Error::::Forbidden); + + Staking::::try_mutate(|staking| { + Self::update_rewards(staking)?; + + Positions::::try_mutate(position_id, |maybe_position| { + let position = maybe_position + .as_mut() + .defensive_ok_or::>(InconsistentStateError::PositionNotFound.into())?; + + Self::ensure_stakable_balance(&who, amount, Some(&position))?; + + Self::process_votes(position_id, position)?; + + let current_period = Self::get_current_period().ok_or(Error::::Arithmetic)?; + let created_at = Self::get_period_number(position.created_at).ok_or(Error::::NotInitialized)?; //TOOD: better error + + let (rewards_to_lock, slashed_points) = + Self::do_increase_stake(position, staking, amount, current_period, created_at) + .ok_or(Error::::Arithmetic)?; + + let pot = Self::pot_account_id(); + T::Currency::transfer(T::HdxAssetId::get(), &pot, &who, rewards_to_lock)?; + + staking.accumulated_claimable_rewards = staking + .accumulated_claimable_rewards + .checked_sub(rewards_to_lock) + .ok_or(Error::::Arithmetic)?; + + T::Currency::set_lock( + STAKING_LOCK_ID, + T::HdxAssetId::get(), + &who, + position.get_total_locked()?, + )?; + + staking.add_stake(amount)?; + + Self::deposit_event(Event::StakeAdded { + who, + position_id, + stake: amount, + total_stake: position.stake, + locked_rewards: rewards_to_lock, + slashed_points, + }); + + Ok(()) + }) + }) + } + + #[pallet::call_index(3)] + #[pallet::weight(1_000)] + pub fn claim(origin: OriginFor, position_id: T::PositionItemId) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(Self::is_initialized(), Error::::NotInitialized); + + ensure!(Self::is_owner(&who, position_id), Error::::Forbidden); + Staking::::try_mutate(|staking| { Self::update_rewards(staking)?; @@ -451,14 +501,15 @@ pub mod pallet { }) } - #[pallet::call_index(3)] + #[pallet::call_index(4)] #[pallet::weight(1_000)] - pub fn unstake(origin: OriginFor) -> DispatchResult { + pub fn unstake(origin: OriginFor, position_id: T::PositionItemId) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(Self::is_initialized(), Error::::NotInitialized); - let position_id = Self::get_user_position_id(&who)?.ok_or(Error::::PositionNotFound)?; + ensure!(Self::is_owner(&who, position_id), Error::::Forbidden); + Staking::::try_mutate(|staking| { Self::update_rewards(staking)?; @@ -567,6 +618,13 @@ impl Pallet { Ok(None) } + fn is_owner(who: &T::AccountId, id: T::PositionItemId) -> bool { + match ::NFTHandler::owner(&::NFTCollectionId::get(), &id) { + Some(owner) => owner == *who, + None => false, + } + } + fn create_position_and_mint_nft( who: &T::AccountId, staked_amount: Balance, diff --git a/pallets/staking/src/tests/claim.rs b/pallets/staking/src/tests/claim.rs index 79b3e5344..5907f7346 100644 --- a/pallets/staking/src/tests/claim.rs +++ b/pallets/staking/src/tests/claim.rs @@ -4,6 +4,93 @@ use mock::Staking; use pretty_assertions::assert_eq; use sp_runtime::FixedU128; +#[test] +fn claim_should_not_work_when_origin_is_not_position_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 150_000 * ONE), + (BOB, HDX, 250_000 * ONE), + (CHARLIE, HDX, 10_000 * ONE), + (DAVE, HDX, 100_000 * ONE), + ]) + .with_initialized_staking() + .start_at_block(1_452_987) + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 200_000 * ONE), + (CHARLIE, 10_000 * ONE, 1_455_000, 10_000 * ONE), + (DAVE, 10 * ONE, 1_465_000, 1), + ]) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(10_000 * ONE); + set_block_number(1_700_000); + let alice_position_id = Staking::get_user_position_id(&ALICE).unwrap().unwrap(); + + //Act & assert & assert & assert & assert + assert_noop!( + Staking::claim(RuntimeOrigin::signed(BOB), alice_position_id), + Error::::Forbidden + ); + }); +} + +#[test] +fn claim_should_work_when_claiming_multiple_times() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 150_000 * ONE), + (BOB, HDX, 250_000 * ONE), + (CHARLIE, HDX, 10_000 * ONE), + (DAVE, HDX, 100_000 * ONE), + ]) + .start_at_block(1_452_987) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 200_000 * ONE), + (BOB, 120_000 * ONE, 1_452_987, 0), + (CHARLIE, 10_000 * ONE, 1_455_000, 10_000 * ONE), + (DAVE, 10 * ONE, 1_465_000, 1), + ]) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(10_000 * ONE); + set_block_number(1_700_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); + + set_pending_rewards(100_000 * ONE); + set_block_number(1_800_000); + + //Act - 2nd claim + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); + + //Assert + assert_unlocked_balance!(&BOB, HDX, 130_382_474_184_859_806_u128); + assert_hdx_lock!(BOB, 120_000 * ONE, STAKING_LOCK); + assert_eq!( + Staking::positions(bob_position_id).unwrap(), + Position { + stake: 120_000 * ONE, + action_points: Zero::zero(), + created_at: 1_452_987, + reward_per_stake: FixedU128::from_inner(2_567_179_189_756_705_033_u128), + accumulated_slash_points: 56, + accumulated_locked_rewards: Zero::zero(), + accumulated_unpaid_rewards: Zero::zero(), + } + ); + + assert_staking_data!( + 230_010 * ONE, + FixedU128::from_inner(2_567_179_189_756_705_033_u128), + 262_322_856_849_994_928_u128 + NON_DUSTABLE_BALANCE + ); + }); +} + #[test] fn claim_should_not_work_when_staking_is_not_initialized() { ExtBuilder::default() @@ -18,10 +105,11 @@ fn claim_should_not_work_when_staking_is_not_initialized() { .execute_with(|| { //Arrange set_pending_rewards(10_000 * ONE); + let position_id = 0; //Act assert_noop!( - Staking::claim(RuntimeOrigin::signed(BOB)), + Staking::claim(RuntimeOrigin::signed(BOB), position_id), Error::::NotInitialized ); }); @@ -49,15 +137,16 @@ fn claim_should_work_when_staking_position_exists() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 130_334_912_244_857_841_u128); assert_hdx_lock!(BOB, 120_000 * ONE, STAKING_LOCK); assert_eq!( - Staking::positions(1).unwrap(), + Staking::positions(bob_position_id).unwrap(), Position { stake: 120_000 * ONE, action_points: Zero::zero(), @@ -72,7 +161,7 @@ fn claim_should_work_when_staking_position_exists() { assert_staking_data!( 230_010 * ONE, FixedU128::from_inner(2_088_930_916_047_128_389_u128), - 209_663_202_319_202_436_u128 + 209_663_202_319_202_436_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -99,15 +188,16 @@ fn claim_should_claim_nothing_when_claiming_during_unclaimable_periods() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_470_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 130_000 * ONE); assert_hdx_lock!(BOB, 120_000 * ONE, STAKING_LOCK); assert_eq!( - Staking::positions(1).unwrap(), + Staking::positions(bob_position_id).unwrap(), Position { stake: 120_000 * ONE, action_points: Zero::zero(), @@ -122,7 +212,7 @@ fn claim_should_claim_nothing_when_claiming_during_unclaimable_periods() { assert_staking_data!( 230_010 * ONE, FixedU128::from_inner(2_088_930_916_047_128_389_u128), - 220_000_000_000_000_001_u128 + 220_000_000_000_000_001_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -151,15 +241,16 @@ fn claim_should_claim_nothing_when_claiming_during_unclaimable_periods_and_stake //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_490_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 80_000 * ONE); assert_hdx_lock!(BOB, 420_000 * ONE, STAKING_LOCK); assert_eq!( - Staking::positions(1).unwrap(), + Staking::positions(bob_position_id).unwrap(), Position { stake: 420_000 * ONE, action_points: Zero::zero(), @@ -174,7 +265,7 @@ fn claim_should_claim_nothing_when_claiming_during_unclaimable_periods_and_stake assert_staking_data!( 530_010 * ONE, FixedU128::from_inner(2_502_134_933_892_361_376_u128), - 321_000_000_000_000_001_u128 + 321_000_000_000_000_001_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -203,15 +294,16 @@ fn claim_should_work_when_claiming_after_unclaimable_periods() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_640_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 80_587_506_297_210_440_u128); assert_hdx_lock!(BOB, 420_000 * ONE, STAKING_LOCK); assert_eq!( - Staking::positions(1).unwrap(), + Staking::positions(bob_position_id).unwrap(), Position { stake: 420_000 * ONE, action_points: Zero::zero(), @@ -226,13 +318,13 @@ fn claim_should_work_when_claiming_after_unclaimable_periods() { assert_staking_data!( 530_010 * ONE, FixedU128::from_inner(2_502_134_933_892_361_376_u128), - 255_368_022_548_622_160_u128 + 255_368_022_548_622_160_u128 + NON_DUSTABLE_BALANCE ); }); } #[test] -fn claim_should_work_when_claiming_multiple_times() { +fn claim_should_work_when_staked_was_increased() { ExtBuilder::default() .with_endowed_accounts(vec![ (ALICE, HDX, 150_000 * ONE), @@ -254,23 +346,37 @@ fn claim_should_work_when_claiming_multiple_times() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); set_block_number(1_750_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); set_pending_rewards(100_000 * ONE); set_block_number(2_100_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 281_979_585_716_709_787_u128); assert_hdx_lock!(BOB, 281_886_766_680_027_536_u128, STAKING_LOCK); assert_eq!( - Staking::positions(1).unwrap(), + Staking::positions(bob_position_id).unwrap(), Position { stake: 250_000 * ONE, action_points: Zero::zero(), @@ -285,7 +391,7 @@ fn claim_should_work_when_claiming_multiple_times() { assert_staking_data!( 360_010 * ONE, FixedU128::from_inner(3_061_853_686_489_680_776_u128), - 333_933_922_975_778_772_u128 + 333_933_922_975_778_772_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -313,19 +419,33 @@ fn claim_should_claim_zero_rewards_when_claiming_in_same_block_without_additiona //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); set_block_number(1_750_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); set_pending_rewards(100_000 * ONE); set_block_number(2_100_000); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 50_000 * ONE)); + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(BOB), + bob_position_id, + 50_000 * ONE + )); - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Act - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 281_979_585_716_709_787_u128); assert_hdx_lock!(BOB, 281_886_766_680_027_536_u128, STAKING_LOCK); @@ -345,7 +465,7 @@ fn claim_should_claim_zero_rewards_when_claiming_in_same_block_without_additiona assert_staking_data!( 360_010 * ONE, FixedU128::from_inner(3_123_517_875_338_556_934_u128), - 340_717_600_391_043_639_u128 + 340_717_600_391_043_639_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -371,11 +491,12 @@ fn claim_should_not_work_when_staking_position_doesnt_exists() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); + let non_existing_id = 131_234_123_421; //Act & assert & assert & assert & assert assert_noop!( - Staking::claim(RuntimeOrigin::signed(BOB)), - Error::::PositionNotFound + Staking::claim(RuntimeOrigin::signed(BOB), non_existing_id), + Error::::Forbidden ); }); } diff --git a/pallets/staking/src/tests/increase_stake.rs b/pallets/staking/src/tests/increase_stake.rs new file mode 100644 index 000000000..ac3d6d273 --- /dev/null +++ b/pallets/staking/src/tests/increase_stake.rs @@ -0,0 +1,338 @@ +use super::*; + +use mock::Staking; +use pretty_assertions::assert_eq; +use sp_runtime::FixedU128; + +#[test] +fn claim_should_not_work_when_staking_is_not_initialized() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 150_000 * ONE), + (BOB, HDX, 250_000 * ONE), + (CHARLIE, HDX, 10_000 * ONE), + (DAVE, HDX, 100_000 * ONE), + ]) + .start_at_block(1_452_987) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(10_000 * ONE); + let position_id = 0; + + //Act + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(ALICE), position_id, 100_000 * ONE), + Error::::NotInitialized + ); + }); +} + +#[test] +fn increase_stake_should_not_work_when_staking_position_doesnt_exists() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .start_at_block(1_452_987) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(5_000 * ONE); + set_block_number(1_600_000); + + let non_existing_position_id = 14_321_432_u128; + //Act + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(ALICE), non_existing_position_id, 100_000 * ONE), + Error::::Forbidden + ); + }); +} + +#[test] +fn increase_stake_should_not_work_when_origin_is_not_position_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .start_at_block(1_452_987) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(5_000 * ONE); + set_block_number(1_600_000); + + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + + //Act + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(ALICE), bob_position_id, 100_000 * ONE), + Error::::Forbidden + ); + }); +} + +#[test] +fn increase_stake_should_not_work_when_tokens_are_vested() { + ExtBuilder::default() + .with_endowed_accounts(vec![(VESTED_100K, HDX, 150_000 * ONE)]) + .with_stakes(vec![(VESTED_100K, 40_000 * ONE, 1_452_987, 100_000 * ONE)]) + .with_initialized_staking() + .build() + .execute_with(|| { + //Arrange + let pending_rewards = 200_000 * ONE; + set_pending_rewards(pending_rewards); + let staked_amount = 100_000 * ONE; + let position_id = 0; + + //Act & assert + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(VESTED_100K), position_id, staked_amount), + Error::::InsufficientBalance + ); + }); +} + +#[test] +fn increase_stake_should_work_when_user_already_staked() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .start_at_block(1_452_987) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(5_000 * ONE); + set_block_number(1_600_000); + + let alice_position_id = 0; + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 100_000 * ONE + )); + + //Assert + assert_staking_data!( + 250_000 * ONE, + FixedU128::from_inner(1_033_333_333_333_333_333_u128), + 104_567_913_548_294_171_u128 + NON_DUSTABLE_BALANCE + ); + assert_hdx_lock!(ALICE, 200_432_086_451_705_829_u128, STAKING_LOCK); + assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); + + assert_eq!( + Staking::positions(alice_position_id).unwrap(), + Position { + stake: 200_000 * ONE, + reward_per_stake: FixedU128::from_inner(1_033_333_333_333_333_333_u128), + created_at: 1_452_987, + accumulated_unpaid_rewards: 102_901_246_881_627_504, + action_points: 0, + accumulated_slash_points: 12, + accumulated_locked_rewards: 432_086_451_705_829_u128, + } + ); + }); +} + +#[test] +fn increase_stake_should_slash_no_points_when_increase_is_small() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(5_000 * ONE); + set_block_number(1_600_000); + + let alice_position_id = 0; + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 10 * ONE + )); + + //Assert + assert_eq!( + Staking::positions(alice_position_id).unwrap().accumulated_slash_points, + 0 + ); + }); +} + +#[test] +fn increase_stake_should_slash_all_points_when_increase_is_big() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 20_050_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .build() + .execute_with(|| { + //Arrange + let alice_position_id = 0; + let pending_rewards = 5_000 * ONE; + set_pending_rewards(pending_rewards); + + set_block_number(1_600_000); + + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 15_000_000 * ONE + )); + + //Assert + assert_eq!( + Staking::positions(alice_position_id).unwrap().accumulated_slash_points, + 24 + ); + }); +} + +#[test] +fn increase_stake_should_accumulate_slash_points_when_called_multiple_times() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 500_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), + (BOB, 50_000 * ONE, 1_452_987, 0), + ]) + .build() + .execute_with(|| { + //Arrange + let alice_position_id = 0; + let pending_rewards = 5_000 * ONE; + set_pending_rewards(pending_rewards); + + set_block_number(1_600_000); + + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 100_000 * ONE + )); + + //Assert + assert_eq!( + Staking::positions(alice_position_id).unwrap().accumulated_slash_points, + 12 + ); + + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 100_000 * ONE + )); + + //Assert + assert_eq!( + Staking::positions(alice_position_id).unwrap().accumulated_slash_points, + 15 + ); + + //Arrange + set_block_number(1_700_000); + //Act + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(ALICE), + alice_position_id, + 100_000 * ONE + )); + + //Assert + assert_staking_data!( + 450_000 * ONE, + FixedU128::from_inner(1_033_333_333_333_333_333_u128), + 104_034_822_662_406_905_u128 + NON_DUSTABLE_BALANCE + ); + assert_hdx_lock!(ALICE, 400_965_177_337_593_095_u128, STAKING_LOCK); + assert_unlocked_balance!(ALICE, HDX, 100_000 * ONE); + + assert_eq!( + Staking::positions(alice_position_id).unwrap(), + Position { + stake: 400_000 * ONE, + reward_per_stake: FixedU128::from_inner(1_033_333_333_333_333_333_u128), + created_at: 1_452_987, + accumulated_unpaid_rewards: 102_368_155_995_740_238_u128, + action_points: 0, + accumulated_slash_points: 19, + accumulated_locked_rewards: 965_177_337_593_095_u128, + } + ); + }); +} + +#[test] +fn increase_stake_should_not_work_when_increase_is_lt_min_stake() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .start_at_block(1_452_987) + .build() + .execute_with(|| { + //Arrange + let pending_rewards = 0; + set_pending_rewards(pending_rewards); + let staked_amount = 100_000 * ONE; + let alice_position_id = 0; + + assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount)); + assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), staked_amount / 2)); + + //Act & assert + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(ALICE), alice_position_id, MinStake::get() - 1), + Error::::InsufficientStake + ); + }); +} + +#[test] +fn increase_stake_should_not_work_when_tokens_are_are_alredy_staked() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE)]) + .with_initialized_staking() + .with_stakes(vec![(ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE)]) + .build() + .execute_with(|| { + //Arrange + let pending_rewards = 200_000 * ONE; + set_pending_rewards(pending_rewards); + let staked_amount = 100_000 * ONE; + let alice_position_id = 0; + + //Act & assert + assert_noop!( + Staking::increase_stake(RuntimeOrigin::signed(ALICE), alice_position_id, staked_amount), + Error::::InsufficientBalance + ); + }); +} diff --git a/pallets/staking/src/tests/mock.rs b/pallets/staking/src/tests/mock.rs index 795d737b8..265957bf0 100644 --- a/pallets/staking/src/tests/mock.rs +++ b/pallets/staking/src/tests/mock.rs @@ -52,6 +52,8 @@ pub const ONE: u128 = 1_000_000_000_000; pub const STAKING_LOCK: LockIdentifier = crate::STAKING_LOCK_ID; +pub const NON_DUSTABLE_BALANCE: Balance = 1_000 * ONE; + construct_runtime!( pub enum Test where Block = Block, @@ -317,7 +319,13 @@ impl ExtBuilder { if self.init_staking { let pot = Staking::pot_account_id(); - assert_ok!(Tokens::set_balance(RawOrigin::Root.into(), pot, HDX, 1_000 * ONE, 0)); + assert_ok!(Tokens::set_balance( + RawOrigin::Root.into(), + pot, + HDX, + NON_DUSTABLE_BALANCE, + 0 + )); assert_ok!(Staking::initialize_staking(RawOrigin::Root.into())); } @@ -327,7 +335,15 @@ impl ExtBuilder { } set_block_number(at); - assert_ok!(Staking::stake(RuntimeOrigin::signed(who), staked_amount)); + if let Some(position_id) = Staking::get_user_position_id(&who).unwrap() { + assert_ok!(Staking::increase_stake( + RuntimeOrigin::signed(who), + position_id, + staked_amount + )); + } else { + assert_ok!(Staking::stake(RuntimeOrigin::signed(who), staked_amount)); + } } }); diff --git a/pallets/staking/src/tests/mod.rs b/pallets/staking/src/tests/mod.rs index 468df0200..533d3bf9f 100644 --- a/pallets/staking/src/tests/mod.rs +++ b/pallets/staking/src/tests/mod.rs @@ -6,6 +6,7 @@ use frame_support::{assert_noop, assert_ok}; use orml_tokens::BalanceLock; mod claim; +mod increase_stake; pub(crate) mod mock; mod stake; mod unstake; diff --git a/pallets/staking/src/tests/stake.rs b/pallets/staking/src/tests/stake.rs index e63439ca4..3e44af4b4 100644 --- a/pallets/staking/src/tests/stake.rs +++ b/pallets/staking/src/tests/stake.rs @@ -25,36 +25,7 @@ fn stake_should_not_work_when_staking_is_not_initialized() { } #[test] -fn new_stake_should_work_when_staking_is_empty() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .start_at_block(1_452_987) - .build() - .execute_with(|| { - //Arrange - let pending_rewards = 200_000 * ONE; - set_pending_rewards(pending_rewards); - let staked_amount = 100_000 * ONE; - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount)); - - //Assert - assert_staking_data!(staked_amount, FixedU128::from(0), 0); - assert_hdx_lock!(ALICE, staked_amount, STAKING_LOCK); - assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); - - let next_position_id = Staking::next_position_id(); - assert_eq!( - Staking::positions(next_position_id - 1).unwrap(), - Position::new(staked_amount, FixedU128::from(0), 1_452_987) - ); - }); -} - -#[test] -fn new_stake_should_work_when_staking_is_not_empty() { +fn stake_should_work_when_staking_position_doesnt_exists() { ExtBuilder::default() .with_endowed_accounts(vec![ (ALICE, HDX, 150_000 * ONE), @@ -74,7 +45,7 @@ fn new_stake_should_work_when_staking_is_not_empty() { assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); //Assert //NOTE: first person doesn't distribute rewards because staking is empty. - assert_staking_data!(100_000 * ONE, FixedU128::from(0), 0); + assert_staking_data!(100_000 * ONE, FixedU128::from(0), NON_DUSTABLE_BALANCE); assert_hdx_lock!(ALICE, 100_000 * ONE, STAKING_LOCK); assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); @@ -86,7 +57,11 @@ fn new_stake_should_work_when_staking_is_not_empty() { //Act assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), 120_000 * ONE)); //Assert - assert_staking_data!(220_000 * ONE, FixedU128::from(2), pending_rewards); + assert_staking_data!( + 220_000 * ONE, + FixedU128::from(2), + pending_rewards + NON_DUSTABLE_BALANCE + ); assert_hdx_lock!(BOB, 120_000 * ONE, STAKING_LOCK); assert_unlocked_balance!(BOB, HDX, 130_000 * ONE); @@ -106,7 +81,7 @@ fn new_stake_should_work_when_staking_is_not_empty() { assert_staking_data!( 230_000 * ONE, FixedU128::from_inner(2_045_454_545_454_545_454_u128), - 200_000 * ONE + pending_rewards + 200_000 * ONE + pending_rewards + NON_DUSTABLE_BALANCE ); assert_hdx_lock!(CHARLIE, 10_000 * ONE, STAKING_LOCK); assert_unlocked_balance!(CHARLIE, HDX, 0); @@ -132,7 +107,7 @@ fn new_stake_should_work_when_staking_is_not_empty() { assert_staking_data!( 230_010 * ONE, FixedU128::from_inner(2_045_454_545_454_545_458_u128), - 210_000 * ONE + pending_rewards + 210_000 * ONE + pending_rewards + NON_DUSTABLE_BALANCE ); assert_hdx_lock!(DAVE, 10 * ONE, STAKING_LOCK); assert_unlocked_balance!(DAVE, HDX, 99_990 * ONE); @@ -149,254 +124,73 @@ fn new_stake_should_work_when_staking_is_not_empty() { } #[test] -fn new_stake_should_work_when_there_are_no_rewards_to_distribute() { +fn stake_should_not_work_when_staking_position_exits() { ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE)]) .with_initialized_staking() - .start_at_block(1_452_987) + .with_stakes(vec![(ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE)]) .build() .execute_with(|| { //Arrange - let pending_rewards = 0; + let pending_rewards = 200_000 * ONE; set_pending_rewards(pending_rewards); let staked_amount = 100_000 * ONE; - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount)); - - //Assert - assert_staking_data!(staked_amount, FixedU128::from(0), pending_rewards); - assert_hdx_lock!(ALICE, staked_amount, STAKING_LOCK); - assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); - - let next_position_id = Staking::next_position_id(); - assert_eq!( - Staking::positions(next_position_id - 1).unwrap(), - Position::new(staked_amount, FixedU128::from(0), 1_452_987) - ); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), staked_amount / 2)); - - //Assert - assert_staking_data!(staked_amount + staked_amount / 2, FixedU128::from(0), pending_rewards); - assert_hdx_lock!(BOB, staked_amount / 2, STAKING_LOCK); - assert_unlocked_balance!(BOB, HDX, 100_000 * ONE); - - let next_position_id = Staking::next_position_id(); - assert_eq!( - Staking::positions(next_position_id - 1).unwrap(), - Position::new(staked_amount / 2, FixedU128::from(0), 1_452_987) + //Act & assert + assert_noop!( + Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount), + Error::::PositionAlreadyExits ); }); } #[test] -fn increase_stake_should_work_when_user_already_staked() { +fn stake_should_work_when_there_are_no_rewards_to_distribute() { ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) + .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE), (BOB, HDX, 150_000 * ONE)]) .with_initialized_staking() - .with_stakes(vec![ - (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), - (BOB, 50_000 * ONE, 1_452_987, 0), - ]) .start_at_block(1_452_987) .build() .execute_with(|| { //Arrange - set_pending_rewards(5_000 * ONE); - set_block_number(1_600_000); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); - - //Assert - assert_staking_data!( - 250_000 * ONE, - FixedU128::from_inner(1_033_333_333_333_333_333_u128), - 104_567_913_548_294_171_u128 - ); - assert_hdx_lock!(ALICE, 200_432_086_451_705_829_u128, STAKING_LOCK); - assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); - - let alice_position_id = 0; - assert_eq!( - Staking::positions(alice_position_id).unwrap(), - Position { - stake: 200_000 * ONE, - reward_per_stake: FixedU128::from_inner(1_033_333_333_333_333_333_u128), - created_at: 1_452_987, - accumulated_unpaid_rewards: 102_901_246_881_627_504, - action_points: 0, - accumulated_slash_points: 12, - accumulated_locked_rewards: 432_086_451_705_829_u128, - } - ); - }); -} - -#[test] -fn increase_stake_should_work_when_user_staked_multiple_times() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 450_000 * ONE), (BOB, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .with_stakes(vec![ - (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), - (BOB, 50_000 * ONE, 1_452_987, 0), - (ALICE, 100_000 * ONE, 1_600_000, 5_000 * ONE), - ]) - .build() - .execute_with(|| { - //Arrange - let pending_rewards = 10_000 * ONE; + let pending_rewards = 0; set_pending_rewards(pending_rewards); - set_block_number(1_650_000); + let staked_amount = 100_000 * ONE; //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); + assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount)); //Assert assert_staking_data!( - 350_000 * ONE, - FixedU128::from_inner(1_073_333_333_333_333_333_u128), - 114_343_792_368_747_459_u128 + staked_amount, + FixedU128::from(0), + pending_rewards + NON_DUSTABLE_BALANCE ); - assert_hdx_lock!(ALICE, 300_656_207_631_252_541_u128, STAKING_LOCK); - assert_unlocked_balance!(ALICE, HDX, 150_000 * ONE); - - let alice_position_id = 0; - assert_eq!( - Staking::positions(alice_position_id).unwrap(), - Position { - stake: 300_000 * ONE, - reward_per_stake: FixedU128::from_inner(1_073_333_333_333_333_333_u128), - created_at: 1_452_987, - accumulated_unpaid_rewards: 110_677_125_702_080_792_u128, - action_points: 0, - accumulated_slash_points: 17, - accumulated_locked_rewards: 656_207_631_252_541_u128, - } - ); - }); -} - -#[test] -fn increase_stake_should_slash_no_points_when_increase_is_small() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 250_000 * ONE), (BOB, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .with_stakes(vec![ - (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), - (BOB, 50_000 * ONE, 1_452_987, 0), - ]) - .build() - .execute_with(|| { - //Arrange - set_pending_rewards(5_000 * ONE); - set_block_number(1_600_000); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 10 * ONE)); - - //Assert - let alice_position_id = 0; - assert_eq!( - Staking::positions(alice_position_id).unwrap().accumulated_slash_points, - 0 - ); - }); -} - -#[test] -fn increase_stake_should_slash_all_points_when_increase_is_big() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 20_050_000 * ONE), (BOB, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .with_stakes(vec![ - (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), - (BOB, 50_000 * ONE, 1_452_987, 0), - ]) - .build() - .execute_with(|| { - //Arrange - let alice_position_id = 0; - let pending_rewards = 5_000 * ONE; - set_pending_rewards(pending_rewards); - - set_block_number(1_600_000); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 15_000_000 * ONE)); - - //Assert - assert_eq!( - Staking::positions(alice_position_id).unwrap().accumulated_slash_points, - 24 - ); - }); -} - -#[test] -fn increase_stake_should_accumulate_slash_points_when_called_multiple_times() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 500_000 * ONE), (BOB, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .with_stakes(vec![ - (ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE), - (BOB, 50_000 * ONE, 1_452_987, 0), - ]) - .build() - .execute_with(|| { - //Arrange - let alice_position_id = 0; - let pending_rewards = 5_000 * ONE; - set_pending_rewards(pending_rewards); - - set_block_number(1_600_000); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); - - //Assert - assert_eq!( - Staking::positions(alice_position_id).unwrap().accumulated_slash_points, - 12 - ); - - //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); + assert_hdx_lock!(ALICE, staked_amount, STAKING_LOCK); + assert_unlocked_balance!(ALICE, HDX, 50_000 * ONE); - //Assert + let next_position_id = Staking::next_position_id(); assert_eq!( - Staking::positions(alice_position_id).unwrap().accumulated_slash_points, - 15 + Staking::positions(next_position_id - 1).unwrap(), + Position::new(staked_amount, FixedU128::from(0), 1_452_987) ); - //Arrange - set_block_number(1_700_000); //Act - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), 100_000 * ONE)); + assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), staked_amount / 2)); //Assert assert_staking_data!( - 450_000 * ONE, - FixedU128::from_inner(1_033_333_333_333_333_333_u128), - 104_034_822_662_406_905_u128 + staked_amount + staked_amount / 2, + FixedU128::from(0), + pending_rewards + NON_DUSTABLE_BALANCE ); - assert_hdx_lock!(ALICE, 400_965_177_337_593_095_u128, STAKING_LOCK); - assert_unlocked_balance!(ALICE, HDX, 100_000 * ONE); + assert_hdx_lock!(BOB, staked_amount / 2, STAKING_LOCK); + assert_unlocked_balance!(BOB, HDX, 100_000 * ONE); + let next_position_id = Staking::next_position_id(); assert_eq!( - Staking::positions(alice_position_id).unwrap(), - Position { - stake: 400_000 * ONE, - reward_per_stake: FixedU128::from_inner(1_033_333_333_333_333_333_u128), - created_at: 1_452_987, - accumulated_unpaid_rewards: 102_368_155_995_740_238_u128, - action_points: 0, - accumulated_slash_points: 19, - accumulated_locked_rewards: 965_177_337_593_095_u128, - } + Staking::positions(next_position_id - 1).unwrap(), + Position::new(staked_amount / 2, FixedU128::from(0), 1_452_987) ); }); } @@ -423,31 +217,7 @@ fn stake_should_not_work_when_stake_amount_is_lt_min_stake() { } #[test] -fn increase_stake_should_not_work_when_increase_is_lt_min_stake() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE), (BOB, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .start_at_block(1_452_987) - .build() - .execute_with(|| { - //Arrange - let pending_rewards = 0; - set_pending_rewards(pending_rewards); - let staked_amount = 100_000 * ONE; - - assert_ok!(Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount)); - assert_ok!(Staking::stake(RuntimeOrigin::signed(BOB), staked_amount / 2)); - - //Act & assert - assert_noop!( - Staking::stake(RuntimeOrigin::signed(ALICE), MinStake::get() - 1), - Error::::InsufficientStake - ); - }); -} - -#[test] -fn new_stake_should_not_work_when_tokens_are_vestred() { +fn stake_should_not_work_when_tokens_are_vestred() { ExtBuilder::default() .with_endowed_accounts(vec![(VESTED_100K, HDX, 150_000 * ONE)]) .with_initialized_staking() @@ -466,24 +236,3 @@ fn new_stake_should_not_work_when_tokens_are_vestred() { ); }); } - -#[test] -fn stake_should_not_work_when_tokens_are_are_alredy_staked() { - ExtBuilder::default() - .with_endowed_accounts(vec![(ALICE, HDX, 150_000 * ONE)]) - .with_initialized_staking() - .with_stakes(vec![(ALICE, 100_000 * ONE, 1_452_987, 100_000 * ONE)]) - .build() - .execute_with(|| { - //Arrange - let pending_rewards = 200_000 * ONE; - set_pending_rewards(pending_rewards); - let staked_amount = 100_000 * ONE; - - //Act & assert - assert_noop!( - Staking::stake(RuntimeOrigin::signed(ALICE), staked_amount), - Error::::InsufficientBalance - ); - }); -} diff --git a/pallets/staking/src/tests/unstake.rs b/pallets/staking/src/tests/unstake.rs index 39655f083..17473cb1f 100644 --- a/pallets/staking/src/tests/unstake.rs +++ b/pallets/staking/src/tests/unstake.rs @@ -4,6 +4,37 @@ use mock::Staking; use pretty_assertions::assert_eq; use sp_runtime::FixedU128; +#[test] +fn unstake_should_now_work_when_origin_is_not_position_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 150_000 * ONE), + (BOB, HDX, 250_000 * ONE), + (CHARLIE, HDX, 10_000 * ONE), + (DAVE, HDX, 100_000 * ONE), + ]) + .with_initialized_staking() + .start_at_block(1_452_987) + .with_stakes(vec![ + (ALICE, 100_000 * ONE, 1_452_987, 200_000 * ONE), + (BOB, 120_000 * ONE, 1_452_987, 0), + (CHARLIE, 10_000 * ONE, 1_455_000, 10_000 * ONE), + ]) + .build() + .execute_with(|| { + //Arrange + set_pending_rewards(10_000 * ONE); + set_block_number(1_700_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + + //Act & assert + assert_noop!( + Staking::unstake(RuntimeOrigin::signed(DAVE), bob_position_id), + Error::::Forbidden + ); + }); +} + #[test] fn unstake_should_not_work_when_staking_is_not_initialized() { ExtBuilder::default() @@ -19,10 +50,11 @@ fn unstake_should_not_work_when_staking_is_not_initialized() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); + let bob_position_id = 0; //Act & assert assert_noop!( - Staking::unstake(RuntimeOrigin::signed(BOB)), + Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id), Error::::NotInitialized ); }); @@ -50,21 +82,22 @@ fn unstake_should_work_when_staking_position_exists() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 250_334_912_244_857_841_u128); assert_hdx_lock!(BOB, 0, STAKING_LOCK); - assert_eq!(Staking::positions(1), None); + assert_eq!(Staking::positions(bob_position_id), None); assert_eq!(Staking::get_user_position_id(&BOB).unwrap(), None); assert_staking_data!( 110_010 * ONE, FixedU128::from_inner(2_088_930_916_047_128_389_u128), - 209_663_202_319_202_436_u128 + 209_663_202_319_202_436_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -91,20 +124,21 @@ fn unstake_should_claim_zero_rewards_when_unstaking_during_unclaimable_periods() //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_470_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 250_000 * ONE); assert_hdx_lock!(BOB, 0, STAKING_LOCK); - assert_eq!(Staking::positions(1), None); + assert_eq!(Staking::positions(bob_position_id), None); assert_eq!(Staking::get_user_position_id(&BOB).unwrap(), None); assert_staking_data!( 110_010 * ONE, FixedU128::from_inner(2_088_930_916_047_128_389_u128), - 209_328_290_074_344_595_u128 + 209_328_290_074_344_595_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -133,20 +167,21 @@ fn unstake_should_work_when_called_after_unclaimable_periods_and_stake_was_incre //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_690_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); //Act - assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, 500_682_646_815_225_830_u128); assert_hdx_lock!(BOB, 0, STAKING_LOCK); - assert_eq!(Staking::positions(1), None); + assert_eq!(Staking::positions(bob_position_id), None); assert_eq!(Staking::get_user_position_id(&BOB).unwrap(), None); assert_staking_data!( 110_010 * ONE, FixedU128::from_inner(2_502_134_933_892_361_376_u128), - 255_367_170_895_881_767_u128 + 255_367_170_895_881_767_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -175,23 +210,24 @@ fn unstake_should_claim_no_additional_rewards_when_called_immediately_after_clai //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_690_000); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); - assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::claim(RuntimeOrigin::signed(BOB), bob_position_id)); let bob_balance = Tokens::free_balance(HDX, &BOB); //Act - assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB))); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id)); //Assert assert_unlocked_balance!(&BOB, HDX, bob_balance); assert_hdx_lock!(BOB, 0, STAKING_LOCK); - assert_eq!(Staking::positions(1), None); + assert_eq!(Staking::positions(bob_position_id), None); assert_eq!(Staking::get_user_position_id(&BOB).unwrap(), None); assert_staking_data!( 110_010 * ONE, FixedU128::from_inner(2_624_680_135_471_373_855_u128), - 268_848_368_521_588_930_u128 + 268_848_368_521_588_930_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -220,12 +256,16 @@ fn unstake_should_work_when_called_by_all_stakers() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_690_000); + let alice_position_id = Staking::get_user_position_id(&ALICE).unwrap().unwrap(); + let bob_position_id = Staking::get_user_position_id(&BOB).unwrap().unwrap(); + let charlie_position_id = Staking::get_user_position_id(&CHARLIE).unwrap().unwrap(); + let dave_position_id = Staking::get_user_position_id(&DAVE).unwrap().unwrap(); //Act - assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB))); - assert_ok!(Staking::unstake(RuntimeOrigin::signed(ALICE))); - assert_ok!(Staking::unstake(RuntimeOrigin::signed(CHARLIE))); - assert_ok!(Staking::unstake(RuntimeOrigin::signed(DAVE))); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(BOB), bob_position_id)); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(ALICE), alice_position_id)); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(CHARLIE), charlie_position_id)); + assert_ok!(Staking::unstake(RuntimeOrigin::signed(DAVE), dave_position_id)); //Assert assert_unlocked_balance!(&ALICE, HDX, 157_951_370_453_331_101_u128); @@ -238,10 +278,10 @@ fn unstake_should_work_when_called_by_all_stakers() { assert_hdx_lock!(CHARLIE, 0, STAKING_LOCK); assert_hdx_lock!(DAVE, 0, STAKING_LOCK); - assert_eq!(Staking::positions(0), None); - assert_eq!(Staking::positions(1), None); - assert_eq!(Staking::positions(2), None); - assert_eq!(Staking::positions(3), None); + assert_eq!(Staking::positions(alice_position_id), None); + assert_eq!(Staking::positions(bob_position_id), None); + assert_eq!(Staking::positions(charlie_position_id), None); + assert_eq!(Staking::positions(dave_position_id), None); assert_eq!(Staking::get_user_position_id(&ALICE).unwrap(), None); assert_eq!(Staking::get_user_position_id(&BOB).unwrap(), None); @@ -251,7 +291,7 @@ fn unstake_should_work_when_called_by_all_stakers() { assert_staking_data!( 0, FixedU128::from_inner(28_824_441_394_573_800_928_500_u128), - 21_714_122_066_870_846_u128 + 21_714_122_066_870_846_u128 + NON_DUSTABLE_BALANCE ); }); } @@ -277,11 +317,12 @@ fn unstake_should_not_work_when_staking_position_doesnt_exists() { //Arrange set_pending_rewards(10_000 * ONE); set_block_number(1_700_000); + let non_existing_position_id = 122_341_234_213_u128; //Act & assert assert_noop!( - Staking::unstake(RuntimeOrigin::signed(DAVE)), - Error::::PositionNotFound + Staking::unstake(RuntimeOrigin::signed(DAVE), non_existing_position_id), + Error::::Forbidden ); }); }