Skip to content

Commit 5800c29

Browse files
committed
test(tokens): mutation hooks (OnDeposit, OnSlash, OnTransfer)
1 parent fb59233 commit 5800c29

File tree

4 files changed

+183
-7
lines changed

4 files changed

+183
-7
lines changed

tokens/src/lib.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -901,11 +901,11 @@ impl<T: Config> Pallet<T> {
901901
amount: T::Balance,
902902
existence_requirement: ExistenceRequirement,
903903
) -> DispatchResult {
904-
T::OnTransfer::on_transfer(currency_id, from, to, amount)?;
905904
if amount.is_zero() || from == to {
906905
return Ok(());
907906
}
908907

908+
T::OnTransfer::on_transfer(currency_id, from, to, amount)?;
909909
Self::try_mutate_account(to, currency_id, |to_account, _existed| -> DispatchResult {
910910
Self::try_mutate_account(from, currency_id, |from_account, _existed| -> DispatchResult {
911911
from_account.free = from_account
@@ -1027,11 +1027,11 @@ impl<T: Config> Pallet<T> {
10271027
require_existed: bool,
10281028
change_total_issuance: bool,
10291029
) -> DispatchResult {
1030-
T::OnDeposit::on_deposit(currency_id, who, amount)?;
10311030
if amount.is_zero() {
10321031
return Ok(());
10331032
}
10341033

1034+
T::OnDeposit::on_deposit(currency_id, who, amount)?;
10351035
Self::try_mutate_account(who, currency_id, |account, existed| -> DispatchResult {
10361036
if require_existed {
10371037
ensure!(existed, Error::<T>::DeadAccount);
@@ -1123,11 +1123,11 @@ impl<T: Config> MultiCurrency<T::AccountId> for Pallet<T> {
11231123
/// reserved funds, however we err on the side of punishment if things
11241124
/// are inconsistent or `can_slash` wasn't used appropriately.
11251125
fn slash(currency_id: Self::CurrencyId, who: &T::AccountId, amount: Self::Balance) -> Self::Balance {
1126-
T::OnSlash::on_slash(currency_id, who, amount);
11271126
if amount.is_zero() {
11281127
return amount;
11291128
}
11301129

1130+
T::OnSlash::on_slash(currency_id, who, amount);
11311131
let account = Self::accounts(who, currency_id);
11321132
let free_slashed_amount = account.free.min(amount);
11331133
// Cannot underflow because free_slashed_amount can never be greater than amount
@@ -1290,11 +1290,11 @@ impl<T: Config> MultiReservableCurrency<T::AccountId> for Pallet<T> {
12901290
///
12911291
/// Is a no-op if the value to be slashed is zero.
12921292
fn slash_reserved(currency_id: Self::CurrencyId, who: &T::AccountId, value: Self::Balance) -> Self::Balance {
1293-
T::OnSlash::on_slash(currency_id, who, value);
12941293
if value.is_zero() {
12951294
return value;
12961295
}
12971296

1297+
T::OnSlash::on_slash(currency_id, who, value);
12981298
let reserved_balance = Self::reserved_balance(currency_id, who);
12991299
let actual = reserved_balance.min(value);
13001300
Self::mutate_account(who, currency_id, |account, _| {

tokens/src/mock.rs

+52-3
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,55 @@ impl Happened<(AccountId, CurrencyId)> for TrackKilledAccounts {
268268
}
269269
}
270270

271+
thread_local! {
272+
pub static ON_SLASH_CALLS: RefCell<u32> = RefCell::new(0);
273+
pub static ON_DEPOSIT_CALLS: RefCell<u32> = RefCell::new(0);
274+
pub static ON_TRANSFER_CALLS: RefCell<u32> = RefCell::new(0);
275+
}
276+
277+
pub struct OnSlashHook<T>(marker::PhantomData<T>);
278+
impl<T: Config> OnSlash<T::AccountId, CurrencyId, Balance> for OnSlashHook<T> {
279+
fn on_slash(_currency_id: CurrencyId, _account_id: &T::AccountId, _amount: Balance) {
280+
ON_SLASH_CALLS.with(|cell| *cell.borrow_mut() += 1);
281+
}
282+
}
283+
impl<T: Config> OnSlashHook<T> {
284+
pub fn calls() -> u32 {
285+
ON_SLASH_CALLS.with(|accounts| accounts.borrow().clone())
286+
}
287+
}
288+
289+
pub struct OnDepositHook<T>(marker::PhantomData<T>);
290+
impl<T: Config> OnDeposit<T::AccountId, CurrencyId, Balance> for OnDepositHook<T> {
291+
fn on_deposit(_currency_id: CurrencyId, _account_id: &T::AccountId, _amount: Balance) -> DispatchResult {
292+
ON_DEPOSIT_CALLS.with(|cell| *cell.borrow_mut() += 1);
293+
Ok(())
294+
}
295+
}
296+
impl<T: Config> OnDepositHook<T> {
297+
pub fn calls() -> u32 {
298+
ON_DEPOSIT_CALLS.with(|accounts| accounts.borrow().clone())
299+
}
300+
}
301+
302+
pub struct OnTransferHook<T>(marker::PhantomData<T>);
303+
impl<T: Config> OnTransfer<T::AccountId, CurrencyId, Balance> for OnTransferHook<T> {
304+
fn on_transfer(
305+
_currency_id: CurrencyId,
306+
_from: &T::AccountId,
307+
_to: &T::AccountId,
308+
_amount: Balance,
309+
) -> DispatchResult {
310+
ON_TRANSFER_CALLS.with(|cell| *cell.borrow_mut() += 1);
311+
Ok(())
312+
}
313+
}
314+
impl<T: Config> OnTransferHook<T> {
315+
pub fn calls() -> u32 {
316+
ON_TRANSFER_CALLS.with(|accounts| accounts.borrow().clone())
317+
}
318+
}
319+
271320
parameter_types! {
272321
pub DustReceiver: AccountId = PalletId(*b"orml/dst").into_account_truncating();
273322
}
@@ -280,9 +329,9 @@ impl Config for Runtime {
280329
type WeightInfo = ();
281330
type ExistentialDeposits = ExistentialDeposits;
282331
type OnDust = TransferDust<Runtime, DustReceiver>;
283-
type OnSlash = ();
284-
type OnDeposit = ();
285-
type OnTransfer = ();
332+
type OnSlash = OnSlashHook<Runtime>;
333+
type OnDeposit = OnDepositHook<Runtime>;
334+
type OnTransfer = OnTransferHook<Runtime>;
286335
type OnNewTokenAccount = TrackCreatedAccounts;
287336
type OnKilledTokenAccount = TrackKilledAccounts;
288337
type MaxLocks = ConstU32<2>;

tokens/src/tests.rs

+58
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,61 @@ fn lifecycle_callbacks_are_activated() {
11671167
assert_eq!(TrackKilledAccounts::accounts(), vec![(ALICE, BTC)]);
11681168
})
11691169
}
1170+
1171+
// *************************************************
1172+
// tests for mutation hooks (OnDeposit, OnTransfer)
1173+
// (tests for the OnSlash hook can be found in `./tests_multicurrency.rs`)
1174+
// *************************************************
1175+
1176+
#[test]
1177+
fn deposit_hook_works() {
1178+
ExtBuilder::default().build().execute_with(|| {
1179+
let initial_hook_calls = OnDepositHook::<Runtime>::calls();
1180+
assert_ok!(Tokens::do_deposit(DOT, &CHARLIE, 0, false, true),);
1181+
assert_eq!(OnDepositHook::<Runtime>::calls(), initial_hook_calls);
1182+
1183+
assert_ok!(Tokens::do_deposit(DOT, &CHARLIE, 100, false, true),);
1184+
assert_eq!(OnDepositHook::<Runtime>::calls(), initial_hook_calls + 1);
1185+
1186+
// The hook must be called even if the actual deposit ends up failing
1187+
assert_noop!(
1188+
Tokens::do_deposit(DOT, &BOB, 1, false, true),
1189+
Error::<Runtime>::ExistentialDeposit
1190+
);
1191+
assert_eq!(OnDepositHook::<Runtime>::calls(), initial_hook_calls + 2);
1192+
});
1193+
}
1194+
1195+
#[test]
1196+
fn transfer_hook_works() {
1197+
ExtBuilder::default()
1198+
.balances(vec![(ALICE, DOT, 100)])
1199+
.build()
1200+
.execute_with(|| {
1201+
let initial_hook_calls = OnTransferHook::<Runtime>::calls();
1202+
assert_ok!(Tokens::do_transfer(
1203+
DOT,
1204+
&ALICE,
1205+
&CHARLIE,
1206+
0,
1207+
ExistenceRequirement::AllowDeath
1208+
),);
1209+
assert_eq!(OnTransferHook::<Runtime>::calls(), initial_hook_calls);
1210+
1211+
assert_ok!(Tokens::do_transfer(
1212+
DOT,
1213+
&ALICE,
1214+
&CHARLIE,
1215+
10,
1216+
ExistenceRequirement::AllowDeath
1217+
));
1218+
assert_eq!(OnTransferHook::<Runtime>::calls(), initial_hook_calls + 1);
1219+
1220+
// The hook must be called even if the actual transfer ends up failing
1221+
assert_noop!(
1222+
Tokens::do_transfer(DOT, &ALICE, &BOB, 1, ExistenceRequirement::AllowDeath),
1223+
Error::<Runtime>::ExistentialDeposit
1224+
);
1225+
assert_eq!(OnTransferHook::<Runtime>::calls(), initial_hook_calls + 2);
1226+
});
1227+
}

tokens/src/tests_multicurrency.rs

+69
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,72 @@ fn named_multi_reservable_repatriate_all_reserved_named_works() {
731731
}));
732732
});
733733
}
734+
735+
#[test]
736+
fn slash_hook_works() {
737+
ExtBuilder::default()
738+
.balances(vec![(ALICE, DOT, 100)])
739+
.build()
740+
.execute_with(|| {
741+
let initial_hook_calls = OnSlashHook::<Runtime>::calls();
742+
743+
// slashing zero tokens is a no-op
744+
assert_eq!(Tokens::slash(DOT, &ALICE, 0), 0);
745+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_hook_calls);
746+
747+
assert_eq!(Tokens::slash(DOT, &ALICE, 50), 0);
748+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_hook_calls + 1);
749+
750+
// `slash` calls the hook even if no amount was slashed
751+
assert_eq!(Tokens::slash(DOT, &ALICE, 100), 50);
752+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_hook_calls + 2);
753+
});
754+
}
755+
756+
#[test]
757+
fn slash_hook_works_for_reserved() {
758+
ExtBuilder::default()
759+
.balances(vec![(ALICE, DOT, 100)])
760+
.build()
761+
.execute_with(|| {
762+
let initial_slash_hook_calls = OnSlashHook::<Runtime>::calls();
763+
764+
assert_ok!(Tokens::reserve(DOT, &ALICE, 50));
765+
// slashing zero tokens is a no-op
766+
assert_eq!(Tokens::slash_reserved(DOT, &ALICE, 0), 0);
767+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls);
768+
769+
assert_eq!(Tokens::slash_reserved(DOT, &ALICE, 50), 0);
770+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls + 1);
771+
772+
// `slash_reserved` calls the hook even if no amount was slashed
773+
assert_eq!(Tokens::slash_reserved(DOT, &ALICE, 50), 50);
774+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls + 2);
775+
});
776+
}
777+
778+
#[test]
779+
fn slash_hook_works_for_reserved_named() {
780+
ExtBuilder::default()
781+
.balances(vec![(ALICE, DOT, 100)])
782+
.build()
783+
.execute_with(|| {
784+
let initial_slash_hook_calls = OnSlashHook::<Runtime>::calls();
785+
786+
assert_ok!(Tokens::reserve_named(&RID_1, DOT, &ALICE, 10));
787+
// slashing zero tokens is a no-op
788+
assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 0), 0);
789+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls);
790+
791+
assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 10), 0);
792+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls + 1);
793+
794+
// `slash_reserved_named` calls `slash_reserved` under-the-hood with a
795+
// value to slash based on the account's balance. Because the account's
796+
// balance is currently zero, `slash_reserved` will be a no-op and
797+
// the OnSlash hook will not be called.
798+
assert_eq!(Tokens::slash_reserved_named(&RID_1, DOT, &ALICE, 50), 50);
799+
// Same value as previously because of the no-op
800+
assert_eq!(OnSlashHook::<Runtime>::calls(), initial_slash_hook_calls + 1);
801+
});
802+
}

0 commit comments

Comments
 (0)