1use crate::pallet::{
20 BalanceOf, CandidateInfo, Config, DelegationScheduledRequests,
21 DelegationScheduledRequestsPerCollator, DelegatorState, Error, Event, Pallet, Round,
22 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 <DelegatorState<T>>::insert(delegator.clone(), state);
161
162 Self::deposit_event(Event::DelegationRevocationScheduled {
163 round: now,
164 delegator,
165 candidate: collator,
166 scheduled_exit: when,
167 });
168 Ok(().into())
169 }
170
171 pub(crate) fn delegation_schedule_bond_decrease(
173 collator: T::AccountId,
174 delegator: T::AccountId,
175 decrease_amount: BalanceOf<T>,
176 ) -> DispatchResultWithPostInfo {
177 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
178 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
179
180 let actual_weight = <T as Config>::WeightInfo::schedule_delegator_bond_less(
181 scheduled_requests.len() as u32,
182 );
183
184 let is_new_delegator = scheduled_requests.is_empty();
188 if is_new_delegator {
189 let current = <DelegationScheduledRequestsPerCollator<T>>::get(&collator);
190 let max_delegators = Pallet::<T>::max_delegators_per_candidate();
191 if current >= max_delegators {
192 return Err(DispatchErrorWithPostInfo {
193 post_info: Some(actual_weight).into(),
194 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
195 });
196 }
197 }
198
199 ensure!(
200 !scheduled_requests
201 .iter()
202 .any(|req| matches!(req.action, DelegationAction::Revoke(_))),
203 DispatchErrorWithPostInfo {
204 post_info: Some(actual_weight).into(),
205 error: <Error<T>>::PendingDelegationRequestAlreadyExists.into(),
206 },
207 );
208
209 let bonded_amount = state
210 .get_bond_amount(&collator)
211 .ok_or(DispatchErrorWithPostInfo {
212 post_info: Some(actual_weight).into(),
213 error: <Error<T>>::DelegationDNE.into(),
214 })?;
215 ensure!(
218 bonded_amount > decrease_amount,
219 DispatchErrorWithPostInfo {
220 post_info: Some(actual_weight).into(),
221 error: <Error<T>>::DelegatorBondBelowMin.into(),
222 },
223 );
224
225 let pending_decrease_total: BalanceOf<T> = scheduled_requests
230 .iter()
231 .filter_map(|req| match req.action {
232 DelegationAction::Decrease(amount) => Some(amount),
233 _ => None,
234 })
235 .fold(BalanceOf::<T>::zero(), |acc, amount| {
236 acc.saturating_add(amount)
237 });
238 let total_decrease_after = pending_decrease_total.saturating_add(decrease_amount);
239 let new_amount_after_all = bonded_amount.saturating_sub(total_decrease_after);
240 ensure!(
241 new_amount_after_all >= T::MinDelegation::get(),
242 DispatchErrorWithPostInfo {
243 post_info: Some(actual_weight).into(),
244 error: <Error<T>>::DelegationBelowMin.into(),
245 },
246 );
247
248 let net_total = state.total().saturating_sub(state.less_total);
250 let max_subtracted_amount = net_total.saturating_sub(T::MinDelegation::get().into());
252 ensure!(
253 decrease_amount <= max_subtracted_amount,
254 DispatchErrorWithPostInfo {
255 post_info: Some(actual_weight).into(),
256 error: <Error<T>>::DelegatorBondBelowMin.into(),
257 },
258 );
259
260 let now = <Round<T>>::get().current;
261 let when = now.saturating_add(T::DelegationBondLessDelay::get());
262 scheduled_requests
263 .try_push(ScheduledRequest {
264 action: DelegationAction::Decrease(decrease_amount),
265 when_executable: when,
266 })
267 .map_err(|_| DispatchErrorWithPostInfo {
268 post_info: Some(actual_weight).into(),
269 error: Error::<T>::ExceedMaxDelegationsPerDelegator.into(),
270 })?;
271 state.less_total = state.less_total.saturating_add(decrease_amount);
272 if is_new_delegator {
273 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
274 *c = c.saturating_add(1);
275 });
276 }
277 <DelegationScheduledRequests<T>>::insert(
278 collator.clone(),
279 delegator.clone(),
280 scheduled_requests,
281 );
282 <DelegatorState<T>>::insert(delegator.clone(), state);
283
284 Self::deposit_event(Event::DelegationDecreaseScheduled {
285 delegator,
286 candidate: collator,
287 amount_to_decrease: decrease_amount,
288 execute_round: when,
289 });
290 Ok(Some(actual_weight).into())
291 }
292
293 pub(crate) fn delegation_cancel_request(
295 collator: T::AccountId,
296 delegator: T::AccountId,
297 ) -> DispatchResultWithPostInfo {
298 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
299 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
300 let actual_weight =
301 <T as Config>::WeightInfo::cancel_delegation_request(scheduled_requests.len() as u32);
302
303 let request = Self::cancel_request_with_state(&mut state, &mut scheduled_requests).ok_or(
304 DispatchErrorWithPostInfo {
305 post_info: Some(actual_weight).into(),
306 error: <Error<T>>::PendingDelegationRequestDNE.into(),
307 },
308 )?;
309
310 if scheduled_requests.is_empty() {
311 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
312 *c = c.saturating_sub(1);
313 });
314 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
315 } else {
316 <DelegationScheduledRequests<T>>::insert(
317 collator.clone(),
318 delegator.clone(),
319 scheduled_requests,
320 );
321 }
322 <DelegatorState<T>>::insert(delegator.clone(), state);
323
324 Self::deposit_event(Event::CancelledDelegationRequest {
325 delegator,
326 collator,
327 cancelled_request: request.into(),
328 });
329 Ok(Some(actual_weight).into())
330 }
331
332 fn cancel_request_with_state(
333 state: &mut Delegator<T::AccountId, BalanceOf<T>>,
334 scheduled_requests: &mut BoundedVec<
335 ScheduledRequest<BalanceOf<T>>,
336 T::MaxScheduledRequestsPerDelegator,
337 >,
338 ) -> Option<ScheduledRequest<BalanceOf<T>>> {
339 if scheduled_requests.is_empty() {
340 return None;
341 }
342
343 let request = scheduled_requests.remove(0);
346 let amount = request.action.amount();
347 state.less_total = state.less_total.saturating_sub(amount);
348 Some(request)
349 }
350
351 pub(crate) fn delegation_execute_scheduled_request(
353 collator: T::AccountId,
354 delegator: T::AccountId,
355 ) -> DispatchResultWithPostInfo {
356 let mut state = <DelegatorState<T>>::get(&delegator).ok_or(<Error<T>>::DelegatorDNE)?;
357 let mut scheduled_requests = <DelegationScheduledRequests<T>>::get(&collator, &delegator);
358 let request = scheduled_requests
359 .first()
360 .ok_or(<Error<T>>::PendingDelegationRequestDNE)?;
361
362 let now = <Round<T>>::get().current;
363 ensure!(
364 request.when_executable <= now,
365 <Error<T>>::PendingDelegationRequestNotDueYet
366 );
367
368 match request.action {
369 DelegationAction::Revoke(amount) => {
370 let actual_weight =
371 <T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
372
373 let leaving = if state.delegations.0.len() == 1usize {
375 true
376 } else {
377 ensure!(
378 state.total().saturating_sub(T::MinDelegation::get().into()) >= amount,
379 DispatchErrorWithPostInfo {
380 post_info: Some(actual_weight).into(),
381 error: <Error<T>>::DelegatorBondBelowMin.into(),
382 }
383 );
384 false
385 };
386
387 let amount = scheduled_requests.remove(0).action.amount();
390 state.less_total = state.less_total.saturating_sub(amount);
391
392 state.rm_delegation::<T>(&collator);
394
395 <AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
397
398 Self::delegator_leaves_candidate(collator.clone(), delegator.clone(), amount)
400 .map_err(|err| DispatchErrorWithPostInfo {
401 post_info: Some(actual_weight).into(),
402 error: err,
403 })?;
404 Self::deposit_event(Event::DelegationRevoked {
405 delegator: delegator.clone(),
406 candidate: collator.clone(),
407 unstaked_amount: amount,
408 });
409 if scheduled_requests.is_empty() {
410 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
411 <DelegationScheduledRequestsPerCollator<T>>::mutate(&collator, |c| {
412 *c = c.saturating_sub(1);
413 });
414 } else {
415 <DelegationScheduledRequests<T>>::insert(
416 collator.clone(),
417 delegator.clone(),
418 scheduled_requests,
419 );
420 }
421 if leaving {
422 <DelegatorState<T>>::remove(&delegator);
423 Self::deposit_event(Event::DelegatorLeft {
424 delegator,
425 unstaked_amount: amount,
426 });
427 } else {
428 <DelegatorState<T>>::insert(&delegator, state);
429 }
430 Ok(Some(actual_weight).into())
431 }
432 DelegationAction::Decrease(_) => {
433 let actual_weight =
434 <T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst();
435
436 let amount = scheduled_requests.remove(0).action.amount();
439 state.less_total = state.less_total.saturating_sub(amount);
440
441 for bond in &mut state.delegations.0 {
443 if bond.owner == collator {
444 return if bond.amount > amount {
445 let amount_before: BalanceOf<T> = bond.amount.into();
446 bond.amount = bond.amount.saturating_sub(amount);
447 let mut collator_info = <CandidateInfo<T>>::get(&collator)
448 .ok_or(<Error<T>>::CandidateDNE)
449 .map_err(|err| DispatchErrorWithPostInfo {
450 post_info: Some(actual_weight).into(),
451 error: err.into(),
452 })?;
453
454 state
455 .total_sub_if::<T, _>(amount, |total| {
456 let new_total: BalanceOf<T> = total.into();
457 ensure!(
458 new_total >= T::MinDelegation::get(),
459 <Error<T>>::DelegationBelowMin
460 );
461
462 Ok(())
463 })
464 .map_err(|err| DispatchErrorWithPostInfo {
465 post_info: Some(actual_weight).into(),
466 error: err,
467 })?;
468
469 let in_top = collator_info
471 .decrease_delegation::<T>(
472 &collator,
473 delegator.clone(),
474 amount_before,
475 amount,
476 )
477 .map_err(|err| DispatchErrorWithPostInfo {
478 post_info: Some(actual_weight).into(),
479 error: err,
480 })?;
481 <CandidateInfo<T>>::insert(&collator, collator_info);
482 let new_total_staked = <Total<T>>::get().saturating_sub(amount);
483 <Total<T>>::put(new_total_staked);
484
485 if scheduled_requests.is_empty() {
486 <DelegationScheduledRequests<T>>::remove(&collator, &delegator);
487 <DelegationScheduledRequestsPerCollator<T>>::mutate(
488 &collator,
489 |c| {
490 *c = c.saturating_sub(1);
491 },
492 );
493 } else {
494 <DelegationScheduledRequests<T>>::insert(
495 collator.clone(),
496 delegator.clone(),
497 scheduled_requests,
498 );
499 }
500 <DelegatorState<T>>::insert(delegator.clone(), state);
501 Self::deposit_event(Event::DelegationDecreased {
502 delegator,
503 candidate: collator.clone(),
504 amount,
505 in_top,
506 });
507 Ok(Some(actual_weight).into())
508 } else {
509 Err(DispatchErrorWithPostInfo {
511 post_info: Some(actual_weight).into(),
512 error: <Error<T>>::DelegationBelowMin.into(),
513 })
514 };
515 }
516 }
517 Err(DispatchErrorWithPostInfo {
518 post_info: Some(actual_weight).into(),
519 error: <Error<T>>::DelegationDNE.into(),
520 })
521 }
522 }
523 }
524
525 pub(crate) fn delegation_remove_request_with_state(
528 collator: &T::AccountId,
529 delegator: &T::AccountId,
530 state: &mut Delegator<T::AccountId, BalanceOf<T>>,
531 ) {
532 let scheduled_requests = <DelegationScheduledRequests<T>>::get(collator, delegator);
533
534 if scheduled_requests.is_empty() {
535 return;
536 }
537
538 let total_amount: BalanceOf<T> = scheduled_requests
540 .iter()
541 .map(|request| request.action.amount())
542 .fold(BalanceOf::<T>::zero(), |acc, amount| {
543 acc.saturating_add(amount)
544 });
545
546 state.less_total = state.less_total.saturating_sub(total_amount);
547 <DelegationScheduledRequests<T>>::remove(collator, delegator);
548 <DelegationScheduledRequestsPerCollator<T>>::mutate(collator, |c| {
549 *c = c.saturating_sub(1);
550 });
551 }
552
553 pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
555 !<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
556 }
557
558 pub fn delegation_request_revoke_exists(
560 collator: &T::AccountId,
561 delegator: &T::AccountId,
562 ) -> bool {
563 <DelegationScheduledRequests<T>>::get(collator, delegator)
564 .iter()
565 .any(|req| matches!(req.action, DelegationAction::Revoke(_)))
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::{mock::Test, set::OrderedSet, Bond};
573
574 #[test]
575 fn test_cancel_request_with_state_removes_request_for_correct_delegator_and_updates_state() {
576 let mut state = Delegator {
577 id: 1,
578 delegations: OrderedSet::from(vec![Bond {
579 amount: 100,
580 owner: 2,
581 }]),
582 total: 100,
583 less_total: 150,
584 status: crate::DelegatorStatus::Active,
585 };
586 let mut scheduled_requests = vec![
587 ScheduledRequest {
588 when_executable: 1,
589 action: DelegationAction::Revoke(100),
590 },
591 ScheduledRequest {
592 when_executable: 1,
593 action: DelegationAction::Decrease(50),
594 },
595 ]
596 .try_into()
597 .expect("must succeed");
598 let removed_request =
599 <Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
600
601 assert_eq!(
602 removed_request,
603 Some(ScheduledRequest {
604 when_executable: 1,
605 action: DelegationAction::Revoke(100),
606 })
607 );
608 assert_eq!(
609 scheduled_requests,
610 vec![ScheduledRequest {
611 when_executable: 1,
612 action: DelegationAction::Decrease(50),
613 },]
614 );
615 assert_eq!(
616 state.less_total, 50,
617 "less_total should be reduced by the amount of the cancelled request"
618 );
619 }
620
621 #[test]
622 fn test_cancel_request_with_state_does_nothing_when_request_does_not_exist() {
623 let mut state = Delegator {
624 id: 1,
625 delegations: OrderedSet::from(vec![Bond {
626 amount: 100,
627 owner: 2,
628 }]),
629 total: 100,
630 less_total: 100,
631 status: crate::DelegatorStatus::Active,
632 };
633 let mut scheduled_requests: BoundedVec<
634 ScheduledRequest<u128>,
635 <Test as crate::pallet::Config>::MaxScheduledRequestsPerDelegator,
636 > = BoundedVec::default();
637 let removed_request =
638 <Pallet<Test>>::cancel_request_with_state(&mut state, &mut scheduled_requests);
639
640 assert_eq!(removed_request, None,);
641 assert_eq!(
642 scheduled_requests.len(),
643 0,
644 "scheduled_requests should remain empty"
645 );
646 assert_eq!(
647 state.less_total, 100,
648 "less_total should remain unchanged when there is nothing to cancel"
649 );
650 }
651}