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

            
19
use crate::pallet::{
20
	AddGet, AutoCompoundingDelegations as AutoCompoundingDelegationsStorage, BalanceOf,
21
	CandidateInfo, Config, DelegatorState, Error, Event, Pallet, Total,
22
};
23
use crate::types::{Bond, BondAdjust, Delegator};
24
use frame_support::dispatch::DispatchResultWithPostInfo;
25
use frame_support::ensure;
26
use frame_support::traits::Get;
27
use parity_scale_codec::{Decode, Encode};
28
use scale_info::TypeInfo;
29
use sp_runtime::traits::Saturating;
30
use sp_runtime::{BoundedVec, Percent, RuntimeDebug};
31
use sp_std::prelude::*;
32

            
33
/// Represents the auto-compounding amount for a delegation.
34
40
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)]
35
pub 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)]
42
pub struct AutoCompoundDelegations<T: Config>(
43
	BoundedVec<
44
		AutoCompoundConfig<T::AccountId>,
45
		AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
46
	>,
47
);
48

            
49
impl<T> AutoCompoundDelegations<T>
50
where
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
8
	pub fn new(
57
8
		sorted_delegations: BoundedVec<
58
8
			AutoCompoundConfig<T::AccountId>,
59
8
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
60
8
		>,
61
8
	) -> Self {
62
8
		Self(sorted_delegations)
63
8
	}
64

            
65
16
	pub fn get_auto_compounding_delegation_count(candidate: &T::AccountId) -> usize {
66
16
		<AutoCompoundingDelegationsStorage<T>>::decode_len(candidate).unwrap_or_default()
67
16
	}
68

            
69
	/// Retrieves an instance of [AutoCompoundingDelegations] storage as [AutoCompoundDelegations].
70
80
	pub fn get_storage(candidate: &T::AccountId) -> Self {
71
80
		Self(<AutoCompoundingDelegationsStorage<T>>::get(candidate))
72
80
	}
73

            
74
	/// Inserts the current state to [AutoCompoundingDelegations] storage.
75
38
	pub fn set_storage(self, candidate: &T::AccountId) {
76
38
		<AutoCompoundingDelegationsStorage<T>>::insert(candidate, self.0)
77
38
	}
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
5
	pub fn get_for_delegator(&self, delegator: &T::AccountId) -> Option<Percent> {
82
5
		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
83
3
			Ok(index) => Some(self.0[index].value),
84
2
			Err(_) => None,
85
		}
86
5
	}
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
32
	pub fn set_for_delegator(
91
32
		&mut self,
92
32
		delegator: T::AccountId,
93
32
		value: Percent,
94
32
	) -> Result<bool, Error<T>> {
95
32
		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
96
3
			Ok(index) => {
97
3
				if self.0[index].value == value {
98
1
					Ok(false)
99
				} else {
100
2
					self.0[index].value = value;
101
2
					Ok(true)
102
				}
103
			}
104
29
			Err(index) => {
105
29
				self.0
106
29
					.try_insert(index, AutoCompoundConfig { delegator, value })
107
29
					.map_err(|_| Error::<T>::ExceedMaxDelegationsPerDelegator)?;
108
29
				Ok(true)
109
			}
110
		}
111
32
	}
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
47
	pub fn remove_for_delegator(&mut self, delegator: &T::AccountId) -> bool {
117
47
		match self.0.binary_search_by(|d| d.delegator.cmp(&delegator)) {
118
7
			Ok(index) => {
119
7
				self.0.remove(index);
120
7
				true
121
			}
122
40
			Err(_) => false,
123
		}
124
47
	}
125

            
126
	/// Returns the length of the inner vector.
127
19
	pub fn len(&self) -> u32 {
128
19
		self.0.len() as u32
129
19
	}
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
3
	pub fn into_inner(
145
3
		self,
146
3
	) -> BoundedVec<
147
3
		AutoCompoundConfig<T::AccountId>,
148
3
		AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
149
3
	> {
150
3
		self.0
151
3
	}
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
9084
	pub(crate) fn delegate_with_auto_compound(
158
9084
		candidate: T::AccountId,
159
9084
		delegator: T::AccountId,
160
9084
		amount: BalanceOf<T>,
161
9084
		auto_compound: Percent,
162
9084
		candidate_delegation_count_hint: u32,
163
9084
		candidate_auto_compounding_delegation_count_hint: u32,
164
9084
		delegation_count_hint: u32,
165
9084
	) -> DispatchResultWithPostInfo {
166
9084
		// check that caller can lock the amount before any changes to storage
167
9084
		ensure!(
168
9084
			<Pallet<T>>::get_delegator_stakable_balance(&delegator) >= amount,
169
			Error::<T>::InsufficientBalance
170
		);
171
9084
		ensure!(
172
9084
			amount >= T::MinDelegation::get(),
173
			Error::<T>::DelegationBelowMin
174
		);
175

            
176
9084
		let mut delegator_state = if let Some(mut state) = <DelegatorState<T>>::get(&delegator) {
177
			// delegation after first
178
76
			ensure!(
179
76
				delegation_count_hint >= state.delegations.0.len() as u32,
180
9
				Error::<T>::TooLowDelegationCountToDelegate
181
			);
182
67
			ensure!(
183
67
				(state.delegations.0.len() as u32) < T::MaxDelegationsPerDelegator::get(),
184
2
				Error::<T>::ExceedMaxDelegationsPerDelegator
185
			);
186
65
			ensure!(
187
65
				state.add_delegation(Bond {
188
65
					owner: candidate.clone(),
189
65
					amount
190
65
				}),
191
2
				Error::<T>::AlreadyDelegatedCandidate
192
			);
193
63
			state
194
		} else {
195
			// first delegation
196
9008
			ensure!(
197
9008
				!<Pallet<T>>::is_candidate(&delegator),
198
2
				Error::<T>::CandidateExists
199
			);
200
9006
			Delegator::new(delegator.clone(), candidate.clone(), amount)
201
		};
202
9065
		let mut candidate_state =
203
9069
			<CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
204
9065
		ensure!(
205
9065
			candidate_delegation_count_hint >= candidate_state.delegation_count,
206
5
			Error::<T>::TooLowCandidateDelegationCountToDelegate
207
		);
208

            
209
9060
		if !auto_compound.is_zero() {
210
16
			ensure!(
211
16
				Self::get_auto_compounding_delegation_count(&candidate) as u32
212
16
					<= candidate_auto_compounding_delegation_count_hint,
213
1
				<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToDelegate,
214
			);
215
9044
		}
216

            
217
		// add delegation to candidate
218
9059
		let (delegator_position, less_total_staked) = candidate_state.add_delegation::<T>(
219
9059
			&candidate,
220
9059
			Bond {
221
9059
				owner: delegator.clone(),
222
9059
				amount,
223
9059
			},
224
9059
		)?;
225

            
226
		// lock delegator amount
227
589
		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
589
		let net_total_increase = if let Some(less) = less_total_staked {
232
4
			amount.saturating_sub(less)
233
		} else {
234
585
			amount
235
		};
236
589
		let new_total_locked = <Total<T>>::get().saturating_add(net_total_increase);
237
589

            
238
589
		// set auto-compound config if the percent is non-zero
239
589
		if !auto_compound.is_zero() {
240
14
			let mut auto_compounding_state = Self::get_storage(&candidate);
241
14
			auto_compounding_state.set_for_delegator(delegator.clone(), auto_compound.clone())?;
242
14
			auto_compounding_state.set_storage(&candidate);
243
575
		}
244

            
245
589
		<Total<T>>::put(new_total_locked);
246
589
		<CandidateInfo<T>>::insert(&candidate, candidate_state);
247
589
		<DelegatorState<T>>::insert(&delegator, delegator_state);
248
589
		<Pallet<T>>::deposit_event(Event::Delegation {
249
589
			delegator: delegator,
250
589
			locked_amount: amount,
251
589
			candidate: candidate,
252
589
			delegator_position: delegator_position,
253
589
			auto_compound,
254
589
		});
255
589

            
256
589
		Ok(().into())
257
9084
	}
258

            
259
	/// Sets the auto-compounding value for a delegation. The config is removed if value is zero.
260
21
	pub(crate) fn set_auto_compound(
261
21
		candidate: T::AccountId,
262
21
		delegator: T::AccountId,
263
21
		value: Percent,
264
21
		candidate_auto_compounding_delegation_count_hint: u32,
265
21
		delegation_count_hint: u32,
266
21
	) -> DispatchResultWithPostInfo {
267
20
		let delegator_state =
268
21
			<DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
269
20
		ensure!(
270
20
			delegator_state.delegations.0.len() <= delegation_count_hint as usize,
271
1
			<Error<T>>::TooLowDelegationCountToAutoCompound,
272
		);
273
19
		ensure!(
274
19
			delegator_state
275
19
				.delegations
276
19
				.0
277
19
				.iter()
278
22
				.any(|b| b.owner == candidate),
279
			<Error<T>>::DelegationDNE,
280
		);
281

            
282
19
		let mut auto_compounding_state = Self::get_storage(&candidate);
283
19
		ensure!(
284
19
			auto_compounding_state.len() <= candidate_auto_compounding_delegation_count_hint,
285
1
			<Error<T>>::TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
286
		);
287
18
		let state_updated = if value.is_zero() {
288
3
			auto_compounding_state.remove_for_delegator(&delegator)
289
		} else {
290
15
			auto_compounding_state.set_for_delegator(delegator.clone(), value)?
291
		};
292
18
		if state_updated {
293
16
			auto_compounding_state.set_storage(&candidate);
294
16
		}
295

            
296
18
		<Pallet<T>>::deposit_event(Event::AutoCompoundSet {
297
18
			candidate,
298
18
			delegator,
299
18
			value,
300
18
		});
301
18

            
302
18
		Ok(().into())
303
21
	}
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
42
	pub(crate) fn remove_auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) {
308
42
		let mut auto_compounding_state = Self::get_storage(candidate);
309
42
		if auto_compounding_state.remove_for_delegator(delegator) {
310
5
			auto_compounding_state.set_storage(&candidate);
311
37
		}
312
42
	}
313

            
314
	/// Returns the value of auto-compound, if it exists for a given delegation, zero otherwise.
315
5
	pub(crate) fn auto_compound(candidate: &T::AccountId, delegator: &T::AccountId) -> Percent {
316
5
		let delegations_config = Self::get_storage(candidate);
317
5
		delegations_config
318
5
			.get_for_delegator(&delegator)
319
5
			.unwrap_or_else(|| Percent::zero())
320
5
	}
321
}
322

            
323
#[cfg(test)]
324
mod tests {
325
	use super::*;
326
	use crate::mock::Test;
327

            
328
	#[test]
329
1
	fn test_set_for_delegator_inserts_config_and_returns_true_if_entry_missing() {
330
1
		let mut delegations_config =
331
1
			AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
332
1
		assert_eq!(
333
1
			true,
334
1
			delegations_config
335
1
				.set_for_delegator(1, Percent::from_percent(50))
336
1
				.expect("must succeed")
337
1
		);
338
1
		assert_eq!(
339
1
			vec![AutoCompoundConfig {
340
1
				delegator: 1,
341
1
				value: Percent::from_percent(50),
342
1
			}],
343
1
			delegations_config.into_inner().into_inner(),
344
1
		);
345
1
	}
346

            
347
	#[test]
348
1
	fn test_set_for_delegator_updates_config_and_returns_true_if_entry_changed() {
349
1
		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
350
1
			vec![AutoCompoundConfig {
351
1
				delegator: 1,
352
1
				value: Percent::from_percent(10),
353
1
			}]
354
1
			.try_into()
355
1
			.expect("must succeed"),
356
1
		);
357
1
		assert_eq!(
358
1
			true,
359
1
			delegations_config
360
1
				.set_for_delegator(1, Percent::from_percent(50))
361
1
				.expect("must succeed")
362
1
		);
363
1
		assert_eq!(
364
1
			vec![AutoCompoundConfig {
365
1
				delegator: 1,
366
1
				value: Percent::from_percent(50),
367
1
			}],
368
1
			delegations_config.into_inner().into_inner(),
369
1
		);
370
1
	}
371

            
372
	#[test]
373
1
	fn test_set_for_delegator_updates_config_and_returns_false_if_entry_unchanged() {
374
1
		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
375
1
			vec![AutoCompoundConfig {
376
1
				delegator: 1,
377
1
				value: Percent::from_percent(10),
378
1
			}]
379
1
			.try_into()
380
1
			.expect("must succeed"),
381
1
		);
382
1
		assert_eq!(
383
1
			false,
384
1
			delegations_config
385
1
				.set_for_delegator(1, Percent::from_percent(10))
386
1
				.expect("must succeed")
387
1
		);
388
1
		assert_eq!(
389
1
			vec![AutoCompoundConfig {
390
1
				delegator: 1,
391
1
				value: Percent::from_percent(10),
392
1
			}],
393
1
			delegations_config.into_inner().into_inner(),
394
1
		);
395
1
	}
396

            
397
	#[test]
398
1
	fn test_remove_for_delegator_returns_false_if_entry_was_missing() {
399
1
		let mut delegations_config =
400
1
			AutoCompoundDelegations::<Test>::new(vec![].try_into().expect("must succeed"));
401
1
		assert_eq!(false, delegations_config.remove_for_delegator(&1),);
402
1
	}
403

            
404
	#[test]
405
1
	fn test_remove_delegation_config_returns_true_if_entry_existed() {
406
1
		let mut delegations_config = AutoCompoundDelegations::<Test>::new(
407
1
			vec![AutoCompoundConfig {
408
1
				delegator: 1,
409
1
				value: Percent::from_percent(10),
410
1
			}]
411
1
			.try_into()
412
1
			.expect("must succeed"),
413
1
		);
414
1
		assert_eq!(true, delegations_config.remove_for_delegator(&1));
415
1
	}
416
}