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}