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
//! Scheduled requests functionality for delegators
18

            
19
use crate::pallet::{
20
	BalanceOf, CandidateInfo, Config, DelegationScheduledRequests, DelegatorState, Error, Event,
21
	Pallet, Round, RoundIndex, Total,
22
};
23
use crate::weights::WeightInfo;
24
use crate::{auto_compound::AutoCompoundDelegations, AddGet, Delegator};
25
use frame_support::dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo};
26
use frame_support::ensure;
27
use frame_support::traits::Get;
28
use frame_support::BoundedVec;
29
use parity_scale_codec::{Decode, Encode};
30
use scale_info::TypeInfo;
31
use sp_runtime::{traits::Saturating, RuntimeDebug};
32

            
33
/// An action that can be performed upon a delegation
34
60
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)]
35
pub enum DelegationAction<Balance> {
36
121
	Revoke(Balance),
37
65
	Decrease(Balance),
38
}
39

            
40
impl<Balance: Copy> DelegationAction<Balance> {
41
	/// Returns the wrapped amount value.
42
52
	pub fn amount(&self) -> Balance {
43
52
		match self {
44
33
			DelegationAction::Revoke(amount) => *amount,
45
19
			DelegationAction::Decrease(amount) => *amount,
46
		}
47
52
	}
48
}
49

            
50
/// Represents a scheduled request that define a [DelegationAction]. The request is executable
51
/// iff the provided [RoundIndex] is achieved.
52
60
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, PartialOrd, Ord)]
53
pub struct ScheduledRequest<AccountId, Balance> {
54
	pub delegator: AccountId,
55
	pub when_executable: RoundIndex,
56
	pub action: DelegationAction<Balance>,
57
}
58

            
59
/// Represents a cancelled scheduled request for emitting an event.
60
40
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)]
61
pub struct CancelledScheduledRequest<Balance> {
62
	pub when_executable: RoundIndex,
63
	pub action: DelegationAction<Balance>,
64
}
65

            
66
impl<A, B> From<ScheduledRequest<A, B>> for CancelledScheduledRequest<B> {
67
10
	fn from(request: ScheduledRequest<A, B>) -> Self {
68
10
		CancelledScheduledRequest {
69
10
			when_executable: request.when_executable,
70
10
			action: request.action,
71
10
		}
72
10
	}
73
}
74

            
75
impl<T: Config> Pallet<T> {
76
	/// Schedules a [DelegationAction::Revoke] for the delegator, towards a given collator.
77
51
	pub(crate) fn delegation_schedule_revoke(
78
51
		collator: T::AccountId,
79
51
		delegator: T::AccountId,
80
51
	) -> DispatchResultWithPostInfo {
81
51
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
82
49
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
83
49

            
84
49
		let actual_weight =
85
49
			<T as Config>::WeightInfo::schedule_revoke_delegation(scheduled_requests.len() as u32);
86
49

            
87
49
		ensure!(
88
49
			!scheduled_requests
89
49
				.iter()
90
49
				.any(|req| req.delegator == delegator),
91
			DispatchErrorWithPostInfo {
92
				post_info: Some(actual_weight).into(),
93
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
94
			},
95
		);
96

            
97
49
		let bonded_amount = state
98
49
			.get_bond_amount(&collator)
99
49
			.ok_or(<Error<T>>::DelegationDNE)?;
100
48
		let now = <Round<T>>::get().current;
101
48
		let when = now.saturating_add(T::RevokeDelegationDelay::get());
102
48
		scheduled_requests
103
48
			.try_push(ScheduledRequest {
104
48
				delegator: delegator.clone(),
105
48
				action: DelegationAction::Revoke(bonded_amount),
106
48
				when_executable: when,
107
48
			})
108
48
			.map_err(|_| DispatchErrorWithPostInfo {
109
				post_info: Some(actual_weight).into(),
110
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
111
48
			})?;
112
48
		state.less_total = state.less_total.saturating_add(bonded_amount);
113
48
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
114
48
		<DelegatorState<T>>::insert(delegator.clone(), state);
115
48

            
116
48
		Self::deposit_event(Event::DelegationRevocationScheduled {
117
48
			round: now,
118
48
			delegator,
119
48
			candidate: collator,
120
48
			scheduled_exit: when,
121
48
		});
122
48
		Ok(().into())
123
51
	}
124

            
125
	/// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator.
126
31
	pub(crate) fn delegation_schedule_bond_decrease(
127
31
		collator: T::AccountId,
128
31
		delegator: T::AccountId,
129
31
		decrease_amount: BalanceOf<T>,
130
31
	) -> DispatchResultWithPostInfo {
131
31
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
132
30
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
133
30

            
134
30
		let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
135
30
			scheduled_requests.len() as u32,
136
30
		);
137
30

            
138
30
		ensure!(
139
30
			!scheduled_requests
140
30
				.iter()
141
30
				.any(|req| req.delegator == delegator),
142
2
			DispatchErrorWithPostInfo {
143
2
				post_info: Some(actual_weight).into(),
144
2
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
145
2
			},
146
		);
147

            
148
28
		let bonded_amount = state
149
28
			.get_bond_amount(&collator)
150
28
			.ok_or(DispatchErrorWithPostInfo {
151
28
				post_info: Some(actual_weight).into(),
152
28
				error: <Error<T>>::DelegationDNE.into(),
153
28
			})?;
154
26
		ensure!(
155
26
			bonded_amount > decrease_amount,
156
1
			DispatchErrorWithPostInfo {
157
1
				post_info: Some(actual_weight).into(),
158
1
				error: <Error<T>>::DelegatorBondBelowMin.into(),
159
1
			},
160
		);
161
25
		let new_amount: BalanceOf<T> = (bonded_amount - decrease_amount).into();
162
25
		ensure!(
163
25
			new_amount >= T::MinDelegation::get(),
164
1
			DispatchErrorWithPostInfo {
165
1
				post_info: Some(actual_weight).into(),
166
1
				error: <Error<T>>::DelegationBelowMin.into(),
167
1
			},
168
		);
169

            
170
		// Net Total is total after pending orders are executed
171
24
		let net_total = state.total().saturating_sub(state.less_total);
172
24
		// Net Total is always >= MinDelegation
173
24
		let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
174
24
		ensure!(
175
24
			decrease_amount <= max_subtracted_amount,
176
			DispatchErrorWithPostInfo {
177
				post_info: Some(actual_weight).into(),
178
				error: <Error<T>>::DelegatorBondBelowMin.into(),
179
			},
180
		);
181

            
182
24
		let now = <Round<T>>::get().current;
183
24
		let when = now.saturating_add(T::DelegationBondLessDelay::get());
184
24
		scheduled_requests
185
24
			.try_push(ScheduledRequest {
186
24
				delegator: delegator.clone(),
187
24
				action: DelegationAction::Decrease(decrease_amount),
188
24
				when_executable: when,
189
24
			})
190
24
			.map_err(|_| DispatchErrorWithPostInfo {
191
				post_info: Some(actual_weight).into(),
192
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
193
24
			})?;
194
24
		state.less_total = state.less_total.saturating_add(decrease_amount);
195
24
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
196
24
		<DelegatorState<T>>::insert(delegator.clone(), state);
197
24

            
198
24
		Self::deposit_event(Event::DelegationDecreaseScheduled {
199
24
			delegator,
200
24
			candidate: collator,
201
24
			amount_to_decrease: decrease_amount,
202
24
			execute_round: when,
203
24
		});
204
24
		Ok(Some(actual_weight).into())
205
31
	}
206

            
207
	/// Cancels the delegator's existing [ScheduledRequest] towards a given collator.
208
10
	pub(crate) fn delegation_cancel_request(
209
10
		collator: T::AccountId,
210
10
		delegator: T::AccountId,
211
10
	) -> DispatchResultWithPostInfo {
212
10
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
213
10
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
214
10
		let actual_weight =
215
10
			<T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
216

            
217
10
		let request =
218
10
			Self::cancel_request_with_state(&delegator, &mut state, &mut scheduled_requests)
219
10
				.ok_or(DispatchErrorWithPostInfo {
220
10
					post_info: Some(actual_weight).into(),
221
10
					error: <Error<T>>::PendingDelegationRequestDNE.into(),
222
10
				})?;
223

            
224
10
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
225
10
		<DelegatorState<T>>::insert(delegator.clone(), state);
226
10

            
227
10
		Self::deposit_event(Event::CancelledDelegationRequest {
228
10
			delegator,
229
10
			collator,
230
10
			cancelled_request: request.into(),
231
10
		});
232
10
		Ok(Some(actual_weight).into())
233
10
	}
234

            
235
12
	fn cancel_request_with_state(
236
12
		delegator: &T::AccountId,
237
12
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
238
12
		scheduled_requests: &mut BoundedVec<
239
12
			ScheduledRequest<T::AccountId, BalanceOf<T>>,
240
12
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
241
12
		>,
242
12
	) -> Option<ScheduledRequest<T::AccountId, BalanceOf<T>>> {
243
12
		let request_idx = scheduled_requests
244
12
			.iter()
245
12
			.position(|req| &req.delegator == delegator)?;
246

            
247
11
		let request = scheduled_requests.remove(request_idx);
248
11
		let amount = request.action.amount();
249
11
		state.less_total = state.less_total.saturating_sub(amount);
250
11
		Some(request)
251
12
	}
252

            
253
	/// Executes the delegator's existing [ScheduledRequest] towards a given collator.
254
37
	pub(crate) fn delegation_execute_scheduled_request(
255
37
		collator: T::AccountId,
256
37
		delegator: T::AccountId,
257
37
	) -> DispatchResultWithPostInfo {
258
37
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
259
37
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
260
37
		let request_idx = scheduled_requests
261
37
			.iter()
262
37
			.position(|req| req.delegator == delegator)
263
37
			.ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
264
37
		let request = &scheduled_requests[request_idx];
265
37

            
266
37
		let now = <Round<T>>::get().current;
267
37
		ensure!(
268
37
			request.when_executable <= now,
269
			<Error<T>>::PendingDelegationRequestNotDueYet
270
		);
271

            
272
37
		match request.action {
273
23
			DelegationAction::Revoke(amount) => {
274
23
				let actual_weight =
275
23
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
276

            
277
				// revoking last delegation => leaving set of delegators
278
23
				let leaving = if state.delegations.0.len() == 1usize {
279
12
					true
280
				} else {
281
11
					ensure!(
282
11
						state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
283
						DispatchErrorWithPostInfo {
284
							post_info: Some(actual_weight).into(),
285
							error: <Error<T>>::DelegatorBondBelowMin.into(),
286
						}
287
					);
288
11
					false
289
				};
290

            
291
				// remove from pending requests
292
23
				let amount = scheduled_requests.remove(request_idx).action.amount();
293
23
				state.less_total = state.less_total.saturating_sub(amount);
294
23

            
295
23
				// remove delegation from delegator state
296
23
				state.rm_delegation::<T>(&collator);
297
23

            
298
23
				// remove delegation from auto-compounding info
299
23
				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
300
23

            
301
23
				// remove delegation from collator state delegations
302
23
				Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
303
23
					.map_err(|err| DispatchErrorWithPostInfo {
304
						post_info: Some(actual_weight).into(),
305
						error: err,
306
23
					})?;
307
23
				Self::deposit_event(Event::DelegationRevoked {
308
23
					delegator: delegator.clone(),
309
23
					candidate: collator.clone(),
310
23
					unstaked_amount: amount,
311
23
				});
312
23

            
313
23
				<DelegationScheduledRequests<T>>::insert(collator, scheduled_requests);
314
23
				if leaving {
315
12
					<DelegatorState<T>>::remove(&delegator);
316
12
					Self::deposit_event(Event::DelegatorLeft {
317
12
						delegator,
318
12
						unstaked_amount: amount,
319
12
					});
320
12
				} else {
321
11
					<DelegatorState<T>>::insert(&delegator, state);
322
11
				}
323
23
				Ok(Some(actual_weight).into())
324
			}
325
			DelegationAction::Decrease(_) => {
326
14
				let actual_weight =
327
14
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
328
14

            
329
14
				// remove from pending requests
330
14
				let amount = scheduled_requests.remove(request_idx).action.amount();
331
14
				state.less_total = state.less_total.saturating_sub(amount);
332

            
333
				// decrease delegation
334
14
				for bond in &mut state.delegations.0 {
335
14
					if bond.owner == collator {
336
14
						return if bond.amount > amount {
337
14
							let amount_before: BalanceOf<T> = bond.amount.into();
338
14
							bond.amount = bond.amount.saturating_sub(amount);
339
14
							let mut collator_info = <CandidateInfo<T>>::get(&collator)
340
14
								.ok_or(<Error<T>>::CandidateDNE)
341
14
								.map_err(|err| DispatchErrorWithPostInfo {
342
									post_info: Some(actual_weight).into(),
343
									error: err.into(),
344
14
								})?;
345

            
346
14
							state
347
14
								.total_sub_if::<T, _>(amount, |total| {
348
14
									let new_total: BalanceOf<T> = total.into();
349
14
									ensure!(
350
14
										new_total >= T::MinDelegation::get(),
351
										<Error<T>>::DelegationBelowMin
352
									);
353

            
354
14
									Ok(())
355
14
								})
356
14
								.map_err(|err| DispatchErrorWithPostInfo {
357
									post_info: Some(actual_weight).into(),
358
									error: err,
359
14
								})?;
360

            
361
							// need to go into decrease_delegation
362
14
							let in_top = collator_info
363
14
								.decrease_delegation::<T>(
364
14
									&collator,
365
14
									delegator.clone(),
366
14
									amount_before,
367
14
									amount,
368
14
								)
369
14
								.map_err(|err| DispatchErrorWithPostInfo {
370
									post_info: Some(actual_weight).into(),
371
									error: err,
372
14
								})?;
373
14
							<CandidateInfo<T>>::insert(&collator, collator_info);
374
14
							let new_total_staked = <Total<T>>::get().saturating_sub(amount);
375
14
							<Total<T>>::put(new_total_staked);
376
14

            
377
14
							<DelegationScheduledRequests<T>>::insert(
378
14
								collator.clone(),
379
14
								scheduled_requests,
380
14
							);
381
14
							<DelegatorState<T>>::insert(delegator.clone(), state);
382
14
							Self::deposit_event(Event::DelegationDecreased {
383
14
								delegator,
384
14
								candidate: collator.clone(),
385
14
								amount,
386
14
								in_top,
387
14
							});
388
14
							Ok(Some(actual_weight).into())
389
						} else {
390
							// must rm entire delegation if bond.amount <= less or cancel request
391
							Err(DispatchErrorWithPostInfo {
392
								post_info: Some(actual_weight).into(),
393
								error: <Error<T>>::DelegationBelowMin.into(),
394
							})
395
						};
396
					}
397
				}
398
				Err(DispatchErrorWithPostInfo {
399
					post_info: Some(actual_weight).into(),
400
					error: <Error<T>>::DelegationDNE.into(),
401
				})
402
			}
403
		}
404
37
	}
405

            
406
	/// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists.
407
	/// The state needs to be persisted by the caller of this function.
408
19
	pub(crate) fn delegation_remove_request_with_state(
409
19
		collator: &T::AccountId,
410
19
		delegator: &T::AccountId,
411
19
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
412
19
	) {
413
19
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(collator);
414
19

            
415
19
		let maybe_request_idx = scheduled_requests
416
19
			.iter()
417
19
			.position(|req| &req.delegator == delegator);
418

            
419
19
		if let Some(request_idx) = maybe_request_idx {
420
4
			let request = scheduled_requests.remove(request_idx);
421
4
			let amount = request.action.amount();
422
4
			state.less_total = state.less_total.saturating_sub(amount);
423
4
			<DelegationScheduledRequests<T>>::insert(collator, scheduled_requests);
424
15
		}
425
19
	}
426

            
427
	/// Returns true if a [ScheduledRequest] exists for a given delegation
428
6
	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
429
6
		<DelegationScheduledRequests<T>>::get(collator)
430
6
			.iter()
431
6
			.any(|req| &req.delegator == delegator)
432
6
	}
433

            
434
	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
435
26
	pub fn delegation_request_revoke_exists(
436
26
		collator: &T::AccountId,
437
26
		delegator: &T::AccountId,
438
26
	) -> bool {
439
26
		<DelegationScheduledRequests<T>>::get(collator)
440
26
			.iter()
441
26
			.any(|req| {
442
6
				&req.delegator == delegator && matches!(req.action, DelegationAction::Revoke(_))
443
26
			})
444
26
	}
445
}
446

            
447
#[cfg(test)]
448
mod tests {
449
	use super::*;
450
	use crate::{mock::Test, set::OrderedSet, Bond};
451

            
452
	#[test]
453
1
	fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
454
1
		let mut state = Delegator {
455
1
			id: 1,
456
1
			delegations: OrderedSet::from(vec![Bond {
457
1
				amount: 100,
458
1
				owner: 2,
459
1
			}]),
460
1
			total: 100,
461
1
			less_total: 100,
462
1
			status: crate::DelegatorStatus::Active,
463
1
		};
464
1
		let mut scheduled_requests = vec![
465
1
			ScheduledRequest {
466
1
				delegator: 1,
467
1
				when_executable: 1,
468
1
				action: DelegationAction::Revoke(100),
469
1
			},
470
1
			ScheduledRequest {
471
1
				delegator: 2,
472
1
				when_executable: 1,
473
1
				action: DelegationAction::Decrease(50),
474
1
			},
475
1
		]
476
1
		.try_into()
477
1
		.expect("must succeed");
478
1
		let removed_request =
479
1
			<Pallet<Test>>::cancel_request_with_state(&1, &mut state, &mut scheduled_requests);
480
1

            
481
1
		assert_eq!(
482
1
			removed_request,
483
1
			Some(ScheduledRequest {
484
1
				delegator: 1,
485
1
				when_executable: 1,
486
1
				action: DelegationAction::Revoke(100),
487
1
			})
488
1
		);
489
1
		assert_eq!(
490
1
			scheduled_requests,
491
1
			vec![ScheduledRequest {
492
1
				delegator: 2,
493
1
				when_executable: 1,
494
1
				action: DelegationAction::Decrease(50),
495
1
			},]
496
1
		);
497
1
		assert_eq!(
498
1
			state,
499
1
			Delegator {
500
1
				id: 1,
501
1
				delegations: OrderedSet::from(vec![Bond {
502
1
					amount: 100,
503
1
					owner: 2,
504
1
				}]),
505
1
				total: 100,
506
1
				less_total: 0,
507
1
				status: crate::DelegatorStatus::Active,
508
1
			}
509
1
		);
510
1
	}
511

            
512
	#[test]
513
1
	fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
514
1
		let mut state = Delegator {
515
1
			id: 1,
516
1
			delegations: OrderedSet::from(vec![Bond {
517
1
				amount: 100,
518
1
				owner: 2,
519
1
			}]),
520
1
			total: 100,
521
1
			less_total: 100,
522
1
			status: crate::DelegatorStatus::Active,
523
1
		};
524
1
		let mut scheduled_requests = vec![ScheduledRequest {
525
1
			delegator: 2,
526
1
			when_executable: 1,
527
1
			action: DelegationAction::Decrease(50),
528
1
		}]
529
1
		.try_into()
530
1
		.expect("must succeed");
531
1
		let removed_request =
532
1
			<Pallet<Test>>::cancel_request_with_state(&1, &mut state, &mut scheduled_requests);
533
1

            
534
1
		assert_eq!(removed_request, None,);
535
1
		assert_eq!(
536
1
			scheduled_requests,
537
1
			vec![ScheduledRequest {
538
1
				delegator: 2,
539
1
				when_executable: 1,
540
1
				action: DelegationAction::Decrease(50),
541
1
			},]
542
1
		);
543
1
		assert_eq!(
544
1
			state,
545
1
			Delegator {
546
1
				id: 1,
547
1
				delegations: OrderedSet::from(vec![Bond {
548
1
					amount: 100,
549
1
					owner: 2,
550
1
				}]),
551
1
				total: 100,
552
1
				less_total: 100,
553
1
				status: crate::DelegatorStatus::Active,
554
1
			}
555
1
		);
556
1
	}
557
}