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