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//! Pallet Recovery allows you to have friends or family recover access to your account if you lose
21//! your seed phrase or private key.
22//!
23//! ## Terminology
24//!
25//! - `lost`: An account that has lost access to its private key and needs to be recovered.
26//! - `friend`: A befriended account that can approve a recovery process.
27//! - `initiator`: An account that initiated a recovery attempt.
28//! - `recovered`: An account that has been successfully recovered.
29//! - `inheritor`: An account that is inheriting access to a lost account after recovery.
30//! - `attempt`: An attempt to recover a lost account by an initiator.
31//! - `priority`: The priority of a friend group in inheritance conflicts. See
32//!   [`InheritancePriority`].
33//! - `deposit`: An amount of currency that needs to be held for allocating on-chain storage.
34//! - `friends_needed`: The number of friends that need to approve an attempt.
35//! - `inheritance delay`: How long an attempt will be delayed before it can succeed.
36//! - `provided block`: The blocks that are *provided* by the `T::BlockNumberProvider`.
37//!
38//! ## Scenario: Recovering a lost account
39//!
40//! Story of how the user Alice loses access and is recovered by her friends.
41//!
42//! 1. Alice uses the recovery pallet to configure one or more friends groups:
43//!   - Alice picks a suitable `inheritor` account that will inherit the access to her account for
44//!     each friend group. This could be a multisig.
45//!   - Alice configures all groups via `set_friend_groups`.
46//! 2. Alice loses access to her account and becomes a `lost` account.
47//! 3. Any member (aka `initiator`) of Alice's friend groups become aware of the situation and
48//!    starts a recovery `attempt` via `initiate_attempt`.
49//! 4. The friend group self-organizes and one-by-one approve the ongoing attempt via
50//!    `approve_attempt`.
51//! 5. Exactly `friends_needed` friends approve the attempt (further approvals will fail since they
52//!    are useless).
53//! 6. Any account finishes the attempt via `finish_attempt` after at least *inheritance delay*
54//!    blocks since the initiation have passed.
55//! 7. Alice's account is now officially `recovered` and accessible by the `inheritor` account.
56//! 8. The `inheritor` may call `control_inherited_account` at any point to transfer Alice's funds
57//!    to her new account.
58//!
59//! ## Scenario: Multiple friend groups try to recover an account
60//!
61//! Alice may have configured multiple friend groups that all try to recover her account at the same
62//! time. This can lead to a conflict of which friend group should eventually inherit the access.
63//!
64//! 1. Alice configures groups *Family* (delay 10d, priority 0) and *Friends* (delay 20d, priority
65//!    1). Since numerical lower values denote higher priority, *Family* therefore has higher
66//!    priority than *Friends*.
67//! 1. Day 0: Alice loses access to her account.
68//! 1. Day 6: *Friends* initiate a recovery attempt for Alice.
69//! 1. Day 15: *Family* finally understands Polkadot and initiates an attempt as well.
70//! 1. Day 25: *Family* inherits access to Alice account.
71//! 1. Day 26: *Friends* group gets nothing since they have lower priority than *Family*.
72//!
73//! In the case above you see how the *Friends* group is now unable to recover Alice account since
74//! the *Family* group already did it and has higher priority.
75//! Now, imagine the case that the *Friends* group would have started on day 4 and would have
76//! already recovered the account on day 24. Two days later, the *Family* group can take access back
77//! and will replace the inheritor account with their own. The *Friends* group had access for two
78//! days since they were faster.
79//! If Alice account has most balance locked in 28 day staking this would not make a big difference,
80//! since only the free balance would be immediately transferable.
81//!
82//! After a recovery attempt was completed, lower-priority friend groups cannot open a new attempt
83//! to recover the account.
84//!
85//! ## Data Structures
86//!
87//! The pallet has three storage items, see the in-code docs [`FriendGroups`], [`Attempt`] and
88//! [`Inheritor`]. Storage items may contain deposit "tickets" or similar noise and should therefore
89//! not be read directly but only through the API.
90//!
91//! ## API
92//!
93//! *Reading* data can be done through the view functions:
94//!
95//! - `provided_block_number`: The block number that will be used to measure time.
96//! - `friend_groups`: The friend groups of an account that can initiate recovery attempts.
97//! - `attempts`: Ongoing recovery attempts for a lost account.
98//! - `inheritor`: The account that inherited full access to the lost account.
99//! - `inheritance`: All the recovered accounts that an account inherited access to.
100
101#![recursion_limit = "1024"]
102#![cfg_attr(not(feature = "std"), no_std)]
103extern crate alloc;
104use alloc::{boxed::Box, vec, vec::Vec};
105
106#[cfg(any(feature = "try-runtime", test))]
107use frame::traits::fungible::hold::Inspect as _;
108use frame::{
109	prelude::*,
110	traits::{
111		fungible::{hold::Balanced, Credit, Inspect, MutateHold},
112		Consideration, Footprint, OnUnbalanced, OriginTrait,
113	},
114};
115use types::{Bitfield, IdentifiedConsideration};
116
117pub use pallet::*;
118pub use weights::WeightInfo;
119
120#[cfg(feature = "runtime-benchmarks")]
121mod benchmarking;
122pub mod migrations;
123#[cfg(test)]
124mod mock;
125#[cfg(test)]
126mod tests;
127pub mod types;
128pub mod weights;
129
130/// Maximum number of friend groups that an account can have.
131pub const MAX_GROUPS_PER_ACCOUNT: u32 = 10;
132
133pub type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
134pub type BalanceOf<T> = <<T as Config>::Currency as Inspect<AccountIdFor<T>>>::Balance;
135pub type CreditOf<T> = Credit<AccountIdFor<T>, <T as Config>::Currency>;
136/// The block number type that will be used to measure time.
137pub type ProvidedBlockNumberOf<T> =
138	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
139
140/// Friends of a friend group.
141pub type FriendsOf<T> =
142	BoundedVec<<T as frame_system::Config>::AccountId, <T as Config>::MaxFriendsPerConfig>;
143pub type HashOf<T> = <T as frame_system::Config>::Hash;
144
145/// Group of friends that can initiate a recovery attempt for a specific lost account.
146#[derive(
147	Clone,
148	Eq,
149	PartialEq,
150	Encode,
151	Decode,
152	Default,
153	Debug,
154	TypeInfo,
155	MaxEncodedLen,
156	DecodeWithMemTracking,
157)]
158pub struct FriendGroup<ProvidedBlockNumber, AccountId, Friends> {
159	/// List of friends that can initiate the recovery process. Always sorted.
160	pub friends: Friends,
161
162	/// The number of approving friends needed to recover an account.
163	pub friends_needed: u32,
164
165	/// The account that will inherit full access to the lost account upon successful recovery.
166	pub inheritor: AccountId,
167
168	/// Minimum time that a recovery attempt must stay active before it can be finished.
169	///
170	/// Uses a provided block number to avoid possible clock skew of parachains.
171	pub inheritance_delay: ProvidedBlockNumber,
172
173	/// Used to resolve inheritance conflicts when multiple friend groups finish a recovery.
174	///
175	/// Higher-priority friend groups can replace the inheritor of a lower-priority group. For
176	/// example: you can set your family group as priority 0, your friends group as priority 1 and
177	/// co-workers as priority 2. This in combination with the `inheritance_delay` enables you to
178	/// ensure that the correct group receives the inheritance. See [`InheritancePriority`] for the
179	/// numeric convention.
180	pub inheritance_priority: InheritancePriority,
181
182	/// The delay since the last approval of an attempt before the attempt can be canceled.
183	///
184	/// It ensures that a malicious recoverer does not abuse the `cancel_attempt` call to dodge an
185	/// incoming slash from the lost account. They could otherwise monitor the TX pool and cancel
186	/// the attempt just in time for the slash transaction to fail. Now instead, the lost account
187	/// has at least `cancel_delay` provided blocks to slash the attempt.
188	pub cancel_delay: ProvidedBlockNumber,
189}
190
191/// Index of a friend group of a lost account.
192pub type FriendGroupIndex = u32;
193
194/// Priority of a friend group in account inheritance conflicts.
195///
196/// Lower numerical values denote higher priority (so `0` is the strongest priority).
197pub type InheritancePriority = u32;
198
199/// A `FriendGroup` for a specific `Config`.
200pub type FriendGroupOf<T> = FriendGroup<ProvidedBlockNumberOf<T>, AccountIdFor<T>, FriendsOf<T>>;
201
202/// Collection of friend groups of a lost account.
203pub type FriendGroupsOf<T> = BoundedVec<FriendGroupOf<T>, ConstU32<MAX_GROUPS_PER_ACCOUNT>>;
204
205/// Approval bitfield for a specific number of friends.
206pub type ApprovalBitfield<MaxFriends> = Bitfield<MaxFriends>;
207
208/// Bitfield to track approval per friend in a friend group.
209pub type ApprovalBitfieldOf<T> = ApprovalBitfield<<T as Config>::MaxFriendsPerConfig>;
210
211/// An attempt to recover an account.
212#[derive(
213	Clone,
214	Eq,
215	PartialEq,
216	Encode,
217	Decode,
218	Default,
219	Debug,
220	TypeInfo,
221	MaxEncodedLen,
222	DecodeWithMemTracking,
223)]
224pub struct Attempt<ProvidedBlockNumber, ApprovalBitfield, AccountId> {
225	/// Index of the friend group that initiated the attempt.
226	///
227	/// This will never be more than `MAX_GROUPS_PER_ACCOUNT`.
228	pub friend_group_index: FriendGroupIndex,
229
230	/// The account that initiated the attempt.
231	pub initiator: AccountId,
232
233	/// The block number when the attempt was initiated.
234	///
235	/// Note that this can be a foreign (ie Relay) block number.
236	pub init_block: ProvidedBlockNumber,
237
238	/// The block number when the last friend approved the attempt.
239	///
240	/// Note that this can be a foreign (ie Relay) block number.
241	pub last_approval_block: ProvidedBlockNumber,
242
243	/// Bitfield tracking which friends approved.
244	///
245	/// Each bit corresponds to a friend in the `friend_group.friends` that has approved the
246	/// attempt.
247	pub approvals: ApprovalBitfield,
248}
249
250/// Attempt to recover an account.
251pub type AttemptOf<T> = Attempt<ProvidedBlockNumberOf<T>, ApprovalBitfieldOf<T>, AccountIdFor<T>>;
252
253/// Ticket for an attempt to recover an account.
254pub type AttemptTicketOf<T> =
255	IdentifiedConsideration<AccountIdFor<T>, Footprint, <T as Config>::AttemptConsideration>;
256
257/// Ticket for the inheritor of an account.
258pub type InheritorTicketOf<T> =
259	IdentifiedConsideration<AccountIdFor<T>, Footprint, <T as Config>::InheritorConsideration>;
260
261/// Amount of a security deposit - as opposed to a storage deposit.
262pub type SecurityDepositOf<T> = BalanceOf<T>;
263
264#[frame::pallet]
265pub mod pallet {
266	use super::*;
267
268	#[pallet::pallet]
269	#[pallet::storage_version(migrations::STORAGE_VERSION)]
270	pub struct Pallet<T>(_);
271
272	#[pallet::config]
273	pub trait Config: frame_system::Config {
274		/// The overarching call type.
275		type RuntimeCall: Parameter
276			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
277			+ GetDispatchInfo
278			+ From<frame_system::Call<Self>>
279			+ IsSubType<Call<Self>>
280			+ IsType<<Self as frame_system::Config>::RuntimeCall>;
281
282		/// The overarching hold reason.
283		type RuntimeHoldReason: Parameter
284			+ Member
285			+ MaxEncodedLen
286			+ Copy
287			+ VariantCount
288			+ From<HoldReason>;
289
290		/// Query the block number that will be used to measure time.
291		///
292		/// Must return monotonically increasing values when called from consecutive blocks. Can be
293		/// configured to return either:
294		/// - the local block number of the runtime via `frame_system::Pallet`
295		/// - a remote block number, eg from the relay chain through `RelaychainDataProvider`
296		/// - an arbitrary value through a custom implementation of the trait
297		///
298		/// There is currently no migration provided to "hot-swap" block number providers and it may
299		/// result in undefined behavior when doing so. Parachains are therefore best off setting
300		/// this to their local block number provider if they have the pallet already deployed.
301		///
302		/// Suggested values:
303		/// - Solo- and Relay-chains: `frame_system::Pallet`
304		/// - Parachains that may produce blocks sparingly or only when needed (on-demand):
305		///   - already have the pallet deployed: `frame_system::Pallet`
306		///   - are freshly deploying this pallet: `RelaychainDataProvider`
307		/// - Parachains with a reliably block production rate (PLO or bulk-coretime):
308		///   - already have the pallet deployed: `frame_system::Pallet`
309		///   - are freshly deploying this pallet: no strong recommendation. Both local and remote
310		///     providers can be used. Relay provider can be a bit better in cases where the
311		///     parachain is lagging its block production to avoid clock skew.
312		type BlockNumberProvider: BlockNumberProvider;
313
314		/// The currency mechanism.
315		#[cfg(not(feature = "runtime-benchmarks"))]
316		type Currency: MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
317			+ Balanced<Self::AccountId>;
318		#[cfg(feature = "runtime-benchmarks")]
319		type Currency: MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
320			+ Balanced<Self::AccountId>
321			+ frame::traits::fungible::Mutate<Self::AccountId>;
322
323		/// Storage consideration for holding friend group configs.
324		type FriendGroupsConsideration: Consideration<Self::AccountId, Footprint>;
325
326		/// Storage consideration for holding an attempt.
327		type AttemptConsideration: Consideration<Self::AccountId, Footprint>;
328
329		/// Storage consideration for holding an inheritor.
330		type InheritorConsideration: Consideration<Self::AccountId, Footprint>;
331
332		/// Security deposit taken for each attempt that the initiator needs to place.
333		#[pallet::constant]
334		type SecurityDeposit: Get<BalanceOf<Self>>;
335
336		/// Handler for the `Credit` produced when a security deposit is slashed.
337		///
338		/// Use `()` to drop the credit and decrease total issuance (i.e. burn). Other common
339		/// choices are a treasury sink or `pallet-dap`.
340		type Slash: OnUnbalanced<CreditOf<Self>>;
341
342		/// DO NOT REDUCE THIS VALUE. Maximum number of friends per account config.
343		///
344		/// Reducing this value can cause decoding errors in the bounded vectors.
345		#[pallet::constant]
346		type MaxFriendsPerConfig: Get<u32>;
347
348		/// Weight information for extrinsics in this pallet.
349		type WeightInfo: WeightInfo;
350	}
351
352	/// The friend groups of an account that can conduct recovery attempts.
353	///
354	/// Modifying this storage is not possible while an account has ongoing recovery attempts.
355	#[pallet::storage]
356	pub type FriendGroups<T: Config> = StorageMap<
357		_,
358		Blake2_128Concat,
359		T::AccountId,
360		(FriendGroupsOf<T>, T::FriendGroupsConsideration),
361	>;
362
363	/// Ongoing recovery attempts of a lost account indexed by `(lost, friend_group)`.
364	#[pallet::storage]
365	pub type Attempt<T: Config> = StorageDoubleMap<
366		_,
367		Blake2_128Concat,
368		T::AccountId,
369		Blake2_128Concat,
370		FriendGroupIndex,
371		(AttemptOf<T>, AttemptTicketOf<T>, SecurityDepositOf<T>),
372	>;
373
374	/// The account that inherited full access to a lost account after successful recovery.
375	///
376	/// The key is the lost account and the value is the inheritor account.
377	///
378	/// NOTE: This could be a multisig or proxy account
379	#[pallet::storage]
380	pub type Inheritor<T: Config> = StorageMap<
381		_,
382		Blake2_128Concat,
383		T::AccountId,
384		(InheritancePriority, T::AccountId, InheritorTicketOf<T>),
385	>;
386
387	#[pallet::composite_enum]
388	pub enum HoldReason {
389		/// Deposit for configuring recovery friend groups.
390		#[codec(index = 0)]
391		FriendGroupsStorage,
392
393		/// Deposit for an ongoing recovery attempt.
394		#[codec(index = 1)]
395		AttemptStorage,
396
397		/// Deposit for the inheritor of a lost account.
398		#[codec(index = 2)]
399		InheritorStorage,
400
401		/// Security deposit for a recovery attempt.
402		#[codec(index = 3)]
403		SecurityDeposit,
404	}
405
406	#[pallet::event]
407	#[pallet::generate_deposit(pub(super) fn deposit_event)]
408	pub enum Event<T: Config> {
409		/// A recovery attempt was approved by a friend.
410		AttemptApproved {
411			lost: T::AccountId,
412			friend_group_index: FriendGroupIndex,
413			friend: T::AccountId,
414		},
415		/// A recovery attempt was canceled by either the lost account or the initiator.
416		AttemptCanceled {
417			lost: T::AccountId,
418			friend_group_index: FriendGroupIndex,
419			canceler: T::AccountId,
420		},
421		/// A recovery attempt was initiated by a friend.
422		AttemptInitiated {
423			lost: T::AccountId,
424			friend_group_index: FriendGroupIndex,
425			initiator: T::AccountId,
426		},
427		/// A recovery attempt was finished.
428		AttemptFinished {
429			lost: T::AccountId,
430			friend_group_index: FriendGroupIndex,
431			inheritor: T::AccountId,
432			previous_inheritor: Option<T::AccountId>,
433		},
434		/// A recovery attempt was discarded because the account was already recovered by a
435		/// friend group of equal or higher priority.
436		///
437		/// The attempt is consumed (removed from storage) and its deposits are released, but
438		/// the existing inheritor remains unchanged.
439		AttemptDiscarded {
440			lost: T::AccountId,
441			friend_group_index: FriendGroupIndex,
442			existing_inheritor: T::AccountId,
443		},
444		/// A recovery attempt was slashed by the lost account.
445		///
446		/// The initiator will lose their security deposit.
447		AttemptSlashed { lost: T::AccountId, friend_group_index: FriendGroupIndex },
448		/// The friend groups of an account have been changed.
449		FriendGroupsChanged { lost: T::AccountId },
450		/// The inheritor of a lost account was revoked by the lost account.
451		InheritorRevoked { lost: T::AccountId },
452		/// A recovered account was controlled by its inheritor.
453		///
454		/// Check the `call_result` to see if it was successful.
455		RecoveredAccountControlled {
456			recovered: T::AccountId,
457			inheritor: T::AccountId,
458			call_hash: HashOf<T>,
459			call_result: DispatchResult,
460		},
461	}
462
463	#[pallet::error]
464	pub enum Error<T> {
465		/// This attempt is already fully approved and does not need any more votes.
466		AlreadyApproved,
467		/// The recovery attempt has already been initiated.
468		AlreadyInitiated,
469		/// The friend already voted for this attempt.
470		AlreadyVoted,
471		/// The lost account has ongoing recovery attempts.
472		HasOngoingAttempts,
473		/// The lost account cannot be a friend of itself.
474		LostAccountInFriendGroup,
475		/// The account was already recovered by a group of equal or higher priority.
476		HigherPriorityRecovered,
477		/// Cancel delay must be at least 1.
478		NoCancelDelay,
479		/// This account does not have any friend groups.
480		NoFriendGroups,
481		/// The friend group has no friends.
482		NoFriends,
483		/// The lost account does not have any inheritor.
484		NoInheritor,
485		/// Not enough friends approved this attempt.
486		NotApproved,
487		/// The referenced recovery attempt was not found.
488		NotAttempt,
489		/// The caller is not the initiator or the lost account.
490		NotCanceller,
491		/// The caller is not a friend of the lost account.
492		NotFriend,
493		/// A specific referenced friend group was not found.
494		NotFriendGroup,
495		/// The caller is not the inheritor of the lost account.
496		NotInheritor,
497		/// The cancel delay since the last approval or initialization has not yet passed.
498		NotYetCancelable,
499		/// The inheritance delay of this attempt has not yet passed.
500		NotYetInheritable,
501		/// Too many friend groups.
502		TooManyFriendGroups,
503		/// The number of friends needed is greater than the number of friends.
504		TooManyFriendsNeeded,
505		/// The number of friends needed is zero.
506		NoFriendsNeeded,
507		/// The friends of a friend group are not sorted or not unique.
508		FriendsNotSortedOrUnique,
509		/// Two friend groups have the same set of friends.
510		DuplicateFriendGroups,
511	}
512
513	#[pallet::view_functions]
514	impl<T: Config> Pallet<T> {
515		/// The provided block number that will be used to measure time.
516		pub fn provided_block_number() -> ProvidedBlockNumberOf<T> {
517			T::BlockNumberProvider::current_block_number()
518		}
519
520		/// The friend groups of an account that can initiate recovery attempts.
521		pub fn friend_groups(lost: T::AccountId) -> Vec<FriendGroupOf<T>> {
522			FriendGroups::<T>::get(lost).map(|(g, _t)| g.into_inner()).unwrap_or_default()
523		}
524
525		/// Ongoing recovery attempts for a lost account.
526		pub fn attempts(lost: T::AccountId) -> Vec<(FriendGroupOf<T>, AttemptOf<T>)> {
527			Attempt::<T>::iter_prefix(&lost)
528				.filter_map(|(friend_group_index, (attempt, _ticket, _deposit))| {
529					let friend_group = Self::friend_group_of(&lost, friend_group_index).ok()?;
530					Some((friend_group, attempt))
531				})
532				.collect()
533		}
534
535		/// The account that inherited full access to the lost account.
536		pub fn inheritor(lost: T::AccountId) -> Option<T::AccountId> {
537			Inheritor::<T>::get(lost).map(|(_, inheritor, _)| inheritor)
538		}
539
540		/// All the recovered accounts that `heir` inherited access to.
541		pub fn inheritance(heir: T::AccountId) -> Vec<T::AccountId> {
542			let mut inheritance = Vec::new();
543
544			for (recovered, (_, inheritor, _)) in Inheritor::<T>::iter() {
545				if inheritor != heir {
546					continue;
547				}
548				let Err(pos) = inheritance.binary_search(&recovered) else { continue };
549
550				inheritance.insert(pos, recovered);
551			}
552
553			inheritance
554		}
555	}
556
557	#[pallet::call]
558	impl<T: Config> Pallet<T> {
559		/// Allows the inheritor of a recovered account to control it.
560		///
561		/// The controller is not allowed to dispatch calls of the recovery pallet. Otherwise they
562		/// could mess with the recovery configuration and possibly cancel or slash attempts from
563		/// higher-priority friend groups.
564		#[pallet::call_index(0)]
565		#[pallet::weight({
566			let di = call.get_dispatch_info();
567			(T::WeightInfo::control_inherited_account().saturating_add(di.call_weight), di.class)
568		})]
569		pub fn control_inherited_account(
570			origin: OriginFor<T>,
571			recovered: AccountIdLookupOf<T>,
572			call: Box<<T as Config>::RuntimeCall>,
573		) -> DispatchResult {
574			let maybe_inheritor = ensure_signed(origin)?;
575			let recovered = T::Lookup::lookup(recovered)?;
576
577			let inheritor = Inheritor::<T>::get(&recovered)
578				.map(|(_, inheritor, _ticket)| inheritor)
579				.ok_or(Error::<T>::NoInheritor)?;
580			ensure!(maybe_inheritor == inheritor, Error::<T>::NotInheritor);
581
582			let mut origin: T::RuntimeOrigin =
583				frame_system::RawOrigin::Signed(recovered.clone()).into();
584			// Reentrancy guard
585			origin.add_filter(|c: &<T as frame_system::Config>::RuntimeCall| {
586				let c = <T as Config>::RuntimeCall::from_ref(c);
587				c.is_sub_type().is_none()
588			});
589
590			let call_hash = call.using_encoded(&T::Hashing::hash);
591			let call_result = call.dispatch(origin).map(|_| ()).map_err(|r| r.error);
592
593			Self::deposit_event(Event::<T>::RecoveredAccountControlled {
594				recovered,
595				inheritor,
596				call_hash,
597				call_result,
598			});
599
600			// NOTE: We ALWAYS return okay if the caller had the permission to control the lost
601			// account regardless of the inner call result.
602			Ok(())
603		}
604
605		/// Revoke the inheritor of the calling (lost) account.
606		///
607		/// This removes the inheritor entry and refunds the inheritor deposit. Can only be called
608		/// by the lost account itself after it regains access.
609		#[pallet::call_index(1)]
610		#[pallet::weight(T::WeightInfo::revoke_inheritor())]
611		pub fn revoke_inheritor(origin: OriginFor<T>) -> DispatchResult {
612			let lost = ensure_signed(origin)?;
613
614			let (_priority, _inheritor, ticket) =
615				Inheritor::<T>::take(&lost).ok_or(Error::<T>::NoInheritor)?;
616
617			let _: Result<(), DispatchError> = ticket.try_drop().defensive();
618
619			Self::deposit_event(Event::<T>::InheritorRevoked { lost });
620
621			Ok(())
622		}
623
624		/// Set the friend groups of the calling account before it lost access.
625		///
626		/// Cannot be used while there are ongoing recovery attempts. The friends of each group
627		/// MUST be sorted and unique. Trying to insert two friend groups with the same set of
628		/// friends will result in an error.
629		///
630		/// A `FriendGroupsChanged` event is emitted only when the new friends groups differed from
631		/// the old ones.
632		#[pallet::call_index(2)]
633		#[pallet::weight(T::WeightInfo::set_friend_groups())]
634		pub fn set_friend_groups(
635			origin: OriginFor<T>,
636			friend_groups: Vec<FriendGroupOf<T>>,
637		) -> DispatchResult {
638			let lost = ensure_signed(origin)?;
639
640			if Attempt::<T>::iter_prefix(&lost).next().is_some() {
641				return Err(Error::<T>::HasOngoingAttempts.into());
642			}
643
644			let (old_friend_groups, old_ticket) = match FriendGroups::<T>::get(&lost) {
645				Some((g, t)) => (g, Some(t)),
646				None => Default::default(),
647			};
648
649			let new_friend_groups = Self::bound_friend_groups(&lost, friend_groups)?;
650
651			// Easy case where all are removed:
652			if new_friend_groups.is_empty() {
653				if let Some(old_ticket) = old_ticket {
654					old_ticket.drop(&lost)?;
655				}
656				FriendGroups::<T>::remove(&lost);
657				if !old_friend_groups.is_empty() {
658					Self::deposit_event(Event::<T>::FriendGroupsChanged { lost });
659				}
660				return Ok(());
661			}
662
663			let new_footprint = Self::friend_group_footprint(&new_friend_groups);
664			let new_ticket = if let Some(old_ticket) = old_ticket {
665				old_ticket.update(&lost, new_footprint)?
666			} else {
667				T::FriendGroupsConsideration::new(&lost, new_footprint)?
668			};
669			FriendGroups::<T>::insert(&lost, (&new_friend_groups, &new_ticket));
670
671			if new_friend_groups != old_friend_groups {
672				Self::deposit_event(Event::<T>::FriendGroupsChanged { lost });
673			}
674
675			Ok(())
676		}
677
678		/// Attempt to recover a lost account by a friend within the given friend group.
679		///
680		/// The initiator's approval is recorded automatically, so they do not need to call
681		/// `approve_attempt` themselves.
682		///
683		/// Once an account has been recovered by a friend group, no friend group of equal or lower
684		/// priority can open a new attempt: it will fail with [`Error::HigherPriorityRecovered`].
685		/// Only a strictly higher-priority group (lower numerical
686		/// [`FriendGroup::inheritance_priority`]) can take over the inheritor.
687		#[pallet::call_index(3)]
688		#[pallet::weight(T::WeightInfo::initiate_attempt())]
689		pub fn initiate_attempt(
690			origin: OriginFor<T>,
691			lost: AccountIdLookupOf<T>,
692			friend_group_index: FriendGroupIndex,
693		) -> DispatchResult {
694			let initiator = ensure_signed(origin)?;
695			let lost = T::Lookup::lookup(lost)?;
696
697			if Self::attempt_of(&lost, friend_group_index).is_ok() {
698				return Err(Error::<T>::AlreadyInitiated.into());
699			}
700
701			let friend_group = Self::friend_group_of(&lost, friend_group_index)?;
702			let initiator_index = friend_group
703				.friends
704				.iter()
705				.position(|f| f == &initiator)
706				.ok_or(Error::<T>::NotFriend)?;
707
708			if let Some((inheritance_priority, _, _)) = Inheritor::<T>::get(&lost) {
709				ensure!(
710					friend_group.inheritance_priority < inheritance_priority,
711					Error::<T>::HigherPriorityRecovered
712				);
713			}
714
715			// The initiator counts as the first approval, so they don't have to sign twice.
716			let approvals = ApprovalBitfield::default()
717				.with_bits([initiator_index])
718				.defensive_proof("initiator_index < friends.len() <= MaxFriendsPerConfig; qed")
719				.unwrap_or_default();
720
721			let now = T::BlockNumberProvider::current_block_number();
722			let attempt = AttemptOf::<T> {
723				friend_group_index,
724				initiator: initiator.clone(),
725				init_block: now,
726				last_approval_block: now,
727				approvals,
728			};
729
730			let deposit = T::SecurityDeposit::get();
731			let () = T::Currency::hold(&HoldReason::SecurityDeposit.into(), &initiator, deposit)?;
732
733			let ticket = AttemptTicketOf::<T>::new(&initiator, Self::attempt_footprint())?;
734			Attempt::<T>::insert(&lost, friend_group_index, (&attempt, &ticket, &deposit));
735
736			Self::deposit_event(Event::<T>::AttemptInitiated {
737				lost: lost.clone(),
738				friend_group_index,
739				initiator: initiator.clone(),
740			});
741			Self::deposit_event(Event::<T>::AttemptApproved {
742				lost,
743				friend_group_index,
744				friend: initiator,
745			});
746
747			Ok(())
748		}
749
750		/// Approve the recovery for a lost account.
751		///
752		/// Must be called by a friend of the friend group that the recovery attempt belongs to that
753		/// did not yet vote. Voting is only allowed until the threshold is reached.
754		/// `finish_attempt` should be called after the last friend voted.
755		#[pallet::call_index(4)]
756		#[pallet::weight(T::WeightInfo::approve_attempt())]
757		pub fn approve_attempt(
758			origin: OriginFor<T>,
759			lost: AccountIdLookupOf<T>,
760			friend_group_index: FriendGroupIndex,
761		) -> DispatchResult {
762			let friend = ensure_signed(origin)?;
763			let lost = T::Lookup::lookup(lost)?;
764			let now = T::BlockNumberProvider::current_block_number();
765
766			let (mut attempt, ticket, deposit) = Self::attempt_of(&lost, friend_group_index)?;
767			let friend_group = Self::friend_group_of(&lost, friend_group_index).defensive()?;
768
769			let friend_index = friend_group
770				.friends
771				.iter()
772				.position(|f| f == &friend)
773				.ok_or(Error::<T>::NotFriend)?;
774
775			let friends_voted = attempt.approvals.count_ones();
776			ensure!(friends_voted < friend_group.friends_needed, Error::<T>::AlreadyApproved);
777			attempt.last_approval_block = now;
778
779			attempt
780				.approvals
781				.set_if_not_set(friend_index)
782				.map_err(|_| Error::<T>::AlreadyVoted)?;
783
784			// NOTE: We do not update the ticket since the attempt has static size.
785			Attempt::<T>::insert(&lost, friend_group_index, (&attempt, &ticket, &deposit));
786
787			Self::deposit_event(Event::<T>::AttemptApproved { lost, friend_group_index, friend });
788
789			Ok(())
790		}
791
792		/// Finish a recovery attempt and make the lost account accessible from the inheritor.
793		///
794		/// Can be called by anyone who is willing to pay for the inheritor deposit.
795		#[pallet::call_index(5)]
796		#[pallet::weight(T::WeightInfo::finish_attempt())]
797		pub fn finish_attempt(
798			origin: OriginFor<T>,
799			lost: AccountIdLookupOf<T>,
800			friend_group_index: FriendGroupIndex,
801		) -> DispatchResult {
802			let caller = ensure_signed(origin)?;
803			let lost = T::Lookup::lookup(lost)?;
804			let now = T::BlockNumberProvider::current_block_number();
805
806			let (attempt, attempts_ticket, deposit) =
807				Attempt::<T>::take(&lost, &friend_group_index).ok_or(Error::<T>::NotAttempt)?;
808
809			// We NEVER block a recovery on a buggy initiator account.
810			let _: Result<(), DispatchError> = attempts_ticket.try_drop().defensive();
811			let _: Result<BalanceOf<T>, DispatchError> = T::Currency::release(
812				&HoldReason::SecurityDeposit.into(),
813				&attempt.initiator,
814				deposit,
815				Precision::BestEffort,
816			)
817			.defensive();
818
819			let friend_group = Self::friend_group_of(&lost, friend_group_index).defensive()?;
820
821			// Check if the attempt is now complete
822			let approvals = attempt.approvals.count_ones();
823			ensure!(
824				// We use >= defensively, but it should be at most ==
825				approvals >= friend_group.friends_needed,
826				Error::<T>::NotApproved
827			);
828
829			let inheritable_at = attempt
830				.init_block
831				.checked_add(&friend_group.inheritance_delay)
832				.ok_or(ArithmeticError::Overflow)?;
833			ensure!(now >= inheritable_at, Error::<T>::NotYetInheritable);
834			// NOTE: We dont need to check the cancel delay, since enough friends voted and we dont
835			// assume fully malicious behavior.
836
837			let inheritor = friend_group.inheritor;
838			let inheritance_priority = friend_group.inheritance_priority;
839
840			match Inheritor::<T>::get(&lost) {
841				None => {
842					let ticket = Self::inheritor_ticket(&caller)?;
843					Inheritor::<T>::insert(&lost, (inheritance_priority, &inheritor, ticket));
844					Self::deposit_event(Event::<T>::AttemptFinished {
845						lost,
846						friend_group_index,
847						inheritor,
848						previous_inheritor: None,
849					});
850				},
851				// new recovery has a higher priority, we replace the existing inheritor
852				Some((old_priority, old_inheritor, ticket))
853					if inheritance_priority < old_priority =>
854				{
855					let ticket = ticket.update(&caller, Self::inheritor_footprint())?;
856					Inheritor::<T>::insert(&lost, (inheritance_priority, &inheritor, ticket));
857					Self::deposit_event(Event::<T>::AttemptFinished {
858						lost,
859						friend_group_index,
860						inheritor,
861						previous_inheritor: Some(old_inheritor),
862					});
863				},
864				Some((_, existing_inheritor, _)) => {
865					// The existing inheritor stays since an equal or higher priority group
866					// already recovered the account.
867					Self::deposit_event(Event::<T>::AttemptDiscarded {
868						lost,
869						friend_group_index,
870						existing_inheritor,
871					});
872				},
873			};
874
875			Ok(())
876		}
877
878		/// The lost account can cancel an attempt at any moment; the initiator, only after a delay.
879		///
880		/// This will release the security deposit back to the initiator. The cancel delay must be
881		/// respected if the initiator calls it to prevent it from front-running the lost account
882		/// from slashing the attempt.
883		#[pallet::call_index(6)]
884		#[pallet::weight(T::WeightInfo::cancel_attempt())]
885		pub fn cancel_attempt(
886			origin: OriginFor<T>,
887			lost: AccountIdLookupOf<T>,
888			friend_group_index: FriendGroupIndex,
889		) -> DispatchResult {
890			let canceler = ensure_signed(origin)?;
891			let lost = T::Lookup::lookup(lost)?;
892			let now = T::BlockNumberProvider::current_block_number();
893
894			let (attempt, ticket, deposit) =
895				Attempt::<T>::take(&lost, &friend_group_index).ok_or(Error::<T>::NotAttempt)?;
896
897			ensure!(canceler == attempt.initiator || canceler == lost, Error::<T>::NotCanceller);
898
899			// Ignore the return value since we always want to allow to cancel an attempt.
900			let _ignored = ticket.try_drop().defensive();
901			let _: Result<BalanceOf<T>, DispatchError> = T::Currency::release(
902				&HoldReason::SecurityDeposit.into(),
903				&attempt.initiator,
904				deposit,
905				Precision::BestEffort,
906			)
907			.defensive();
908
909			let friend_group = Self::friend_group_of(&lost, friend_group_index).defensive()?;
910
911			if canceler != lost {
912				let cancelable_at = attempt
913					.last_approval_block
914					.checked_add(&friend_group.cancel_delay)
915					.ok_or(ArithmeticError::Overflow)?;
916				ensure!(now >= cancelable_at, Error::<T>::NotYetCancelable);
917			}
918			// NOTE: It is possible to cancel a fully approved attempt, but since we check the
919			// cancel delay, we ensure that every friend had enough time to call
920			// `finish_attempt`.
921
922			Self::deposit_event(Event::<T>::AttemptCanceled { lost, friend_group_index, canceler });
923
924			Ok(())
925		}
926
927		/// Slash a malicious recovery attempt and burn the security deposit of the initiator.
928		#[pallet::call_index(7)]
929		#[pallet::weight(T::WeightInfo::slash_attempt())]
930		pub fn slash_attempt(
931			origin: OriginFor<T>,
932			friend_group_index: FriendGroupIndex,
933		) -> DispatchResult {
934			let lost = ensure_signed(origin)?;
935
936			let (attempt, ticket, deposit) =
937				Attempt::<T>::take(&lost, &friend_group_index).ok_or(Error::<T>::NotAttempt)?;
938
939			let _: Result<(), DispatchError> = ticket.try_drop().defensive();
940			Self::handle_slash(&attempt.initiator, deposit);
941
942			Self::deposit_event(Event::<T>::AttemptSlashed { lost, friend_group_index });
943
944			Ok(())
945		}
946	}
947
948	#[pallet::hooks]
949	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
950		fn integrity_test() {
951			assert!(
952				T::MaxFriendsPerConfig::get() > 0,
953				"MaxFriendsPerConfig must be greater than 0"
954			);
955
956			let bitfield = ApprovalBitfieldOf::<T>::default();
957			assert!(bitfield.0.len() >= 1, "Default works");
958		}
959
960		#[cfg(feature = "try-runtime")]
961		fn try_state(
962			_n: BlockNumberFor<T>,
963		) -> Result<(), frame::deps::sp_runtime::TryRuntimeError> {
964			Pallet::<T>::do_try_state()
965		}
966	}
967}
968
969impl<T: Config> Pallet<T> {
970	pub fn friend_group_footprint(friend_groups: &FriendGroupsOf<T>) -> Footprint {
971		if friend_groups.is_empty() {
972			defensive!("Do not call with empty friend groups");
973		}
974
975		Footprint::from_encodable(friend_groups)
976	}
977
978	pub fn attempt_footprint() -> Footprint {
979		Footprint::from_mel::<AttemptOf<T>>()
980	}
981
982	pub fn inheritor_footprint() -> Footprint {
983		Footprint::from_mel::<(InheritancePriority, T::AccountId)>()
984	}
985
986	pub fn inheritor_ticket(who: &T::AccountId) -> Result<InheritorTicketOf<T>, DispatchError> {
987		InheritorTicketOf::<T>::new(&who, Self::inheritor_footprint())
988	}
989
990	pub fn friend_group_of(
991		lost: &T::AccountId,
992		friend_group_index: FriendGroupIndex,
993	) -> Result<FriendGroupOf<T>, Error<T>> {
994		let friend_groups = match FriendGroups::<T>::get(lost) {
995			Some((g, _t)) => g,
996			None => return Err(Error::<T>::NoFriendGroups),
997		};
998		friend_groups
999			.get(friend_group_index as usize)
1000			.cloned()
1001			.ok_or(Error::<T>::NotFriendGroup)
1002	}
1003
1004	pub fn attempt_of(
1005		lost: &T::AccountId,
1006		friend_group_index: FriendGroupIndex,
1007	) -> Result<(AttemptOf<T>, AttemptTicketOf<T>, SecurityDepositOf<T>), Error<T>> {
1008		pallet::Attempt::<T>::get(lost, friend_group_index).ok_or(Error::<T>::NotAttempt)
1009	}
1010
1011	/// Sanity check the friend groups and bound them into a bounded vector.
1012	pub fn bound_friend_groups(
1013		lost: &T::AccountId,
1014		mut friend_groups: Vec<FriendGroupOf<T>>,
1015	) -> Result<FriendGroupsOf<T>, Error<T>> {
1016		for friend_group in &mut friend_groups {
1017			ensure!(!friend_group.friends.is_empty(), Error::<T>::NoFriends);
1018			// cannot contain the lost account itself
1019			ensure!(!friend_group.friends.contains(&lost), Error::<T>::LostAccountInFriendGroup);
1020			ensure!(
1021				friend_group.friends.windows(2).all(|w| w[0] < w[1]),
1022				Error::<T>::FriendsNotSortedOrUnique
1023			);
1024			ensure!(
1025				friend_group.friends_needed as usize <= friend_group.friends.len(),
1026				Error::<T>::TooManyFriendsNeeded
1027			);
1028			ensure!(friend_group.friends_needed > 0, Error::<T>::NoFriendsNeeded);
1029			// prevent mempool frontrunning by requiring at least 1 block
1030			ensure!(!friend_group.cancel_delay.is_zero(), Error::<T>::NoCancelDelay);
1031		}
1032
1033		for (i, group_a) in friend_groups.iter().enumerate() {
1034			for group_b in friend_groups.iter().skip(i + 1) {
1035				ensure!(group_a.friends != group_b.friends, Error::<T>::DuplicateFriendGroups);
1036			}
1037		}
1038
1039		friend_groups.try_into().map_err(|_| Error::<T>::TooManyFriendGroups)
1040	}
1041
1042	/// Slash a security deposit and hand the resulting `Credit` to `T::Slash`.
1043	fn handle_slash(who: &T::AccountId, amount: SecurityDepositOf<T>) {
1044		let (credit, missing) =
1045			T::Currency::slash(&HoldReason::SecurityDeposit.into(), who, amount);
1046		if !missing.is_zero() {
1047			defensive!("could not slash full security deposit");
1048		}
1049		T::Slash::on_unbalanced(credit);
1050	}
1051
1052	/// State-consistency checks for `try-runtime` and verification contexts.
1053	///
1054	/// Invariants checked:
1055	/// 1. Every `Attempt[lost, _]` has a matching `FriendGroups[lost]` entry.
1056	/// 2. Every `Attempt[lost, idx]` has `idx < FriendGroups[lost].0.len()`.
1057	/// 3. No bit is set in `attempt.approvals` at a position past `friends.len()` for the attempt's
1058	///    friend group.
1059	/// 5. Every storage entry has a non-zero balance on hold for its depositor under the
1060	///    corresponding `HoldReason`. Catches a diverged ticket/balance bookkeeping (one-direction
1061	///    check; the inverse, orphan holds, would require an account iteration we don't perform).
1062	/// 6. Every `attempt.approvals.count_ones()` is `<= friends_needed`.
1063	/// 7. Every attempt has `init_block <= last_approval_block`.
1064	/// 8. `Inheritor[lost].1 != lost`. Surfaces an unenforced precondition in `set_friend_groups`.
1065	#[cfg(any(feature = "try-runtime", test))]
1066	pub(crate) fn do_try_state() -> Result<(), frame::deps::sp_runtime::TryRuntimeError> {
1067		for (lost, friend_group_index, (attempt, ticket, _deposit)) in pallet::Attempt::<T>::iter()
1068		{
1069			// 1. No orphan attempts.
1070			let friend_groups_entry = FriendGroups::<T>::get(&lost)
1071				.ok_or("Attempt without matching FriendGroups entry")?;
1072			let friend_groups = friend_groups_entry.0;
1073
1074			// 2. In-range friend group index.
1075			let friend_group = friend_groups
1076				.get(friend_group_index as usize)
1077				.ok_or("Attempt friend_group_index out of range")?;
1078
1079			let friends_len = friend_group.friends.len();
1080
1081			// 3. No bit set past friends.len() in attempt.approvals.
1082			if attempt.approvals.iter_ones().any(|index| index >= friends_len) {
1083				return Err("Attempt approvals has a bit set past friends.len()".into());
1084			}
1085
1086			// 6. approvals count <= friends_needed.
1087			let count = attempt.approvals.count_ones();
1088			if count > friend_group.friends_needed {
1089				return Err("Attempt approvals count exceeds friends_needed".into());
1090			}
1091
1092			// 7. init_block <= last_approval_block.
1093			if attempt.init_block > attempt.last_approval_block {
1094				return Err("Attempt init_block is later than last_approval_block".into());
1095			}
1096
1097			// 5a. Attempt depositor has a non-zero AttemptStorage hold.
1098			if ticket.ticket.is_some() &&
1099				T::Currency::balance_on_hold(
1100					&HoldReason::AttemptStorage.into(),
1101					&ticket.depositor,
1102				)
1103				.is_zero()
1104			{
1105				return Err(
1106					"Attempt ticket exists but depositor has zero AttemptStorage hold".into()
1107				);
1108			}
1109			// 5b. Attempt initiator has a non-zero SecurityDeposit hold.
1110			if T::Currency::balance_on_hold(&HoldReason::SecurityDeposit.into(), &attempt.initiator)
1111				.is_zero()
1112			{
1113				return Err("Attempt has zero SecurityDeposit hold on initiator".into());
1114			}
1115		}
1116
1117		for (lost, (friend_groups, _consideration)) in FriendGroups::<T>::iter() {
1118			// 5c. The lost account, as depositor of its FriendGroups entry, has a
1119			// non-zero FriendGroupsStorage hold whenever the group set is non-empty.
1120			if !friend_groups.is_empty() &&
1121				T::Currency::balance_on_hold(&HoldReason::FriendGroupsStorage.into(), &lost)
1122					.is_zero()
1123			{
1124				return Err("FriendGroups entry has zero FriendGroupsStorage hold on lost".into());
1125			}
1126		}
1127
1128		for (lost, (_priority, inheritor, ticket)) in Inheritor::<T>::iter() {
1129			// 8. inheritor != lost.
1130			if inheritor == lost {
1131				return Err("Inheritor entry has inheritor == lost".into());
1132			}
1133
1134			// 5d. Inheritor ticket depositor has a non-zero InheritorStorage hold.
1135			if ticket.ticket.is_some() &&
1136				T::Currency::balance_on_hold(
1137					&HoldReason::InheritorStorage.into(),
1138					&ticket.depositor,
1139				)
1140				.is_zero()
1141			{
1142				return Err(
1143					"Inheritor ticket exists but depositor has zero InheritorStorage hold".into()
1144				);
1145			}
1146		}
1147
1148		Ok(())
1149	}
1150}