Slash relayers for invalid transactions (#2025)

* slash relayer balance for invalid transactions

* require some gap before unstake is possible

* more clippy

* log priority boost

* add issue ref to TODO

* fix typo

* is_message_delivery_call -> is_receive_messages_proof_call

* moved is_receive_messages_proof_call above

* only slash relayers for priority transactions

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update bin/runtime-common/src/refund_relayer_extension.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update modules/relayers/src/lib.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* Update primitives/relayers/src/registration.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

* benificiary -> beneficiary

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
This commit is contained in:
Svyatoslav Nikolsky
2023-04-25 16:24:13 +03:00
committed by Bastian Köcher
parent 3b47f957db
commit 53e1b7e264
12 changed files with 1497 additions and 185 deletions
+574 -7
View File
@@ -20,20 +20,25 @@
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
use bp_relayers::{PaymentProcedure, RelayerRewardsKeyProvider, RewardsAccountParams};
use bp_relayers::{
PaymentProcedure, Registration, RelayerRewardsKeyProvider, RewardsAccountParams, StakeAndSlash,
};
use bp_runtime::StorageDoubleMapKeyProvider;
use frame_support::sp_runtime::Saturating;
use frame_support::fail;
use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
use sp_runtime::{traits::CheckedSub, Saturating};
use sp_std::marker::PhantomData;
pub use pallet::*;
pub use payment_adapter::DeliveryConfirmationPaymentsAdapter;
pub use stake_adapter::StakeAndSlashNamed;
pub use weights::WeightInfo;
pub mod benchmarking;
mod mock;
mod payment_adapter;
mod stake_adapter;
pub mod weights;
@@ -56,8 +61,10 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type of relayer reward.
type Reward: AtLeast32BitUnsigned + Copy + Parameter + MaxEncodedLen;
/// Pay rewards adapter.
/// Pay rewards scheme.
type PaymentProcedure: PaymentProcedure<Self::AccountId, Self::Reward>;
/// Stake and slash scheme.
type StakeAndSlash: StakeAndSlash<Self::AccountId, Self::BlockNumber, Self::Reward>;
/// Pallet call weights.
type WeightInfo: WeightInfo;
}
@@ -102,9 +109,194 @@ pub mod pallet {
},
)
}
/// Register relayer or update its registration.
///
/// Registration allows relayer to get priority boost for its message delivery transactions.
#[pallet::call_index(1)]
#[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/2033
pub fn register(origin: OriginFor<T>, valid_till: T::BlockNumber) -> DispatchResult {
let relayer = ensure_signed(origin)?;
// valid till must be larger than the current block number and the lease must be larger
// than the `RequiredRegistrationLease`
let lease = valid_till.saturating_sub(frame_system::Pallet::<T>::block_number());
ensure!(
lease > Pallet::<T>::required_registration_lease(),
Error::<T>::InvalidRegistrationLease
);
RegisteredRelayers::<T>::try_mutate(&relayer, |maybe_registration| -> DispatchResult {
let mut registration = maybe_registration
.unwrap_or_else(|| Registration { valid_till, stake: Zero::zero() });
// new `valid_till` must be larger (or equal) than the old one
ensure!(
valid_till >= registration.valid_till,
Error::<T>::CannotReduceRegistrationLease,
);
registration.valid_till = valid_till;
// regarding stake, there are three options:
// - if relayer stake is larger than required stake, we may do unreserve
// - if relayer stake equals to required stake, we do nothing
// - if relayer stake is smaller than required stake, we do additional reserve
let required_stake = Pallet::<T>::required_stake();
if let Some(to_unreserve) = registration.stake.checked_sub(&required_stake) {
Self::do_unreserve(&relayer, to_unreserve)?;
} else if let Some(to_reserve) = required_stake.checked_sub(&registration.stake) {
T::StakeAndSlash::reserve(&relayer, to_reserve).map_err(|e| {
log::trace!(
target: LOG_TARGET,
"Failed to reserve {:?} on relayer {:?} account: {:?}",
to_reserve,
relayer,
e,
);
Error::<T>::FailedToReserve
})?;
}
registration.stake = required_stake;
Self::deposit_event(Event::<T>::RegistrationUpdated {
relayer: relayer.clone(),
registration,
});
*maybe_registration = Some(registration);
Ok(())
})
}
/// `Deregister` relayer.
///
/// After this call, message delivery transactions of the relayer won't get any priority
/// boost.
#[pallet::call_index(2)]
#[pallet::weight(Weight::zero())] // TODO: https://github.com/paritytech/parity-bridges-common/issues/2033
pub fn deregister(origin: OriginFor<T>) -> DispatchResult {
let relayer = ensure_signed(origin)?;
RegisteredRelayers::<T>::try_mutate(&relayer, |maybe_registration| -> DispatchResult {
let registration = match maybe_registration.take() {
Some(registration) => registration,
None => fail!(Error::<T>::NotRegistered),
};
// we can't deregister until `valid_till + 1`
ensure!(
registration.valid_till < frame_system::Pallet::<T>::block_number(),
Error::<T>::RegistrationIsStillActive,
);
// if stake is non-zero, we should do unreserve
if !registration.stake.is_zero() {
Self::do_unreserve(&relayer, registration.stake)?;
}
Self::deposit_event(Event::<T>::Deregistered { relayer: relayer.clone() });
*maybe_registration = None;
Ok(())
})
}
}
impl<T: Config> Pallet<T> {
/// Returns true if given relayer registration is active at current block.
///
/// This call respects both `RequiredStake` and `RequiredRegistrationLease`, meaning that
/// it'll return false if registered stake is lower than required or if remaining lease
/// is less than `RequiredRegistrationLease`.
pub fn is_registration_active(relayer: &T::AccountId) -> bool {
let registration = match Self::registered_relayer(relayer) {
Some(registration) => registration,
None => return false,
};
// registration is inactive if relayer stake is less than required
if registration.stake < Self::required_stake() {
return false
}
// registration is inactive if it ends soon
let remaining_lease = registration
.valid_till
.saturating_sub(frame_system::Pallet::<T>::block_number());
if remaining_lease <= Self::required_registration_lease() {
return false
}
true
}
/// Slash and `deregister` relayer. This function slashes all staked balance.
///
/// It may fail inside, but error is swallowed and we only log it.
pub fn slash_and_deregister(
relayer: &T::AccountId,
slash_destination: RewardsAccountParams,
) {
let registration = match RegisteredRelayers::<T>::take(relayer) {
Some(registration) => registration,
None => {
log::trace!(
target: crate::LOG_TARGET,
"Cannot slash unregistered relayer {:?}",
relayer,
);
return
},
};
match T::StakeAndSlash::repatriate_reserved(
relayer,
slash_destination,
registration.stake,
) {
Ok(failed_to_slash) if failed_to_slash.is_zero() => {
log::trace!(
target: crate::LOG_TARGET,
"Relayer account {:?} has been slashed for {:?}. Funds were deposited to {:?}",
relayer,
registration.stake,
slash_destination,
);
},
Ok(failed_to_slash) => {
log::trace!(
target: crate::LOG_TARGET,
"Relayer account {:?} has been partially slashed for {:?}. Funds were deposited to {:?}. \
Failed to slash: {:?}",
relayer,
registration.stake,
slash_destination,
failed_to_slash,
);
},
Err(e) => {
// TODO: document this. Where?
// it may fail if there's no beneficiary account. For us it means that this
// account must exists before we'll deploy the bridge
log::debug!(
target: crate::LOG_TARGET,
"Failed to slash relayer account {:?}: {:?}. Maybe beneficiary account doesn't exist? \
Beneficiary: {:?}, amount: {:?}, failed to slash: {:?}",
relayer,
e,
slash_destination,
registration.stake,
registration.stake,
);
},
}
}
/// Register reward for given relayer.
pub fn register_relayer_reward(
rewards_account_params: RewardsAccountParams,
@@ -132,6 +324,42 @@ pub mod pallet {
},
);
}
/// Return required registration lease.
fn required_registration_lease() -> T::BlockNumber {
<T::StakeAndSlash as StakeAndSlash<
T::AccountId,
T::BlockNumber,
T::Reward,
>>::RequiredRegistrationLease::get()
}
/// Return required stake.
fn required_stake() -> T::Reward {
<T::StakeAndSlash as StakeAndSlash<
T::AccountId,
T::BlockNumber,
T::Reward,
>>::RequiredStake::get()
}
/// `Unreserve` given amount on relayer account.
fn do_unreserve(relayer: &T::AccountId, amount: T::Reward) -> DispatchResult {
let failed_to_unreserve = T::StakeAndSlash::unreserve(relayer, amount);
if !failed_to_unreserve.is_zero() {
log::trace!(
target: LOG_TARGET,
"Failed to unreserve {:?}/{:?} on relayer {:?} account",
failed_to_unreserve,
amount,
relayer,
);
fail!(Error::<T>::FailedToUnreserve)
}
Ok(())
}
}
#[pallet::event]
@@ -146,6 +374,25 @@ pub mod pallet {
/// Reward amount.
reward: T::Reward,
},
/// Relayer registration has been added or updated.
RegistrationUpdated {
/// Relayer account that has been registered.
relayer: T::AccountId,
/// Relayer registration.
registration: Registration<T::BlockNumber, T::Reward>,
},
/// Relayer has been `deregistered`.
Deregistered {
/// Relayer account that has been `deregistered`.
relayer: T::AccountId,
},
/// Relayer has been slashed and `deregistered`.
SlashedAndDeregistered {
/// Relayer account that has been `deregistered`.
relayer: T::AccountId,
/// Registration that was removed.
registration: Registration<T::BlockNumber, T::Reward>,
},
}
#[pallet::error]
@@ -154,6 +401,19 @@ pub mod pallet {
NoRewardForRelayer,
/// Reward payment procedure has failed.
FailedToPayReward,
/// The relayer has tried to register for past block or registration lease
/// is too short.
InvalidRegistrationLease,
/// New registration lease is less than the previous one.
CannotReduceRegistrationLease,
/// Failed to reserve enough funds on relayer account.
FailedToReserve,
/// Failed to `unreserve` enough funds on relayer account.
FailedToUnreserve,
/// Cannot `deregister` if not registered.
NotRegistered,
/// Failed to `deregister` relayer, because lease is still active.
RegistrationIsStillActive,
}
/// Map of the relayer => accumulated reward.
@@ -168,6 +428,22 @@ pub mod pallet {
<RelayerRewardsKeyProviderOf<T> as StorageDoubleMapKeyProvider>::Value,
OptionQuery,
>;
/// Relayers that have reserved some of their balance to get free priority boost
/// for their message delivery transactions.
///
/// Other relayers may submit transactions as well, but they will have default
/// priority and will be rejected (without significant tip) in case if registered
/// relayer is present.
#[pallet::storage]
#[pallet::getter(fn registered_relayer)]
pub type RegisteredRelayers<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Registration<T::BlockNumber, T::Reward>,
OptionQuery,
>;
}
#[cfg(test)]
@@ -253,10 +529,10 @@ mod tests {
None
);
//Check if the `RewardPaid` event was emitted.
// Check if the `RewardPaid` event was emitted.
assert_eq!(
System::<TestRuntime>::events(),
vec![EventRecord {
System::<TestRuntime>::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: TestEvent::Relayers(RewardPaid {
relayer: REGULAR_RELAYER,
@@ -264,7 +540,7 @@ mod tests {
reward: 100
}),
topics: vec![],
}],
}),
);
});
}
@@ -306,4 +582,295 @@ mod tests {
assert_eq!(Balances::balance(&1), 200);
});
}
#[test]
fn register_fails_if_valid_till_is_a_past_block() {
run_test(|| {
System::<TestRuntime>::set_block_number(100);
assert_noop!(
Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 50),
Error::<TestRuntime>::InvalidRegistrationLease,
);
});
}
#[test]
fn register_fails_if_valid_till_lease_is_less_than_required() {
run_test(|| {
System::<TestRuntime>::set_block_number(100);
assert_noop!(
Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
99 + Lease::get()
),
Error::<TestRuntime>::InvalidRegistrationLease,
);
});
}
#[test]
fn register_works() {
run_test(|| {
get_ready_for_events();
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
assert_eq!(
Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
Some(Registration { valid_till: 150, stake: Stake::get() }),
);
assert_eq!(
System::<TestRuntime>::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: TestEvent::Relayers(Event::RegistrationUpdated {
relayer: REGISTER_RELAYER,
registration: Registration { valid_till: 150, stake: Stake::get() },
}),
topics: vec![],
}),
);
});
}
#[test]
fn register_fails_if_new_valid_till_is_lesser_than_previous() {
run_test(|| {
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
assert_noop!(
Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 125),
Error::<TestRuntime>::CannotReduceRegistrationLease,
);
});
}
#[test]
fn register_fails_if_it_cant_unreserve_some_balance_if_required_stake_decreases() {
run_test(|| {
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() + 1 },
);
assert_noop!(
Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
Error::<TestRuntime>::FailedToUnreserve,
);
});
}
#[test]
fn register_unreserves_some_balance_if_required_stake_decreases() {
run_test(|| {
get_ready_for_events();
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() + 1 },
);
TestStakeAndSlash::reserve(&REGISTER_RELAYER, Stake::get() + 1).unwrap();
assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get() + 1);
let free_balance = Balances::free_balance(REGISTER_RELAYER);
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance + 1);
assert_eq!(
Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
Some(Registration { valid_till: 150, stake: Stake::get() }),
);
assert_eq!(
System::<TestRuntime>::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: TestEvent::Relayers(Event::RegistrationUpdated {
relayer: REGISTER_RELAYER,
registration: Registration { valid_till: 150, stake: Stake::get() }
}),
topics: vec![],
}),
);
});
}
#[test]
fn register_fails_if_it_cant_reserve_some_balance() {
run_test(|| {
Balances::set_balance(&REGISTER_RELAYER, 0);
assert_noop!(
Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
Error::<TestRuntime>::FailedToReserve,
);
});
}
#[test]
fn register_fails_if_it_cant_reserve_some_balance_if_required_stake_increases() {
run_test(|| {
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() - 1 },
);
Balances::set_balance(&REGISTER_RELAYER, 0);
assert_noop!(
Pallet::<TestRuntime>::register(RuntimeOrigin::signed(REGISTER_RELAYER), 150),
Error::<TestRuntime>::FailedToReserve,
);
});
}
#[test]
fn register_reserves_some_balance_if_required_stake_increases() {
run_test(|| {
get_ready_for_events();
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() - 1 },
);
TestStakeAndSlash::reserve(&REGISTER_RELAYER, Stake::get() - 1).unwrap();
let free_balance = Balances::free_balance(REGISTER_RELAYER);
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
assert_eq!(Balances::reserved_balance(REGISTER_RELAYER), Stake::get());
assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance - 1);
assert_eq!(
Pallet::<TestRuntime>::registered_relayer(REGISTER_RELAYER),
Some(Registration { valid_till: 150, stake: Stake::get() }),
);
assert_eq!(
System::<TestRuntime>::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: TestEvent::Relayers(Event::RegistrationUpdated {
relayer: REGISTER_RELAYER,
registration: Registration { valid_till: 150, stake: Stake::get() }
}),
topics: vec![],
}),
);
});
}
#[test]
fn deregister_fails_if_not_registered() {
run_test(|| {
assert_noop!(
Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)),
Error::<TestRuntime>::NotRegistered,
);
});
}
#[test]
fn deregister_fails_if_registration_is_still_active() {
run_test(|| {
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
System::<TestRuntime>::set_block_number(100);
assert_noop!(
Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)),
Error::<TestRuntime>::RegistrationIsStillActive,
);
});
}
#[test]
fn deregister_works() {
run_test(|| {
get_ready_for_events();
assert_ok!(Pallet::<TestRuntime>::register(
RuntimeOrigin::signed(REGISTER_RELAYER),
150
));
System::<TestRuntime>::set_block_number(151);
let reserved_balance = Balances::reserved_balance(REGISTER_RELAYER);
let free_balance = Balances::free_balance(REGISTER_RELAYER);
assert_ok!(Pallet::<TestRuntime>::deregister(RuntimeOrigin::signed(REGISTER_RELAYER)));
assert_eq!(
Balances::reserved_balance(REGISTER_RELAYER),
reserved_balance - Stake::get()
);
assert_eq!(Balances::free_balance(REGISTER_RELAYER), free_balance + Stake::get());
assert_eq!(
System::<TestRuntime>::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: TestEvent::Relayers(Event::Deregistered { relayer: REGISTER_RELAYER }),
topics: vec![],
}),
);
});
}
#[test]
fn is_registration_active_is_false_for_unregistered_relayer() {
run_test(|| {
assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
});
}
#[test]
fn is_registration_active_is_false_when_stake_is_too_low() {
run_test(|| {
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() - 1 },
);
assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
});
}
#[test]
fn is_registration_active_is_false_when_remaining_lease_is_too_low() {
run_test(|| {
System::<TestRuntime>::set_block_number(150 - Lease::get());
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 150, stake: Stake::get() },
);
assert!(!Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
});
}
#[test]
fn is_registration_active_is_true_when_relayer_is_properly_registeered() {
run_test(|| {
System::<TestRuntime>::set_block_number(150 - Lease::get());
RegisteredRelayers::<TestRuntime>::insert(
REGISTER_RELAYER,
Registration { valid_till: 151, stake: Stake::get() },
);
assert!(Pallet::<TestRuntime>::is_registration_active(&REGISTER_RELAYER));
});
}
}
+38 -7
View File
@@ -19,8 +19,10 @@
use crate as pallet_bridge_relayers;
use bp_messages::LaneId;
use bp_relayers::{PaymentProcedure, RewardsAccountOwner, RewardsAccountParams};
use frame_support::{parameter_types, weights::RuntimeDbWeight};
use bp_relayers::{
PayRewardFromAccount, PaymentProcedure, RewardsAccountOwner, RewardsAccountParams,
};
use frame_support::{parameter_types, traits::fungible::Mutate, weights::RuntimeDbWeight};
use sp_core::H256;
use sp_runtime::{
testing::Header as SubstrateHeader,
@@ -29,6 +31,16 @@ use sp_runtime::{
pub type AccountId = u64;
pub type Balance = u64;
pub type BlockNumber = u64;
pub type TestStakeAndSlash = pallet_bridge_relayers::StakeAndSlashNamed<
AccountId,
BlockNumber,
Balances,
ReserveId,
Stake,
Lease,
>;
type Block = frame_system::mocking::MockBlock<TestRuntime>;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
@@ -47,13 +59,17 @@ frame_support::construct_runtime! {
parameter_types! {
pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 };
pub const ExistentialDeposit: Balance = 1;
pub const ReserveId: [u8; 8] = *b"brdgrlrs";
pub const Stake: Balance = 1_000;
pub const Lease: BlockNumber = 8;
}
impl frame_system::Config for TestRuntime {
type RuntimeOrigin = RuntimeOrigin;
type Index = u64;
type RuntimeCall = RuntimeCall;
type BlockNumber = u64;
type BlockNumber = BlockNumber;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
@@ -81,11 +97,11 @@ impl pallet_balances::Config for TestRuntime {
type Balance = Balance;
type DustRemoval = ();
type RuntimeEvent = RuntimeEvent;
type ExistentialDeposit = frame_support::traits::ConstU64<1>;
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = frame_system::Pallet<TestRuntime>;
type WeightInfo = ();
type MaxReserves = ();
type ReserveIdentifier = ();
type MaxReserves = ConstU32<1>;
type ReserveIdentifier = [u8; 8];
type HoldIdentifier = ();
type FreezeIdentifier = ();
type MaxHolds = ConstU32<0>;
@@ -96,6 +112,7 @@ impl pallet_bridge_relayers::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type Reward = Balance;
type PaymentProcedure = TestPaymentProcedure;
type StakeAndSlash = TestStakeAndSlash;
type WeightInfo = ();
}
@@ -121,9 +138,18 @@ pub const REGULAR_RELAYER: AccountId = 1;
/// Relayer that can't receive rewards.
pub const FAILING_RELAYER: AccountId = 2;
/// Relayer that is able to register.
pub const REGISTER_RELAYER: AccountId = 42;
/// Payment procedure that rejects payments to the `FAILING_RELAYER`.
pub struct TestPaymentProcedure;
impl TestPaymentProcedure {
pub fn rewards_account(params: RewardsAccountParams) -> AccountId {
PayRewardFromAccount::<(), AccountId>::rewards_account(params)
}
}
impl PaymentProcedure<AccountId, Balance> for TestPaymentProcedure {
type Error = ();
@@ -147,5 +173,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
/// Run pallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(test)
new_test_ext().execute_with(|| {
Balances::mint_into(&REGISTER_RELAYER, ExistentialDeposit::get() + 10 * Stake::get())
.unwrap();
test()
})
}
@@ -0,0 +1,186 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Code that allows `NamedReservableCurrency` to be used as a `StakeAndSlash`
//! mechanism of the relayers pallet.
use bp_relayers::{PayRewardFromAccount, RewardsAccountParams, StakeAndSlash};
use codec::Codec;
use frame_support::traits::{tokens::BalanceStatus, NamedReservableCurrency};
use sp_runtime::{traits::Get, DispatchError, DispatchResult};
use sp_std::{fmt::Debug, marker::PhantomData};
/// `StakeAndSlash` that works with `NamedReservableCurrency` and uses named
/// reservations.
///
/// **WARNING**: this implementation assumes that the relayers pallet is configured to
/// use the [`bp_relayers::PayRewardFromAccount`] as its relayers payment scheme.
pub struct StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>(
PhantomData<(AccountId, BlockNumber, Currency, ReserveId, Stake, Lease)>,
);
impl<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
StakeAndSlash<AccountId, BlockNumber, Currency::Balance>
for StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
where
AccountId: Codec + Debug,
Currency: NamedReservableCurrency<AccountId>,
ReserveId: Get<Currency::ReserveIdentifier>,
Stake: Get<Currency::Balance>,
Lease: Get<BlockNumber>,
{
type RequiredStake = Stake;
type RequiredRegistrationLease = Lease;
fn reserve(relayer: &AccountId, amount: Currency::Balance) -> DispatchResult {
Currency::reserve_named(&ReserveId::get(), relayer, amount)
}
fn unreserve(relayer: &AccountId, amount: Currency::Balance) -> Currency::Balance {
Currency::unreserve_named(&ReserveId::get(), relayer, amount)
}
fn repatriate_reserved(
relayer: &AccountId,
beneficiary: RewardsAccountParams,
amount: Currency::Balance,
) -> Result<Currency::Balance, DispatchError> {
let beneficiary_account =
PayRewardFromAccount::<(), AccountId>::rewards_account(beneficiary);
Currency::repatriate_reserved_named(
&ReserveId::get(),
relayer,
&beneficiary_account,
amount,
BalanceStatus::Free,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::*;
use frame_support::traits::fungible::Mutate;
fn test_stake() -> Balance {
Stake::get()
}
#[test]
fn reserve_works() {
run_test(|| {
assert!(TestStakeAndSlash::reserve(&1, test_stake()).is_err());
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
Balances::mint_into(&2, test_stake() - 1).unwrap();
assert!(TestStakeAndSlash::reserve(&2, test_stake()).is_err());
assert_eq!(Balances::free_balance(2), test_stake() - 1);
assert_eq!(Balances::reserved_balance(2), 0);
Balances::mint_into(&3, test_stake() * 2).unwrap();
assert_eq!(TestStakeAndSlash::reserve(&3, test_stake()), Ok(()));
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), test_stake());
})
}
#[test]
fn unreserve_works() {
run_test(|| {
assert_eq!(TestStakeAndSlash::unreserve(&1, test_stake()), test_stake());
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
Balances::mint_into(&2, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
assert_eq!(
TestStakeAndSlash::unreserve(&2, test_stake()),
test_stake() - test_stake() / 3
);
assert_eq!(Balances::free_balance(2), test_stake() * 2);
assert_eq!(Balances::reserved_balance(2), 0);
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert_eq!(TestStakeAndSlash::unreserve(&3, test_stake()), 0);
assert_eq!(Balances::free_balance(3), test_stake() * 2);
assert_eq!(Balances::reserved_balance(3), 0);
})
}
#[test]
fn repatriate_reserved_works() {
run_test(|| {
let beneficiary = TEST_REWARDS_ACCOUNT_PARAMS;
let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
let mut expected_balance = ExistentialDeposit::get();
Balances::mint_into(&beneficiary_account, expected_balance).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(&1, beneficiary, test_stake()),
Ok(test_stake())
);
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
expected_balance += test_stake() / 3;
Balances::mint_into(&2, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(&2, beneficiary, test_stake()),
Ok(test_stake() - test_stake() / 3)
);
assert_eq!(Balances::free_balance(2), test_stake() * 2 - test_stake() / 3);
assert_eq!(Balances::reserved_balance(2), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
expected_balance += test_stake();
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(&3, beneficiary, test_stake()),
Ok(0)
);
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
})
}
#[test]
fn repatriate_reserved_doesnt_work_when_beneficiary_account_is_missing() {
run_test(|| {
let beneficiary = TEST_REWARDS_ACCOUNT_PARAMS;
let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert!(TestStakeAndSlash::repatriate_reserved(&3, beneficiary, test_stake()).is_err());
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), test_stake());
assert_eq!(Balances::free_balance(beneficiary_account), 0);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
});
}
}