use crate::pallet::{
AddGet, AutoCompoundingDelegations as AutoCompoundingDelegationsStorage, BalanceOf,
CandidateInfo, Config, DelegatorState, Error, Event, Pallet, Total,
};
use crate::types::{Bond, BondAdjust, Delegator};
use frame_support::dispatch::DispatchResultWithPostInfo;
use frame_support::ensure;
use frame_support::traits::Get;
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::traits::Saturating;
use sp_runtime::{BoundedVec, Percent, RuntimeDebug};
use sp_std::prelude::*;
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)]
pub struct AutoCompoundConfig<AccountId> {
pub delegator: AccountId,
pub value: Percent,
}
#[derive(Clone, Eq, PartialEq, RuntimeDebug)]
pub struct AutoCompoundDelegations<T: Config>(
BoundedVec<
AutoCompoundConfig<T::AccountId>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
>,
);
impl<T> AutoCompoundDelegations<T>
where
T: Config,
{
#[cfg(test)]
pub fn new(
sorted_delegations: BoundedVec<
AutoCompoundConfig<T::AccountId>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
>,
) -> Self {
Self(sorted_delegations)
}
pub fn get_auto_compounding_delegation_count(candidate: &T::AccountId) -> usize {
<AutoCompoundingDelegationsStorage<T>>::decode_len(candidate).unwrap_or_default()
}
pub fn get_storage(candidate: &T::AccountId) -> Self {
Self(<AutoCompoundingDelegationsStorage<T>>::get(candidate))
}
pub fn set_storage(self, candidate: &T::AccountId) {
<AutoCompoundingDelegationsStorage<T>>::insert(candidate, self.0)
}
pub fn get_for_delegator(&self, delegator: &T::AccountId) -> Option<Percent> {
match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
Ok(index) => Some(self.0[index].value),
Err(_) => None,
}
}
pub fn set_for_delegator(
&mut self,
delegator: T::AccountId,
value: Percent,
) -> Result<bool, Error<T>> {
match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
Ok(index) => {
if self.0[index].value == value {
Ok(false)
} else {
self.0[index].value = value;
Ok(true)
}
}
Err(index) => {
self.0
.try_insert(index, AutoCompoundConfig { delegator, value })
.map_err(|_| Error::<T>::ExceedMaxDelegationsPerDelegator)?;
Ok(true)
}
}
}
pub fn remove_for_delegator(&mut self, delegator: &T::AccountId) -> bool {
match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
Ok(index) => {
self.0.remove(index);
true
}
Err(_) => false,
}
}
pub fn len(&self) -> u32 {
self.0.len() as u32
}
#[cfg(test)]
pub fn inner(
&self,
) -> &BoundedVec<
AutoCompoundConfig<T::AccountId>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
> {
&self.0
}
#[cfg(test)]
pub fn into_inner(
self,
) -> BoundedVec<
AutoCompoundConfig<T::AccountId>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
> {
self.0
}
pub(crate) fn delegate_with_auto_compound(
candidate: T::AccountId,
delegator: T::AccountId,
amount: BalanceOf<T>,
auto_compound: Percent,
candidate_delegation_count_hint: u32,
candidate_auto_compounding_delegation_count_hint: u32,
delegation_count_hint: u32,
) -> DispatchResultWithPostInfo {
ensure!(
<Pallet<T>>::get_delegator_stakable_balance(&delegator) >= amount,
Error::<T>::InsufficientBalance
);
ensure!(
amount >= T::MinDelegation::get(),
Error::<T>::DelegationBelowMin
);
let mut delegator_state = if let Some(mut state) = <DelegatorState<T>>::get(&delegator) {
ensure!(
delegation_count_hint >= state.delegations.0.len() as u32,
Error::<T>::TooLowDelegationCountToDelegate
);
ensure!(
(state.delegations.0.len() as u32) < T::MaxDelegationsPerDelegator::get(),
Error::<T>::ExceedMaxDelegationsPerDelegator
);
ensure!(
state.add_delegation(Bond {
owner: candidate.clone(),
amount
}),
Error::<T>::AlreadyDelegatedCandidate
);
state
} else {
ensure!(
!<Pallet<T>>::is_candidate(&delegator),
Error::<T>::CandidateExists
);
Delegator::new(delegator.clone(), candidate.clone(), amount)
};
let mut candidate_state =
<CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
ensure!(
candidate_delegation_count_hint >= candidate_state.delegation_count,
Error::<T>::TooLowCandidateDelegationCountToDelegate
);
if !auto_compound.is_zero() {
ensure!(
Self::get_auto_compounding_delegation_count(&candidate) as u32
<= candidate_auto_compounding_delegation_count_hint,
<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToDelegate,
);
}
let (delegator_position, less_total_staked) = candidate_state.add_delegation::<T>(
&candidate,
Bond {
owner: delegator.clone(),
amount,
},
)?;
delegator_state.adjust_bond_lock::<T>(BondAdjust::Increase(amount))?;
let net_total_increase = if let Some(less) = less_total_staked {
amount.saturating_sub(less)
} else {
amount
};
let new_total_locked = <Total<T>>::get().saturating_add(net_total_increase);
if !auto_compound.is_zero() {
let mut auto_compounding_state = Self::get_storage(&candidate);
auto_compounding_state.set_for_delegator(delegator.clone(), auto_compound.clone())?;
auto_compounding_state.set_storage(&candidate);
}
<Total<T>>::put(new_total_locked);
<CandidateInfo<T>>::insert(&candidate, candidate_state);
<DelegatorState<T>>::insert(&delegator, delegator_state);
<Pallet<T>>::deposit_event(Event::Delegation {
delegator: delegator,
locked_amount: amount,
candidate: candidate,
delegator_position: delegator_position,
auto_compound,
});
Ok(().into())
}
pub(crate) fn set_auto_compound(
candidate: T::AccountId,
delegator: T::AccountId,
value: Percent,
candidate_auto_compounding_delegation_count_hint: u32,
delegation_count_hint: u32,
) -> DispatchResultWithPostInfo {
let delegator_state =
<DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
ensure!(
delegator_state.delegations.0.len() <= delegation_count_hint as usize,
<Error<T>>::TooLowDelegationCountToAutoCompound,
);
ensure!(
delegator_state
.delegations
.0
.iter()
.any(|b| b.owner == candidate),
<Error<T>>::DelegationDNE,
);
let mut auto_compounding_state = Self::get_storage(&candidate);
ensure!(
auto_compounding_state.len() <= candidate_auto_compounding_delegation_count_hint,
<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
);
let state_updated = if value.is_zero() {
auto_compounding_state.remove_for_delegator(&delegator)
} else {
auto_compounding_state.set_for_delegator(delegator.clone(), value)?
};
if state_updated {
auto_compounding_state.set_storage(&candidate);
}
<Pallet<T>>::deposit_event(Event::AutoCompoundSet {
candidate,
delegator,
value,
});
Ok(().into())
}
pub(crate) fn remove_auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) {
let mut auto_compounding_state = Self::get_storage(candidate);
if auto_compounding_state.remove_for_delegator(delegator) {
auto_compounding_state.set_storage(&candidate);
}
}
pub(crate) fn auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) -> Percent {
let delegations_config = Self::get_storage(candidate);
delegations_config
.get_for_delegator(&delegator)
.unwrap_or_else(|| Percent::zero())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::Test;
#[test]
fn test_set_for_delegator_inserts_config_and_returns_true_if_entry_missing() {
let mut delegations_config =
AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
assert_eq!(
true,
delegations_config
.set_for_delegator(1, Percent::from_percent(50))
.expect("must succeed")
);
assert_eq!(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(50),
}],
delegations_config.into_inner().into_inner(),
);
}
#[test]
fn test_set_for_delegator_updates_config_and_returns_true_if_entry_changed() {
let mut delegations_config = AutoCompoundDelegations::<Test>::new(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(10),
}]
.try_into()
.expect("must succeed"),
);
assert_eq!(
true,
delegations_config
.set_for_delegator(1, Percent::from_percent(50))
.expect("must succeed")
);
assert_eq!(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(50),
}],
delegations_config.into_inner().into_inner(),
);
}
#[test]
fn test_set_for_delegator_updates_config_and_returns_false_if_entry_unchanged() {
let mut delegations_config = AutoCompoundDelegations::<Test>::new(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(10),
}]
.try_into()
.expect("must succeed"),
);
assert_eq!(
false,
delegations_config
.set_for_delegator(1, Percent::from_percent(10))
.expect("must succeed")
);
assert_eq!(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(10),
}],
delegations_config.into_inner().into_inner(),
);
}
#[test]
fn test_remove_for_delegator_returns_false_if_entry_was_missing() {
let mut delegations_config =
AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
assert_eq!(false, delegations_config.remove_for_delegator(&1),);
}
#[test]
fn test_remove_delegation_config_returns_true_if_entry_existed() {
let mut delegations_config = AutoCompoundDelegations::<Test>::new(
vec![AutoCompoundConfig {
delegator: 1,
value: Percent::from_percent(10),
}]
.try_into()
.expect("must succeed"),
);
assert_eq!(true, delegations_config.remove_for_delegator(&1));
}
}