Skip to content

Commit

Permalink
Add move stake feature to creator staking pallet (#242)
Browse files Browse the repository at this point in the history
* Add move stake feature to creator staking pallet

* Fix call indexes in creator staking pallet

* Rename `calculate_final_unstaking_amount` to `calculate_and_apply_stake_decrease`

* Add tests for move_stake

* Update spec_version to 34

* Update transaction_version to 7

* Improve namings in creator staking testing utils

---------

Co-authored-by: Alex Siman <[email protected]>
  • Loading branch information
F3Joule and siman authored Dec 6, 2023
1 parent 292a919 commit bdf4d2b
Show file tree
Hide file tree
Showing 6 changed files with 500 additions and 33 deletions.
19 changes: 12 additions & 7 deletions pallets/creator-staking/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ impl<T: Config> Pallet<T> {
.map_or(false, |info| info.status == CreatorStatus::Active)
}

pub(crate) fn ensure_creator_is_active(creator_id: CreatorId) -> DispatchResult {
ensure!(Self::is_creator_active(creator_id), Error::<T>::InactiveCreator);
Ok(())
}

pub(crate) fn ensure_creator_active_in_era(
creator_info: &CreatorInfo<T::AccountId>,
era: EraIndex,
Expand Down Expand Up @@ -145,7 +150,7 @@ impl<T: Config> Pallet<T> {
/// If the unstake operation was successful, the given structs are properly modified and the total
/// unstaked value is returned. If not, an error is returned and the structs are left in
/// an undefined state.
pub(crate) fn calculate_final_unstaking_amount(
pub(crate) fn calculate_and_apply_stake_decrease(
backer_stakes: &mut StakesInfoOf<T>,
stake_info: &mut CreatorStakeInfo<BalanceOf<T>>,
desired_amount: BalanceOf<T>,
Expand All @@ -154,29 +159,29 @@ impl<T: Config> Pallet<T> {
let staked_value = backer_stakes.current_stake();
ensure!(staked_value > Zero::zero(), Error::<T>::NotStakedCreator);

// Calculate the value which will be unstaked.
let remaining = staked_value.saturating_sub(desired_amount);

// If the remaining amount is less than the minimum staking amount, unstake the entire amount.
let amount_to_unstake = if remaining < T::MinimumStake::get() {
let amount_to_decrease = if remaining < T::MinimumStake::get() {
stake_info.backers_count = stake_info.backers_count.saturating_sub(1);
staked_value
} else {
desired_amount
};

// Sanity check
ensure!(amount_to_unstake > Zero::zero(), Error::<T>::CannotUnstakeZero);
ensure!(amount_to_decrease > Zero::zero(), Error::<T>::CannotUnstakeZero);

stake_info.total_staked = stake_info.total_staked.saturating_sub(amount_to_unstake);
// Modify data
stake_info.total_staked = stake_info.total_staked.saturating_sub(amount_to_decrease);

backer_stakes
.decrease_stake(current_era, amount_to_unstake)
.decrease_stake(current_era, amount_to_decrease)
.map_err(|_| Error::<T>::CannotChangeStakeInPastEra)?;

Self::ensure_can_add_stake_item(backer_stakes)?;

Ok(amount_to_unstake)
Ok(amount_to_decrease)
}

/// Update the locks for a backer. This will also update the stash lock.
Expand Down
104 changes: 85 additions & 19 deletions pallets/creator-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ pub mod pallet {
pub enum Event<T: Config> {
Staked { who: T::AccountId, creator_id: CreatorId, era: EraIndex, amount: BalanceOf<T> },
Unstaked { who: T::AccountId, creator_id: CreatorId, era: EraIndex, amount: BalanceOf<T> },
StakeMoved { who: T::AccountId, from_creator_id: CreatorId, to_creator_id: CreatorId, amount: BalanceOf<T> },
BackerRewardsClaimed { who: T::AccountId, creator_id: CreatorId, amount: BalanceOf<T> },
CreatorRewardsClaimed { who: T::AccountId, amount: BalanceOf<T> },
StakeWithdrawn { who: T::AccountId, amount: BalanceOf<T> },
Expand Down Expand Up @@ -273,6 +274,8 @@ pub mod pallet {
MaintenanceModeNotChanged,
InvalidSumOfRewardDistributionConfig,
StakeHasExpired,
CannotMoveStakeToSameCreator,
CannotMoveZeroStake,
}

#[pallet::hooks]
Expand Down Expand Up @@ -392,7 +395,7 @@ pub mod pallet {
let backer = ensure_signed(origin)?;

// Check that a creator is ready for staking.
ensure!(Self::is_creator_active(creator_id), Error::<T>::InactiveCreator);
Self::ensure_creator_is_active(creator_id)?;

// Retrieve the backer's locks, or create an entry if it doesn't exist.
let mut backer_locks = Self::backer_locks(&backer);
Expand Down Expand Up @@ -435,16 +438,6 @@ pub mod pallet {
Ok(().into())
}

// #[weight = 10_000]
// fn increase_stake(origin, creator_id, additional_amount) {
// todo!()
// }
//
// #[weight = 10_000]
// fn move_stake(origin, from_creator_id, to_creator_id, amount) {
// todo!()
// }

/// Start unbonding process and unstake balance from the creator.
///
/// The unstaked amount will no longer be eligible for rewards but still won't be unlocked.
Expand All @@ -464,15 +457,15 @@ pub mod pallet {
let backer = ensure_signed(origin)?;

ensure!(amount > Zero::zero(), Error::<T>::CannotUnstakeZero);
ensure!(Self::is_creator_active(creator_id), Error::<T>::InactiveCreator);
Self::ensure_creator_is_active(creator_id)?;

let current_era = Self::current_era();
let mut backer_stakes = Self::backer_stakes(&backer, creator_id);
let mut stake_info =
Self::creator_stake_info(creator_id, current_era).unwrap_or_default();

let amount_to_unstake =
Self::calculate_final_unstaking_amount(&mut backer_stakes, &mut stake_info, amount, current_era)?;
Self::calculate_and_apply_stake_decrease(&mut backer_stakes, &mut stake_info, amount, current_era)?;

// Update the chunks and write them to storage
let mut backer_locks = Self::backer_locks(&backer);
Expand Down Expand Up @@ -589,11 +582,84 @@ pub mod pallet {
Ok(().into())
}

/// Move stake from one creator to another.
///
/// It follows the same rules as the `stake` and `unstake` functions, with one notable
/// difference: there is no unbonding period.
///
/// # Parameters
/// - `from_creator_id`: The ID of the source creator.
/// - `to_creator_id`: The ID of the target creator.
/// - `amount`: The amount of stake to be moved.
///
#[pallet::call_index(7)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn move_stake(
origin: OriginFor<T>,
from_creator_id: CreatorId,
to_creator_id: CreatorId,
#[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
Self::ensure_pallet_enabled()?;
let backer = ensure_signed(origin)?;

// Creators must differ and both must be active
ensure!(
from_creator_id != to_creator_id,
Error::<T>::CannotMoveStakeToSameCreator
);

ensure!(amount > Zero::zero(), Error::<T>::CannotMoveZeroStake);

Self::ensure_creator_is_active(to_creator_id)?;

// Validate and update previous creator related data
let current_era = Self::current_era();
let mut backer_stakes_by_source_creator = Self::backer_stakes(&backer, from_creator_id);
let mut source_creator_info =
Self::creator_stake_info(from_creator_id, current_era).unwrap_or_default();

// Backer stake is decreased in `calculate_and_apply_stake_decrease`
let stake_amount_to_move = Self::calculate_and_apply_stake_decrease(
&mut backer_stakes_by_source_creator,
&mut source_creator_info,
amount,
current_era,
)?;

// Validate and update target creator related data
let mut backer_stakes_by_target_creator = Self::backer_stakes(&backer, to_creator_id);
let mut target_creator_info =
Self::creator_stake_info(to_creator_id, current_era).unwrap_or_default();

Self::stake_to_creator(
&mut backer_stakes_by_target_creator,
&mut target_creator_info,
stake_amount_to_move,
current_era,
)?;

CreatorStakeInfoByEra::<T>::insert(from_creator_id, current_era, source_creator_info);
Self::update_backer_stakes(&backer, from_creator_id, backer_stakes_by_source_creator);

CreatorStakeInfoByEra::<T>::insert(to_creator_id, current_era, target_creator_info);
Self::update_backer_stakes(&backer, to_creator_id, backer_stakes_by_target_creator);

Self::deposit_event(Event::<T>::StakeMoved {
who: backer,
from_creator_id,
to_creator_id,
amount: stake_amount_to_move,
});

Ok(().into())
}

// Claim rewards for the backer on the oldest unclaimed era where they have a stake
// and optionally restake the rewards to the same creator.
// Not sure here whether to calculate total rewards for all creators
// or to withdraw per-creator rewards (preferably)
#[pallet::call_index(7)]
#[pallet::call_index(8)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn claim_backer_reward(origin: OriginFor<T>, creator_id: CreatorId, restake: bool) -> DispatchResultWithPostInfo {
Self::ensure_pallet_enabled()?;
Expand Down Expand Up @@ -654,7 +720,7 @@ pub mod pallet {
Ok(().into())
}

#[pallet::call_index(8)]
#[pallet::call_index(9)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn claim_creator_reward(origin: OriginFor<T>, creator_id: CreatorId, era: EraIndex) -> DispatchResultWithPostInfo {
Self::ensure_pallet_enabled()?;
Expand Down Expand Up @@ -711,7 +777,7 @@ pub mod pallet {
Ok(().into())
}

#[pallet::call_index(9)]
#[pallet::call_index(10)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn set_maintenance_mode(
origin: OriginFor<T>,
Expand All @@ -732,15 +798,15 @@ pub mod pallet {
Ok(().into())
}

#[pallet::call_index(10)]
#[pallet::call_index(11)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn force_new_era(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
ForceEra::<T>::put(Forcing::ForceNew);
Ok(Pays::No.into())
}

#[pallet::call_index(11)]
#[pallet::call_index(12)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn set_reward_distribution_config(
origin: OriginFor<T>,
Expand All @@ -756,7 +822,7 @@ pub mod pallet {
Ok(())
}

#[pallet::call_index(12)]
#[pallet::call_index(13)]
#[pallet::weight(T::DbWeight::get().writes(1) + Weight::from_parts(10_000, 0))]
pub fn set_per_block_reward(origin: OriginFor<T>, new_reward: BalanceOf<T>) -> DispatchResult {
ensure_root(origin)?;
Expand Down
98 changes: 93 additions & 5 deletions pallets/creator-staking/src/tests/testing_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,7 @@ pub(super) fn assert_claim_backer(claimer: AccountId, creator_id: SpaceId, resta
let mut init_state_current_era = MemorySnapshot::all(current_era, creator_id, claimer);

// Calculate backer's portion of the reward
let stakes = init_state_claim_era.backer_stakes.stakes.clone();
let (claim_era, staked) = StakesInfo { stakes }.claim();
let (claim_era, staked) = init_state_claim_era.backer_stakes.clone().claim();
assert!(claim_era > 0); // Sanity check - if this fails, method is being used incorrectly

// Cannot claim rewards post unregister era, this indicates a bug!
Expand Down Expand Up @@ -479,8 +478,7 @@ pub(super) fn assert_claim_backer(claimer: AccountId, creator_id: SpaceId, resta
amount: calculated_reward,
}));

let stakes = final_state_current_era.backer_stakes.stakes.clone();
let (new_era, _) = StakesInfo { stakes }.claim();
let (new_era, _) = final_state_current_era.backer_stakes.clone().claim();
if final_state_current_era.backer_stakes.is_empty() {
assert!(new_era.is_zero());
assert!(!BackerStakesByCreator::<TestRuntime>::contains_key(
Expand Down Expand Up @@ -513,7 +511,7 @@ fn assert_restake_reward(
final_state_current_era: &MemorySnapshot,
reward: Balance,
) {
let mut init_backer_stakes = StakesInfo { stakes: init_state_current_era.backer_stakes.stakes.clone() };
let mut init_backer_stakes = init_state_current_era.backer_stakes.clone();
if CreatorStaking::ensure_can_restake_reward(
restake,
init_state_current_era.clone().creator_info.status,
Expand Down Expand Up @@ -599,3 +597,93 @@ pub(super) fn assert_claim_creator(creator_id: SpaceId, claim_era: EraIndex) {
assert_eq!(init_state.backer_locks.total_locked, final_state.backer_locks.total_locked);
assert_eq!(init_state.backer_locks.unbonding_info.vec(), final_state.backer_locks.unbonding_info.vec());
}

/// Used to perform move stake with success and storage assertions.
pub(crate) fn assert_move_stake(
backer: AccountId,
from_creator_id: CreatorId,
to_creator_id: CreatorId,
amount: Balance,
) {
// Get latest staking info
let current_era = CreatorStaking::current_era();
let source_creator_init_state = MemorySnapshot::all(current_era, from_creator_id, backer);
let target_creator_init_state = MemorySnapshot::all(current_era, to_creator_id, backer);

// Calculate value which will actually be transfered
let source_creator_init_stake_amount = source_creator_init_state.backer_stakes.current_stake();
let expected_amount_to_move = if source_creator_init_stake_amount - amount >= MINIMUM_STAKING_AMOUNT {
amount
} else {
source_creator_init_stake_amount
};

// Ensure op is successful and event is emitted
assert_ok!(CreatorStaking::move_stake(
RuntimeOrigin::signed(backer),
from_creator_id,
to_creator_id,
amount,
));
System::assert_last_event(RuntimeEvent::CreatorStaking(Event::StakeMoved {
who: backer.clone(),
from_creator_id,
to_creator_id,
amount: expected_amount_to_move,
}));

let source_creator_final_state = MemorySnapshot::all(current_era, from_creator_id, backer);
let target_creator_final_state = MemorySnapshot::all(current_era, to_creator_id, backer);

// Ensure backer stakes has increased/decreased staked amount
assert_eq!(
source_creator_final_state.backer_stakes.current_stake(),
source_creator_init_stake_amount - expected_amount_to_move
);
assert_eq!(
target_creator_final_state.backer_stakes.current_stake(),
target_creator_init_state.backer_stakes.current_stake() + expected_amount_to_move
);

// Ensure total value staked on creators has appropriately increased/decreased
assert_eq!(
source_creator_final_state.creator_stakes_info.total_staked,
source_creator_init_state.creator_stakes_info.total_staked - expected_amount_to_move
);
assert_eq!(
target_creator_final_state.creator_stakes_info.total_staked,
target_creator_init_state.creator_stakes_info.total_staked + expected_amount_to_move
);

// Ensure number of backers has been reduced on source creator if it is fully unstaked
let is_source_creator_fully_unstaked = source_creator_init_stake_amount == expected_amount_to_move;
if is_source_creator_fully_unstaked {
assert_eq!(
source_creator_final_state.creator_stakes_info.backers_count,
source_creator_init_state.creator_stakes_info.backers_count - 1,
);
}

// Ensure the number of backers has been increased on the target creator
// if it is first stake by the backer
let no_init_stake_on_target_creator = target_creator_init_state
.backer_stakes
.current_stake()
.is_zero();
if no_init_stake_on_target_creator {
assert_eq!(
target_creator_final_state.creator_stakes_info.backers_count,
target_creator_init_state.creator_stakes_info.backers_count + 1
);
}

// Ensure DB entry has been removed if era stake vector is empty
let fully_unstaked_and_nothing_to_claim =
is_source_creator_fully_unstaked && source_creator_final_state.backer_stakes.clone().claim() == (0, 0);
if fully_unstaked_and_nothing_to_claim {
assert!(!BackerStakesByCreator::<TestRuntime>::contains_key(
&backer,
&from_creator_id
));
}
}
Loading

0 comments on commit bdf4d2b

Please sign in to comment.