referrerpolicy=no-referrer-when-downgrade

pallet_recovery/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Recovery Pallet
19//!
20//! - [`Config`]
21//! - [`Call`]
22//!
23//! ## Overview
24//!
25//! The Recovery pallet is an M-of-N social recovery tool for users to gain
26//! access to their accounts if the private key or other authentication mechanism
27//! is lost. Through this pallet, a user is able to make calls on-behalf-of another
28//! account which they have recovered. The recovery process is protected by trusted
29//! "friends" whom the original account owner chooses. A threshold (M) out of N
30//! friends are needed to give another account access to the recoverable account.
31//!
32//! ### Recovery Configuration
33//!
34//! The recovery process for each recoverable account can be configured by the account owner.
35//! They are able to choose:
36//! * `friends` - The list of friends that the account owner trusts to protect the recovery process
37//!   for their account.
38//! * `threshold` - The number of friends that need to approve a recovery process for the account to
39//!   be successfully recovered.
40//! * `delay_period` - The minimum number of blocks after the beginning of the recovery process that
41//!   need to pass before the account can be successfully recovered.
42//!
43//! There is a configurable deposit that all users need to pay to create a recovery
44//! configuration. This deposit is composed of a base deposit plus a multiplier for
45//! the number of friends chosen. This deposit is returned in full when the account
46//! owner removes their recovery configuration.
47//!
48//! ### Recovery Life Cycle
49//!
50//! The intended life cycle of a successful recovery takes the following steps:
51//! 1. The account owner calls `create_recovery` to set up a recovery configuration for their
52//!    account.
53//! 2. At some later time, the account owner loses access to their account and wants to recover it.
54//!    Likely, they will need to create a new account and fund it with enough balance to support the
55//!    transaction fees and the deposit for the recovery process.
56//! 3. Using this new account, they call `initiate_recovery`.
57//! 4. Then the account owner would contact their configured friends to vouch for the recovery
58//!    attempt. The account owner would provide their old account id and the new account id, and
59//!    friends would call `vouch_recovery` with those parameters.
60//! 5. Once a threshold number of friends have vouched for the recovery attempt, the account owner
61//!    needs to wait until the delay period has passed, starting when they initiated the recovery
62//!    process.
63//! 6. Now the account owner is able to call `claim_recovery`, which subsequently allows them to
64//!    call `as_recovered` and directly make calls on-behalf-of the lost account.
65//! 7. Using the now recovered account, the account owner can call `close_recovery` on the recovery
66//!    process they opened, reclaiming the recovery deposit they placed.
67//! 8. Then the account owner should then call `remove_recovery` to remove the recovery
68//!    configuration on the recovered account and reclaim the recovery configuration deposit they
69//!    placed.
70//! 9. Using `as_recovered`, the account owner is able to call any other pallets to clean up their
71//!    state and reclaim any reserved or locked funds. They can then transfer all funds from the
72//!    recovered account to the new account.
73//! 10. When the recovered account becomes reaped (i.e. its free and reserved balance drops to
74//!     zero), the final recovery link is removed.
75//!
76//! ### Malicious Recovery Attempts
77//!
78//! Initializing the recovery process for a recoverable account is open and
79//! permissionless. However, the recovery deposit is an economic deterrent that
80//! should disincentivize would-be attackers from trying to maliciously recover
81//! accounts.
82//!
83//! The recovery deposit can always be claimed by the account which is trying
84//! to be recovered. In the case of a malicious recovery attempt, the account
85//! owner who still has access to their account can claim the deposit and
86//! essentially punish the malicious user.
87//!
88//! Furthermore, the malicious recovery attempt can only be successful if the
89//! attacker is also able to get enough friends to vouch for the recovery attempt.
90//! In the case where the account owner prevents a malicious recovery process,
91//! this pallet makes it near-zero cost to re-configure the recovery settings and
92//! remove/replace friends who are acting inappropriately.
93//!
94//! ### Safety Considerations
95//!
96//! It is important to note that this is a powerful pallet that can compromise the
97//! security of an account if used incorrectly. Some recommended practices for users
98//! of this pallet are:
99//!
100//! * Configure a significant `delay_period` for your recovery process: As long as you have access
101//!   to your recoverable account, you need only check the blockchain once every `delay_period`
102//!   blocks to ensure that no recovery attempt is successful against your account. Using off-chain
103//!   notification systems can help with this, but ultimately, setting a large `delay_period` means
104//!   that even the most skilled attacker will need to wait this long before they can access your
105//!   account.
106//! * Use a high threshold of approvals: Setting a value of 1 for the threshold means that any of
107//!   your friends would be able to recover your account. They would simply need to start a recovery
108//!   process and approve their own process. Similarly, a threshold of 2 would mean that any 2
109//!   friends could work together to gain access to your account. The only way to prevent against
110//!   these kinds of attacks is to choose a high threshold of approvals and select from a diverse
111//!   friend group that would not be able to reasonably coordinate with one another.
112//! * Reset your configuration over time: Since the entire deposit of creating a recovery
113//!   configuration is returned to the user, the only cost of updating your recovery configuration
114//!   is the transaction fees for the calls. Thus, it is strongly encouraged to regularly update
115//!   your recovery configuration as your life changes and your relationship with new and existing
116//!   friends change as well.
117//!
118//! ## Interface
119//!
120//! ### Dispatchable Functions
121//!
122//! #### For General Users
123//!
124//! * `create_recovery` - Create a recovery configuration for your account and make it recoverable.
125//! * `initiate_recovery` - Start the recovery process for a recoverable account.
126//!
127//! #### For Friends of a Recoverable Account
128//! * `vouch_recovery` - As a `friend` of a recoverable account, vouch for a recovery attempt on the
129//!   account.
130//!
131//! #### For a User Who Successfully Recovered an Account
132//!
133//! * `claim_recovery` - Claim access to the account that you have successfully completed the
134//!   recovery process for.
135//! * `as_recovered` - Send a transaction as an account that you have recovered. See other functions
136//!   below.
137//!
138//! #### For the Recoverable Account
139//!
140//! * `close_recovery` - Close an active recovery process for your account and reclaim the recovery
141//!   deposit.
142//! * `remove_recovery` - Remove the recovery configuration from the account, making it
143//!   un-recoverable.
144//!
145//! #### For Super Users
146//!
147//! * `set_recovered` - The ROOT origin is able to skip the recovery process and directly allow one
148//!   account to access another.
149
150// Ensure we're `no_std` when compiling for Wasm.
151#![cfg_attr(not(feature = "std"), no_std)]
152
153extern crate alloc;
154
155use alloc::{boxed::Box, vec::Vec};
156
157use frame::{
158	prelude::*,
159	traits::{Currency, ReservableCurrency},
160};
161
162pub use pallet::*;
163pub use weights::WeightInfo;
164
165#[cfg(feature = "runtime-benchmarks")]
166mod benchmarking;
167
168#[cfg(test)]
169mod mock;
170#[cfg(test)]
171mod tests;
172pub mod weights;
173
174pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
175pub type BalanceOf<T> =
176	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
177pub type BlockNumberFromProviderOf<T> =
178	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
179pub type FriendsOf<T> =
180	BoundedVec<<T as frame_system::Config>::AccountId, <T as Config>::MaxFriends>;
181
182/// An active recovery process.
183#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo, MaxEncodedLen)]
184pub struct ActiveRecovery<BlockNumber, Balance, Friends> {
185	/// The block number when the recovery process started.
186	pub created: BlockNumber,
187	/// The amount held in reserve of the `depositor`,
188	/// to be returned once this recovery process is closed.
189	pub deposit: Balance,
190	/// The friends which have vouched so far. Always sorted.
191	pub friends: Friends,
192}
193
194/// Configuration for recovering an account.
195#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo, MaxEncodedLen)]
196pub struct RecoveryConfig<BlockNumber, Balance, Friends> {
197	/// The minimum number of blocks since the start of the recovery process before the account
198	/// can be recovered.
199	pub delay_period: BlockNumber,
200	/// The amount held in reserve of the `depositor`,
201	/// to be returned once this configuration is removed.
202	pub deposit: Balance,
203	/// The list of friends which can help recover an account. Always sorted.
204	pub friends: Friends,
205	/// The number of approving friends needed to recover an account.
206	pub threshold: u16,
207}
208
209/// The type of deposit
210#[derive(
211	Clone,
212	Eq,
213	PartialEq,
214	Encode,
215	Decode,
216	DebugNoBound,
217	TypeInfo,
218	MaxEncodedLen,
219	DecodeWithMemTracking,
220)]
221pub enum DepositKind<T: Config> {
222	/// Recovery configuration deposit
223	RecoveryConfig,
224	/// Active recovery deposit for an account
225	ActiveRecoveryFor(<T as frame_system::Config>::AccountId),
226}
227
228#[frame::pallet]
229pub mod pallet {
230	use super::*;
231
232	#[pallet::pallet]
233	pub struct Pallet<T>(_);
234
235	/// Configuration trait.
236	#[pallet::config]
237	pub trait Config: frame_system::Config {
238		/// The overarching event type.
239		#[allow(deprecated)]
240		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
241
242		/// Weight information for extrinsics in this pallet.
243		type WeightInfo: WeightInfo;
244
245		/// The overarching call type.
246		type RuntimeCall: Parameter
247			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
248			+ GetDispatchInfo
249			+ From<frame_system::Call<Self>>;
250
251		/// Query the current block number.
252		///
253		/// Must return monotonically increasing values when called from consecutive blocks.
254		/// Can be configured to return either:
255		/// - the local block number of the runtime via `frame_system::Pallet`
256		/// - a remote block number, eg from the relay chain through `RelaychainDataProvider`
257		/// - an arbitrary value through a custom implementation of the trait
258		///
259		/// There is currently no migration provided to "hot-swap" block number providers and it may
260		/// result in undefined behavior when doing so. Parachains are therefore best off setting
261		/// this to their local block number provider if they have the pallet already deployed.
262		///
263		/// Suggested values:
264		/// - Solo- and Relay-chains: `frame_system::Pallet`
265		/// - Parachains that may produce blocks sparingly or only when needed (on-demand):
266		///   - already have the pallet deployed: `frame_system::Pallet`
267		///   - are freshly deploying this pallet: `RelaychainDataProvider`
268		/// - Parachains with a reliably block production rate (PLO or bulk-coretime):
269		///   - already have the pallet deployed: `frame_system::Pallet`
270		///   - are freshly deploying this pallet: no strong recommendation. Both local and remote
271		///     providers can be used. Relay provider can be a bit better in cases where the
272		///     parachain is lagging its block production to avoid clock skew.
273		type BlockNumberProvider: BlockNumberProvider;
274
275		/// The currency mechanism.
276		type Currency: ReservableCurrency<Self::AccountId>;
277
278		/// The base amount of currency needed to reserve for creating a recovery configuration.
279		///
280		/// This is held for an additional storage item whose value size is
281		/// `2 + sizeof(BlockNumber, Balance)` bytes.
282		#[pallet::constant]
283		type ConfigDepositBase: Get<BalanceOf<Self>>;
284
285		/// The amount of currency needed per additional user when creating a recovery
286		/// configuration.
287		///
288		/// This is held for adding `sizeof(AccountId)` bytes more into a pre-existing storage
289		/// value.
290		#[pallet::constant]
291		type FriendDepositFactor: Get<BalanceOf<Self>>;
292
293		/// The maximum amount of friends allowed in a recovery configuration.
294		///
295		/// NOTE: The threshold programmed in this Pallet uses u16, so it does
296		/// not really make sense to have a limit here greater than u16::MAX.
297		/// But also, that is a lot more than you should probably set this value
298		/// to anyway...
299		#[pallet::constant]
300		type MaxFriends: Get<u32>;
301
302		/// The base amount of currency needed to reserve for starting a recovery.
303		///
304		/// This is primarily held for deterring malicious recovery attempts, and should
305		/// have a value large enough that a bad actor would choose not to place this
306		/// deposit. It also acts to fund additional storage item whose value size is
307		/// `sizeof(BlockNumber, Balance + T * AccountId)` bytes. Where T is a configurable
308		/// threshold.
309		#[pallet::constant]
310		type RecoveryDeposit: Get<BalanceOf<Self>>;
311	}
312
313	/// Events type.
314	#[pallet::event]
315	#[pallet::generate_deposit(pub(super) fn deposit_event)]
316	pub enum Event<T: Config> {
317		/// A recovery process has been set up for an account.
318		RecoveryCreated { account: T::AccountId },
319		/// A recovery process has been initiated for lost account by rescuer account.
320		RecoveryInitiated { lost_account: T::AccountId, rescuer_account: T::AccountId },
321		/// A recovery process for lost account by rescuer account has been vouched for by sender.
322		RecoveryVouched {
323			lost_account: T::AccountId,
324			rescuer_account: T::AccountId,
325			sender: T::AccountId,
326		},
327		/// A recovery process for lost account by rescuer account has been closed.
328		RecoveryClosed { lost_account: T::AccountId, rescuer_account: T::AccountId },
329		/// Lost account has been successfully recovered by rescuer account.
330		AccountRecovered { lost_account: T::AccountId, rescuer_account: T::AccountId },
331		/// A recovery process has been removed for an account.
332		RecoveryRemoved { lost_account: T::AccountId },
333		/// A deposit has been updated.
334		DepositPoked {
335			who: T::AccountId,
336			kind: DepositKind<T>,
337			old_deposit: BalanceOf<T>,
338			new_deposit: BalanceOf<T>,
339		},
340	}
341
342	#[pallet::error]
343	pub enum Error<T> {
344		/// User is not allowed to make a call on behalf of this account
345		NotAllowed,
346		/// Threshold must be greater than zero
347		ZeroThreshold,
348		/// Friends list must be greater than zero and threshold
349		NotEnoughFriends,
350		/// Friends list must be less than max friends
351		MaxFriends,
352		/// Friends list must be sorted and free of duplicates
353		NotSorted,
354		/// This account is not set up for recovery
355		NotRecoverable,
356		/// This account is already set up for recovery
357		AlreadyRecoverable,
358		/// A recovery process has already started for this account
359		AlreadyStarted,
360		/// A recovery process has not started for this rescuer
361		NotStarted,
362		/// This account is not a friend who can vouch
363		NotFriend,
364		/// The friend must wait until the delay period to vouch for this recovery
365		DelayPeriod,
366		/// This user has already vouched for this recovery
367		AlreadyVouched,
368		/// The threshold for recovering this account has not been met
369		Threshold,
370		/// There are still active recovery attempts that need to be closed
371		StillActive,
372		/// This account is already set up for recovery
373		AlreadyProxy,
374		/// Some internal state is broken.
375		BadState,
376	}
377
378	/// The set of recoverable accounts and their recovery configuration.
379	#[pallet::storage]
380	#[pallet::getter(fn recovery_config)]
381	pub type Recoverable<T: Config> = StorageMap<
382		_,
383		Twox64Concat,
384		T::AccountId,
385		RecoveryConfig<BlockNumberFromProviderOf<T>, BalanceOf<T>, FriendsOf<T>>,
386	>;
387
388	/// Active recovery attempts.
389	///
390	/// First account is the account to be recovered, and the second account
391	/// is the user trying to recover the account.
392	#[pallet::storage]
393	#[pallet::getter(fn active_recovery)]
394	pub type ActiveRecoveries<T: Config> = StorageDoubleMap<
395		_,
396		Twox64Concat,
397		T::AccountId,
398		Twox64Concat,
399		T::AccountId,
400		ActiveRecovery<BlockNumberFromProviderOf<T>, BalanceOf<T>, FriendsOf<T>>,
401	>;
402
403	/// The list of allowed proxy accounts.
404	///
405	/// Map from the user who can access it to the recovered account.
406	#[pallet::storage]
407	#[pallet::getter(fn proxy)]
408	pub type Proxy<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>;
409
410	#[pallet::call]
411	impl<T: Config> Pallet<T> {
412		/// Send a call through a recovered account.
413		///
414		/// The dispatch origin for this call must be _Signed_ and registered to
415		/// be able to make calls on behalf of the recovered account.
416		///
417		/// Parameters:
418		/// - `account`: The recovered account you want to make a call on-behalf-of.
419		/// - `call`: The call you want to make with the recovered account.
420		#[pallet::call_index(0)]
421		#[pallet::weight({
422			let dispatch_info = call.get_dispatch_info();
423			(
424				T::WeightInfo::as_recovered().saturating_add(dispatch_info.call_weight),
425				dispatch_info.class,
426			)})]
427		pub fn as_recovered(
428			origin: OriginFor<T>,
429			account: AccountIdLookupOf<T>,
430			call: Box<<T as Config>::RuntimeCall>,
431		) -> DispatchResult {
432			let who = ensure_signed(origin)?;
433			let account = T::Lookup::lookup(account)?;
434			// Check `who` is allowed to make a call on behalf of `account`
435			let target = Self::proxy(&who).ok_or(Error::<T>::NotAllowed)?;
436			ensure!(target == account, Error::<T>::NotAllowed);
437			call.dispatch(frame_system::RawOrigin::Signed(account).into())
438				.map(|_| ())
439				.map_err(|e| e.error)
440		}
441
442		/// Allow ROOT to bypass the recovery process and set a rescuer account
443		/// for a lost account directly.
444		///
445		/// The dispatch origin for this call must be _ROOT_.
446		///
447		/// Parameters:
448		/// - `lost`: The "lost account" to be recovered.
449		/// - `rescuer`: The "rescuer account" which can call as the lost account.
450		#[pallet::call_index(1)]
451		#[pallet::weight(T::WeightInfo::set_recovered())]
452		pub fn set_recovered(
453			origin: OriginFor<T>,
454			lost: AccountIdLookupOf<T>,
455			rescuer: AccountIdLookupOf<T>,
456		) -> DispatchResult {
457			ensure_root(origin)?;
458			let lost = T::Lookup::lookup(lost)?;
459			let rescuer = T::Lookup::lookup(rescuer)?;
460			// Create the recovery storage item.
461			<Proxy<T>>::insert(&rescuer, &lost);
462			Self::deposit_event(Event::<T>::AccountRecovered {
463				lost_account: lost,
464				rescuer_account: rescuer,
465			});
466			Ok(())
467		}
468
469		/// Create a recovery configuration for your account. This makes your account recoverable.
470		///
471		/// Payment: `ConfigDepositBase` + `FriendDepositFactor` * #_of_friends balance
472		/// will be reserved for storing the recovery configuration. This deposit is returned
473		/// in full when the user calls `remove_recovery`.
474		///
475		/// The dispatch origin for this call must be _Signed_.
476		///
477		/// Parameters:
478		/// - `friends`: A list of friends you trust to vouch for recovery attempts. Should be
479		///   ordered and contain no duplicate values.
480		/// - `threshold`: The number of friends that must vouch for a recovery attempt before the
481		///   account can be recovered. Should be less than or equal to the length of the list of
482		///   friends.
483		/// - `delay_period`: The number of blocks after a recovery attempt is initialized that
484		///   needs to pass before the account can be recovered.
485		#[pallet::call_index(2)]
486		#[pallet::weight(T::WeightInfo::create_recovery(friends.len() as u32))]
487		pub fn create_recovery(
488			origin: OriginFor<T>,
489			friends: Vec<T::AccountId>,
490			threshold: u16,
491			delay_period: BlockNumberFromProviderOf<T>,
492		) -> DispatchResult {
493			let who = ensure_signed(origin)?;
494			// Check account is not already set up for recovery
495			ensure!(!<Recoverable<T>>::contains_key(&who), Error::<T>::AlreadyRecoverable);
496			// Check user input is valid
497			ensure!(threshold >= 1, Error::<T>::ZeroThreshold);
498			ensure!(!friends.is_empty(), Error::<T>::NotEnoughFriends);
499			ensure!(threshold as usize <= friends.len(), Error::<T>::NotEnoughFriends);
500			let bounded_friends: FriendsOf<T> =
501				friends.try_into().map_err(|_| Error::<T>::MaxFriends)?;
502			ensure!(Self::is_sorted_and_unique(&bounded_friends), Error::<T>::NotSorted);
503			// Calculate total deposit required
504			let total_deposit = Self::get_recovery_config_deposit(bounded_friends.len())?;
505			// Reserve the deposit
506			T::Currency::reserve(&who, total_deposit)?;
507			// Create the recovery configuration
508			let recovery_config = RecoveryConfig {
509				delay_period,
510				deposit: total_deposit,
511				friends: bounded_friends,
512				threshold,
513			};
514			// Create the recovery configuration storage item
515			<Recoverable<T>>::insert(&who, recovery_config);
516
517			Self::deposit_event(Event::<T>::RecoveryCreated { account: who });
518			Ok(())
519		}
520
521		/// Initiate the process for recovering a recoverable account.
522		///
523		/// Payment: `RecoveryDeposit` balance will be reserved for initiating the
524		/// recovery process. This deposit will always be repatriated to the account
525		/// trying to be recovered. See `close_recovery`.
526		///
527		/// The dispatch origin for this call must be _Signed_.
528		///
529		/// Parameters:
530		/// - `account`: The lost account that you want to recover. This account needs to be
531		///   recoverable (i.e. have a recovery configuration).
532		#[pallet::call_index(3)]
533		#[pallet::weight(T::WeightInfo::initiate_recovery())]
534		pub fn initiate_recovery(
535			origin: OriginFor<T>,
536			account: AccountIdLookupOf<T>,
537		) -> DispatchResult {
538			let who = ensure_signed(origin)?;
539			let account = T::Lookup::lookup(account)?;
540			// Check that the account is recoverable
541			ensure!(<Recoverable<T>>::contains_key(&account), Error::<T>::NotRecoverable);
542			// Check that the recovery process has not already been started
543			ensure!(
544				!<ActiveRecoveries<T>>::contains_key(&account, &who),
545				Error::<T>::AlreadyStarted
546			);
547			// Take recovery deposit
548			let recovery_deposit = T::RecoveryDeposit::get();
549			T::Currency::reserve(&who, recovery_deposit)?;
550			// Create an active recovery status
551			let recovery_status = ActiveRecovery {
552				created: T::BlockNumberProvider::current_block_number(),
553				deposit: recovery_deposit,
554				friends: Default::default(),
555			};
556			// Create the active recovery storage item
557			<ActiveRecoveries<T>>::insert(&account, &who, recovery_status);
558			Self::deposit_event(Event::<T>::RecoveryInitiated {
559				lost_account: account,
560				rescuer_account: who,
561			});
562			Ok(())
563		}
564
565		/// Allow a "friend" of a recoverable account to vouch for an active recovery
566		/// process for that account.
567		///
568		/// The dispatch origin for this call must be _Signed_ and must be a "friend"
569		/// for the recoverable account.
570		///
571		/// Parameters:
572		/// - `lost`: The lost account that you want to recover.
573		/// - `rescuer`: The account trying to rescue the lost account that you want to vouch for.
574		///
575		/// The combination of these two parameters must point to an active recovery
576		/// process.
577		#[pallet::call_index(4)]
578		#[pallet::weight(T::WeightInfo::vouch_recovery(T::MaxFriends::get()))]
579		pub fn vouch_recovery(
580			origin: OriginFor<T>,
581			lost: AccountIdLookupOf<T>,
582			rescuer: AccountIdLookupOf<T>,
583		) -> DispatchResult {
584			let who = ensure_signed(origin)?;
585			let lost = T::Lookup::lookup(lost)?;
586			let rescuer = T::Lookup::lookup(rescuer)?;
587			// Get the recovery configuration for the lost account.
588			let recovery_config = Self::recovery_config(&lost).ok_or(Error::<T>::NotRecoverable)?;
589			// Get the active recovery process for the rescuer.
590			let mut active_recovery =
591				Self::active_recovery(&lost, &rescuer).ok_or(Error::<T>::NotStarted)?;
592			// Make sure the voter is a friend
593			ensure!(Self::is_friend(&recovery_config.friends, &who), Error::<T>::NotFriend);
594			// Either insert the vouch, or return an error that the user already vouched.
595			match active_recovery.friends.binary_search(&who) {
596				Ok(_pos) => return Err(Error::<T>::AlreadyVouched.into()),
597				Err(pos) => active_recovery
598					.friends
599					.try_insert(pos, who.clone())
600					.map_err(|_| Error::<T>::MaxFriends)?,
601			}
602			// Update storage with the latest details
603			<ActiveRecoveries<T>>::insert(&lost, &rescuer, active_recovery);
604			Self::deposit_event(Event::<T>::RecoveryVouched {
605				lost_account: lost,
606				rescuer_account: rescuer,
607				sender: who,
608			});
609			Ok(())
610		}
611
612		/// Allow a successful rescuer to claim their recovered account.
613		///
614		/// The dispatch origin for this call must be _Signed_ and must be a "rescuer"
615		/// who has successfully completed the account recovery process: collected
616		/// `threshold` or more vouches, waited `delay_period` blocks since initiation.
617		///
618		/// Parameters:
619		/// - `account`: The lost account that you want to claim has been successfully recovered by
620		///   you.
621		#[pallet::call_index(5)]
622		#[pallet::weight(T::WeightInfo::claim_recovery(T::MaxFriends::get()))]
623		pub fn claim_recovery(
624			origin: OriginFor<T>,
625			account: AccountIdLookupOf<T>,
626		) -> DispatchResult {
627			let who = ensure_signed(origin)?;
628			let account = T::Lookup::lookup(account)?;
629			// Get the recovery configuration for the lost account
630			let recovery_config =
631				Self::recovery_config(&account).ok_or(Error::<T>::NotRecoverable)?;
632			// Get the active recovery process for the rescuer
633			let active_recovery =
634				Self::active_recovery(&account, &who).ok_or(Error::<T>::NotStarted)?;
635			ensure!(!Proxy::<T>::contains_key(&who), Error::<T>::AlreadyProxy);
636			// Make sure the delay period has passed
637			let current_block_number = T::BlockNumberProvider::current_block_number();
638			let recoverable_block_number = active_recovery
639				.created
640				.checked_add(&recovery_config.delay_period)
641				.ok_or(ArithmeticError::Overflow)?;
642			ensure!(recoverable_block_number <= current_block_number, Error::<T>::DelayPeriod);
643			// Make sure the threshold is met
644			ensure!(
645				recovery_config.threshold as usize <= active_recovery.friends.len(),
646				Error::<T>::Threshold
647			);
648			frame_system::Pallet::<T>::inc_consumers(&who).map_err(|_| Error::<T>::BadState)?;
649			// Create the recovery storage item
650			Proxy::<T>::insert(&who, &account);
651			Self::deposit_event(Event::<T>::AccountRecovered {
652				lost_account: account,
653				rescuer_account: who,
654			});
655			Ok(())
656		}
657
658		/// As the controller of a recoverable account, close an active recovery
659		/// process for your account.
660		///
661		/// Payment: By calling this function, the recoverable account will receive
662		/// the recovery deposit `RecoveryDeposit` placed by the rescuer.
663		///
664		/// The dispatch origin for this call must be _Signed_ and must be a
665		/// recoverable account with an active recovery process for it.
666		///
667		/// Parameters:
668		/// - `rescuer`: The account trying to rescue this recoverable account.
669		#[pallet::call_index(6)]
670		#[pallet::weight(T::WeightInfo::close_recovery(T::MaxFriends::get()))]
671		pub fn close_recovery(
672			origin: OriginFor<T>,
673			rescuer: AccountIdLookupOf<T>,
674		) -> DispatchResult {
675			let who = ensure_signed(origin)?;
676			let rescuer = T::Lookup::lookup(rescuer)?;
677			// Take the active recovery process started by the rescuer for this account.
678			let active_recovery =
679				<ActiveRecoveries<T>>::take(&who, &rescuer).ok_or(Error::<T>::NotStarted)?;
680			// Move the reserved funds from the rescuer to the rescued account.
681			// Acts like a slashing mechanism for those who try to maliciously recover accounts.
682			let res = T::Currency::repatriate_reserved(
683				&rescuer,
684				&who,
685				active_recovery.deposit,
686				BalanceStatus::Free,
687			);
688			debug_assert!(res.is_ok());
689			Self::deposit_event(Event::<T>::RecoveryClosed {
690				lost_account: who,
691				rescuer_account: rescuer,
692			});
693			Ok(())
694		}
695
696		/// Remove the recovery process for your account. Recovered accounts are still accessible.
697		///
698		/// NOTE: The user must make sure to call `close_recovery` on all active
699		/// recovery attempts before calling this function else it will fail.
700		///
701		/// Payment: By calling this function the recoverable account will unreserve
702		/// their recovery configuration deposit.
703		/// (`ConfigDepositBase` + `FriendDepositFactor` * #_of_friends)
704		///
705		/// The dispatch origin for this call must be _Signed_ and must be a
706		/// recoverable account (i.e. has a recovery configuration).
707		#[pallet::call_index(7)]
708		#[pallet::weight(T::WeightInfo::remove_recovery(T::MaxFriends::get()))]
709		pub fn remove_recovery(origin: OriginFor<T>) -> DispatchResult {
710			let who = ensure_signed(origin)?;
711			// Check there are no active recoveries
712			let mut active_recoveries = <ActiveRecoveries<T>>::iter_prefix_values(&who);
713			ensure!(active_recoveries.next().is_none(), Error::<T>::StillActive);
714			// Take the recovery configuration for this account.
715			let recovery_config = <Recoverable<T>>::take(&who).ok_or(Error::<T>::NotRecoverable)?;
716
717			// Unreserve the initial deposit for the recovery configuration.
718			T::Currency::unreserve(&who, recovery_config.deposit);
719			Self::deposit_event(Event::<T>::RecoveryRemoved { lost_account: who });
720			Ok(())
721		}
722
723		/// Cancel the ability to use `as_recovered` for `account`.
724		///
725		/// The dispatch origin for this call must be _Signed_ and registered to
726		/// be able to make calls on behalf of the recovered account.
727		///
728		/// Parameters:
729		/// - `account`: The recovered account you are able to call on-behalf-of.
730		#[pallet::call_index(8)]
731		#[pallet::weight(T::WeightInfo::cancel_recovered())]
732		pub fn cancel_recovered(
733			origin: OriginFor<T>,
734			account: AccountIdLookupOf<T>,
735		) -> DispatchResult {
736			let who = ensure_signed(origin)?;
737			let account = T::Lookup::lookup(account)?;
738			// Check `who` is allowed to make a call on behalf of `account`
739			ensure!(Self::proxy(&who) == Some(account), Error::<T>::NotAllowed);
740			Proxy::<T>::remove(&who);
741
742			frame_system::Pallet::<T>::dec_consumers(&who);
743			Ok(())
744		}
745
746		/// Poke deposits for recovery configurations and / or active recoveries.
747		///
748		/// This can be used by accounts to possibly lower their locked amount.
749		///
750		/// The dispatch origin for this call must be _Signed_.
751		///
752		/// Parameters:
753		/// - `maybe_account`: Optional recoverable account for which you have an active recovery
754		/// and want to adjust the deposit for the active recovery.
755		///
756		/// This function checks both recovery configuration deposit and active recovery deposits
757		/// of the caller:
758		/// - If the caller has created a recovery configuration, checks and adjusts its deposit
759		/// - If the caller has initiated any active recoveries, and provides the account in
760		/// `maybe_account`, checks and adjusts those deposits
761		///
762		/// If any deposit is updated, the difference will be reserved/unreserved from the caller's
763		/// account.
764		///
765		/// The transaction is made free if any deposit is updated and paid otherwise.
766		///
767		/// Emits `DepositPoked` if any deposit is updated.
768		/// Multiple events may be emitted in case both types of deposits are updated.
769		#[pallet::call_index(9)]
770		#[pallet::weight(T::WeightInfo::poke_deposit(T::MaxFriends::get()))]
771		pub fn poke_deposit(
772			origin: OriginFor<T>,
773			maybe_account: Option<AccountIdLookupOf<T>>,
774		) -> DispatchResultWithPostInfo {
775			let who = ensure_signed(origin)?;
776			let mut deposit_updated = false;
777
778			// Check and update recovery config deposit
779			deposit_updated |= Self::poke_recovery_config_deposit(&who)?;
780
781			// Check and update active recovery deposit
782			if let Some(lost_account) = maybe_account {
783				let lost_account = T::Lookup::lookup(lost_account)?;
784				deposit_updated |= Self::poke_active_recovery_deposit(&who, &lost_account)?;
785			}
786
787			Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
788		}
789	}
790}
791
792impl<T: Config> Pallet<T> {
793	/// Check that friends list is sorted and has no duplicates.
794	fn is_sorted_and_unique(friends: &Vec<T::AccountId>) -> bool {
795		friends.windows(2).all(|w| w[0] < w[1])
796	}
797
798	/// Check that a user is a friend in the friends list.
799	fn is_friend(friends: &Vec<T::AccountId>, friend: &T::AccountId) -> bool {
800		friends.binary_search(&friend).is_ok()
801	}
802
803	/// Helper function to calculate recovery config deposit
804	/// Total deposit is base fee + number of friends * factor fee
805	fn get_recovery_config_deposit(friends_count: usize) -> Result<BalanceOf<T>, DispatchError> {
806		let friend_deposit = T::FriendDepositFactor::get()
807			.checked_mul(&friends_count.saturated_into())
808			.ok_or(ArithmeticError::Overflow)?;
809		T::ConfigDepositBase::get()
810			.checked_add(&friend_deposit)
811			.ok_or(ArithmeticError::Overflow.into())
812	}
813
814	/// Helper function to poke the deposit reserved for creating a recovery config
815	fn poke_recovery_config_deposit(who: &T::AccountId) -> Result<bool, DispatchError> {
816		<Recoverable<T>>::try_mutate(&who, |maybe_config| -> Result<bool, DispatchError> {
817			let Some(config) = maybe_config.as_mut() else { return Ok(false) };
818			let old_deposit = config.deposit;
819			let new_deposit = Self::get_recovery_config_deposit(config.friends.len())?;
820
821			if old_deposit == new_deposit {
822				return Ok(false);
823			}
824
825			if new_deposit > old_deposit {
826				let extra = new_deposit.saturating_sub(old_deposit);
827				T::Currency::reserve(&who, extra)?;
828			} else {
829				let excess = old_deposit.saturating_sub(new_deposit);
830				let remaining_unreserved = T::Currency::unreserve(&who, excess);
831				if !remaining_unreserved.is_zero() {
832					defensive!(
833						"Failed to unreserve full amount. (Requested, Actual)",
834						(excess, excess.saturating_sub(remaining_unreserved))
835					);
836				}
837			}
838			config.deposit = new_deposit;
839
840			Self::deposit_event(Event::<T>::DepositPoked {
841				who: who.clone(),
842				kind: DepositKind::RecoveryConfig,
843				old_deposit,
844				new_deposit,
845			});
846			Ok(true)
847		})
848	}
849
850	/// Helper function to poke the deposit reserved for an active recovery
851	fn poke_active_recovery_deposit(
852		who: &T::AccountId,
853		lost_account: &T::AccountId,
854	) -> Result<bool, DispatchError> {
855		let new_deposit = T::RecoveryDeposit::get();
856		<ActiveRecoveries<T>>::try_mutate(
857			lost_account,
858			who,
859			|maybe_recovery| -> Result<bool, DispatchError> {
860				let recovery = maybe_recovery.as_mut().ok_or(Error::<T>::NotStarted)?;
861				let old_deposit = recovery.deposit;
862
863				// Skip if deposit hasn't changed
864				if recovery.deposit == new_deposit {
865					return Ok(false);
866				}
867
868				// Update deposit
869				if new_deposit > old_deposit {
870					let extra = new_deposit.saturating_sub(old_deposit);
871					T::Currency::reserve(who, extra)?;
872				} else {
873					let excess = old_deposit.saturating_sub(new_deposit);
874					let remaining_unreserved = T::Currency::unreserve(who, excess);
875					if !remaining_unreserved.is_zero() {
876						defensive!(
877							"Failed to unreserve full amount. (Requested, Actual)",
878							(excess, excess.saturating_sub(remaining_unreserved))
879						);
880					}
881				}
882				recovery.deposit = new_deposit;
883
884				Self::deposit_event(Event::<T>::DepositPoked {
885					who: who.clone(),
886					kind: DepositKind::ActiveRecoveryFor(lost_account.clone()),
887					old_deposit,
888					new_deposit,
889				});
890				Ok(true)
891			},
892		)
893	}
894}