1use crate::pallet::{
20 BalanceOf, CandidateInfo, Config, DelegationScheduledRequests,
21 DelegationScheduledRequestsPerCollator, DelegationScheduledRequestsSummaryMap, DelegatorState,
22 Error, Event, Pallet, Round, RoundIndex, Total,
23};
24use crate::weights::WeightInfo;
25use crate::{auto_compound::AutoCompoundDelegations, Delegator};
26use frame_support::dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo};
27use frame_support::ensure;
28use frame_support::traits::Get;
29use frame_support::BoundedVec;
30use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
31use scale_info::TypeInfo;
32use sp_runtime::{
33 traits::{Saturating, Zero},
34 RuntimeDebug,
35};
36
37#[derive(
39 Clone,
40 Eq,
41 PartialEq,
42 Encode,
43 Decode,
44 RuntimeDebug,
45 TypeInfo,
46 PartialOrd,
47 Ord,
48 DecodeWithMemTracking,
49 MaxEncodedLen,
50)]
51pub enum DelegationAction<Balance> {
52 Revoke(Balance),
53 Decrease(Balance),
54}
55
56impl<Balance: Copy> DelegationAction<Balance> {
57 pub fn amount(&self) -> Balance {
59 match self {
60 DelegationAction::Revoke(amount) => *amount,
61 DelegationAction::Decrease(amount) => *amount,
62 }
63 }
64}
65
66#[derive(
69 Clone,
70 Eq,
71 PartialEq,
72 Encode,
73 Decode,
74 RuntimeDebug,
75 TypeInfo,
76 PartialOrd,
77 Ord,
78 DecodeWithMemTracking,
79 MaxEncodedLen,
80)]
81pub struct ScheduledRequest<Balance> {
82 pub when_executable: RoundIndex,
83 pub action: DelegationAction<Balance>,
84}
85
86#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, DecodeWithMemTracking)]
88pub struct CancelledScheduledRequest<Balance> {
89 pub when_executable: RoundIndex,
90 pub action: DelegationAction<Balance>,
91}
92
93impl<B> From<ScheduledRequest<B>> for CancelledScheduledRequest<B> {
94 fn from(request: ScheduledRequest<B>) -> Self {
95 CancelledScheduledRequest {
96 when_executable: request.when_executable,
97 action: request.action,
98 }
99 }
100}
101
102impl<T: Config> Pallet<T> {
103 pub(crate) fn delegation_schedule_revoke(
105 collator: T::AccountId,
106 delegator: T::AccountId,
107 ) -> DispatchResultWithPostInfo {
108 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
109 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
110
111 let actual_weight =
112 <T as Config>::WeightInfo::schedule_revoke_delegation(scheduled_requests.len() as u32);
113
114 let is_new_delegator = scheduled_requests.is_empty();
115
116 ensure!(
117 is_new_delegator,
118 DispatchErrorWithPostInfo {
119 post_info: Some(actual_weight).into(),
120 error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
121 },
122 );
123
124 let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
128 if current >= Pallet::<T>::max_delegators_per_candidate() {
129 return Err(DispatchErrorWithPostInfo {
130 post_info: Some(actual_weight).into(),
131 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
132 });
133 }
134
135 let bonded_amount = state
136 .get_bond_amount(&collator)
137 .ok_or(<Error<T>>::DelegationDNE)?;
138 let now = <Round<T>>::get().current;
139 let when = now.saturating_add(T::RevokeDelegationDelay::get());
140 scheduled_requests
141 .try_push(ScheduledRequest {
142 action: DelegationAction::Revoke(bonded_amount),
143 when_executable: when,
144 })
145 .map_err(|_| DispatchErrorWithPostInfo {
146 post_info: Some(actual_weight).into(),
147 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
148 })?;
149 state.less_total = state.less_total.saturating_add(bonded_amount);
150
151 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
152 *c = c.saturating_add(1);
153 });
154
155 <DelegationScheduledRequests<T>>::insert(
156 collator.clone(),
157 delegator.clone(),
158 scheduled_requests,
159 );
160 <DelegationScheduledRequestsSummaryMap<T>>::insert(
161 collator.clone(),
162 delegator.clone(),
163 DelegationAction::Revoke(bonded_amount),
164 );
165 <DelegatorState<T>>::insert(delegator.clone(), state);
166
167 Self::deposit_event(Event::DelegationRevocationScheduled {
168 round: now,
169 delegator,
170 candidate: collator,
171 scheduled_exit: when,
172 });
173 Ok(().into())
174 }
175
176 pub(crate) fn delegation_schedule_bond_decrease(
178 collator: T::AccountId,
179 delegator: T::AccountId,
180 decrease_amount: BalanceOf<T>,
181 ) -> DispatchResultWithPostInfo {
182 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
183 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
184
185 let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
186 scheduled_requests.len() as u32,
187 );
188
189 let is_new_delegator = scheduled_requests.is_empty();
193 if is_new_delegator {
194 let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
195 let max_delegators = Pallet::<T>::max_delegators_per_candidate();
196 if current >= max_delegators {
197 return Err(DispatchErrorWithPostInfo {
198 post_info: Some(actual_weight).into(),
199 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
200 });
201 }
202 }
203
204 ensure!(
205 !scheduled_requests
206 .iter()
207 .any(|req| matches!(req.action, DelegationAction::Revoke(_))),
208 DispatchErrorWithPostInfo {
209 post_info: Some(actual_weight).into(),
210 error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
211 },
212 );
213
214 let bonded_amount = state
215 .get_bond_amount(&collator)
216 .ok_or(DispatchErrorWithPostInfo {
217 post_info: Some(actual_weight).into(),
218 error: <Error<T>>::DelegationDNE.into(),
219 })?;
220 ensure!(
223 bonded_amount > decrease_amount,
224 DispatchErrorWithPostInfo {
225 post_info: Some(actual_weight).into(),
226 error: <Error<T>>::DelegatorBondBelowMin.into(),
227 },
228 );
229
230 let pending_decrease_total: BalanceOf<T> = scheduled_requests
235 .iter()
236 .filter_map(|req| match req.action {
237 DelegationAction::Decrease(amount) => Some(amount),
238 _ => None,
239 })
240 .fold(BalanceOf::<T>::zero(), |acc, amount| {
241 acc.saturating_add(amount)
242 });
243 let total_decrease_after = pending_decrease_total.saturating_add(decrease_amount);
244 let new_amount_after_all = bonded_amount.saturating_sub(total_decrease_after);
245 ensure!(
246 new_amount_after_all >= T::MinDelegation::get(),
247 DispatchErrorWithPostInfo {
248 post_info: Some(actual_weight).into(),
249 error: <Error<T>>::DelegationBelowMin.into(),
250 },
251 );
252
253 let net_total = state.total().saturating_sub(state.less_total);
255 let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
257 ensure!(
258 decrease_amount <= max_subtracted_amount,
259 DispatchErrorWithPostInfo {
260 post_info: Some(actual_weight).into(),
261 error: <Error<T>>::DelegatorBondBelowMin.into(),
262 },
263 );
264
265 let now = <Round<T>>::get().current;
266 let when = now.saturating_add(T::DelegationBondLessDelay::get());
267 scheduled_requests
268 .try_push(ScheduledRequest {
269 action: DelegationAction::Decrease(decrease_amount),
270 when_executable: when,
271 })
272 .map_err(|_| DispatchErrorWithPostInfo {
273 post_info: Some(actual_weight).into(),
274 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
275 })?;
276 state.less_total = state.less_total.saturating_add(decrease_amount);
277 if is_new_delegator {
278 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
279 *c = c.saturating_add(1);
280 });
281 }
282 <DelegationScheduledRequests<T>>::insert(
283 collator.clone(),
284 delegator.clone(),
285 scheduled_requests,
286 );
287 <DelegationScheduledRequestsSummaryMap<T>>::mutate(&collator, &delegator, |entry| {
289 *entry = Some(match entry.take() {
290 Some(DelegationAction::Decrease(existing)) => {
291 DelegationAction::Decrease(existing.saturating_add(decrease_amount))
292 }
293 _ => DelegationAction::Decrease(decrease_amount),
294 });
295 });
296 <DelegatorState<T>>::insert(delegator.clone(), state);
297
298 Self::deposit_event(Event::DelegationDecreaseScheduled {
299 delegator,
300 candidate: collator,
301 amount_to_decrease: decrease_amount,
302 execute_round: when,
303 });
304 Ok(Some(actual_weight).into())
305 }
306
307 pub(crate) fn delegation_cancel_request(
309 collator: T::AccountId,
310 delegator: T::AccountId,
311 ) -> DispatchResultWithPostInfo {
312 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
313 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
314 let actual_weight =
315 <T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
316
317 let request = Self::cancel_request_with_state(&mut state, &mut scheduled_requests).ok_or(
318 DispatchErrorWithPostInfo {
319 post_info: Some(actual_weight).into(),
320 error: <Error<T>>::PendingDelegationRequestDNE.into(),
321 },
322 )?;
323
324 match &request.action {
325 DelegationAction::Revoke(_) => {
326 <DelegationScheduledRequestsSummaryMap<T>>::remove(&collator, &delegator);
327 }
328 DelegationAction::Decrease(amount) => {
329 let amount = *amount;
330 <DelegationScheduledRequestsSummaryMap<T>>::mutate_exists(
331 &collator,
332 &delegator,
333 |entry| {
334 if let Some(DelegationAction::Decrease(existing)) = entry {
335 let remaining = existing.saturating_sub(amount);
336 if remaining.is_zero() {
337 *entry = None;
338 } else {
339 *existing = remaining;
340 }
341 }
342 },
343 );
344 }
345 }
346
347 if scheduled_requests.is_empty() {
348 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
349 *c = c.saturating_sub(1);
350 });
351 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
352 } else {
353 <DelegationScheduledRequests<T>>::insert(
354 collator.clone(),
355 delegator.clone(),
356 scheduled_requests,
357 );
358 }
359 <DelegatorState<T>>::insert(delegator.clone(), state);
360
361 Self::deposit_event(Event::CancelledDelegationRequest {
362 delegator,
363 collator,
364 cancelled_request: request.into(),
365 });
366 Ok(Some(actual_weight).into())
367 }
368
369 fn cancel_request_with_state(
370 state: &mut Delegator<T::AccountId, BalanceOf<T>>,
371 scheduled_requests: &mut BoundedVec<
372 ScheduledRequest<BalanceOf<T>>,
373 T::MaxScheduledRequestsPerDelegator,
374 >,
375 ) -> Option<ScheduledRequest<BalanceOf<T>>> {
376 if scheduled_requests.is_empty() {
377 return None;
378 }
379
380 let request = scheduled_requests.remove(0);
383 let amount = request.action.amount();
384 state.less_total = state.less_total.saturating_sub(amount);
385 Some(request)
386 }
387
388 pub(crate) fn delegation_execute_scheduled_request(
390 collator: T::AccountId,
391 delegator: T::AccountId,
392 ) -> DispatchResultWithPostInfo {
393 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
394 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
395 let request = scheduled_requests
396 .first()
397 .ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
398
399 let now = <Round<T>>::get().current;
400 ensure!(
401 request.when_executable <= now,
402 <Error<T>>::PendingDelegationRequestNotDueYet
403 );
404
405 match request.action {
406 DelegationAction::Revoke(amount) => {
407 let actual_weight =
408 <T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
409
410 let leaving = if state.delegations.0.len() == 1usize {
412 true
413 } else {
414 ensure!(
415 state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
416 DispatchErrorWithPostInfo {
417 post_info: Some(actual_weight).into(),
418 error: <Error<T>>::DelegatorBondBelowMin.into(),
419 }
420 );
421 false
422 };
423
424 let amount = scheduled_requests.remove(0).action.amount();
427 state.less_total = state.less_total.saturating_sub(amount);
428
429 state.rm_delegation::<T>(&collator);
431
432 <AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
434
435 <DelegationScheduledRequestsSummaryMap<T>>::remove(&collator, &delegator);
437
438 Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
440 .map_err(|err| DispatchErrorWithPostInfo {
441 post_info: Some(actual_weight).into(),
442 error: err,
443 })?;
444 Self::deposit_event(Event::DelegationRevoked {
445 delegator: delegator.clone(),
446 candidate: collator.clone(),
447 unstaked_amount: amount,
448 });
449 if scheduled_requests.is_empty() {
450 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
451 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
452 *c = c.saturating_sub(1);
453 });
454 } else {
455 <DelegationScheduledRequests<T>>::insert(
456 collator.clone(),
457 delegator.clone(),
458 scheduled_requests,
459 );
460 }
461 if leaving {
462 <DelegatorState<T>>::remove(&delegator);
463 Self::deposit_event(Event::DelegatorLeft {
464 delegator,
465 unstaked_amount: amount,
466 });
467 } else {
468 <DelegatorState<T>>::insert(&delegator, state);
469 }
470 Ok(Some(actual_weight).into())
471 }
472 DelegationAction::Decrease(_) => {
473 let actual_weight =
474 <T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
475
476 let amount = scheduled_requests.remove(0).action.amount();
479 state.less_total = state.less_total.saturating_sub(amount);
480
481 for bond in &mut state.delegations.0 {
483 if bond.owner == collator {
484 return if bond.amount > amount {
485 let amount_before: BalanceOf<T> = bond.amount.into();
486 bond.amount = bond.amount.saturating_sub(amount);
487 let mut collator_info = <CandidateInfo<T>>::get(&collator)
488 .ok_or(<Error<T>>::CandidateDNE)
489 .map_err(|err| DispatchErrorWithPostInfo {
490 post_info: Some(actual_weight).into(),
491 error: err.into(),
492 })?;
493
494 state
495 .total_sub_if::<T, _>(amount, |total| {
496 let new_total: BalanceOf<T> = total.into();
497 ensure!(
498 new_total >= T::MinDelegation::get(),
499 <Error<T>>::DelegationBelowMin
500 );
501
502 Ok(())
503 })
504 .map_err(|err| DispatchErrorWithPostInfo {
505 post_info: Some(actual_weight).into(),
506 error: err,
507 })?;
508
509 let in_top = collator_info
511 .decrease_delegation::<T>(
512 &collator,
513 delegator.clone(),
514 amount_before,
515 amount,
516 )
517 .map_err(|err| DispatchErrorWithPostInfo {
518 post_info: Some(actual_weight).into(),
519 error: err,
520 })?;
521 <CandidateInfo<T>>::insert(&collator, collator_info);
522 let new_total_staked = <Total<T>>::get().saturating_sub(amount);
523 <Total<T>>::put(new_total_staked);
524
525 if scheduled_requests.is_empty() {
526 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
527 <DelegationScheduledRequestsPerCollator<T>>::mutate(
528 &collator,
529 |c| {
530 *c = c.saturating_sub(1);
531 },
532 );
533 } else {
534 <DelegationScheduledRequests<T>>::insert(
535 collator.clone(),
536 delegator.clone(),
537 scheduled_requests,
538 );
539 }
540 <DelegationScheduledRequestsSummaryMap<T>>::mutate_exists(
542 &collator,
543 &delegator,
544 |entry| {
545 if let Some(DelegationAction::Decrease(existing)) = entry {
546 let remaining = existing.saturating_sub(amount);
547 if remaining.is_zero() {
548 *entry = None;
549 } else {
550 *existing = remaining;
551 }
552 }
553 },
554 );
555 <DelegatorState<T>>::insert(delegator.clone(), state);
556 Self::deposit_event(Event::DelegationDecreased {
557 delegator,
558 candidate: collator.clone(),
559 amount,
560 in_top,
561 });
562 Ok(Some(actual_weight).into())
563 } else {
564 Err(DispatchErrorWithPostInfo {
566 post_info: Some(actual_weight).into(),
567 error: <Error<T>>::DelegationBelowMin.into(),
568 })
569 };
570 }
571 }
572 Err(DispatchErrorWithPostInfo {
573 post_info: Some(actual_weight).into(),
574 error: <Error<T>>::DelegationDNE.into(),
575 })
576 }
577 }
578 }
579
580 pub(crate) fn delegation_remove_request_with_state(
583 collator: &T::AccountId,
584 delegator: &T::AccountId,
585 state: &mut Delegator<T::AccountId, BalanceOf<T>>,
586 ) {
587 let scheduled_requests = <DelegationScheduledRequests<T>>::get(collator, delegator);
588
589 if scheduled_requests.is_empty() {
590 return;
591 }
592
593 let total_amount: BalanceOf<T> = scheduled_requests
595 .iter()
596 .map(|request| request.action.amount())
597 .fold(BalanceOf::<T>::zero(), |acc, amount| {
598 acc.saturating_add(amount)
599 });
600
601 state.less_total = state.less_total.saturating_sub(total_amount);
602 <DelegationScheduledRequests<T>>::remove(collator, delegator);
603 <DelegationScheduledRequestsSummaryMap<T>>::remove(collator, delegator);
604 <DelegationScheduledRequestsPerCollator<T>>::mutate(collator, |c| {
605 *c = c.saturating_sub(1);
606 });
607 }
608
609 pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
611 !<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
612 }
613
614 pub fn delegation_request_revoke_exists(
616 collator: &T::AccountId,
617 delegator: &T::AccountId,
618 ) -> bool {
619 matches!(
620 <DelegationScheduledRequestsSummaryMap<T>>::get(collator, delegator),
621 Some(DelegationAction::Revoke(_))
622 )
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::{mock::Test, set::OrderedSet, Bond};
630
631 #[test]
632 fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
633 let mut state = Delegator {
634 id: 1,
635 delegations: OrderedSet::from(vec![Bond {
636 amount: 100,
637 owner: 2,
638 }]),
639 total: 100,
640 less_total: 150,
641 status: crate::DelegatorStatus::Active,
642 };
643 let mut scheduled_requests = vec![
644 ScheduledRequest {
645 when_executable: 1,
646 action: DelegationAction::Revoke(100),
647 },
648 ScheduledRequest {
649 when_executable: 1,
650 action: DelegationAction::Decrease(50),
651 },
652 ]
653 .try_into()
654 .expect("must succeed");
655 let removed_request =
656 <Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
657
658 assert_eq!(
659 removed_request,
660 Some(ScheduledRequest {
661 when_executable: 1,
662 action: DelegationAction::Revoke(100),
663 })
664 );
665 assert_eq!(
666 scheduled_requests,
667 vec![ScheduledRequest {
668 when_executable: 1,
669 action: DelegationAction::Decrease(50),
670 },]
671 );
672 assert_eq!(
673 state.less_total, 50,
674 "less_total should be reduced by the amount of the cancelled request"
675 );
676 }
677
678 #[test]
679 fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
680 let mut state = Delegator {
681 id: 1,
682 delegations: OrderedSet::from(vec![Bond {
683 amount: 100,
684 owner: 2,
685 }]),
686 total: 100,
687 less_total: 100,
688 status: crate::DelegatorStatus::Active,
689 };
690 let mut scheduled_requests: BoundedVec<
691 ScheduledRequest<u128>,
692 <Test as crate::pallet::Config>::MaxScheduledRequestsPerDelegator,
693 > = BoundedVec::default();
694 let removed_request =
695 <Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
696
697 assert_eq!(removed_request, None,);
698 assert_eq!(
699 scheduled_requests.len(),
700 0,
701 "scheduled_requests should remain empty"
702 );
703 assert_eq!(
704 state.less_total, 100,
705 "less_total should remain unchanged when there is nothing to cancel"
706 );
707 }
708}