pallet_parachain_staking/
auto_compound.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Auto-compounding functionality for staking rewards
18
19use crate::pallet::{
20	AddGet, AutoCompoundingDelegations as AutoCompoundingDelegationsStorage, BalanceOf,
21	CandidateInfo, Config, DelegatorState, Error, Event, Pallet, Total,
22};
23use crate::types::{Bond, BondAdjust, Delegator};
24use frame_support::dispatch::DispatchResultWithPostInfo;
25use frame_support::ensure;
26use frame_support::traits::Get;
27use parity_scale_codec::{Decode, Encode};
28use scale_info::TypeInfo;
29use sp_runtime::traits::Saturating;
30use sp_runtime::{BoundedVec, Percent, RuntimeDebug};
31use sp_std::prelude::*;
32
33/// Represents the auto-compounding amount for a delegation.
34#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)]
35pub struct AutoCompoundConfig<AccountId> {
36	pub delegator: AccountId,
37	pub value: Percent,
38}
39
40/// Represents the auto-compounding [Delegations] for `T: Config`
41#[derive(Clone, Eq, PartialEq, RuntimeDebug)]
42pub struct AutoCompoundDelegations<T: Config>(
43	BoundedVec<
44		AutoCompoundConfig<T::AccountId>,
45		AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
46	>,
47);
48
49impl<T> AutoCompoundDelegations<T>
50where
51	T: Config,
52{
53	/// Creates a new instance of [AutoCompoundingDelegations] from a vector of sorted_delegations.
54	/// This is used for testing purposes only.
55	#[cfg(test)]
56	pub fn new(
57		sorted_delegations: BoundedVec<
58			AutoCompoundConfig<T::AccountId>,
59			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
60		>,
61	) -> Self {
62		Self(sorted_delegations)
63	}
64
65	pub fn get_auto_compounding_delegation_count(candidate: &T::AccountId) -> usize {
66		<AutoCompoundingDelegationsStorage<T>>::decode_len(candidate).unwrap_or_default()
67	}
68
69	/// Retrieves an instance of [AutoCompoundingDelegations] storage as [AutoCompoundDelegations].
70	pub fn get_storage(candidate: &T::AccountId) -> Self {
71		Self(<AutoCompoundingDelegationsStorage<T>>::get(candidate))
72	}
73
74	/// Inserts the current state to [AutoCompoundingDelegations] storage.
75	pub fn set_storage(self, candidate: &T::AccountId) {
76		<AutoCompoundingDelegationsStorage<T>>::insert(candidate, self.0)
77	}
78
79	/// Retrieves the auto-compounding value for a delegation. The `delegations_config` must be a
80	/// sorted vector for binary_search to work.
81	pub fn get_for_delegator(&self, delegator: &T::AccountId) -> Option<Percent> {
82		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
83			Ok(index) => Some(self.0[index].value),
84			Err(_) => None,
85		}
86	}
87
88	/// Sets the auto-compounding value for a delegation. The `delegations_config` must be a sorted
89	/// vector for binary_search to work.
90	pub fn set_for_delegator(
91		&mut self,
92		delegator: T::AccountId,
93		value: Percent,
94	) -> Result<bool, Error<T>> {
95		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
96			Ok(index) => {
97				if self.0[index].value == value {
98					Ok(false)
99				} else {
100					self.0[index].value = value;
101					Ok(true)
102				}
103			}
104			Err(index) => {
105				self.0
106					.try_insert(index, AutoCompoundConfig { delegator, value })
107					.map_err(|_| Error::<T>::ExceedMaxDelegationsPerDelegator)?;
108				Ok(true)
109			}
110		}
111	}
112
113	/// Removes the auto-compounding value for a delegation.
114	/// Returns `true` if the entry was removed, `false` otherwise. The `delegations_config` must be a
115	/// sorted vector for binary_search to work.
116	pub fn remove_for_delegator(&mut self, delegator: &T::AccountId) -> bool {
117		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
118			Ok(index) => {
119				self.0.remove(index);
120				true
121			}
122			Err(_) => false,
123		}
124	}
125
126	/// Returns the length of the inner vector.
127	pub fn len(&self) -> u32 {
128		self.0.len() as u32
129	}
130
131	/// Returns a reference to the inner vector.
132	#[cfg(test)]
133	pub fn inner(
134		&self,
135	) -> &BoundedVec<
136		AutoCompoundConfig<T::AccountId>,
137		AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
138	> {
139		&self.0
140	}
141
142	/// Converts the [AutoCompoundDelegations] into the inner vector.
143	#[cfg(test)]
144	pub fn into_inner(
145		self,
146	) -> BoundedVec<
147		AutoCompoundConfig<T::AccountId>,
148		AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
149	> {
150		self.0
151	}
152
153	// -- pallet functions --
154
155	/// Delegates and sets the auto-compounding config. The function skips inserting auto-compound
156	/// storage and validation, if the auto-compound value is 0%.
157	pub(crate) fn delegate_with_auto_compound(
158		candidate: T::AccountId,
159		delegator: T::AccountId,
160		amount: BalanceOf<T>,
161		auto_compound: Percent,
162		candidate_delegation_count_hint: u32,
163		candidate_auto_compounding_delegation_count_hint: u32,
164		delegation_count_hint: u32,
165	) -> DispatchResultWithPostInfo {
166		// check that caller can lock the amount before any changes to storage
167		ensure!(
168			<Pallet<T>>::get_delegator_stakable_balance(&delegator) >= amount,
169			Error::<T>::InsufficientBalance
170		);
171		ensure!(
172			amount >= T::MinDelegation::get(),
173			Error::<T>::DelegationBelowMin
174		);
175
176		let mut delegator_state = if let Some(mut state) = <DelegatorState<T>>::get(&delegator) {
177			// delegation after first
178			ensure!(
179				delegation_count_hint >= state.delegations.0.len() as u32,
180				Error::<T>::TooLowDelegationCountToDelegate
181			);
182			ensure!(
183				(state.delegations.0.len() as u32) < T::MaxDelegationsPerDelegator::get(),
184				Error::<T>::ExceedMaxDelegationsPerDelegator
185			);
186			ensure!(
187				state.add_delegation(Bond {
188					owner: candidate.clone(),
189					amount
190				}),
191				Error::<T>::AlreadyDelegatedCandidate
192			);
193			state
194		} else {
195			// first delegation
196			ensure!(
197				!<Pallet<T>>::is_candidate(&delegator),
198				Error::<T>::CandidateExists
199			);
200			Delegator::new(delegator.clone(), candidate.clone(), amount)
201		};
202		let mut candidate_state =
203			<CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
204		ensure!(
205			candidate_delegation_count_hint >= candidate_state.delegation_count,
206			Error::<T>::TooLowCandidateDelegationCountToDelegate
207		);
208
209		if !auto_compound.is_zero() {
210			ensure!(
211				Self::get_auto_compounding_delegation_count(&candidate) as u32
212					<= candidate_auto_compounding_delegation_count_hint,
213				<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToDelegate,
214			);
215		}
216
217		// add delegation to candidate
218		let (delegator_position, less_total_staked) = candidate_state.add_delegation::<T>(
219			&candidate,
220			Bond {
221				owner: delegator.clone(),
222				amount,
223			},
224		)?;
225
226		// lock delegator amount
227		delegator_state.adjust_bond_lock::<T>(BondAdjust::Increase(amount))?;
228
229		// adjust total locked,
230		// only is_some if kicked the lowest bottom as a consequence of this new delegation
231		let net_total_increase = if let Some(less) = less_total_staked {
232			amount.saturating_sub(less)
233		} else {
234			amount
235		};
236		let new_total_locked = <Total<T>>::get().saturating_add(net_total_increase);
237
238		// set auto-compound config if the percent is non-zero
239		if !auto_compound.is_zero() {
240			let mut auto_compounding_state = Self::get_storage(&candidate);
241			auto_compounding_state.set_for_delegator(delegator.clone(), auto_compound.clone())?;
242			auto_compounding_state.set_storage(&candidate);
243		}
244
245		<Total<T>>::put(new_total_locked);
246		<CandidateInfo<T>>::insert(&candidate, candidate_state);
247		<DelegatorState<T>>::insert(&delegator, delegator_state);
248		<Pallet<T>>::deposit_event(Event::Delegation {
249			delegator: delegator,
250			locked_amount: amount,
251			candidate: candidate,
252			delegator_position: delegator_position,
253			auto_compound,
254		});
255
256		Ok(().into())
257	}
258
259	/// Sets the auto-compounding value for a delegation. The config is removed if value is zero.
260	pub(crate) fn set_auto_compound(
261		candidate: T::AccountId,
262		delegator: T::AccountId,
263		value: Percent,
264		candidate_auto_compounding_delegation_count_hint: u32,
265		delegation_count_hint: u32,
266	) -> DispatchResultWithPostInfo {
267		let delegator_state =
268			<DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
269		ensure!(
270			delegator_state.delegations.0.len() <= delegation_count_hint as usize,
271			<Error<T>>::TooLowDelegationCountToAutoCompound,
272		);
273		ensure!(
274			delegator_state
275				.delegations
276				.0
277				.iter()
278				.any(|b| b.owner == candidate),
279			<Error<T>>::DelegationDNE,
280		);
281
282		let mut auto_compounding_state = Self::get_storage(&candidate);
283		ensure!(
284			auto_compounding_state.len() <= candidate_auto_compounding_delegation_count_hint,
285			<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
286		);
287		let state_updated = if value.is_zero() {
288			auto_compounding_state.remove_for_delegator(&delegator)
289		} else {
290			auto_compounding_state.set_for_delegator(delegator.clone(), value)?
291		};
292		if state_updated {
293			auto_compounding_state.set_storage(&candidate);
294		}
295
296		<Pallet<T>>::deposit_event(Event::AutoCompoundSet {
297			candidate,
298			delegator,
299			value,
300		});
301
302		Ok(().into())
303	}
304
305	/// Removes the auto-compounding value for a delegation. This should be called when the
306	/// delegation is revoked to cleanup storage. Storage is only written iff the entry existed.
307	pub(crate) fn remove_auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) {
308		let mut auto_compounding_state = Self::get_storage(candidate);
309		if auto_compounding_state.remove_for_delegator(delegator) {
310			auto_compounding_state.set_storage(&candidate);
311		}
312	}
313
314	/// Returns the value of auto-compound, if it exists for a given delegation, zero otherwise.
315	pub(crate) fn auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) -> Percent {
316		let delegations_config = Self::get_storage(candidate);
317		delegations_config
318			.get_for_delegator(&delegator)
319			.unwrap_or_else(|| Percent::zero())
320	}
321}
322
323#[cfg(test)]
324mod tests {
325	use super::*;
326	use crate::mock::Test;
327
328	#[test]
329	fn test_set_for_delegator_inserts_config_and_returns_true_if_entry_missing() {
330		let mut delegations_config =
331			AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
332		assert_eq!(
333			true,
334			delegations_config
335				.set_for_delegator(1, Percent::from_percent(50))
336				.expect("must succeed")
337		);
338		assert_eq!(
339			vec![AutoCompoundConfig {
340				delegator: 1,
341				value: Percent::from_percent(50),
342			}],
343			delegations_config.into_inner().into_inner(),
344		);
345	}
346
347	#[test]
348	fn test_set_for_delegator_updates_config_and_returns_true_if_entry_changed() {
349		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
350			vec![AutoCompoundConfig {
351				delegator: 1,
352				value: Percent::from_percent(10),
353			}]
354			.try_into()
355			.expect("must succeed"),
356		);
357		assert_eq!(
358			true,
359			delegations_config
360				.set_for_delegator(1, Percent::from_percent(50))
361				.expect("must succeed")
362		);
363		assert_eq!(
364			vec![AutoCompoundConfig {
365				delegator: 1,
366				value: Percent::from_percent(50),
367			}],
368			delegations_config.into_inner().into_inner(),
369		);
370	}
371
372	#[test]
373	fn test_set_for_delegator_updates_config_and_returns_false_if_entry_unchanged() {
374		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
375			vec![AutoCompoundConfig {
376				delegator: 1,
377				value: Percent::from_percent(10),
378			}]
379			.try_into()
380			.expect("must succeed"),
381		);
382		assert_eq!(
383			false,
384			delegations_config
385				.set_for_delegator(1, Percent::from_percent(10))
386				.expect("must succeed")
387		);
388		assert_eq!(
389			vec![AutoCompoundConfig {
390				delegator: 1,
391				value: Percent::from_percent(10),
392			}],
393			delegations_config.into_inner().into_inner(),
394		);
395	}
396
397	#[test]
398	fn test_remove_for_delegator_returns_false_if_entry_was_missing() {
399		let mut delegations_config =
400			AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
401		assert_eq!(false, delegations_config.remove_for_delegator(&1),);
402	}
403
404	#[test]
405	fn test_remove_delegation_config_returns_true_if_entry_existed() {
406		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
407			vec![AutoCompoundConfig {
408				delegator: 1,
409				value: Percent::from_percent(10),
410			}]
411			.try_into()
412			.expect("must succeed"),
413		);
414		assert_eq!(true, delegations_config.remove_for_delegator(&1));
415	}
416}