Skip to content

Commit 2c48b62

Browse files
authored
feat(tokens): OnDeposit, OnTransfer, OnSlash hooks (#815)
* feat(tokens): OnDeposit, OnTransfer, OnSlash hooks * test(tokens): mutation hooks (OnDeposit, OnSlash, OnTransfer)
1 parent 7e0de57 commit 2c48b62

File tree

11 files changed

+244
-1
lines changed

11 files changed

+244
-1
lines changed

asset-registry/src/mock/para.rs

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ impl orml_tokens::Config for Runtime {
9393
type WeightInfo = ();
9494
type ExistentialDeposits = ExistentialDeposits;
9595
type OnDust = ();
96+
type OnSlash = ();
97+
type OnDeposit = ();
98+
type OnTransfer = ();
9699
type ReserveIdentifier = [u8; 8];
97100
type MaxReserves = ();
98101
type MaxLocks = ConstU32<50>;

currencies/src/mock.rs

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ impl orml_tokens::Config for Runtime {
8181
type WeightInfo = ();
8282
type ExistentialDeposits = ExistentialDeposits;
8383
type OnDust = orml_tokens::TransferDust<Runtime, DustAccount>;
84+
type OnSlash = ();
85+
type OnDeposit = ();
86+
type OnTransfer = ();
8487
type MaxLocks = ConstU32<100_000>;
8588
type MaxReserves = ConstU32<100_000>;
8689
type ReserveIdentifier = ReserveIdentifier;

payments/src/mock.rs

+3
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ impl orml_tokens::Config for Test {
9999
type Event = Event;
100100
type ExistentialDeposits = ExistentialDeposits;
101101
type OnDust = ();
102+
type OnSlash = ();
103+
type OnDeposit = ();
104+
type OnTransfer = ();
102105
type WeightInfo = ();
103106
type MaxLocks = MaxLocks;
104107
type DustRemovalWhitelist = MockDustRemovalWhitelist;

tokens/src/lib.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ use sp_std::{cmp, convert::Infallible, marker, prelude::*, vec::Vec};
6666

6767
use orml_traits::{
6868
arithmetic::{self, Signed},
69-
currency::TransferAll,
69+
currency::{OnDeposit, OnSlash, OnTransfer, TransferAll},
7070
BalanceStatus, GetByKey, Happened, LockIdentifier, MultiCurrency, MultiCurrencyExtended, MultiLockableCurrency,
7171
MultiReservableCurrency, NamedMultiReservableCurrency, OnDust,
7272
};
@@ -173,6 +173,8 @@ pub use module::*;
173173

174174
#[frame_support::pallet]
175175
pub mod module {
176+
use orml_traits::currency::{OnDeposit, OnSlash, OnTransfer};
177+
176178
use super::*;
177179

178180
#[pallet::config]
@@ -216,6 +218,15 @@ pub mod module {
216218
/// Handler to burn or transfer account's dust
217219
type OnDust: OnDust<Self::AccountId, Self::CurrencyId, Self::Balance>;
218220

221+
/// Hook to run before slashing an account.
222+
type OnSlash: OnSlash<Self::AccountId, Self::CurrencyId, Self::Balance>;
223+
224+
/// Hook to run before depositing into an account.
225+
type OnDeposit: OnDeposit<Self::AccountId, Self::CurrencyId, Self::Balance>;
226+
227+
/// Hook to run before transferring from an account to another.
228+
type OnTransfer: OnTransfer<Self::AccountId, Self::CurrencyId, Self::Balance>;
229+
219230
/// Handler for when an account was created
220231
type OnNewTokenAccount: Happened<(Self::AccountId, Self::CurrencyId)>;
221232

@@ -894,6 +905,7 @@ impl<T: Config> Pallet<T> {
894905
return Ok(());
895906
}
896907

908+
T::OnTransfer::on_transfer(currency_id, from, to, amount)?;
897909
Self::try_mutate_account(to, currency_id, |to_account, _existed| -> DispatchResult {
898910
Self::try_mutate_account(from, currency_id, |from_account, _existed| -> DispatchResult {
899911
from_account.free = from_account
@@ -1019,6 +1031,7 @@ impl<T: Config> Pallet<T> {
10191031
return Ok(());
10201032
}
10211033

1034+
T::OnDeposit::on_deposit(currency_id, who, amount)?;
10221035
Self::try_mutate_account(who, currency_id, |account, existed| -> DispatchResult {
10231036
if require_existed {
10241037
ensure!(existed, Error::<T>::DeadAccount);
@@ -1114,6 +1127,7 @@ impl<T: Config> MultiCurrency<T::AccountId> for Pallet<T> {
11141127
return amount;
11151128
}
11161129

1130+
T::OnSlash::on_slash(currency_id, who, amount);
11171131
let account = Self::accounts(who, currency_id);
11181132
let free_slashed_amount = account.free.min(amount);
11191133
// Cannot underflow because free_slashed_amount can never be greater than amount
@@ -1280,6 +1294,7 @@ impl<T: Config> MultiReservableCurrency<T::AccountId> for Pallet<T> {
12801294
return value;
12811295
}
12821296

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

tokens/src/mock.rs

+52
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,6 +329,9 @@ impl Config for Runtime {
280329
type WeightInfo = ();
281330
type ExistentialDeposits = ExistentialDeposits;
282331
type OnDust = TransferDust<Runtime, DustReceiver>;
332+
type OnSlash = OnSlashHook<Runtime>;
333+
type OnDeposit = OnDepositHook<Runtime>;
334+
type OnTransfer = OnTransferHook<Runtime>;
283335
type OnNewTokenAccount = TrackCreatedAccounts;
284336
type OnKilledTokenAccount = TrackKilledAccounts;
285337
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+
}

traits/src/currency.rs

+31
Original file line numberDiff line numberDiff line change
@@ -657,3 +657,34 @@ impl<AccountId> TransferAll<AccountId> for Tuple {
657657
Ok(())
658658
}
659659
}
660+
661+
/// Hook to run before slashing an account.
662+
pub trait OnSlash<AccountId, CurrencyId, Balance> {
663+
fn on_slash(currency_id: CurrencyId, who: &AccountId, amount: Balance);
664+
}
665+
666+
impl<AccountId, CurrencyId, Balance> OnSlash<AccountId, CurrencyId, Balance> for () {
667+
fn on_slash(_: CurrencyId, _: &AccountId, _: Balance) {}
668+
}
669+
670+
/// Hook to run before depositing into an account.
671+
pub trait OnDeposit<AccountId, CurrencyId, Balance> {
672+
fn on_deposit(currency_id: CurrencyId, who: &AccountId, amount: Balance) -> DispatchResult;
673+
}
674+
675+
impl<AccountId, CurrencyId, Balance> OnDeposit<AccountId, CurrencyId, Balance> for () {
676+
fn on_deposit(_: CurrencyId, _: &AccountId, _: Balance) -> DispatchResult {
677+
Ok(())
678+
}
679+
}
680+
681+
/// Hook to run before transferring from an account to another.
682+
pub trait OnTransfer<AccountId, CurrencyId, Balance> {
683+
fn on_transfer(currency_id: CurrencyId, from: &AccountId, to: &AccountId, amount: Balance) -> DispatchResult;
684+
}
685+
686+
impl<AccountId, CurrencyId, Balance> OnTransfer<AccountId, CurrencyId, Balance> for () {
687+
fn on_transfer(_: CurrencyId, _: &AccountId, _: &AccountId, _: Balance) -> DispatchResult {
688+
Ok(())
689+
}
690+
}

xtokens/src/mock/para.rs

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ impl orml_tokens::Config for Runtime {
8484
type WeightInfo = ();
8585
type ExistentialDeposits = ExistentialDeposits;
8686
type OnDust = ();
87+
type OnSlash = ();
88+
type OnDeposit = ();
89+
type OnTransfer = ();
8790
type MaxLocks = ConstU32<50>;
8891
type MaxReserves = ConstU32<50>;
8992
type ReserveIdentifier = [u8; 8];

xtokens/src/mock/para_relative_view.rs

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ impl orml_tokens::Config for Runtime {
8787
type WeightInfo = ();
8888
type ExistentialDeposits = ExistentialDeposits;
8989
type OnDust = ();
90+
type OnSlash = ();
91+
type OnDeposit = ();
92+
type OnTransfer = ();
9093
type MaxLocks = ConstU32<50>;
9194
type MaxReserves = ConstU32<50>;
9295
type ReserveIdentifier = [u8; 8];

xtokens/src/mock/para_teleport.rs

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ impl orml_tokens::Config for Runtime {
8585
type WeightInfo = ();
8686
type ExistentialDeposits = ExistentialDeposits;
8787
type OnDust = ();
88+
type OnSlash = ();
89+
type OnDeposit = ();
90+
type OnTransfer = ();
8891
type MaxLocks = ConstU32<50>;
8992
type MaxReserves = ConstU32<50>;
9093
type ReserveIdentifier = [u8; 8];

0 commit comments

Comments
 (0)