mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-28 00:28:01 +00:00
A Social Account Recovery Pallet (#4531)
* Initial sketch of social recovery pallet * Fix compilation issues * Use a single total delay, rename stuff * Check possible overflow * Copyright bump * Add mock for tests * Add basic end to end test * Add `create_recovery` tests * Add malicious recovery lifecycle test * Make clear we check for sorted and unique friends * Work on some tests, clean up imports * Change `if let Some(_)` to `ok_or()` * More tests * Finish tests, except issue with `on_free_balance_zero` * Fix `on_free_balance_zero` * Pallet docs * Add function/weight docs * Fix merge master * OnReapAccount for System too * Update weight docs * Allow passthrough to support fee-less extrinsics
This commit is contained in:
Generated
+16
@@ -3894,6 +3894,22 @@ dependencies = [
|
||||
"sp-std 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-recovery"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"enumflags2 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"frame-support 2.0.0",
|
||||
"frame-system 2.0.0",
|
||||
"pallet-balances 2.0.0",
|
||||
"parity-scale-codec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sp-core 2.0.0",
|
||||
"sp-io 2.0.0",
|
||||
"sp-runtime 2.0.0",
|
||||
"sp-std 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-scored-pool"
|
||||
version = "2.0.0"
|
||||
|
||||
@@ -78,6 +78,7 @@ members = [
|
||||
"frame/nicks",
|
||||
"frame/offences",
|
||||
"frame/randomness-collective-flip",
|
||||
"frame/recovery",
|
||||
"frame/scored-pool",
|
||||
"frame/session",
|
||||
"frame/society",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "pallet-recovery"
|
||||
version = "2.0.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.101", optional = true }
|
||||
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
|
||||
enumflags2 = { version = "0.6.2" }
|
||||
sp-std = { version = "2.0.0", default-features = false, path = "../../primitives/std" }
|
||||
sp-io = { version = "2.0.0", default-features = false, path = "../../primitives/io" }
|
||||
sp-runtime = { version = "2.0.0", default-features = false, path = "../../primitives/runtime" }
|
||||
frame-support = { version = "2.0.0", default-features = false, path = "../support" }
|
||||
frame-system = { version = "2.0.0", default-features = false, path = "../system" }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-core = { version = "2.0.0", path = "../../primitives/core" }
|
||||
pallet-balances = { version = "2.0.0", path = "../balances" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"serde",
|
||||
"codec/std",
|
||||
"sp-std/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
]
|
||||
@@ -0,0 +1,666 @@
|
||||
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! # Recovery Pallet
|
||||
//!
|
||||
//! - [`recovery::Trait`](./trait.Trait.html)
|
||||
//! - [`Call`](./enum.Call.html)
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Recovery pallet is an M-of-N social recovery tool for users to gain
|
||||
//! access to their accounts if the private key or other authentication mechanism
|
||||
//! is lost. Through this pallet, a user is able to make calls on-behalf-of another
|
||||
//! account which they have recovered. The recovery process is protected by trusted
|
||||
//! "friends" whom the original account owner chooses. A threshold (M) out of N
|
||||
//! friends are needed to give another account access to the recoverable account.
|
||||
//!
|
||||
//! ### Recovery Configuration
|
||||
//!
|
||||
//! The recovery process for each recoverable account can be configured by the account owner.
|
||||
//! They are able to choose:
|
||||
//! * `friends` - The list of friends that the account owner trusts to protect the
|
||||
//! recovery process for their account.
|
||||
//! * `threshold` - The number of friends that need to approve a recovery process for
|
||||
//! the account to be successfully recovered.
|
||||
//! * `delay_period` - The minimum number of blocks after the beginning of the recovery
|
||||
//! process that need to pass before the account can be successfully recovered.
|
||||
//!
|
||||
//! There is a configurable deposit that all users need to pay to create a recovery
|
||||
//! configuration. This deposit is composed of a base deposit plus a multiplier for
|
||||
//! the number of friends chosen. This deposit is returned in full when the account
|
||||
//! owner removes their recovery configuration.
|
||||
//!
|
||||
//! ### Recovery Lifecycle
|
||||
//!
|
||||
//! The intended lifecycle of a successful recovery takes the following steps:
|
||||
//! 1. The account owner calls `create_recovery` to set up a recovery configuration
|
||||
//! for their account.
|
||||
//! 2. At some later time, the account owner loses access to their account and wants
|
||||
//! to recover it. Likely, they will need to create a new account and fund it with
|
||||
//! enough balance to support the transaction fees and the deposit for the
|
||||
//! recovery process.
|
||||
//! 3. Using this new account, they call `initiate_recovery`.
|
||||
//! 4. Then the account owner would contact their configured friends to vouch for
|
||||
//! the recovery attempt. The account owner would provide their old account id
|
||||
//! and the new account id, and friends would call `vouch_recovery` with those
|
||||
//! parameters.
|
||||
//! 5. Once a threshold number of friends have vouched for the recovery attempt,
|
||||
//! the account owner needs to wait until the delay period has passed, starting
|
||||
//! when they initiated the recovery process.
|
||||
//! 6. Now the account owner is able to call `claim_recovery`, which subsequently
|
||||
//! allows them to call `as_recovered` and directly make calls on-behalf-of the lost
|
||||
//! account.
|
||||
//! 7. Using the now recovered account, the account owner can call `close_recovery`
|
||||
//! on the recovery process they opened, reclaiming the recovery deposit they
|
||||
//! placed.
|
||||
//! 8. Then the account owner should then call `remove_recovery` to remove the recovery
|
||||
//! configuration on the recovered account and reclaim the recovery configuration
|
||||
//! deposit they placed.
|
||||
//! 9. Using `as_recovered`, the account owner is able to call any other pallets
|
||||
//! to clean up their state and reclaim any reserved or locked funds. They
|
||||
//! can then transfer all funds from the recovered account to the new account.
|
||||
//! 10. When the recovered account becomes reaped (i.e. its free and reserved
|
||||
//! balance drops to zero), the final recovery link is removed.
|
||||
//!
|
||||
//! ### Malicious Recovery Attempts
|
||||
//!
|
||||
//! Initializing a the recovery process for a recoverable account is open and
|
||||
//! permissionless. However, the recovery deposit is an economic deterrent that
|
||||
//! should disincentivize would-be attackers from trying to maliciously recover
|
||||
//! accounts.
|
||||
//!
|
||||
//! The recovery deposit can always be claimed by the account which is trying to
|
||||
//! to be recovered. In the case of a malicious recovery attempt, the account
|
||||
//! owner who still has access to their account can claim the deposit and
|
||||
//! essentially punish the malicious user.
|
||||
//!
|
||||
//! Furthermore, the malicious recovery attempt can only be successful if the
|
||||
//! attacker is also able to get enough friends to vouch for the recovery attempt.
|
||||
//! In the case where the account owner prevents a malicious recovery process,
|
||||
//! this pallet makes it near-zero cost to re-configure the recovery settings and
|
||||
//! remove/replace friends who are acting inappropriately.
|
||||
//!
|
||||
//! ### Safety Considerations
|
||||
//!
|
||||
//! It is important to note that this is a powerful pallet that can compromise the
|
||||
//! security of an account if used incorrectly. Some recommended practices for users
|
||||
//! of this pallet are:
|
||||
//!
|
||||
//! * Configure a significant `delay_period` for your recovery process: As long as you
|
||||
//! have access to your recoverable account, you need only check the blockchain once
|
||||
//! every `delay_period` blocks to ensure that no recovery attempt is successful
|
||||
//! against your account. Using off-chain notification systems can help with this,
|
||||
//! but ultimately, setting a large `delay_period` means that even the most skilled
|
||||
//! attacker will need to wait this long before they can access your account.
|
||||
//! * Use a high threshold of approvals: Setting a value of 1 for the threshold means
|
||||
//! that any of your friends would be able to recover your account. They would
|
||||
//! simply need to start a recovery process and approve their own process. Similarly,
|
||||
//! a threshold of 2 would mean that any 2 friends could work together to gain
|
||||
//! access to your account. The only way to prevent against these kinds of attacks
|
||||
//! is to choose a high threshold of approvals and select from a diverse friend
|
||||
//! group that would not be able to reasonably coordinate with one another.
|
||||
//! * Reset your configuration over time: Since the entire deposit of creating a
|
||||
//! recovery configuration is returned to the user, the only cost of updating
|
||||
//! your recovery configuration is the transaction fees for the calls. Thus,
|
||||
//! it is strongly encouraged to regularly update your recovery configuration
|
||||
//! as your life changes and your relationship with new and existing friends
|
||||
//! change as well.
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Dispatchable Functions
|
||||
//!
|
||||
//! #### For General Users
|
||||
//!
|
||||
//! * `create_recovery` - Create a recovery configuration for your account and make it recoverable.
|
||||
//! * `initiate_recovery` - Start the recovery process for a recoverable account.
|
||||
//!
|
||||
//! #### For Friends of a Recoverable Account
|
||||
//! * `vouch_recovery` - As a `friend` of a recoverable account, vouch for a recovery attempt on the account.
|
||||
//!
|
||||
//! #### For a User Who Successfully Recovered an Account
|
||||
//!
|
||||
//! * `claim_recovery` - Claim access to the account that you have successfully completed the recovery process for.
|
||||
//! * `as_recovered` - Send a transaction as an account that you have recovered. See other functions below.
|
||||
//!
|
||||
//! #### For the Recoverable Account
|
||||
//!
|
||||
//! * `close_recovery` - Close an active recovery process for your account and reclaim the recovery deposit.
|
||||
//! * `remove_recovery` - Remove the recovery configuration from the account, making it un-recoverable.
|
||||
//!
|
||||
//! #### For Super Users
|
||||
//!
|
||||
//! * `set_recovered` - The ROOT origin is able to skip the recovery process and directly allow
|
||||
//! one account to access another.
|
||||
|
||||
// Ensure we're `no_std` when compiling for Wasm.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use sp_std::prelude::*;
|
||||
use sp_runtime::{
|
||||
traits::{Dispatchable, SaturatedConversion, CheckedAdd, CheckedMul},
|
||||
DispatchResult
|
||||
};
|
||||
use codec::{Encode, Decode};
|
||||
|
||||
use frame_support::{
|
||||
decl_module, decl_event, decl_storage, decl_error, ensure,
|
||||
Parameter, RuntimeDebug,
|
||||
weights::{
|
||||
GetDispatchInfo, PaysFee, DispatchClass, ClassifyDispatch, Weight, WeighData,
|
||||
SimpleDispatchInfo,
|
||||
},
|
||||
traits::{Currency, ReservableCurrency, Get, OnReapAccount},
|
||||
};
|
||||
use frame_system::{self as system, ensure_signed, ensure_root};
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::Balance;
|
||||
|
||||
/// Configuration trait.
|
||||
pub trait Trait: frame_system::Trait {
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
|
||||
|
||||
/// The overarching call type.
|
||||
type Call: Parameter + Dispatchable<Origin=Self::Origin> + GetDispatchInfo;
|
||||
|
||||
/// The currency mechanism.
|
||||
type Currency: ReservableCurrency<Self::AccountId>;
|
||||
|
||||
/// The base amount of currency needed to reserve for creating a recovery configuration.
|
||||
///
|
||||
/// This is held for an additional storage item whose value size is
|
||||
/// `2 + sizeof(BlockNumber, Balance)` bytes.
|
||||
type ConfigDepositBase: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The amount of currency needed per additional user when creating a recovery configuration.
|
||||
///
|
||||
/// This is held for adding `sizeof(AccountId)` bytes more into a pre-existing storage value.
|
||||
type FriendDepositFactor: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The maximum amount of friends allowed in a recovery configuration.
|
||||
type MaxFriends: Get<u16>;
|
||||
|
||||
/// The base amount of currency needed to reserve for starting a recovery.
|
||||
///
|
||||
/// This is primarily held for deterring malicious recovery attempts, and should
|
||||
/// have a value large enough that a bad actor would choose not to place this
|
||||
/// deposit. It also acts to fund additional storage item whose value size is
|
||||
/// `sizeof(BlockNumber, Balance + T * AccountId)` bytes. Where T is a configurable
|
||||
/// threshold.
|
||||
type RecoveryDeposit: Get<BalanceOf<Self>>;
|
||||
}
|
||||
|
||||
/// An active recovery process.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)]
|
||||
pub struct ActiveRecovery<BlockNumber, Balance, AccountId> {
|
||||
/// The block number when the recovery process started.
|
||||
created: BlockNumber,
|
||||
/// The amount held in reserve of the `depositor`,
|
||||
/// To be returned once this recovery process is closed.
|
||||
deposit: Balance,
|
||||
/// The friends which have vouched so far. Always sorted.
|
||||
friends: Vec<AccountId>,
|
||||
}
|
||||
|
||||
/// Configuration for recovering an account.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)]
|
||||
pub struct RecoveryConfig<BlockNumber, Balance, AccountId> {
|
||||
/// The minimum number of blocks since the start of the recovery process before the account
|
||||
/// can be recovered.
|
||||
delay_period: BlockNumber,
|
||||
/// The amount held in reserve of the `depositor`,
|
||||
/// to be returned once this configuration is removed.
|
||||
deposit: Balance,
|
||||
/// The list of friends which can help recover an account. Always sorted.
|
||||
friends: Vec<AccountId>,
|
||||
/// The number of approving friends needed to recover an account.
|
||||
threshold: u16,
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait> as Recovery {
|
||||
/// The set of recoverable accounts and their recovery configuration.
|
||||
pub Recoverable get(fn recovery_config):
|
||||
map T::AccountId => Option<RecoveryConfig<T::BlockNumber, BalanceOf<T>, T::AccountId>>;
|
||||
/// Active recovery attempts.
|
||||
///
|
||||
/// First account is the account to be recovered, and the second account
|
||||
/// is the user trying to recover the account.
|
||||
pub ActiveRecoveries get(fn active_recovery):
|
||||
double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId =>
|
||||
Option<ActiveRecovery<T::BlockNumber, BalanceOf<T>, T::AccountId>>;
|
||||
/// The final list of recovered accounts.
|
||||
///
|
||||
/// Map from the recovered account to the user who can access it.
|
||||
pub Recovered get(fn recovered_account): map T::AccountId => Option<T::AccountId>;
|
||||
}
|
||||
}
|
||||
|
||||
decl_event! {
|
||||
/// Events type.
|
||||
pub enum Event<T> where
|
||||
AccountId = <T as system::Trait>::AccountId,
|
||||
{
|
||||
/// A recovery process has been set up for an account
|
||||
RecoveryCreated(AccountId),
|
||||
/// A recovery process has been initiated for account_1 by account_2
|
||||
RecoveryInitiated(AccountId, AccountId),
|
||||
/// A recovery process for account_1 by account_2 has been vouched for by account_3
|
||||
RecoveryVouched(AccountId, AccountId, AccountId),
|
||||
/// A recovery process for account_1 by account_2 has been closed
|
||||
RecoveryClosed(AccountId, AccountId),
|
||||
/// Account_1 has been successfully recovered by account_2
|
||||
AccountRecovered(AccountId, AccountId),
|
||||
/// A recovery process has been removed for an account
|
||||
RecoveryRemoved(AccountId),
|
||||
}
|
||||
}
|
||||
|
||||
decl_error! {
|
||||
pub enum Error for Module<T: Trait> {
|
||||
/// User is not allowed to make a call on behalf of this account
|
||||
NotAllowed,
|
||||
/// Threshold must be greater than zero
|
||||
ZeroThreshold,
|
||||
/// Friends list must be greater than zero and threshold
|
||||
NotEnoughFriends,
|
||||
/// Friends list must be less than max friends
|
||||
MaxFriends,
|
||||
/// Friends list must be sorted and free of duplicates
|
||||
NotSorted,
|
||||
/// This account is not set up for recovery
|
||||
NotRecoverable,
|
||||
/// This account is already set up for recovery
|
||||
AlreadyRecoverable,
|
||||
/// A recovery process has already started for this account
|
||||
AlreadyStarted,
|
||||
/// A recovery process has not started for this rescuer
|
||||
NotStarted,
|
||||
/// This account is not a friend who can vouch
|
||||
NotFriend,
|
||||
/// The friend must wait until the delay period to vouch for this recovery
|
||||
DelayPeriod,
|
||||
/// This user has already vouched for this recovery
|
||||
AlreadyVouched,
|
||||
/// The threshold for recovering this account has not been met
|
||||
Threshold,
|
||||
/// There are still active recovery attempts that need to be closed
|
||||
StillActive,
|
||||
/// There was an overflow in a calculation
|
||||
Overflow,
|
||||
}
|
||||
}
|
||||
|
||||
decl_module! {
|
||||
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
|
||||
/// Deposit one of this module's events by using the default implementation.
|
||||
fn deposit_event() = default;
|
||||
|
||||
/// Send a call through a recovered account.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and registered to
|
||||
/// be able to make calls on behalf of the recovered account.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `account`: The recovered account you want to make a call on-behalf-of.
|
||||
/// - `call`: The call you want to make with the recovered account.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - The weight of the `call`.
|
||||
/// - One storage lookup to check account is recovered by `who`. O(1)
|
||||
/// # </weight>
|
||||
#[weight = <Passthrough<T::AccountId, <T as Trait>::Call>>::new()]
|
||||
fn as_recovered(origin,
|
||||
account: T::AccountId,
|
||||
call: Box<<T as Trait>::Call>
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check `who` is allowed to make a call on behalf of `account`
|
||||
ensure!(Self::recovered_account(&account) == Some(who), Error::<T>::NotAllowed);
|
||||
call.dispatch(frame_system::RawOrigin::Signed(account).into())
|
||||
}
|
||||
|
||||
/// Allow ROOT to bypass the recovery process and set an a rescuer account
|
||||
/// for a lost account directly.
|
||||
///
|
||||
/// The dispatch origin for this call must be _ROOT_.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `lost`: The "lost account" to be recovered.
|
||||
/// - `rescuer`: The "rescuer account" which can call as the lost account.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - One storage write O(1)
|
||||
/// - One event
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(10_000)]
|
||||
fn set_recovered(origin, lost: T::AccountId, rescuer: T::AccountId) {
|
||||
ensure_root(origin)?;
|
||||
// Create the recovery storage item.
|
||||
<Recovered<T>>::insert(&lost, &rescuer);
|
||||
Self::deposit_event(RawEvent::AccountRecovered(lost, rescuer));
|
||||
}
|
||||
|
||||
/// Create a recovery configuration for your account. This makes your account recoverable.
|
||||
///
|
||||
/// Payment: `ConfigDepositBase` + `FriendDepositFactor` * #_of_friends balance
|
||||
/// will be reserved for storing the recovery configuration. This deposit is returned
|
||||
/// in full when the user calls `remove_recovery`.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `friends`: A list of friends you trust to vouch for recovery attempts.
|
||||
/// Should be ordered and contain no duplicate values.
|
||||
/// - `threshold`: The number of friends that must vouch for a recovery attempt
|
||||
/// before the account can be recovered. Should be less than or equal to
|
||||
/// the length of the list of friends.
|
||||
/// - `delay_period`: The number of blocks after a recovery attempt is initialized
|
||||
/// that needs to pass before the account can be recovered.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - Key: F (len of friends)
|
||||
/// - One storage read to check that account is not already recoverable. O(1).
|
||||
/// - A check that the friends list is sorted and unique. O(F)
|
||||
/// - One currency reserve operation. O(X)
|
||||
/// - One storage write. O(1). Codec O(F).
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(F + X)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(100_000)]
|
||||
fn create_recovery(origin,
|
||||
friends: Vec<T::AccountId>,
|
||||
threshold: u16,
|
||||
delay_period: T::BlockNumber
|
||||
) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check account is not already set up for recovery
|
||||
ensure!(!<Recoverable<T>>::exists(&who), Error::<T>::AlreadyRecoverable);
|
||||
// Check user input is valid
|
||||
ensure!(threshold >= 1, Error::<T>::ZeroThreshold);
|
||||
ensure!(!friends.is_empty(), Error::<T>::NotEnoughFriends);
|
||||
ensure!(threshold as usize <= friends.len(), Error::<T>::NotEnoughFriends);
|
||||
let max_friends = T::MaxFriends::get() as usize;
|
||||
ensure!(friends.len() <= max_friends, Error::<T>::MaxFriends);
|
||||
ensure!(Self::is_sorted_and_unique(&friends), Error::<T>::NotSorted);
|
||||
// Total deposit is base fee + number of friends * factor fee
|
||||
let friend_deposit = T::FriendDepositFactor::get()
|
||||
.checked_mul(&friends.len().saturated_into())
|
||||
.ok_or(Error::<T>::Overflow)?;
|
||||
let total_deposit = T::ConfigDepositBase::get()
|
||||
.checked_add(&friend_deposit)
|
||||
.ok_or(Error::<T>::Overflow)?;
|
||||
// Reserve the deposit
|
||||
T::Currency::reserve(&who, total_deposit)?;
|
||||
// Create the recovery configuration
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period,
|
||||
deposit: total_deposit,
|
||||
friends,
|
||||
threshold,
|
||||
};
|
||||
// Create the recovery configuration storage item
|
||||
<Recoverable<T>>::insert(&who, recovery_config);
|
||||
Self::deposit_event(RawEvent::RecoveryCreated(who));
|
||||
}
|
||||
|
||||
/// Initiate the process for recovering a recoverable account.
|
||||
///
|
||||
/// Payment: `RecoveryDeposit` balance will be reserved for initiating the
|
||||
/// recovery process. This deposit will always be repatriated to the account
|
||||
/// trying to be recovered. See `close_recovery`.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `account`: The lost account that you want to recover. This account
|
||||
/// needs to be recoverable (i.e. have a recovery configuration).
|
||||
///
|
||||
/// # <weight>
|
||||
/// - One storage read to check that account is recoverable. O(F)
|
||||
/// - One storage read to check that this recovery process hasn't already started. O(1)
|
||||
/// - One currency reserve operation. O(X)
|
||||
/// - One storage read to get the current block number. O(1)
|
||||
/// - One storage write. O(1).
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(F + X)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(100_000)]
|
||||
fn initiate_recovery(origin, account: T::AccountId) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check that the account is recoverable
|
||||
ensure!(<Recoverable<T>>::exists(&account), Error::<T>::NotRecoverable);
|
||||
// Check that the recovery process has not already been started
|
||||
ensure!(!<ActiveRecoveries<T>>::exists(&account, &who), Error::<T>::AlreadyStarted);
|
||||
// Take recovery deposit
|
||||
let recovery_deposit = T::RecoveryDeposit::get();
|
||||
T::Currency::reserve(&who, recovery_deposit)?;
|
||||
// Create an active recovery status
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: <system::Module<T>>::block_number(),
|
||||
deposit: recovery_deposit,
|
||||
friends: vec![],
|
||||
};
|
||||
// Create the active recovery storage item
|
||||
<ActiveRecoveries<T>>::insert(&account, &who, recovery_status);
|
||||
Self::deposit_event(RawEvent::RecoveryInitiated(account, who));
|
||||
}
|
||||
|
||||
/// Allow a "friend" of a recoverable account to vouch for an active recovery
|
||||
/// process for that account.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and must be a "friend"
|
||||
/// for the recoverable account.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `lost`: The lost account that you want to recover.
|
||||
/// - `rescuer`: The account trying to rescue the lost account that you
|
||||
/// want to vouch for.
|
||||
///
|
||||
/// The combination of these two parameters must point to an active recovery
|
||||
/// process.
|
||||
///
|
||||
/// # <weight>
|
||||
/// Key: F (len of friends in config), V (len of vouching friends)
|
||||
/// - One storage read to get the recovery configuration. O(1), Codec O(F)
|
||||
/// - One storage read to get the active recovery process. O(1), Codec O(V)
|
||||
/// - One binary search to confirm caller is a friend. O(logF)
|
||||
/// - One binary search to confirm caller has not already vouched. O(logV)
|
||||
/// - One storage write. O(1), Codec O(V).
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(F + logF + V + logV)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(100_000)]
|
||||
fn vouch_recovery(origin, lost: T::AccountId, rescuer: T::AccountId) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Get the recovery configuration for the lost account.
|
||||
let recovery_config = Self::recovery_config(&lost).ok_or(Error::<T>::NotRecoverable)?;
|
||||
// Get the active recovery process for the rescuer.
|
||||
let mut active_recovery = Self::active_recovery(&lost, &rescuer).ok_or(Error::<T>::NotStarted)?;
|
||||
// Make sure the voter is a friend
|
||||
ensure!(Self::is_friend(&recovery_config.friends, &who), Error::<T>::NotFriend);
|
||||
// Either insert the vouch, or return an error that the user already vouched.
|
||||
match active_recovery.friends.binary_search(&who) {
|
||||
Ok(_pos) => Err(Error::<T>::AlreadyVouched)?,
|
||||
Err(pos) => active_recovery.friends.insert(pos, who.clone()),
|
||||
}
|
||||
// Update storage with the latest details
|
||||
<ActiveRecoveries<T>>::insert(&lost, &rescuer, active_recovery);
|
||||
Self::deposit_event(RawEvent::RecoveryVouched(lost, rescuer, who));
|
||||
}
|
||||
|
||||
/// Allow a successful rescuer to claim their recovered account.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and must be a "rescuer"
|
||||
/// who has successfully completed the account recovery process: collected
|
||||
/// `threshold` or more vouches, waited `delay_period` blocks since initiation.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `account`: The lost account that you want to claim has been successfully
|
||||
/// recovered by you.
|
||||
///
|
||||
/// # <weight>
|
||||
/// Key: F (len of friends in config), V (len of vouching friends)
|
||||
/// - One storage read to get the recovery configuration. O(1), Codec O(F)
|
||||
/// - One storage read to get the active recovery process. O(1), Codec O(V)
|
||||
/// - One storage read to get the current block number. O(1)
|
||||
/// - One storage write. O(1), Codec O(V).
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(F + V)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(100_000)]
|
||||
fn claim_recovery(origin, account: T::AccountId) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Get the recovery configuration for the lost account
|
||||
let recovery_config = Self::recovery_config(&account).ok_or(Error::<T>::NotRecoverable)?;
|
||||
// Get the active recovery process for the rescuer
|
||||
let active_recovery = Self::active_recovery(&account, &who).ok_or(Error::<T>::NotStarted)?;
|
||||
// Make sure the delay period has passed
|
||||
let current_block_number = <system::Module<T>>::block_number();
|
||||
let recoverable_block_number = active_recovery.created
|
||||
.checked_add(&recovery_config.delay_period)
|
||||
.ok_or(Error::<T>::Overflow)?;
|
||||
ensure!(recoverable_block_number <= current_block_number, Error::<T>::DelayPeriod);
|
||||
// Make sure the threshold is met
|
||||
ensure!(
|
||||
recovery_config.threshold as usize <= active_recovery.friends.len(),
|
||||
Error::<T>::Threshold
|
||||
);
|
||||
// Create the recovery storage item
|
||||
<Recovered<T>>::insert(&account, &who);
|
||||
Self::deposit_event(RawEvent::AccountRecovered(account, who));
|
||||
}
|
||||
|
||||
/// As the controller of a recoverable account, close an active recovery
|
||||
/// process for your account.
|
||||
///
|
||||
/// Payment: By calling this function, the recoverable account will receive
|
||||
/// the recovery deposit `RecoveryDeposit` placed by the rescuer.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and must be a
|
||||
/// recoverable account with an active recovery process for it.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `rescuer`: The account trying to rescue this recoverable account.
|
||||
///
|
||||
/// # <weight>
|
||||
/// Key: V (len of vouching friends)
|
||||
/// - One storage read/remove to get the active recovery process. O(1), Codec O(V)
|
||||
/// - One balance call to repatriate reserved. O(X)
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(V + X)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(30_000)]
|
||||
fn close_recovery(origin, rescuer: T::AccountId) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Take the active recovery process started by the rescuer for this account.
|
||||
let active_recovery = <ActiveRecoveries<T>>::take(&who, &rescuer).ok_or(Error::<T>::NotStarted)?;
|
||||
// Move the reserved funds from the rescuer to the rescued account.
|
||||
// Acts like a slashing mechanism for those who try to maliciously recover accounts.
|
||||
let _ = T::Currency::repatriate_reserved(&rescuer, &who, active_recovery.deposit);
|
||||
Self::deposit_event(RawEvent::RecoveryClosed(who, rescuer));
|
||||
}
|
||||
|
||||
/// Remove the recovery process for your account.
|
||||
///
|
||||
/// NOTE: The user must make sure to call `close_recovery` on all active
|
||||
/// recovery attempts before calling this function else it will fail.
|
||||
///
|
||||
/// Payment: By calling this function the recoverable account will unreserve
|
||||
/// their recovery configuration deposit.
|
||||
/// (`ConfigDepositBase` + `FriendDepositFactor` * #_of_friends)
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ and must be a
|
||||
/// recoverable account (i.e. has a recovery configuration).
|
||||
///
|
||||
/// # <weight>
|
||||
/// Key: F (len of friends)
|
||||
/// - One storage read to get the prefix iterator for active recoveries. O(1)
|
||||
/// - One storage read/remove to get the recovery configuration. O(1), Codec O(F)
|
||||
/// - One balance call to unreserved. O(X)
|
||||
/// - One event.
|
||||
///
|
||||
/// Total Complexity: O(F + X)
|
||||
/// # </weight>
|
||||
#[weight = SimpleDispatchInfo::FixedNormal(30_000)]
|
||||
fn remove_recovery(origin) {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check there are no active recoveries
|
||||
let mut active_recoveries = <ActiveRecoveries<T>>::iter_prefix(&who);
|
||||
ensure!(active_recoveries.next().is_none(), Error::<T>::StillActive);
|
||||
// Take the recovery configuration for this account.
|
||||
let recovery_config = <Recoverable<T>>::take(&who).ok_or(Error::<T>::NotRecoverable)?;
|
||||
// Unreserve the initial deposit for the recovery configuration.
|
||||
T::Currency::unreserve(&who, recovery_config.deposit);
|
||||
Self::deposit_event(RawEvent::RecoveryRemoved(who));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
/// Check that friends list is sorted and has no duplicates.
|
||||
fn is_sorted_and_unique(friends: &Vec<T::AccountId>) -> bool {
|
||||
friends.windows(2).all(|w| w[0] < w[1])
|
||||
}
|
||||
|
||||
/// Check that a user is a friend in the friends list.
|
||||
fn is_friend(friends: &Vec<T::AccountId>, friend: &T::AccountId) -> bool {
|
||||
friends.binary_search(&friend).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> OnReapAccount<T::AccountId> for Module<T> {
|
||||
/// Remove any existing access another account might have when the account is reaped.
|
||||
/// This removes the final storage item managed by this module for any given account.
|
||||
fn on_reap_account(who: &T::AccountId) {
|
||||
<Recovered<T>>::remove(who);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple pass through for the weight functions.
|
||||
struct Passthrough<AccountId, Call>(sp_std::marker::PhantomData<(AccountId, Call)>);
|
||||
|
||||
impl<AccountId, Call> Passthrough<AccountId, Call> {
|
||||
fn new() -> Self { Self(Default::default()) }
|
||||
}
|
||||
impl<AccountId, Call: GetDispatchInfo> WeighData<(&AccountId, &Box<Call>)> for Passthrough<AccountId, Call> {
|
||||
fn weigh_data(&self, (_, call): (&AccountId, &Box<Call>)) -> Weight {
|
||||
call.get_dispatch_info().weight + 10_000
|
||||
}
|
||||
}
|
||||
impl<AccountId, Call: GetDispatchInfo> ClassifyDispatch<(&AccountId, &Box<Call>)> for Passthrough<AccountId, Call> {
|
||||
fn classify_dispatch(&self, (_, call): (&AccountId, &Box<Call>)) -> DispatchClass {
|
||||
call.get_dispatch_info().class
|
||||
}
|
||||
}
|
||||
impl<AccountId, Call: GetDispatchInfo> PaysFee<(&AccountId, &Box<Call>)> for Passthrough<AccountId, Call> {
|
||||
fn pays_fee(&self, (_, call): (&AccountId, &Box<Call>)) -> bool {
|
||||
call.get_dispatch_info().pays_fee
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Test utilities
|
||||
|
||||
use super::*;
|
||||
|
||||
use frame_support::{
|
||||
impl_outer_origin, impl_outer_dispatch, impl_outer_event, parameter_types,
|
||||
weights::Weight,
|
||||
};
|
||||
use sp_core::H256;
|
||||
// The testing primitives are very useful for avoiding having to work with signatures
|
||||
// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required.
|
||||
use sp_runtime::{
|
||||
Perbill, traits::{BlakeTwo256, IdentityLookup, OnInitialize, OnFinalize}, testing::Header,
|
||||
};
|
||||
use crate as recovery;
|
||||
|
||||
impl_outer_origin! {
|
||||
pub enum Origin for Test where system = frame_system {}
|
||||
}
|
||||
|
||||
impl_outer_event! {
|
||||
pub enum TestEvent for Test {
|
||||
pallet_balances<T>,
|
||||
recovery<T>,
|
||||
}
|
||||
}
|
||||
impl_outer_dispatch! {
|
||||
pub enum Call for Test where origin: Origin {
|
||||
pallet_balances::Balances,
|
||||
recovery::Recovery,
|
||||
}
|
||||
}
|
||||
|
||||
// For testing the module, we construct most of a mock runtime. This means
|
||||
// first constructing a configuration type (`Test`) which `impl`s each of the
|
||||
// configuration traits of modules we want to use.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Test;
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Trait for Test {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Call = Call;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = TestEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type MaximumBlockWeight = MaximumBlockWeight;
|
||||
type MaximumBlockLength = MaximumBlockLength;
|
||||
type AvailableBlockRatio = AvailableBlockRatio;
|
||||
type Version = ();
|
||||
type ModuleToIndex = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 1;
|
||||
pub const TransferFee: u64 = 0;
|
||||
pub const CreationFee: u64 = 0;
|
||||
}
|
||||
|
||||
impl pallet_balances::Trait for Test {
|
||||
type Balance = u128;
|
||||
type OnFreeBalanceZero = ();
|
||||
type OnReapAccount = (System, Recovery);
|
||||
type OnNewAccount = ();
|
||||
type Event = TestEvent;
|
||||
type TransferPayment = ();
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type TransferFee = TransferFee;
|
||||
type CreationFee = CreationFee;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ConfigDepositBase: u64 = 10;
|
||||
pub const FriendDepositFactor: u64 = 1;
|
||||
pub const MaxFriends: u16 = 3;
|
||||
pub const RecoveryDeposit: u64 = 10;
|
||||
}
|
||||
|
||||
impl Trait for Test {
|
||||
type Event = TestEvent;
|
||||
type Call = Call;
|
||||
type Currency = Balances;
|
||||
type ConfigDepositBase = ConfigDepositBase;
|
||||
type FriendDepositFactor = FriendDepositFactor;
|
||||
type MaxFriends = MaxFriends;
|
||||
type RecoveryDeposit = RecoveryDeposit;
|
||||
}
|
||||
|
||||
pub type Recovery = Module<Test>;
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Balances = pallet_balances::Module<Test>;
|
||||
|
||||
pub type BalancesCall = pallet_balances::Call<Test>;
|
||||
pub type RecoveryCall = super::Call<Test>;
|
||||
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
|
||||
pallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)],
|
||||
vesting: vec![],
|
||||
}.assimilate_storage(&mut t).unwrap();
|
||||
t.into()
|
||||
}
|
||||
|
||||
/// Run until a particular block.
|
||||
pub fn run_to_block(n: u64) {
|
||||
while System::block_number() < n {
|
||||
if System::block_number() > 1 {
|
||||
System::on_finalize(System::block_number());
|
||||
}
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
System::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Substrate is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Substrate is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tests for the module.
|
||||
|
||||
use super::*;
|
||||
use mock::{
|
||||
Recovery, Balances, Test, Origin, Call, BalancesCall, RecoveryCall,
|
||||
new_test_ext, run_to_block
|
||||
};
|
||||
use sp_runtime::traits::{BadOrigin};
|
||||
use frame_support::{
|
||||
assert_noop, assert_ok,
|
||||
traits::{Currency},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Nothing in storage to start
|
||||
assert_eq!(Recovery::recovered_account(&1), None);
|
||||
assert_eq!(Recovery::active_recovery(&1, &2), None);
|
||||
assert_eq!(Recovery::recovery_config(&1), None);
|
||||
// Everyone should have starting balance of 100
|
||||
assert_eq!(Balances::free_balance(&1), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_recovered_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Not accessible by a normal user
|
||||
assert_noop!(Recovery::set_recovered(Origin::signed(1), 5, 1), BadOrigin);
|
||||
// Root can set a recovered account though
|
||||
assert_ok!(Recovery::set_recovered(Origin::ROOT, 5, 1));
|
||||
// Account 1 should now be able to make a call through account 5
|
||||
let call = Box::new(Call::Balances(BalancesCall::transfer(1, 100)));
|
||||
assert_ok!(Recovery::as_recovered(Origin::signed(1), 5, call));
|
||||
// Account 1 has successfully drained the funds from account 5
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(5), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_lifecycle_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
// Account 5 sets up a recovery configuration on their account
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends, threshold, delay_period));
|
||||
// Some time has passed, and the user lost their keys!
|
||||
run_to_block(10);
|
||||
// Using account 1, the user begins the recovery process to recover the lost account
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Off chain, the user contacts their friends and asks them to vouch for the recovery attempt
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(4), 5, 1));
|
||||
// We met the threshold, lets try to recover the account...?
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::DelayPeriod);
|
||||
// We need to wait at least the delay_period number of blocks before we can recover
|
||||
run_to_block(20);
|
||||
assert_ok!(Recovery::claim_recovery(Origin::signed(1), 5));
|
||||
// Account 1 can use account 5 to close the active recovery process, claiming the deposited
|
||||
// funds used to initiate the recovery process into account 5.
|
||||
let call = Box::new(Call::Recovery(RecoveryCall::close_recovery(1)));
|
||||
assert_ok!(Recovery::as_recovered(Origin::signed(1), 5, call));
|
||||
// Account 1 can then use account 5 to remove the recovery configuration, claiming the
|
||||
// deposited funds used to create the recovery configuration into account 5.
|
||||
let call = Box::new(Call::Recovery(RecoveryCall::remove_recovery()));
|
||||
assert_ok!(Recovery::as_recovered(Origin::signed(1), 5, call));
|
||||
// Account 1 should now be able to make a call through account 5 to get all of their funds
|
||||
assert_eq!(Balances::free_balance(5), 110);
|
||||
let call = Box::new(Call::Balances(BalancesCall::transfer(1, 110)));
|
||||
assert_ok!(Recovery::as_recovered(Origin::signed(1), 5, call));
|
||||
// All funds have been fully recovered!
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(5), 0);
|
||||
// All storage items are removed from the module
|
||||
assert!(!<ActiveRecoveries<Test>>::exists(&5, &1));
|
||||
assert!(!<Recoverable<Test>>::exists(&5));
|
||||
assert!(!<Recovered<Test>>::exists(&5));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_recovery_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
// Account 5 sets up a recovery configuration on their account
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends, threshold, delay_period));
|
||||
// Some time has passed, and account 1 wants to try and attack this account!
|
||||
run_to_block(10);
|
||||
// Using account 1, the malicious user begins the recovery process on account 5
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Off chain, the user **tricks** their friends and asks them to vouch for the recovery
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1)); // shame on you
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 1)); // shame on you
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(4), 5, 1)); // shame on you
|
||||
// We met the threshold, lets try to recover the account...?
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::DelayPeriod);
|
||||
// Account 1 needs to wait...
|
||||
run_to_block(19);
|
||||
// One more block to wait!
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::DelayPeriod);
|
||||
// Account 5 checks their account every `delay_period` and notices the malicious attack!
|
||||
// Account 5 can close the recovery process before account 1 can claim it
|
||||
assert_ok!(Recovery::close_recovery(Origin::signed(5), 1));
|
||||
// By doing so, account 5 has now claimed the deposit originally reserved by account 1
|
||||
assert_eq!(Balances::total_balance(&1), 90);
|
||||
// Thanks for the free money!
|
||||
assert_eq!(Balances::total_balance(&5), 110);
|
||||
// The recovery process has been closed, so account 1 can't make the claim
|
||||
run_to_block(20);
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::NotStarted);
|
||||
// Account 5 can remove their recovery config and pick some better friends
|
||||
assert_ok!(Recovery::remove_recovery(Origin::signed(5)));
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), vec![22, 33, 44], threshold, delay_period));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// No friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![], 1, 0),
|
||||
Error::<Test>::NotEnoughFriends
|
||||
);
|
||||
// Zero threshold
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![2], 0, 0),
|
||||
Error::<Test>::ZeroThreshold
|
||||
);
|
||||
// Threshold greater than friends length
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![2, 3, 4], 4, 0),
|
||||
Error::<Test>::NotEnoughFriends
|
||||
);
|
||||
// Too many friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![1, 2, 3, 4], 4, 0),
|
||||
Error::<Test>::MaxFriends
|
||||
);
|
||||
// Unsorted friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![3, 2, 4], 3, 0),
|
||||
Error::<Test>::NotSorted
|
||||
);
|
||||
// Duplicate friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![2, 2, 4], 3, 0),
|
||||
Error::<Test>::NotSorted
|
||||
);
|
||||
// Already configured
|
||||
assert_ok!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![2, 3, 4], 3, 10)
|
||||
);
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(Origin::signed(5), vec![2, 3, 4], 3, 10),
|
||||
Error::<Test>::AlreadyRecoverable
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_recovery_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
// Account 5 sets up a recovery configuration on their account
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
// Deposit is taken, and scales with the number of friends they pick
|
||||
// Base 10 + 1 per friends = 13 total reserved
|
||||
assert_eq!(Balances::reserved_balance(5), 13);
|
||||
// Recovery configuration is correctly stored
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period,
|
||||
deposit: 13,
|
||||
friends: friends.clone(),
|
||||
threshold,
|
||||
};
|
||||
assert_eq!(Recovery::recovery_config(5), Some(recovery_config));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initiate_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// No recovery process set up for the account
|
||||
assert_noop!(
|
||||
Recovery::initiate_recovery(Origin::signed(1), 5),
|
||||
Error::<Test>::NotRecoverable
|
||||
);
|
||||
// Create a recovery process for next test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
// Same user cannot recover same account twice
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
assert_noop!(Recovery::initiate_recovery(Origin::signed(1), 5), Error::<Test>::AlreadyStarted);
|
||||
// No double deposit
|
||||
assert_eq!(Balances::reserved_balance(&1), 10);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initiate_recovery_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Create a recovery process for the test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
// Recovery can be initiated
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Deposit is reserved
|
||||
assert_eq!(Balances::reserved_balance(&1), 10);
|
||||
// Recovery status object is created correctly
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: 1,
|
||||
deposit: 10,
|
||||
friends: vec![],
|
||||
};
|
||||
assert_eq!(<ActiveRecoveries<Test>>::get(&5, &1), Some(recovery_status));
|
||||
// Multiple users can attempt to recover the same account
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(2), 5));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vouch_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot vouch for non-recoverable account
|
||||
assert_noop!(Recovery::vouch_recovery(Origin::signed(2), 5, 1), Error::<Test>::NotRecoverable);
|
||||
// Create a recovery process for next tests
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
// Cannot vouch a recovery process that has not started
|
||||
assert_noop!(Recovery::vouch_recovery(Origin::signed(2), 5, 1), Error::<Test>::NotStarted);
|
||||
// Initiate a recovery process
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Cannot vouch if you are not a friend
|
||||
assert_noop!(Recovery::vouch_recovery(Origin::signed(22), 5, 1), Error::<Test>::NotFriend);
|
||||
// Cannot vouch twice
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1));
|
||||
assert_noop!(Recovery::vouch_recovery(Origin::signed(2), 5, 1), Error::<Test>::AlreadyVouched);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vouch_recovery_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Create and initiate a recovery process for the test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Vouching works
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1));
|
||||
// Handles out of order vouches
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(4), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 1));
|
||||
// Final recovery status object is updated correctly
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: 1,
|
||||
deposit: 10,
|
||||
friends: vec![2, 3, 4],
|
||||
};
|
||||
assert_eq!(<ActiveRecoveries<Test>>::get(&5, &1), Some(recovery_status));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot claim a non-recoverable account
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::NotRecoverable);
|
||||
// Create a recovery process for the test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
// Cannot claim an account which has not started the recovery process
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::NotStarted);
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
// Cannot claim an account which has not passed the delay period
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::DelayPeriod);
|
||||
run_to_block(11);
|
||||
// Cannot claim an account which has not passed the threshold number of votes
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 1));
|
||||
// Only 2/3 is not good enough
|
||||
assert_noop!(Recovery::claim_recovery(Origin::signed(1), 5), Error::<Test>::Threshold);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_recovery_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Create, initiate, and vouch recovery process for the test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(4), 5, 1));
|
||||
|
||||
run_to_block(11);
|
||||
|
||||
// Account can be recovered.
|
||||
assert_ok!(Recovery::claim_recovery(Origin::signed(1), 5));
|
||||
// Recovered storage item is correctly created
|
||||
assert_eq!(<Recovered<Test>>::get(&5), Some(1));
|
||||
// Account could be re-recovered in the case that the recoverer account also gets lost.
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(4), 5));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(2), 5, 4));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(3), 5, 4));
|
||||
assert_ok!(Recovery::vouch_recovery(Origin::signed(4), 5, 4));
|
||||
|
||||
run_to_block(21);
|
||||
|
||||
// Account is re-recovered.
|
||||
assert_ok!(Recovery::claim_recovery(Origin::signed(4), 5));
|
||||
// Recovered storage item is correctly updated
|
||||
assert_eq!(<Recovered<Test>>::get(&5), Some(4));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot close a non-active recovery
|
||||
assert_noop!(Recovery::close_recovery(Origin::signed(5), 1), Error::<Test>::NotStarted);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_recovery_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot remove an unrecoverable account
|
||||
assert_noop!(Recovery::remove_recovery(Origin::signed(5)), Error::<Test>::NotRecoverable);
|
||||
// Create and initiate a recovery process for the test
|
||||
let friends = vec![2, 3, 4];
|
||||
let threshold = 3;
|
||||
let delay_period = 10;
|
||||
assert_ok!(Recovery::create_recovery(Origin::signed(5), friends.clone(), threshold, delay_period));
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(1), 5));
|
||||
assert_ok!(Recovery::initiate_recovery(Origin::signed(2), 5));
|
||||
// Cannot remove a recovery when there are active recoveries.
|
||||
assert_noop!(Recovery::remove_recovery(Origin::signed(5)), Error::<Test>::StillActive);
|
||||
assert_ok!(Recovery::close_recovery(Origin::signed(5), 1));
|
||||
// Still need to remove one more!
|
||||
assert_noop!(Recovery::remove_recovery(Origin::signed(5)), Error::<Test>::StillActive);
|
||||
assert_ok!(Recovery::close_recovery(Origin::signed(5), 2));
|
||||
// Finally removed
|
||||
assert_ok!(Recovery::remove_recovery(Origin::signed(5)));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user