pallet_parachain_staking/
delegation_requests.rs

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
19use 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/// An action that can be performed upon a delegation
38#[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	/// Returns the wrapped amount value.
58	pub fn amount(&self) -> Balance {
59		match self {
60			DelegationAction::Revoke(amount) => *amount,
61			DelegationAction::Decrease(amount) => *amount,
62		}
63	}
64}
65
66/// Represents a scheduled request that defines a [`DelegationAction`]. The request is executable
67/// iff the provided [`RoundIndex`] is achieved.
68#[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/// Represents a cancelled scheduled request for emitting an event.
87#[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	/// Schedules a [DelegationAction::Revoke] for the delegator, towards a given collator.
104	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		// This is the first scheduled request for this delegator towards this collator,
125		// ensure we do not exceed the maximum number of delegators that can have pending
126		// requests for the collator.
127		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	/// Schedules a [DelegationAction::Decrease] for the delegator, towards a given collator.
177	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		// If this is the first scheduled request for this delegator towards this collator,
190		// ensure we do not exceed the maximum number of delegators that can have pending
191		// requests for the collator.
192		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		// Per-request safety: a single decrease cannot exceed the current delegation
221		// and must leave at least MinDelegation on that delegation.
222		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		// Cumulative safety: multiple pending Decrease requests for the same
231		// (collator, delegator) pair must also respect the MinDelegation
232		// constraint when applied together. Otherwise, snapshots can become
233		// inconsistent even if each request, in isolation, appears valid.
234		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		// Net Total is total after pending orders are executed
254		let net_total = state.total().saturating_sub(state.less_total);
255		// Net Total is always >= MinDelegation
256		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		// Update summary map: accumulate the new decrease amount
288		<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	/// Cancels the delegator's existing [ScheduledRequest] towards a given collator.
308	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		// `BoundedVec::remove` can panic, but we make sure it will not happen by
381		// checking above that `scheduled_requests` is not empty.
382		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	/// Executes the delegator's existing [ScheduledRequest] towards a given collator.
389	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				// revoking last delegation => leaving set of delegators
411				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				// remove from pending requests
425				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
426				let amount = scheduled_requests.remove(0).action.amount();
427				state.less_total = state.less_total.saturating_sub(amount);
428
429				// remove delegation from delegator state
430				state.rm_delegation::<T>(&collator);
431
432				// remove delegation from auto-compounding info
433				<AutoCompoundDelegations<T>>::remove_auto_compound(&collator, &delegator);
434
435				// clear the summary entry
436				<DelegationScheduledRequestsSummaryMap<T>>::remove(&collator, &delegator);
437
438				// remove delegation from collator state delegations
439				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				// remove from pending requests
477				// `BoundedVec::remove` can panic, but we make sure it will not happen by checking above that `scheduled_requests` is not empty.
478				let amount = scheduled_requests.remove(0).action.amount();
479				state.less_total = state.less_total.saturating_sub(amount);
480
481				// decrease delegation
482				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							// need to go into decrease_delegation
510							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							// Update summary map: subtract executed decrease
541							<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							// must rm entire delegation if bond.amount <= less or cancel request
565							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	/// Removes the delegator's existing [ScheduledRequest] towards a given collator, if exists.
581	/// The state needs to be persisted by the caller of this function.
582	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		// Calculate total amount across all scheduled requests
594		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	/// Returns true if a [ScheduledRequest] exists for a given delegation
610	pub fn delegation_request_exists(collator: &T::AccountId, delegator: &T::AccountId) -> bool {
611		!<DelegationScheduledRequests<T>>::get(collator, delegator).is_empty()
612	}
613
614	/// Returns true if a [DelegationAction::Revoke] [ScheduledRequest] exists for a given delegation
615	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}