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, DecodeWithMemTracking, 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
#[derive(
35
	Clone,
36
	Eq,
37
	PartialEq,
38
	Encode,
39
	Decode,
40
	RuntimeDebug,
41
40
	TypeInfo,
42
	PartialOrd,
43
	Ord,
44
	DecodeWithMemTracking,
45
)]
46
pub enum DelegationAction<Balance> {
47
	Revoke(Balance),
48
	Decrease(Balance),
49
}
50

            
51
impl<Balance: Copy> DelegationAction<Balance> {
52
	/// Returns the wrapped amount value.
53
52
	pub fn amount(&self) -> Balance {
54
52
		match self {
55
33
			DelegationAction::Revoke(amount) => *amount,
56
19
			DelegationAction::Decrease(amount) => *amount,
57
		}
58
52
	}
59
}
60

            
61
/// Represents a scheduled request that define a [DelegationAction]. The request is executable
62
/// iff the provided [RoundIndex] is achieved.
63
#[derive(
64
	Clone,
65
	Eq,
66
	PartialEq,
67
	Encode,
68
	Decode,
69
	RuntimeDebug,
70
60
	TypeInfo,
71
	PartialOrd,
72
	Ord,
73
	DecodeWithMemTracking,
74
)]
75
pub struct ScheduledRequest<AccountId, Balance> {
76
	pub delegator: AccountId,
77
	pub when_executable: RoundIndex,
78
	pub action: DelegationAction<Balance>,
79
}
80

            
81
/// Represents a cancelled scheduled request for emitting an event.
82
40
#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, DecodeWithMemTracking)]
83
pub struct CancelledScheduledRequest<Balance> {
84
	pub when_executable: RoundIndex,
85
	pub action: DelegationAction<Balance>,
86
}
87

            
88
impl<A, B> From<ScheduledRequest<A, B>> for CancelledScheduledRequest<B> {
89
10
	fn from(request: ScheduledRequest<A, B>) -> Self {
90
10
		CancelledScheduledRequest {
91
10
			when_executable: request.when_executable,
92
10
			action: request.action,
93
10
		}
94
10
	}
95
}
96

            
97
impl<T: Config> Pallet<T> {
98
	/// Schedules a [DelegationAction::Revoke] for the delegator, towards a given collator.
99
51
	pub(crate) fn delegation_schedule_revoke(
100
51
		collator: T::AccountId,
101
51
		delegator: T::AccountId,
102
51
	) -> DispatchResultWithPostInfo {
103
51
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
104
49
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
105
49

            
106
49
		let actual_weight =
107
49
			<T as Config>::WeightInfo::schedule_revoke_delegation(scheduled_requests.len() as u32);
108
49

            
109
49
		ensure!(
110
49
			!scheduled_requests
111
49
				.iter()
112
49
				.any(|req| req.delegator == delegator),
113
			DispatchErrorWithPostInfo {
114
				post_info: Some(actual_weight).into(),
115
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
116
			},
117
		);
118

            
119
49
		let bonded_amount = state
120
49
			.get_bond_amount(&collator)
121
49
			.ok_or(<Error<T>>::DelegationDNE)?;
122
48
		let now = <Round<T>>::get().current;
123
48
		let when = now.saturating_add(T::RevokeDelegationDelay::get());
124
48
		scheduled_requests
125
48
			.try_push(ScheduledRequest {
126
48
				delegator: delegator.clone(),
127
48
				action: DelegationAction::Revoke(bonded_amount),
128
48
				when_executable: when,
129
48
			})
130
48
			.map_err(|_| DispatchErrorWithPostInfo {
131
				post_info: Some(actual_weight).into(),
132
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
133
48
			})?;
134
48
		state.less_total = state.less_total.saturating_add(bonded_amount);
135
48
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
136
48
		<DelegatorState<T>>::insert(delegator.clone(), state);
137
48

            
138
48
		Self::deposit_event(Event::DelegationRevocationScheduled {
139
48
			round: now,
140
48
			delegator,
141
48
			candidate: collator,
142
48
			scheduled_exit: when,
143
48
		});
144
48
		Ok(().into())
145
51
	}
146

            
147
	/// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator.
148
31
	pub(crate) fn delegation_schedule_bond_decrease(
149
31
		collator: T::AccountId,
150
31
		delegator: T::AccountId,
151
31
		decrease_amount: BalanceOf<T>,
152
31
	) -> DispatchResultWithPostInfo {
153
31
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
154
30
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
155
30

            
156
30
		let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
157
30
			scheduled_requests.len() as u32,
158
30
		);
159
30

            
160
30
		ensure!(
161
30
			!scheduled_requests
162
30
				.iter()
163
30
				.any(|req| req.delegator == delegator),
164
2
			DispatchErrorWithPostInfo {
165
2
				post_info: Some(actual_weight).into(),
166
2
				error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
167
2
			},
168
		);
169

            
170
28
		let bonded_amount = state
171
28
			.get_bond_amount(&collator)
172
28
			.ok_or(DispatchErrorWithPostInfo {
173
28
				post_info: Some(actual_weight).into(),
174
28
				error: <Error<T>>::DelegationDNE.into(),
175
28
			})?;
176
26
		ensure!(
177
26
			bonded_amount > decrease_amount,
178
1
			DispatchErrorWithPostInfo {
179
1
				post_info: Some(actual_weight).into(),
180
1
				error: <Error<T>>::DelegatorBondBelowMin.into(),
181
1
			},
182
		);
183
25
		let new_amount: BalanceOf<T> = (bonded_amount - decrease_amount).into();
184
25
		ensure!(
185
25
			new_amount >= T::MinDelegation::get(),
186
1
			DispatchErrorWithPostInfo {
187
1
				post_info: Some(actual_weight).into(),
188
1
				error: <Error<T>>::DelegationBelowMin.into(),
189
1
			},
190
		);
191

            
192
		// Net Total is total after pending orders are executed
193
24
		let net_total = state.total().saturating_sub(state.less_total);
194
24
		// Net Total is always >= MinDelegation
195
24
		let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
196
24
		ensure!(
197
24
			decrease_amount <= max_subtracted_amount,
198
			DispatchErrorWithPostInfo {
199
				post_info: Some(actual_weight).into(),
200
				error: <Error<T>>::DelegatorBondBelowMin.into(),
201
			},
202
		);
203

            
204
24
		let now = <Round<T>>::get().current;
205
24
		let when = now.saturating_add(T::DelegationBondLessDelay::get());
206
24
		scheduled_requests
207
24
			.try_push(ScheduledRequest {
208
24
				delegator: delegator.clone(),
209
24
				action: DelegationAction::Decrease(decrease_amount),
210
24
				when_executable: when,
211
24
			})
212
24
			.map_err(|_| DispatchErrorWithPostInfo {
213
				post_info: Some(actual_weight).into(),
214
				error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
215
24
			})?;
216
24
		state.less_total = state.less_total.saturating_add(decrease_amount);
217
24
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
218
24
		<DelegatorState<T>>::insert(delegator.clone(), state);
219
24

            
220
24
		Self::deposit_event(Event::DelegationDecreaseScheduled {
221
24
			delegator,
222
24
			candidate: collator,
223
24
			amount_to_decrease: decrease_amount,
224
24
			execute_round: when,
225
24
		});
226
24
		Ok(Some(actual_weight).into())
227
31
	}
228

            
229
	/// Cancels the delegator's existing [ScheduledRequest] towards a given collator.
230
10
	pub(crate) fn delegation_cancel_request(
231
10
		collator: T::AccountId,
232
10
		delegator: T::AccountId,
233
10
	) -> DispatchResultWithPostInfo {
234
10
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
235
10
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
236
10
		let actual_weight =
237
10
			<T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
238

            
239
10
		let request =
240
10
			Self::cancel_request_with_state(&delegator, &mut state, &mut scheduled_requests)
241
10
				.ok_or(DispatchErrorWithPostInfo {
242
10
					post_info: Some(actual_weight).into(),
243
10
					error: <Error<T>>::PendingDelegationRequestDNE.into(),
244
10
				})?;
245

            
246
10
		<DelegationScheduledRequests<T>>::insert(collator.clone(), scheduled_requests);
247
10
		<DelegatorState<T>>::insert(delegator.clone(), state);
248
10

            
249
10
		Self::deposit_event(Event::CancelledDelegationRequest {
250
10
			delegator,
251
10
			collator,
252
10
			cancelled_request: request.into(),
253
10
		});
254
10
		Ok(Some(actual_weight).into())
255
10
	}
256

            
257
12
	fn cancel_request_with_state(
258
12
		delegator: &T::AccountId,
259
12
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
260
12
		scheduled_requests: &mut BoundedVec<
261
12
			ScheduledRequest<T::AccountId, BalanceOf<T>>,
262
12
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
263
12
		>,
264
12
	) -> Option<ScheduledRequest<T::AccountId, BalanceOf<T>>> {
265
12
		let request_idx = scheduled_requests
266
12
			.iter()
267
12
			.position(|req| &req.delegator == delegator)?;
268

            
269
11
		let request = scheduled_requests.remove(request_idx);
270
11
		let amount = request.action.amount();
271
11
		state.less_total = state.less_total.saturating_sub(amount);
272
11
		Some(request)
273
12
	}
274

            
275
	/// Executes the delegator's existing [ScheduledRequest] towards a given collator.
276
37
	pub(crate) fn delegation_execute_scheduled_request(
277
37
		collator: T::AccountId,
278
37
		delegator: T::AccountId,
279
37
	) -> DispatchResultWithPostInfo {
280
37
		let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
281
37
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator);
282
37
		let request_idx = scheduled_requests
283
37
			.iter()
284
37
			.position(|req| req.delegator == delegator)
285
37
			.ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
286
37
		let request = &scheduled_requests[request_idx];
287
37

            
288
37
		let now = <Round<T>>::get().current;
289
37
		ensure!(
290
37
			request.when_executable <= now,
291
			<Error<T>>::PendingDelegationRequestNotDueYet
292
		);
293

            
294
37
		match request.action {
295
23
			DelegationAction::Revoke(amount) => {
296
23
				let actual_weight =
297
23
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
298

            
299
				// revoking last delegation => leaving set of delegators
300
23
				let leaving = if state.delegations.0.len() == 1usize {
301
12
					true
302
				} else {
303
11
					ensure!(
304
11
						state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
305
						DispatchErrorWithPostInfo {
306
							post_info: Some(actual_weight).into(),
307
							error: <Error<T>>::DelegatorBondBelowMin.into(),
308
						}
309
					);
310
11
					false
311
				};
312

            
313
				// remove from pending requests
314
23
				let amount = scheduled_requests.remove(request_idx).action.amount();
315
23
				state.less_total = state.less_total.saturating_sub(amount);
316
23

            
317
23
				// remove delegation from delegator state
318
23
				state.rm_delegation::<T>(&collator);
319
23

            
320
23
				// remove delegation from auto-compounding info
321
23
				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
322
23

            
323
23
				// remove delegation from collator state delegations
324
23
				Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
325
23
					.map_err(|err| DispatchErrorWithPostInfo {
326
						post_info: Some(actual_weight).into(),
327
						error: err,
328
23
					})?;
329
23
				Self::deposit_event(Event::DelegationRevoked {
330
23
					delegator: delegator.clone(),
331
23
					candidate: collator.clone(),
332
23
					unstaked_amount: amount,
333
23
				});
334
23

            
335
23
				<DelegationScheduledRequests<T>>::insert(collator, scheduled_requests);
336
23
				if leaving {
337
12
					<DelegatorState<T>>::remove(&delegator);
338
12
					Self::deposit_event(Event::DelegatorLeft {
339
12
						delegator,
340
12
						unstaked_amount: amount,
341
12
					});
342
12
				} else {
343
11
					<DelegatorState<T>>::insert(&delegator, state);
344
11
				}
345
23
				Ok(Some(actual_weight).into())
346
			}
347
			DelegationAction::Decrease(_) => {
348
14
				let actual_weight =
349
14
					<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
350
14

            
351
14
				// remove from pending requests
352
14
				let amount = scheduled_requests.remove(request_idx).action.amount();
353
14
				state.less_total = state.less_total.saturating_sub(amount);
354

            
355
				// decrease delegation
356
14
				for bond in &mut state.delegations.0 {
357
14
					if bond.owner == collator {
358
14
						return if bond.amount > amount {
359
14
							let amount_before: BalanceOf<T> = bond.amount.into();
360
14
							bond.amount = bond.amount.saturating_sub(amount);
361
14
							let mut collator_info = <CandidateInfo<T>>::get(&collator)
362
14
								.ok_or(<Error<T>>::CandidateDNE)
363
14
								.map_err(|err| DispatchErrorWithPostInfo {
364
									post_info: Some(actual_weight).into(),
365
									error: err.into(),
366
14
								})?;
367

            
368
14
							state
369
14
								.total_sub_if::<T, _>(amount, |total| {
370
14
									let new_total: BalanceOf<T> = total.into();
371
14
									ensure!(
372
14
										new_total >= T::MinDelegation::get(),
373
										<Error<T>>::DelegationBelowMin
374
									);
375

            
376
14
									Ok(())
377
14
								})
378
14
								.map_err(|err| DispatchErrorWithPostInfo {
379
									post_info: Some(actual_weight).into(),
380
									error: err,
381
14
								})?;
382

            
383
							// need to go into decrease_delegation
384
14
							let in_top = collator_info
385
14
								.decrease_delegation::<T>(
386
14
									&collator,
387
14
									delegator.clone(),
388
14
									amount_before,
389
14
									amount,
390
14
								)
391
14
								.map_err(|err| DispatchErrorWithPostInfo {
392
									post_info: Some(actual_weight).into(),
393
									error: err,
394
14
								})?;
395
14
							<CandidateInfo<T>>::insert(&collator, collator_info);
396
14
							let new_total_staked = <Total<T>>::get().saturating_sub(amount);
397
14
							<Total<T>>::put(new_total_staked);
398
14

            
399
14
							<DelegationScheduledRequests<T>>::insert(
400
14
								collator.clone(),
401
14
								scheduled_requests,
402
14
							);
403
14
							<DelegatorState<T>>::insert(delegator.clone(), state);
404
14
							Self::deposit_event(Event::DelegationDecreased {
405
14
								delegator,
406
14
								candidate: collator.clone(),
407
14
								amount,
408
14
								in_top,
409
14
							});
410
14
							Ok(Some(actual_weight).into())
411
						} else {
412
							// must rm entire delegation if bond.amount <= less or cancel request
413
							Err(DispatchErrorWithPostInfo {
414
								post_info: Some(actual_weight).into(),
415
								error: <Error<T>>::DelegationBelowMin.into(),
416
							})
417
						};
418
					}
419
				}
420
				Err(DispatchErrorWithPostInfo {
421
					post_info: Some(actual_weight).into(),
422
					error: <Error<T>>::DelegationDNE.into(),
423
				})
424
			}
425
		}
426
37
	}
427

            
428
	/// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists.
429
	/// The state needs to be persisted by the caller of this function.
430
19
	pub(crate) fn delegation_remove_request_with_state(
431
19
		collator: &T::AccountId,
432
19
		delegator: &T::AccountId,
433
19
		state: &mut Delegator<T::AccountId, BalanceOf<T>>,
434
19
	) {
435
19
		let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(collator);
436
19

            
437
19
		let maybe_request_idx = scheduled_requests
438
19
			.iter()
439
19
			.position(|req| &req.delegator == delegator);
440

            
441
19
		if let Some(request_idx) = maybe_request_idx {
442
4
			let request = scheduled_requests.remove(request_idx);
443
4
			let amount = request.action.amount();
444
4
			state.less_total = state.less_total.saturating_sub(amount);
445
4
			<DelegationScheduledRequests<T>>::insert(collator, scheduled_requests);
446
15
		}
447
19
	}
448

            
449
	/// Returns true if a [ScheduledRequest] exists for a given delegation
450
6
	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
451
6
		<DelegationScheduledRequests<T>>::get(collator)
452
6
			.iter()
453
6
			.any(|req| &req.delegator == delegator)
454
6
	}
455

            
456
	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
457
26
	pub fn delegation_request_revoke_exists(
458
26
		collator: &T::AccountId,
459
26
		delegator: &T::AccountId,
460
26
	) -> bool {
461
26
		<DelegationScheduledRequests<T>>::get(collator)
462
26
			.iter()
463
26
			.any(|req| {
464
6
				&req.delegator == delegator && matches!(req.action, DelegationAction::Revoke(_))
465
26
			})
466
26
	}
467
}
468

            
469
#[cfg(test)]
470
mod tests {
471
	use super::*;
472
	use crate::{mock::Test, set::OrderedSet, Bond};
473

            
474
	#[test]
475
1
	fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
476
1
		let mut state = Delegator {
477
1
			id: 1,
478
1
			delegations: OrderedSet::from(vec![Bond {
479
1
				amount: 100,
480
1
				owner: 2,
481
1
			}]),
482
1
			total: 100,
483
1
			less_total: 100,
484
1
			status: crate::DelegatorStatus::Active,
485
1
		};
486
1
		let mut scheduled_requests = vec![
487
1
			ScheduledRequest {
488
1
				delegator: 1,
489
1
				when_executable: 1,
490
1
				action: DelegationAction::Revoke(100),
491
1
			},
492
1
			ScheduledRequest {
493
1
				delegator: 2,
494
1
				when_executable: 1,
495
1
				action: DelegationAction::Decrease(50),
496
1
			},
497
1
		]
498
1
		.try_into()
499
1
		.expect("must succeed");
500
1
		let removed_request =
501
1
			<Pallet<Test>>::cancel_request_with_state(&1, &mut state, &mut scheduled_requests);
502
1

            
503
1
		assert_eq!(
504
1
			removed_request,
505
1
			Some(ScheduledRequest {
506
1
				delegator: 1,
507
1
				when_executable: 1,
508
1
				action: DelegationAction::Revoke(100),
509
1
			})
510
1
		);
511
1
		assert_eq!(
512
1
			scheduled_requests,
513
1
			vec![ScheduledRequest {
514
1
				delegator: 2,
515
1
				when_executable: 1,
516
1
				action: DelegationAction::Decrease(50),
517
1
			},]
518
1
		);
519
1
		assert_eq!(
520
1
			state,
521
1
			Delegator {
522
1
				id: 1,
523
1
				delegations: OrderedSet::from(vec![Bond {
524
1
					amount: 100,
525
1
					owner: 2,
526
1
				}]),
527
1
				total: 100,
528
1
				less_total: 0,
529
1
				status: crate::DelegatorStatus::Active,
530
1
			}
531
1
		);
532
1
	}
533

            
534
	#[test]
535
1
	fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
536
1
		let mut state = Delegator {
537
1
			id: 1,
538
1
			delegations: OrderedSet::from(vec![Bond {
539
1
				amount: 100,
540
1
				owner: 2,
541
1
			}]),
542
1
			total: 100,
543
1
			less_total: 100,
544
1
			status: crate::DelegatorStatus::Active,
545
1
		};
546
1
		let mut scheduled_requests = vec![ScheduledRequest {
547
1
			delegator: 2,
548
1
			when_executable: 1,
549
1
			action: DelegationAction::Decrease(50),
550
1
		}]
551
1
		.try_into()
552
1
		.expect("must succeed");
553
1
		let removed_request =
554
1
			<Pallet<Test>>::cancel_request_with_state(&1, &mut state, &mut scheduled_requests);
555
1

            
556
1
		assert_eq!(removed_request, None,);
557
1
		assert_eq!(
558
1
			scheduled_requests,
559
1
			vec![ScheduledRequest {
560
1
				delegator: 2,
561
1
				when_executable: 1,
562
1
				action: DelegationAction::Decrease(50),
563
1
			},]
564
1
		);
565
1
		assert_eq!(
566
1
			state,
567
1
			Delegator {
568
1
				id: 1,
569
1
				delegations: OrderedSet::from(vec![Bond {
570
1
					amount: 100,
571
1
					owner: 2,
572
1
				}]),
573
1
				total: 100,
574
1
				less_total: 100,
575
1
				status: crate::DelegatorStatus::Active,
576
1
			}
577
1
		);
578
1
	}
579
}