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