feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "pezpallet-recovery"
|
||||
version = "28.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "FRAME account recovery pallet"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true }
|
||||
frame = { workspace = true, features = ["runtime"] }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
runtime-benchmarks = [
|
||||
"frame/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
]
|
||||
std = ["codec/std", "frame/std", "pezpallet-balances/std", "scale-info/std"]
|
||||
try-runtime = ["frame/try-runtime", "pezpallet-balances/try-runtime"]
|
||||
@@ -0,0 +1,134 @@
|
||||
# Recovery Pallet
|
||||
|
||||
- [`recovery::Config`](https://docs.rs/pezpallet-recovery/latest/pallet_recovery/trait.Config.html)
|
||||
- [`Call`](https://docs.rs/pezpallet-recovery/latest/pallet_recovery/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 Life Cycle
|
||||
|
||||
The intended life cycle 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 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 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.
|
||||
|
||||
License: Apache-2.0
|
||||
@@ -0,0 +1,469 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::Pallet;
|
||||
use alloc::{boxed::Box, vec, vec::Vec};
|
||||
use frame::benchmarking::prelude::*;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
const DEFAULT_DELAY: u32 = 0;
|
||||
|
||||
fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
|
||||
pezframe_system::Pallet::<T>::assert_last_event(generic_event.into());
|
||||
}
|
||||
|
||||
fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
|
||||
pezframe_system::Pallet::<T>::assert_has_event(generic_event.into());
|
||||
}
|
||||
|
||||
fn get_total_deposit<T: Config>(
|
||||
bounded_friends: &FriendsOf<T>,
|
||||
) -> Option<<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance>
|
||||
{
|
||||
let friend_deposit = T::FriendDepositFactor::get()
|
||||
.checked_mul(&bounded_friends.len().saturated_into())
|
||||
.unwrap();
|
||||
|
||||
T::ConfigDepositBase::get().checked_add(&friend_deposit)
|
||||
}
|
||||
|
||||
fn generate_friends<T: Config>(num: u32) -> Vec<<T as pezframe_system::Config>::AccountId> {
|
||||
// Create friends
|
||||
let mut friends = (0..num).map(|x| account("friend", x, SEED)).collect::<Vec<_>>();
|
||||
// Sort
|
||||
friends.sort();
|
||||
|
||||
for friend in 0..friends.len() {
|
||||
// Top up accounts of friends
|
||||
T::Currency::make_free_balance_be(
|
||||
&friends.get(friend).unwrap(),
|
||||
BalanceOf::<T>::max_value(),
|
||||
);
|
||||
}
|
||||
|
||||
friends
|
||||
}
|
||||
|
||||
fn add_caller_and_generate_friends<T: Config>(
|
||||
caller: T::AccountId,
|
||||
num: u32,
|
||||
) -> Vec<<T as pezframe_system::Config>::AccountId> {
|
||||
// Create friends
|
||||
let mut friends = generate_friends::<T>(num - 1);
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
friends.push(caller);
|
||||
|
||||
// Sort
|
||||
friends.sort();
|
||||
|
||||
friends
|
||||
}
|
||||
|
||||
fn insert_recovery_config_with_max_friends<T: Config>(account: &T::AccountId) {
|
||||
T::Currency::make_free_balance_be(&account, BalanceOf::<T>::max_value());
|
||||
|
||||
let n = T::MaxFriends::get();
|
||||
|
||||
let friends = generate_friends::<T>(n);
|
||||
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
// Get deposit for recovery
|
||||
let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
|
||||
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends,
|
||||
threshold: n as u16,
|
||||
};
|
||||
|
||||
// Reserve deposit for recovery
|
||||
T::Currency::reserve(&account, total_deposit).unwrap();
|
||||
|
||||
<Recoverable<T>>::insert(&account, recovery_config);
|
||||
}
|
||||
|
||||
fn setup_active_recovery_with_max_friends<T: Config>(
|
||||
caller: &T::AccountId,
|
||||
lost_account: &T::AccountId,
|
||||
) {
|
||||
insert_recovery_config_with_max_friends::<T>(&lost_account);
|
||||
let n = T::MaxFriends::get();
|
||||
let friends = generate_friends::<T>(n);
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
let initial_recovery_deposit = T::RecoveryDeposit::get();
|
||||
T::Currency::reserve(caller, initial_recovery_deposit).unwrap();
|
||||
|
||||
let active_recovery = ActiveRecovery {
|
||||
created: DEFAULT_DELAY.into(),
|
||||
deposit: initial_recovery_deposit,
|
||||
friends: bounded_friends,
|
||||
};
|
||||
<ActiveRecoveries<T>>::insert(lost_account, caller, active_recovery);
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn as_recovered() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let recovered_account: T::AccountId = account("recovered_account", 0, SEED);
|
||||
let recovered_account_lookup = T::Lookup::unlookup(recovered_account.clone());
|
||||
let call: <T as Config>::RuntimeCall =
|
||||
pezframe_system::Call::<T>::remark { remark: vec![] }.into();
|
||||
|
||||
Proxy::<T>::insert(&caller, &recovered_account);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller), recovered_account_lookup, Box::new(call))
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn set_recovered() {
|
||||
let lost: T::AccountId = whitelisted_caller();
|
||||
let lost_lookup = T::Lookup::unlookup(lost.clone());
|
||||
let rescuer: T::AccountId = whitelisted_caller();
|
||||
let rescuer_lookup = T::Lookup::unlookup(rescuer.clone());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, lost_lookup, rescuer_lookup);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::AccountRecovered { lost_account: lost, rescuer_account: rescuer }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn create_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
// Create friends
|
||||
let friends = generate_friends::<T>(n);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), friends, n as u16, DEFAULT_DELAY.into());
|
||||
|
||||
assert_last_event::<T>(Event::RecoveryCreated { account: caller }.into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn initiate_recovery() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
let lost_account: T::AccountId = account("lost_account", 0, SEED);
|
||||
let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
|
||||
|
||||
insert_recovery_config_with_max_friends::<T>(&lost_account);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), lost_account_lookup);
|
||||
|
||||
assert_last_event::<T>(
|
||||
Event::RecoveryInitiated { lost_account, rescuer_account: caller }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn vouch_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let lost_account: T::AccountId = account("lost_account", 0, SEED);
|
||||
let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
|
||||
let rescuer_account: T::AccountId = account("rescuer_account", 0, SEED);
|
||||
let rescuer_account_lookup = T::Lookup::unlookup(rescuer_account.clone());
|
||||
|
||||
// Create friends
|
||||
let friends = add_caller_and_generate_friends::<T>(caller.clone(), n);
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
// Get deposit for recovery
|
||||
let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
|
||||
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
threshold: n as u16,
|
||||
};
|
||||
|
||||
// Create the recovery config storage item
|
||||
<Recoverable<T>>::insert(&lost_account, recovery_config.clone());
|
||||
|
||||
// Reserve deposit for recovery
|
||||
T::Currency::reserve(&caller, total_deposit).unwrap();
|
||||
|
||||
// Create an active recovery status
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: generate_friends::<T>(n - 1).try_into().unwrap(),
|
||||
};
|
||||
|
||||
// Create the active recovery storage item
|
||||
<ActiveRecoveries<T>>::insert(&lost_account, &rescuer_account, recovery_status);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), lost_account_lookup, rescuer_account_lookup);
|
||||
assert_last_event::<T>(
|
||||
Event::RecoveryVouched { lost_account, rescuer_account, sender: caller }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn claim_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let lost_account: T::AccountId = account("lost_account", 0, SEED);
|
||||
let lost_account_lookup = T::Lookup::unlookup(lost_account.clone());
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
// Create friends
|
||||
let friends = generate_friends::<T>(n);
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
// Get deposit for recovery
|
||||
let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
|
||||
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period: 0u32.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
threshold: n as u16,
|
||||
};
|
||||
|
||||
// Create the recovery config storage item
|
||||
<Recoverable<T>>::insert(&lost_account, recovery_config.clone());
|
||||
|
||||
// Reserve deposit for recovery
|
||||
T::Currency::reserve(&caller, total_deposit).unwrap();
|
||||
|
||||
// Create an active recovery status
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: 0u32.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
};
|
||||
|
||||
// Create the active recovery storage item
|
||||
<ActiveRecoveries<T>>::insert(&lost_account, &caller, recovery_status);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), lost_account_lookup);
|
||||
assert_last_event::<T>(
|
||||
Event::AccountRecovered { lost_account, rescuer_account: caller }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn close_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let rescuer_account: T::AccountId = account("rescuer_account", 0, SEED);
|
||||
let rescuer_account_lookup = T::Lookup::unlookup(rescuer_account.clone());
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
T::Currency::make_free_balance_be(&rescuer_account, BalanceOf::<T>::max_value());
|
||||
|
||||
// Create friends
|
||||
let friends = generate_friends::<T>(n);
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
// Get deposit for recovery
|
||||
let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
|
||||
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
threshold: n as u16,
|
||||
};
|
||||
|
||||
// Create the recovery config storage item
|
||||
<Recoverable<T>>::insert(&caller, recovery_config.clone());
|
||||
|
||||
// Reserve deposit for recovery
|
||||
T::Currency::reserve(&caller, total_deposit).unwrap();
|
||||
|
||||
// Create an active recovery status
|
||||
let recovery_status = ActiveRecovery {
|
||||
created: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
};
|
||||
|
||||
// Create the active recovery storage item
|
||||
<ActiveRecoveries<T>>::insert(&caller, &rescuer_account, recovery_status);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), rescuer_account_lookup);
|
||||
assert_last_event::<T>(
|
||||
Event::RecoveryClosed { lost_account: caller, rescuer_account }.into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn remove_recovery(n: Linear<1, { T::MaxFriends::get() }>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
// Create friends
|
||||
let friends = generate_friends::<T>(n);
|
||||
let bounded_friends: FriendsOf<T> = friends.try_into().unwrap();
|
||||
|
||||
// Get deposit for recovery
|
||||
let total_deposit = get_total_deposit::<T>(&bounded_friends).unwrap();
|
||||
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period: DEFAULT_DELAY.into(),
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends.clone(),
|
||||
threshold: n as u16,
|
||||
};
|
||||
|
||||
// Create the recovery config storage item
|
||||
<Recoverable<T>>::insert(&caller, recovery_config);
|
||||
|
||||
// Reserve deposit for recovery
|
||||
T::Currency::reserve(&caller, total_deposit).unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
assert_last_event::<T>(Event::RecoveryRemoved { lost_account: caller }.into());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn cancel_recovered() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let account: T::AccountId = account("account", 0, SEED);
|
||||
let account_lookup = T::Lookup::unlookup(account.clone());
|
||||
|
||||
pezframe_system::Pallet::<T>::inc_providers(&caller);
|
||||
|
||||
pezframe_system::Pallet::<T>::inc_consumers(&caller)?;
|
||||
|
||||
Proxy::<T>::insert(&caller, &account);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller), account_lookup);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn poke_deposit(n: Linear<1, { T::MaxFriends::get() }>) -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let lost_account: T::AccountId = account("lost_account", 0, SEED);
|
||||
|
||||
// Fund caller account
|
||||
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
|
||||
|
||||
// 1. Setup recovery config for caller
|
||||
insert_recovery_config_with_max_friends::<T>(&caller);
|
||||
|
||||
// 2. Setup active recovery for lost account
|
||||
setup_active_recovery_with_max_friends::<T>(&caller, &lost_account);
|
||||
|
||||
// 3. Get initial deposits
|
||||
let initial_config = <Recoverable<T>>::get(&caller).unwrap();
|
||||
let initial_config_deposit = initial_config.deposit;
|
||||
let initial_recovery_deposit = T::RecoveryDeposit::get();
|
||||
assert_eq!(
|
||||
T::Currency::reserved_balance(&caller),
|
||||
initial_config_deposit.saturating_add(initial_recovery_deposit)
|
||||
);
|
||||
|
||||
// 4. Artificially increase deposits
|
||||
let increased_config_deposit = initial_config_deposit.saturating_add(2u32.into());
|
||||
let increased_recovery_deposit = initial_recovery_deposit.saturating_add(2u32.into());
|
||||
|
||||
<Recoverable<T>>::try_mutate(&caller, |maybe_config| -> Result<(), BenchmarkError> {
|
||||
let config = maybe_config.as_mut().unwrap();
|
||||
T::Currency::reserve(
|
||||
&caller,
|
||||
increased_config_deposit.saturating_sub(initial_config_deposit),
|
||||
)?;
|
||||
config.deposit = increased_config_deposit;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|_| BenchmarkError::Stop("Failed to mutate storage"))?;
|
||||
|
||||
<ActiveRecoveries<T>>::try_mutate(
|
||||
&lost_account,
|
||||
&caller,
|
||||
|maybe_recovery| -> Result<(), BenchmarkError> {
|
||||
let recovery = maybe_recovery.as_mut().unwrap();
|
||||
T::Currency::reserve(
|
||||
&caller,
|
||||
increased_recovery_deposit.saturating_sub(initial_recovery_deposit),
|
||||
)?;
|
||||
recovery.deposit = increased_recovery_deposit;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.map_err(|_| BenchmarkError::Stop("Failed to mutate storage"))?;
|
||||
|
||||
// 5. Verify increased deposits
|
||||
assert_eq!(
|
||||
T::Currency::reserved_balance(&caller),
|
||||
increased_config_deposit.saturating_add(increased_recovery_deposit)
|
||||
);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), Some(T::Lookup::unlookup(lost_account.clone())));
|
||||
|
||||
// 6. Assert final state
|
||||
assert_eq!(
|
||||
T::Currency::reserved_balance(&caller),
|
||||
initial_config_deposit.saturating_add(initial_recovery_deposit)
|
||||
);
|
||||
|
||||
// 7. Check events were emitted
|
||||
assert_has_event::<T>(
|
||||
Event::DepositPoked {
|
||||
who: caller.clone(),
|
||||
kind: DepositKind::RecoveryConfig,
|
||||
old_deposit: increased_config_deposit,
|
||||
new_deposit: initial_config_deposit,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_has_event::<T>(
|
||||
Event::DepositPoked {
|
||||
who: caller,
|
||||
kind: DepositKind::ActiveRecoveryFor(lost_account),
|
||||
old_deposit: increased_recovery_deposit,
|
||||
new_deposit: initial_recovery_deposit,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,894 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Recovery Pallet
|
||||
//!
|
||||
//! - [`Config`]
|
||||
//! - [`Call`]
|
||||
//!
|
||||
//! ## 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 Life Cycle
|
||||
//!
|
||||
//! The intended life cycle 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 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 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)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
|
||||
use frame::{
|
||||
prelude::*,
|
||||
traits::{Currency, ReservableCurrency},
|
||||
};
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
|
||||
pub type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
|
||||
pub type BlockNumberFromProviderOf<T> =
|
||||
<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
|
||||
pub type FriendsOf<T> =
|
||||
BoundedVec<<T as pezframe_system::Config>::AccountId, <T as Config>::MaxFriends>;
|
||||
|
||||
/// An active recovery process.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct ActiveRecovery<BlockNumber, Balance, Friends> {
|
||||
/// The block number when the recovery process started.
|
||||
pub created: BlockNumber,
|
||||
/// The amount held in reserve of the `depositor`,
|
||||
/// to be returned once this recovery process is closed.
|
||||
pub deposit: Balance,
|
||||
/// The friends which have vouched so far. Always sorted.
|
||||
pub friends: Friends,
|
||||
}
|
||||
|
||||
/// Configuration for recovering an account.
|
||||
#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct RecoveryConfig<BlockNumber, Balance, Friends> {
|
||||
/// The minimum number of blocks since the start of the recovery process before the account
|
||||
/// can be recovered.
|
||||
pub delay_period: BlockNumber,
|
||||
/// The amount held in reserve of the `depositor`,
|
||||
/// to be returned once this configuration is removed.
|
||||
pub deposit: Balance,
|
||||
/// The list of friends which can help recover an account. Always sorted.
|
||||
pub friends: Friends,
|
||||
/// The number of approving friends needed to recover an account.
|
||||
pub threshold: u16,
|
||||
}
|
||||
|
||||
/// The type of deposit
|
||||
#[derive(
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Encode,
|
||||
Decode,
|
||||
DebugNoBound,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
DecodeWithMemTracking,
|
||||
)]
|
||||
pub enum DepositKind<T: Config> {
|
||||
/// Recovery configuration deposit
|
||||
RecoveryConfig,
|
||||
/// Active recovery deposit for an account
|
||||
ActiveRecoveryFor(<T as pezframe_system::Config>::AccountId),
|
||||
}
|
||||
|
||||
#[frame::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// Configuration trait.
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// The overarching call type.
|
||||
type RuntimeCall: Parameter
|
||||
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
|
||||
+ GetDispatchInfo
|
||||
+ From<pezframe_system::Call<Self>>;
|
||||
|
||||
/// Query the current block number.
|
||||
///
|
||||
/// Must return monotonically increasing values when called from consecutive blocks.
|
||||
/// Can be configured to return either:
|
||||
/// - the local block number of the runtime via `pezframe_system::Pallet`
|
||||
/// - a remote block number, eg from the relay chain through `RelaychainDataProvider`
|
||||
/// - an arbitrary value through a custom implementation of the trait
|
||||
///
|
||||
/// There is currently no migration provided to "hot-swap" block number providers and it may
|
||||
/// result in undefined behavior when doing so. Teyrchains are therefore best off setting
|
||||
/// this to their local block number provider if they have the pallet already deployed.
|
||||
///
|
||||
/// Suggested values:
|
||||
/// - Solo- and Relay-chains: `pezframe_system::Pallet`
|
||||
/// - Teyrchains that may produce blocks sparingly or only when needed (on-demand):
|
||||
/// - already have the pallet deployed: `pezframe_system::Pallet`
|
||||
/// - are freshly deploying this pallet: `RelaychainDataProvider`
|
||||
/// - Teyrchains with a reliably block production rate (PLO or bulk-coretime):
|
||||
/// - already have the pallet deployed: `pezframe_system::Pallet`
|
||||
/// - are freshly deploying this pallet: no strong recommendation. Both local and remote
|
||||
/// providers can be used. Relay provider can be a bit better in cases where the
|
||||
/// teyrchain is lagging its block production to avoid clock skew.
|
||||
type BlockNumberProvider: BlockNumberProvider;
|
||||
|
||||
/// 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.
|
||||
#[pallet::constant]
|
||||
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.
|
||||
#[pallet::constant]
|
||||
type FriendDepositFactor: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The maximum amount of friends allowed in a recovery configuration.
|
||||
///
|
||||
/// NOTE: The threshold programmed in this Pallet uses u16, so it does
|
||||
/// not really make sense to have a limit here greater than u16::MAX.
|
||||
/// But also, that is a lot more than you should probably set this value
|
||||
/// to anyway...
|
||||
#[pallet::constant]
|
||||
type MaxFriends: Get<u32>;
|
||||
|
||||
/// 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.
|
||||
#[pallet::constant]
|
||||
type RecoveryDeposit: Get<BalanceOf<Self>>;
|
||||
}
|
||||
|
||||
/// Events type.
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A recovery process has been set up for an account.
|
||||
RecoveryCreated { account: T::AccountId },
|
||||
/// A recovery process has been initiated for lost account by rescuer account.
|
||||
RecoveryInitiated { lost_account: T::AccountId, rescuer_account: T::AccountId },
|
||||
/// A recovery process for lost account by rescuer account has been vouched for by sender.
|
||||
RecoveryVouched {
|
||||
lost_account: T::AccountId,
|
||||
rescuer_account: T::AccountId,
|
||||
sender: T::AccountId,
|
||||
},
|
||||
/// A recovery process for lost account by rescuer account has been closed.
|
||||
RecoveryClosed { lost_account: T::AccountId, rescuer_account: T::AccountId },
|
||||
/// Lost account has been successfully recovered by rescuer account.
|
||||
AccountRecovered { lost_account: T::AccountId, rescuer_account: T::AccountId },
|
||||
/// A recovery process has been removed for an account.
|
||||
RecoveryRemoved { lost_account: T::AccountId },
|
||||
/// A deposit has been updated.
|
||||
DepositPoked {
|
||||
who: T::AccountId,
|
||||
kind: DepositKind<T>,
|
||||
old_deposit: BalanceOf<T>,
|
||||
new_deposit: BalanceOf<T>,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// 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,
|
||||
/// This account is already set up for recovery
|
||||
AlreadyProxy,
|
||||
/// Some internal state is broken.
|
||||
BadState,
|
||||
}
|
||||
|
||||
/// The set of recoverable accounts and their recovery configuration.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn recovery_config)]
|
||||
pub type Recoverable<T: Config> = StorageMap<
|
||||
_,
|
||||
Twox64Concat,
|
||||
T::AccountId,
|
||||
RecoveryConfig<BlockNumberFromProviderOf<T>, BalanceOf<T>, FriendsOf<T>>,
|
||||
>;
|
||||
|
||||
/// Active recovery attempts.
|
||||
///
|
||||
/// First account is the account to be recovered, and the second account
|
||||
/// is the user trying to recover the account.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn active_recovery)]
|
||||
pub type ActiveRecoveries<T: Config> = StorageDoubleMap<
|
||||
_,
|
||||
Twox64Concat,
|
||||
T::AccountId,
|
||||
Twox64Concat,
|
||||
T::AccountId,
|
||||
ActiveRecovery<BlockNumberFromProviderOf<T>, BalanceOf<T>, FriendsOf<T>>,
|
||||
>;
|
||||
|
||||
/// The list of allowed proxy accounts.
|
||||
///
|
||||
/// Map from the user who can access it to the recovered account.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn proxy)]
|
||||
pub type Proxy<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// 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.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight({
|
||||
let dispatch_info = call.get_dispatch_info();
|
||||
(
|
||||
T::WeightInfo::as_recovered().saturating_add(dispatch_info.call_weight),
|
||||
dispatch_info.class,
|
||||
)})]
|
||||
pub fn as_recovered(
|
||||
origin: OriginFor<T>,
|
||||
account: AccountIdLookupOf<T>,
|
||||
call: Box<<T as Config>::RuntimeCall>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let account = T::Lookup::lookup(account)?;
|
||||
// Check `who` is allowed to make a call on behalf of `account`
|
||||
let target = Self::proxy(&who).ok_or(Error::<T>::NotAllowed)?;
|
||||
ensure!(target == account, Error::<T>::NotAllowed);
|
||||
call.dispatch(pezframe_system::RawOrigin::Signed(account).into())
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.error)
|
||||
}
|
||||
|
||||
/// Allow ROOT to bypass the recovery process and set 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.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::set_recovered())]
|
||||
pub fn set_recovered(
|
||||
origin: OriginFor<T>,
|
||||
lost: AccountIdLookupOf<T>,
|
||||
rescuer: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
let lost = T::Lookup::lookup(lost)?;
|
||||
let rescuer = T::Lookup::lookup(rescuer)?;
|
||||
// Create the recovery storage item.
|
||||
<Proxy<T>>::insert(&rescuer, &lost);
|
||||
Self::deposit_event(Event::<T>::AccountRecovered {
|
||||
lost_account: lost,
|
||||
rescuer_account: rescuer,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::create_recovery(friends.len() as u32))]
|
||||
pub fn create_recovery(
|
||||
origin: OriginFor<T>,
|
||||
friends: Vec<T::AccountId>,
|
||||
threshold: u16,
|
||||
delay_period: BlockNumberFromProviderOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check account is not already set up for recovery
|
||||
ensure!(!<Recoverable<T>>::contains_key(&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 bounded_friends: FriendsOf<T> =
|
||||
friends.try_into().map_err(|_| Error::<T>::MaxFriends)?;
|
||||
ensure!(Self::is_sorted_and_unique(&bounded_friends), Error::<T>::NotSorted);
|
||||
// Calculate total deposit required
|
||||
let total_deposit = Self::get_recovery_config_deposit(bounded_friends.len())?;
|
||||
// Reserve the deposit
|
||||
T::Currency::reserve(&who, total_deposit)?;
|
||||
// Create the recovery configuration
|
||||
let recovery_config = RecoveryConfig {
|
||||
delay_period,
|
||||
deposit: total_deposit,
|
||||
friends: bounded_friends,
|
||||
threshold,
|
||||
};
|
||||
// Create the recovery configuration storage item
|
||||
<Recoverable<T>>::insert(&who, recovery_config);
|
||||
|
||||
Self::deposit_event(Event::<T>::RecoveryCreated { account: who });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(T::WeightInfo::initiate_recovery())]
|
||||
pub fn initiate_recovery(
|
||||
origin: OriginFor<T>,
|
||||
account: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let account = T::Lookup::lookup(account)?;
|
||||
// Check that the account is recoverable
|
||||
ensure!(<Recoverable<T>>::contains_key(&account), Error::<T>::NotRecoverable);
|
||||
// Check that the recovery process has not already been started
|
||||
ensure!(
|
||||
!<ActiveRecoveries<T>>::contains_key(&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: T::BlockNumberProvider::current_block_number(),
|
||||
deposit: recovery_deposit,
|
||||
friends: Default::default(),
|
||||
};
|
||||
// Create the active recovery storage item
|
||||
<ActiveRecoveries<T>>::insert(&account, &who, recovery_status);
|
||||
Self::deposit_event(Event::<T>::RecoveryInitiated {
|
||||
lost_account: account,
|
||||
rescuer_account: who,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(T::WeightInfo::vouch_recovery(T::MaxFriends::get()))]
|
||||
pub fn vouch_recovery(
|
||||
origin: OriginFor<T>,
|
||||
lost: AccountIdLookupOf<T>,
|
||||
rescuer: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let lost = T::Lookup::lookup(lost)?;
|
||||
let rescuer = T::Lookup::lookup(rescuer)?;
|
||||
// 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) => return Err(Error::<T>::AlreadyVouched.into()),
|
||||
Err(pos) => active_recovery
|
||||
.friends
|
||||
.try_insert(pos, who.clone())
|
||||
.map_err(|_| Error::<T>::MaxFriends)?,
|
||||
}
|
||||
// Update storage with the latest details
|
||||
<ActiveRecoveries<T>>::insert(&lost, &rescuer, active_recovery);
|
||||
Self::deposit_event(Event::<T>::RecoveryVouched {
|
||||
lost_account: lost,
|
||||
rescuer_account: rescuer,
|
||||
sender: who,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight(T::WeightInfo::claim_recovery(T::MaxFriends::get()))]
|
||||
pub fn claim_recovery(
|
||||
origin: OriginFor<T>,
|
||||
account: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let account = T::Lookup::lookup(account)?;
|
||||
// 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)?;
|
||||
ensure!(!Proxy::<T>::contains_key(&who), Error::<T>::AlreadyProxy);
|
||||
// Make sure the delay period has passed
|
||||
let current_block_number = T::BlockNumberProvider::current_block_number();
|
||||
let recoverable_block_number = active_recovery
|
||||
.created
|
||||
.checked_add(&recovery_config.delay_period)
|
||||
.ok_or(ArithmeticError::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
|
||||
);
|
||||
pezframe_system::Pallet::<T>::inc_consumers(&who).map_err(|_| Error::<T>::BadState)?;
|
||||
// Create the recovery storage item
|
||||
Proxy::<T>::insert(&who, &account);
|
||||
Self::deposit_event(Event::<T>::AccountRecovered {
|
||||
lost_account: account,
|
||||
rescuer_account: who,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[pallet::call_index(6)]
|
||||
#[pallet::weight(T::WeightInfo::close_recovery(T::MaxFriends::get()))]
|
||||
pub fn close_recovery(
|
||||
origin: OriginFor<T>,
|
||||
rescuer: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let rescuer = T::Lookup::lookup(rescuer)?;
|
||||
// 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 res = T::Currency::repatriate_reserved(
|
||||
&rescuer,
|
||||
&who,
|
||||
active_recovery.deposit,
|
||||
BalanceStatus::Free,
|
||||
);
|
||||
debug_assert!(res.is_ok());
|
||||
Self::deposit_event(Event::<T>::RecoveryClosed {
|
||||
lost_account: who,
|
||||
rescuer_account: rescuer,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the recovery process for your account. Recovered accounts are still accessible.
|
||||
///
|
||||
/// 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).
|
||||
#[pallet::call_index(7)]
|
||||
#[pallet::weight(T::WeightInfo::remove_recovery(T::MaxFriends::get()))]
|
||||
pub fn remove_recovery(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
// Check there are no active recoveries
|
||||
let mut active_recoveries = <ActiveRecoveries<T>>::iter_prefix_values(&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(Event::<T>::RecoveryRemoved { lost_account: who });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel the ability to use `as_recovered` for `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 are able to call on-behalf-of.
|
||||
#[pallet::call_index(8)]
|
||||
#[pallet::weight(T::WeightInfo::cancel_recovered())]
|
||||
pub fn cancel_recovered(
|
||||
origin: OriginFor<T>,
|
||||
account: AccountIdLookupOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let account = T::Lookup::lookup(account)?;
|
||||
// Check `who` is allowed to make a call on behalf of `account`
|
||||
ensure!(Self::proxy(&who) == Some(account), Error::<T>::NotAllowed);
|
||||
Proxy::<T>::remove(&who);
|
||||
|
||||
pezframe_system::Pallet::<T>::dec_consumers(&who);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poke deposits for recovery configurations and / or active recoveries.
|
||||
///
|
||||
/// This can be used by accounts to possibly lower their locked amount.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `maybe_account`: Optional recoverable account for which you have an active recovery
|
||||
/// and want to adjust the deposit for the active recovery.
|
||||
///
|
||||
/// This function checks both recovery configuration deposit and active recovery deposits
|
||||
/// of the caller:
|
||||
/// - If the caller has created a recovery configuration, checks and adjusts its deposit
|
||||
/// - If the caller has initiated any active recoveries, and provides the account in
|
||||
/// `maybe_account`, checks and adjusts those deposits
|
||||
///
|
||||
/// If any deposit is updated, the difference will be reserved/unreserved from the caller's
|
||||
/// account.
|
||||
///
|
||||
/// The transaction is made free if any deposit is updated and paid otherwise.
|
||||
///
|
||||
/// Emits `DepositPoked` if any deposit is updated.
|
||||
/// Multiple events may be emitted in case both types of deposits are updated.
|
||||
#[pallet::call_index(9)]
|
||||
#[pallet::weight(T::WeightInfo::poke_deposit(T::MaxFriends::get()))]
|
||||
pub fn poke_deposit(
|
||||
origin: OriginFor<T>,
|
||||
maybe_account: Option<AccountIdLookupOf<T>>,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let who = ensure_signed(origin)?;
|
||||
let mut deposit_updated = false;
|
||||
|
||||
// Check and update recovery config deposit
|
||||
deposit_updated |= Self::poke_recovery_config_deposit(&who)?;
|
||||
|
||||
// Check and update active recovery deposit
|
||||
if let Some(lost_account) = maybe_account {
|
||||
let lost_account = T::Lookup::lookup(lost_account)?;
|
||||
deposit_updated |= Self::poke_active_recovery_deposit(&who, &lost_account)?;
|
||||
}
|
||||
|
||||
Ok(if deposit_updated { Pays::No } else { Pays::Yes }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<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()
|
||||
}
|
||||
|
||||
/// Helper function to calculate recovery config deposit
|
||||
/// Total deposit is base fee + number of friends * factor fee
|
||||
fn get_recovery_config_deposit(friends_count: usize) -> Result<BalanceOf<T>, DispatchError> {
|
||||
let friend_deposit = T::FriendDepositFactor::get()
|
||||
.checked_mul(&friends_count.saturated_into())
|
||||
.ok_or(ArithmeticError::Overflow)?;
|
||||
T::ConfigDepositBase::get()
|
||||
.checked_add(&friend_deposit)
|
||||
.ok_or(ArithmeticError::Overflow.into())
|
||||
}
|
||||
|
||||
/// Helper function to poke the deposit reserved for creating a recovery config
|
||||
fn poke_recovery_config_deposit(who: &T::AccountId) -> Result<bool, DispatchError> {
|
||||
<Recoverable<T>>::try_mutate(&who, |maybe_config| -> Result<bool, DispatchError> {
|
||||
let Some(config) = maybe_config.as_mut() else { return Ok(false) };
|
||||
let old_deposit = config.deposit;
|
||||
let new_deposit = Self::get_recovery_config_deposit(config.friends.len())?;
|
||||
|
||||
if old_deposit == new_deposit {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if new_deposit > old_deposit {
|
||||
let extra = new_deposit.saturating_sub(old_deposit);
|
||||
T::Currency::reserve(&who, extra)?;
|
||||
} else {
|
||||
let excess = old_deposit.saturating_sub(new_deposit);
|
||||
let remaining_unreserved = T::Currency::unreserve(&who, excess);
|
||||
if !remaining_unreserved.is_zero() {
|
||||
defensive!(
|
||||
"Failed to unreserve full amount. (Requested, Actual)",
|
||||
(excess, excess.saturating_sub(remaining_unreserved))
|
||||
);
|
||||
}
|
||||
}
|
||||
config.deposit = new_deposit;
|
||||
|
||||
Self::deposit_event(Event::<T>::DepositPoked {
|
||||
who: who.clone(),
|
||||
kind: DepositKind::RecoveryConfig,
|
||||
old_deposit,
|
||||
new_deposit,
|
||||
});
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to poke the deposit reserved for an active recovery
|
||||
fn poke_active_recovery_deposit(
|
||||
who: &T::AccountId,
|
||||
lost_account: &T::AccountId,
|
||||
) -> Result<bool, DispatchError> {
|
||||
let new_deposit = T::RecoveryDeposit::get();
|
||||
<ActiveRecoveries<T>>::try_mutate(
|
||||
lost_account,
|
||||
who,
|
||||
|maybe_recovery| -> Result<bool, DispatchError> {
|
||||
let recovery = maybe_recovery.as_mut().ok_or(Error::<T>::NotStarted)?;
|
||||
let old_deposit = recovery.deposit;
|
||||
|
||||
// Skip if deposit hasn't changed
|
||||
if recovery.deposit == new_deposit {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Update deposit
|
||||
if new_deposit > old_deposit {
|
||||
let extra = new_deposit.saturating_sub(old_deposit);
|
||||
T::Currency::reserve(who, extra)?;
|
||||
} else {
|
||||
let excess = old_deposit.saturating_sub(new_deposit);
|
||||
let remaining_unreserved = T::Currency::unreserve(who, excess);
|
||||
if !remaining_unreserved.is_zero() {
|
||||
defensive!(
|
||||
"Failed to unreserve full amount. (Requested, Actual)",
|
||||
(excess, excess.saturating_sub(remaining_unreserved))
|
||||
);
|
||||
}
|
||||
}
|
||||
recovery.deposit = new_deposit;
|
||||
|
||||
Self::deposit_event(Event::<T>::DepositPoked {
|
||||
who: who.clone(),
|
||||
kind: DepositKind::ActiveRecoveryFor(lost_account.clone()),
|
||||
old_deposit,
|
||||
new_deposit,
|
||||
});
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test utilities
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate as recovery;
|
||||
use frame::{deps::pezsp_io, testing_prelude::*};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Recovery: recovery,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<u128>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 1;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = u128;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static ConfigDepositBase: u64 = 10;
|
||||
pub const FriendDepositFactor: u64 = 1;
|
||||
pub static RecoveryDeposit: u64 = 10;
|
||||
// Large number of friends for benchmarking.
|
||||
pub const MaxFriends: u32 = 128;
|
||||
}
|
||||
|
||||
impl Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type BlockNumberProvider = System;
|
||||
type Currency = Balances;
|
||||
type ConfigDepositBase = ConfigDepositBase;
|
||||
type FriendDepositFactor = FriendDepositFactor;
|
||||
type MaxFriends = MaxFriends;
|
||||
type RecoveryDeposit = RecoveryDeposit;
|
||||
}
|
||||
|
||||
pub type BalancesCall = pezpallet_balances::Call<Test>;
|
||||
pub type RecoveryCall = super::Call<Test>;
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)],
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
let mut ext: pezsp_io::TestExternalities = t.into();
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
@@ -0,0 +1,788 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests for the module.
|
||||
|
||||
use crate::{mock::*, *};
|
||||
use frame::{deps::pezsp_runtime::bounded_vec, testing_prelude::*};
|
||||
|
||||
#[test]
|
||||
fn basic_setup_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Nothing in storage to start
|
||||
assert_eq!(Recovery::proxy(&2), 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(RuntimeOrigin::signed(1), 5, 1), BadOrigin);
|
||||
// Root can set a recovered account though
|
||||
assert_ok!(Recovery::set_recovered(RuntimeOrigin::root(), 5, 1));
|
||||
// Account 1 should now be able to make a call through account 5
|
||||
let call = Box::new(RuntimeCall::Balances(BalancesCall::transfer_allow_death {
|
||||
dest: 1,
|
||||
value: 100,
|
||||
}));
|
||||
assert_ok!(Recovery::as_recovered(RuntimeOrigin::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_life_cycle_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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends,
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Some time has passed, and the user lost their keys!
|
||||
System::run_to_block::<AllPalletsWithSystem>(10);
|
||||
// Using account 1, the user begins the recovery process to recover the lost account
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Off chain, the user contacts their friends and asks them to vouch for the recovery
|
||||
// attempt
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(4), 5, 1));
|
||||
// We met the threshold, lets try to recover the account...?
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::signed(1), 5),
|
||||
Error::<Test>::DelayPeriod
|
||||
);
|
||||
// We need to wait at least the delay_period number of blocks before we can recover
|
||||
System::run_to_block::<AllPalletsWithSystem>(20);
|
||||
assert_ok!(Recovery::claim_recovery(RuntimeOrigin::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(RuntimeCall::Recovery(RecoveryCall::close_recovery { rescuer: 1 }));
|
||||
assert_ok!(Recovery::as_recovered(RuntimeOrigin::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(RuntimeCall::Recovery(RecoveryCall::remove_recovery {}));
|
||||
assert_ok!(Recovery::as_recovered(RuntimeOrigin::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(RuntimeCall::Balances(BalancesCall::transfer_allow_death {
|
||||
dest: 1,
|
||||
value: 110,
|
||||
}));
|
||||
assert_ok!(Recovery::as_recovered(RuntimeOrigin::signed(1), 5, call));
|
||||
// All funds have been fully recovered!
|
||||
assert_eq!(Balances::free_balance(1), 200);
|
||||
assert_eq!(Balances::free_balance(5), 0);
|
||||
// Remove the proxy link.
|
||||
assert_ok!(Recovery::cancel_recovered(RuntimeOrigin::signed(1), 5));
|
||||
|
||||
// All storage items are removed from the module
|
||||
assert!(!<ActiveRecoveries<Test>>::contains_key(&5, &1));
|
||||
assert!(!<Recoverable<Test>>::contains_key(&5));
|
||||
assert!(!<Proxy<Test>>::contains_key(&1));
|
||||
});
|
||||
}
|
||||
|
||||
#[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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends,
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Some time has passed, and account 1 wants to try and attack this account!
|
||||
System::run_to_block::<AllPalletsWithSystem>(10);
|
||||
// Using account 1, the malicious user begins the recovery process on account 5
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Off chain, the user **tricks** their friends and asks them to vouch for the recovery
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
// shame on you
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 1));
|
||||
// shame on you
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(4), 5, 1));
|
||||
// shame on you
|
||||
// We met the threshold, lets try to recover the account...?
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::signed(1), 5),
|
||||
Error::<Test>::DelayPeriod
|
||||
);
|
||||
// Account 1 needs to wait...
|
||||
System::run_to_block::<AllPalletsWithSystem>(19);
|
||||
// One more block to wait!
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::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(RuntimeOrigin::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
|
||||
System::run_to_block::<AllPalletsWithSystem>(20);
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::signed(1), 5),
|
||||
Error::<Test>::NotStarted
|
||||
);
|
||||
// Account 5 can remove their recovery config and pick some better friends
|
||||
assert_ok!(Recovery::remove_recovery(RuntimeOrigin::signed(5)));
|
||||
assert_ok!(Recovery::create_recovery(
|
||||
RuntimeOrigin::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(RuntimeOrigin::signed(5), vec![], 1, 0),
|
||||
Error::<Test>::NotEnoughFriends
|
||||
);
|
||||
// Zero threshold
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2], 0, 0),
|
||||
Error::<Test>::ZeroThreshold
|
||||
);
|
||||
// Threshold greater than friends length
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 4, 0),
|
||||
Error::<Test>::NotEnoughFriends
|
||||
);
|
||||
// Too many friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(
|
||||
RuntimeOrigin::signed(5),
|
||||
vec![1; (MaxFriends::get() + 1) as usize],
|
||||
1,
|
||||
0
|
||||
),
|
||||
Error::<Test>::MaxFriends
|
||||
);
|
||||
// Unsorted friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(RuntimeOrigin::signed(5), vec![3, 2, 4], 3, 0),
|
||||
Error::<Test>::NotSorted
|
||||
);
|
||||
// Duplicate friends
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 2, 4], 3, 0),
|
||||
Error::<Test>::NotSorted
|
||||
);
|
||||
// Already configured
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
assert_noop!(
|
||||
Recovery::create_recovery(RuntimeOrigin::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(
|
||||
RuntimeOrigin::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.try_into().unwrap(),
|
||||
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(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Same user cannot recover same account twice
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
assert_noop!(
|
||||
Recovery::initiate_recovery(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Recovery can be initiated
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::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: Default::default() };
|
||||
assert_eq!(<ActiveRecoveries<Test>>::get(&5, &1), Some(recovery_status));
|
||||
// Multiple users can attempt to recover the same account
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::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(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Cannot vouch a recovery process that has not started
|
||||
assert_noop!(
|
||||
Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1),
|
||||
Error::<Test>::NotStarted
|
||||
);
|
||||
// Initiate a recovery process
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Cannot vouch if you are not a friend
|
||||
assert_noop!(
|
||||
Recovery::vouch_recovery(RuntimeOrigin::signed(22), 5, 1),
|
||||
Error::<Test>::NotFriend
|
||||
);
|
||||
// Cannot vouch twice
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
assert_noop!(
|
||||
Recovery::vouch_recovery(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Vouching works
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
// Handles out of order vouches
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(4), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 1));
|
||||
// Final recovery status object is updated correctly
|
||||
let recovery_status =
|
||||
ActiveRecovery { created: 1, deposit: 10, friends: bounded_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(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
// Cannot claim an account which has not started the recovery process
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::signed(1), 5),
|
||||
Error::<Test>::NotStarted
|
||||
);
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Cannot claim an account which has not passed the delay period
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::signed(1), 5),
|
||||
Error::<Test>::DelayPeriod
|
||||
);
|
||||
System::run_to_block::<AllPalletsWithSystem>(11);
|
||||
// Cannot claim an account which has not passed the threshold number of votes
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 1));
|
||||
// Only 2/3 is not good enough
|
||||
assert_noop!(
|
||||
Recovery::claim_recovery(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 1));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(4), 5, 1));
|
||||
|
||||
System::run_to_block::<AllPalletsWithSystem>(11);
|
||||
|
||||
// Account can be recovered.
|
||||
assert_ok!(Recovery::claim_recovery(RuntimeOrigin::signed(1), 5));
|
||||
// Recovered storage item is correctly created
|
||||
assert_eq!(<Proxy<Test>>::get(&1), Some(5));
|
||||
// Account could be re-recovered in the case that the recoverer account also gets lost.
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(4), 5));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(2), 5, 4));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(3), 5, 4));
|
||||
assert_ok!(Recovery::vouch_recovery(RuntimeOrigin::signed(4), 5, 4));
|
||||
|
||||
System::run_to_block::<AllPalletsWithSystem>(21);
|
||||
|
||||
// Account is re-recovered.
|
||||
assert_ok!(Recovery::claim_recovery(RuntimeOrigin::signed(4), 5));
|
||||
// Recovered storage item is correctly updated
|
||||
assert_eq!(<Proxy<Test>>::get(&4), Some(5));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_recovery_handles_basic_errors() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot close a non-active recovery
|
||||
assert_noop!(
|
||||
Recovery::close_recovery(RuntimeOrigin::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(RuntimeOrigin::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(
|
||||
RuntimeOrigin::signed(5),
|
||||
friends.clone(),
|
||||
threshold,
|
||||
delay_period
|
||||
));
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(2), 5));
|
||||
// Cannot remove a recovery when there are active recoveries.
|
||||
assert_noop!(
|
||||
Recovery::remove_recovery(RuntimeOrigin::signed(5)),
|
||||
Error::<Test>::StillActive
|
||||
);
|
||||
assert_ok!(Recovery::close_recovery(RuntimeOrigin::signed(5), 1));
|
||||
// Still need to remove one more!
|
||||
assert_noop!(
|
||||
Recovery::remove_recovery(RuntimeOrigin::signed(5)),
|
||||
Error::<Test>::StillActive
|
||||
);
|
||||
assert_ok!(Recovery::close_recovery(RuntimeOrigin::signed(5), 2));
|
||||
// Finally removed
|
||||
assert_ok!(Recovery::remove_recovery(RuntimeOrigin::signed(5)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_handles_unsigned_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(Recovery::poke_deposit(RuntimeOrigin::none(), None), DispatchError::BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_works_for_recovery_config_deposits() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Create initial recovery config
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
|
||||
// Verify initial state
|
||||
let old_deposit = Balances::reserved_balance(5);
|
||||
// Base 10 + 1 per friend
|
||||
assert_eq!(old_deposit, 13);
|
||||
let config = Recovery::recovery_config(5).unwrap();
|
||||
assert_eq!(config.deposit, old_deposit);
|
||||
|
||||
// Change ConfigDepositBase to trigger deposit update
|
||||
ConfigDepositBase::set(20);
|
||||
|
||||
// Poke deposit should work and be free
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), None);
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
// Verify final state
|
||||
let new_deposit = Balances::reserved_balance(5);
|
||||
// New base 20 + 1 per friend
|
||||
assert_eq!(new_deposit, 23);
|
||||
let updated_config = Recovery::recovery_config(5).unwrap();
|
||||
assert_eq!(updated_config.deposit, new_deposit);
|
||||
|
||||
// Check event was emitted
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::RecoveryConfig,
|
||||
old_deposit,
|
||||
new_deposit,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_works_for_active_recovery_deposits() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup recovery config
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
// Account 1 initiates recovery
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(1), 5));
|
||||
|
||||
// Verify initial state
|
||||
let old_deposit = Balances::reserved_balance(1);
|
||||
assert_eq!(old_deposit, 10);
|
||||
let recovery = <ActiveRecoveries<Test>>::get(&5, &1).unwrap();
|
||||
assert_eq!(recovery.deposit, old_deposit);
|
||||
|
||||
// Change RecoveryDeposit to trigger update
|
||||
let new_deposit = 15;
|
||||
RecoveryDeposit::set(new_deposit);
|
||||
|
||||
// Poke deposit should work and be free
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(1), Some(5));
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
// Verify final state
|
||||
assert_eq!(Balances::reserved_balance(1), new_deposit.into());
|
||||
let updated_recovery = <ActiveRecoveries<Test>>::get(&5, &1).unwrap();
|
||||
assert_eq!(updated_recovery.deposit, new_deposit.into());
|
||||
assert_eq!(updated_recovery.friends, recovery.friends);
|
||||
|
||||
// Check event was emitted
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 1,
|
||||
kind: DepositKind::ActiveRecoveryFor(5),
|
||||
old_deposit,
|
||||
new_deposit: new_deposit.into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_works_for_both_deposits() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup recovery config for account 5
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
|
||||
// Account 5 also initiates recovery for another account
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(1), vec![2, 3, 4], 3, 10));
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(5), 1));
|
||||
|
||||
// Verify initial storage state
|
||||
let initial_config_deposit = 13;
|
||||
let initial_recovery_deposit = 10;
|
||||
assert_eq!(Recovery::recovery_config(5).unwrap().deposit, initial_config_deposit);
|
||||
assert_eq!(
|
||||
<ActiveRecoveries<Test>>::get(&1, &5).unwrap().deposit,
|
||||
initial_recovery_deposit
|
||||
);
|
||||
assert_eq!(
|
||||
Balances::reserved_balance(5),
|
||||
initial_config_deposit + initial_recovery_deposit
|
||||
);
|
||||
|
||||
// Change both deposit requirements
|
||||
ConfigDepositBase::set(20);
|
||||
RecoveryDeposit::set(15);
|
||||
|
||||
// Poke deposits
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), Some(1));
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
// Verify storage and balances were updated
|
||||
let new_config_deposit = 23;
|
||||
let new_recovery_deposit = 15;
|
||||
assert_eq!(Recovery::recovery_config(5).unwrap().deposit, new_config_deposit);
|
||||
assert_eq!(<ActiveRecoveries<Test>>::get(&1, &5).unwrap().deposit, new_recovery_deposit);
|
||||
assert_eq!(Balances::reserved_balance(5), new_config_deposit + new_recovery_deposit);
|
||||
|
||||
// Check both events were emitted
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::RecoveryConfig,
|
||||
old_deposit: initial_config_deposit,
|
||||
new_deposit: new_config_deposit,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::ActiveRecoveryFor(1),
|
||||
old_deposit: initial_recovery_deposit,
|
||||
new_deposit: new_recovery_deposit,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_charges_fee_for_no_deposits() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(1), None);
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::Yes.into());
|
||||
|
||||
// No events should be emitted
|
||||
assert!(!System::events().iter().any(|record| matches!(
|
||||
record.event,
|
||||
RuntimeEvent::Recovery(Event::DepositPoked { .. })
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_charges_fee_for_unchanged_deposits() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
|
||||
// Verify initial state
|
||||
let old_deposit = Balances::reserved_balance(5);
|
||||
// Base 10 + 1 per friend
|
||||
assert_eq!(old_deposit, 13);
|
||||
let config = Recovery::recovery_config(5).unwrap();
|
||||
assert_eq!(config.deposit, old_deposit);
|
||||
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), None);
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::Yes.into());
|
||||
|
||||
// Verify final state
|
||||
let new_deposit = Balances::reserved_balance(5);
|
||||
// New base 20 + 1 per friend
|
||||
assert_eq!(new_deposit, old_deposit);
|
||||
let updated_config = Recovery::recovery_config(5).unwrap();
|
||||
assert_eq!(updated_config.deposit, old_deposit);
|
||||
// No events should be emitted
|
||||
assert!(!System::events().iter().any(|record| matches!(
|
||||
record.event,
|
||||
RuntimeEvent::Recovery(Event::DepositPoked { .. })
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_works_with_multiple_active_recoveries() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup multiple accounts with recovery
|
||||
for i in 1..=3 {
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(i), vec![2, 3, 4], 3, 10));
|
||||
}
|
||||
|
||||
// Account 5 initiates recovery for all of them
|
||||
let old_deposit = 10;
|
||||
for i in 1..=3 {
|
||||
assert_ok!(Recovery::initiate_recovery(RuntimeOrigin::signed(5), i));
|
||||
// Verify initial state for each recovery
|
||||
let recovery = <ActiveRecoveries<Test>>::get(&i, &5).unwrap();
|
||||
assert_eq!(recovery.deposit, old_deposit);
|
||||
}
|
||||
|
||||
// Initial total reserved = 3 * old_deposit
|
||||
let initial_total_reserved = old_deposit * 3;
|
||||
assert_eq!(Balances::reserved_balance(5), initial_total_reserved);
|
||||
|
||||
// Change recovery deposit
|
||||
let new_deposit = 15;
|
||||
RecoveryDeposit::set(new_deposit);
|
||||
|
||||
// Poke deposits
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), Some(1));
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), Some(2));
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
let result = Recovery::poke_deposit(RuntimeOrigin::signed(5), Some(3));
|
||||
assert_ok!(result.as_ref());
|
||||
assert_eq!(result.unwrap(), Pays::No.into());
|
||||
|
||||
// Verify final state for each recovery
|
||||
for i in 1..=3 {
|
||||
let updated_recovery = <ActiveRecoveries<Test>>::get(&i, &5).unwrap();
|
||||
assert_eq!(updated_recovery.deposit, new_deposit.into());
|
||||
}
|
||||
|
||||
// All deposits should be updated
|
||||
let new_total_reserved = new_deposit * 3;
|
||||
assert_eq!(Balances::reserved_balance(5), new_total_reserved.into());
|
||||
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::ActiveRecoveryFor(1),
|
||||
old_deposit,
|
||||
new_deposit: new_deposit.into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::ActiveRecoveryFor(2),
|
||||
old_deposit,
|
||||
new_deposit: new_deposit.into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
System::assert_has_event(
|
||||
Event::<Test>::DepositPoked {
|
||||
who: 5,
|
||||
kind: DepositKind::ActiveRecoveryFor(3),
|
||||
old_deposit,
|
||||
new_deposit: new_deposit.into(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poke_deposit_handles_insufficient_balance() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup recovery config
|
||||
assert_ok!(Recovery::create_recovery(RuntimeOrigin::signed(5), vec![2, 3, 4], 3, 10));
|
||||
assert_eq!(Balances::reserved_balance(5), 13);
|
||||
|
||||
// Increase required deposit
|
||||
ConfigDepositBase::set(200);
|
||||
|
||||
// Should fail due to insufficient balance
|
||||
assert_noop!(
|
||||
Recovery::poke_deposit(RuntimeOrigin::signed(5), None),
|
||||
pezpallet_balances::Error::<Test>::InsufficientBalance
|
||||
);
|
||||
// Original deposit should remain unchanged
|
||||
assert_eq!(Balances::reserved_balance(5), 13);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Autogenerated weights for `pezpallet_recovery`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-04-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `25e4b24639fd`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --extrinsic=*
|
||||
// --runtime=target/production/wbuild/kitchensink-runtime/kitchensink_runtime.wasm
|
||||
// --pallet=pezpallet_recovery
|
||||
// --header=/__w/pezkuwi-sdk/pezkuwi-sdk/bizinikiwi/HEADER-APACHE2
|
||||
// --output=/__w/pezkuwi-sdk/pezkuwi-sdk/bizinikiwi/pezframe/recovery/src/weights.rs
|
||||
// --wasm-execution=compiled
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --heap-pages=4096
|
||||
// --template=bizinikiwi/.maintain/frame-umbrella-weight-template.hbs
|
||||
// --no-storage-info
|
||||
// --no-min-squares
|
||||
// --no-median-slopes
|
||||
// --exclude-pallets=pezpallet_xcm,pezpallet_xcm_benchmarks::fungible,pezpallet_xcm_benchmarks::generic,pezpallet_nomination_pools,pezpallet_remark,pezpallet_transaction_storage
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use frame::weights_prelude::*;
|
||||
|
||||
/// Weight functions needed for `pezpallet_recovery`.
|
||||
pub trait WeightInfo {
|
||||
fn as_recovered() -> Weight;
|
||||
fn set_recovered() -> Weight;
|
||||
fn create_recovery(n: u32, ) -> Weight;
|
||||
fn initiate_recovery() -> Weight;
|
||||
fn vouch_recovery(n: u32, ) -> Weight;
|
||||
fn claim_recovery(n: u32, ) -> Weight;
|
||||
fn close_recovery(n: u32, ) -> Weight;
|
||||
fn remove_recovery(n: u32, ) -> Weight;
|
||||
fn cancel_recovered() -> Weight;
|
||||
fn poke_deposit(n: u32, ) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_recovery` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:0)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `SafeMode::EnteredUntil` (r:1 w:0)
|
||||
/// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TxPause::PausedCalls` (r:1 w:0)
|
||||
/// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`)
|
||||
fn as_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `530`
|
||||
// Estimated: `3997`
|
||||
// Minimum execution time: 18_724_000 picoseconds.
|
||||
Weight::from_parts(19_503_000, 3997)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Proxy` (r:0 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn set_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 6_933_000 picoseconds.
|
||||
Weight::from_parts(7_249_000, 0)
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn create_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `279`
|
||||
// Estimated: `3816`
|
||||
// Minimum execution time: 26_004_000 picoseconds.
|
||||
Weight::from_parts(27_597_455, 3816)
|
||||
// Standard Error: 7_992
|
||||
.saturating_add(Weight::from_parts(165_970, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
fn initiate_recovery() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `376`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 33_370_000 picoseconds.
|
||||
Weight::from_parts(34_078_000, 3854)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn vouch_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `464 + n * (64 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 21_199_000 picoseconds.
|
||||
Weight::from_parts(22_295_139, 3854)
|
||||
// Standard Error: 5_681
|
||||
.saturating_add(Weight::from_parts(255_188, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:0)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn claim_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `496 + n * (64 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 26_340_000 picoseconds.
|
||||
Weight::from_parts(27_828_030, 3854)
|
||||
// Standard Error: 7_254
|
||||
.saturating_add(Weight::from_parts(113_751, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn close_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `617 + n * (32 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 36_912_000 picoseconds.
|
||||
Weight::from_parts(38_818_613, 3854)
|
||||
// Standard Error: 7_806
|
||||
.saturating_add(Weight::from_parts(149_710, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:0)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn remove_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `374 + n * (32 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 30_824_000 picoseconds.
|
||||
Weight::from_parts(31_960_172, 3854)
|
||||
// Standard Error: 10_777
|
||||
.saturating_add(Weight::from_parts(237_226, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn cancel_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `385`
|
||||
// Estimated: `3545`
|
||||
// Minimum execution time: 15_369_000 picoseconds.
|
||||
Weight::from_parts(15_730_000, 3545)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn poke_deposit(_n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1110`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 46_517_000 picoseconds.
|
||||
Weight::from_parts(49_326_481, 3854)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:0)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `SafeMode::EnteredUntil` (r:1 w:0)
|
||||
/// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TxPause::PausedCalls` (r:1 w:0)
|
||||
/// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`)
|
||||
fn as_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `530`
|
||||
// Estimated: `3997`
|
||||
// Minimum execution time: 18_724_000 picoseconds.
|
||||
Weight::from_parts(19_503_000, 3997)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Proxy` (r:0 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn set_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 6_933_000 picoseconds.
|
||||
Weight::from_parts(7_249_000, 0)
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn create_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `279`
|
||||
// Estimated: `3816`
|
||||
// Minimum execution time: 26_004_000 picoseconds.
|
||||
Weight::from_parts(27_597_455, 3816)
|
||||
// Standard Error: 7_992
|
||||
.saturating_add(Weight::from_parts(165_970, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
fn initiate_recovery() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `376`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 33_370_000 picoseconds.
|
||||
Weight::from_parts(34_078_000, 3854)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn vouch_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `464 + n * (64 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 21_199_000 picoseconds.
|
||||
Weight::from_parts(22_295_139, 3854)
|
||||
// Standard Error: 5_681
|
||||
.saturating_add(Weight::from_parts(255_188, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:0)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:0)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn claim_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `496 + n * (64 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 26_340_000 picoseconds.
|
||||
Weight::from_parts(27_828_030, 3854)
|
||||
// Standard Error: 7_254
|
||||
.saturating_add(Weight::from_parts(113_751, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn close_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `617 + n * (32 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 36_912_000 picoseconds.
|
||||
Weight::from_parts(38_818_613, 3854)
|
||||
// Standard Error: 7_806
|
||||
.saturating_add(Weight::from_parts(149_710, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:0)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn remove_recovery(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `374 + n * (32 ±0)`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 30_824_000 picoseconds.
|
||||
Weight::from_parts(31_960_172, 3854)
|
||||
// Standard Error: 10_777
|
||||
.saturating_add(Weight::from_parts(237_226, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Proxy` (r:1 w:1)
|
||||
/// Proof: `Recovery::Proxy` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn cancel_recovered() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `385`
|
||||
// Estimated: `3545`
|
||||
// Minimum execution time: 15_369_000 picoseconds.
|
||||
Weight::from_parts(15_730_000, 3545)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Recovery::Recoverable` (r:1 w:1)
|
||||
/// Proof: `Recovery::Recoverable` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Recovery::ActiveRecoveries` (r:1 w:1)
|
||||
/// Proof: `Recovery::ActiveRecoveries` (`max_values`: None, `max_size`: Some(389), added: 2864, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 9]`.
|
||||
fn poke_deposit(_n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1110`
|
||||
// Estimated: `3854`
|
||||
// Minimum execution time: 46_517_000 picoseconds.
|
||||
Weight::from_parts(49_326_481, 3854)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user