Skip to content

Instantly share code, notes, and snippets.

@kianenigma
Created September 18, 2025 00:44
Show Gist options
  • Select an option

  • Save kianenigma/9fe26e0b121a2c6edf50d50fa5da0ee2 to your computer and use it in GitHub Desktop.

Select an option

Save kianenigma/9fe26e0b121a2c6edf50d50fa5da0ee2 to your computer and use it in GitHub Desktop.
Permutations of combining Polkadot-SDK's Currency related primitives.
// This file is part of Substrate.
// 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 the behavior of a runtime when both `fungible` and `Currency` traits are in use and are
//! being mixed.
//!
//! The primitives that we have and can mix are:
//!
//! * locks
//! * reserves
//! * holds
//! * freezes
//!
//! All permutations of which are:
//!
//! * Two primitives combined
//! * locks + reserves
//! * locks + holds
//! * locks + freezes
//! * reserves + holds
//! * reserves + freezes
//! * holds + freezes
//!
//! * Three primitives combined
//! * locks + reserves + holds
//! * locks + reserves + freezes
//! * locks + holds + freezes
//! * reserves + holds + freezes
//!
//! * All four primitives combined
//! * locks + reserves + holds + freezes
//!
//! For each test, after creating the primitive, we check:
//!
//! * The account data triplet.
//! * What `can_reserve` returns and where is the boundary.
//! * What `can_hold` returns and where is the boundary.
use super::*;
use frame_support::traits::{
fungible::{InspectHold, MutateFreeze, MutateHold},
Currency, LockIdentifier, LockableCurrency, ReservableCurrency, WithdrawReasons,
};
fn subject() -> AccountId {
let subject = 1;
Balances::make_free_balance_be(&subject, 100);
subject
}
const ID: LockIdentifier = *b"1 ";
fn b(x: AccountId) -> (Balance, Balance, Balance) {
let a = get_test_account_data(x);
(a.free, a.reserved, a.frozen)
}
fn ensure_max_reserve(who: AccountId, amount: Balance) {
assert!(!<Balances as ReservableCurrency<_>>::can_reserve(&who, amount.max(1) * 2));
assert!(!<Balances as ReservableCurrency<_>>::can_reserve(&who, amount + 1));
assert!(<Balances as ReservableCurrency<_>>::can_reserve(&who, amount));
assert!(<Balances as ReservableCurrency<_>>::can_reserve(&who, amount.saturating_sub(1)));
assert!(<Balances as ReservableCurrency<_>>::can_reserve(&who, amount / 2));
}
fn ensure_max_hold(who: AccountId, amount: Balance) {
assert!(<Balances as InspectHold<_>>::ensure_can_hold(&TestId::Foo, &who, amount.max(1) * 2)
.is_err());
assert!(<Balances as InspectHold<_>>::ensure_can_hold(&TestId::Foo, &who, amount + 1).is_err());
assert!(<Balances as InspectHold<_>>::ensure_can_hold(&TestId::Foo, &who, amount).is_ok());
assert!(<Balances as InspectHold<_>>::ensure_can_hold(
&TestId::Foo,
&who,
amount.saturating_sub(1)
)
.is_ok());
assert!(<Balances as InspectHold<_>>::ensure_can_hold(&TestId::Foo, &who, amount / 2).is_ok());
}
// Two primitives combined
#[test]
fn locks_and_reserves() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 50, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 50));
// ❌ THIS IS NOT COOL!
// Can reserve up to 50 (total 100 >= frozen 50, free >= ED after reserve)
ensure_max_reserve(who, 50);
// Can hold up to 99 (leaving 1 for ED)
ensure_max_hold(who, 99);
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 30));
assert_eq!(b(who), (70, 30, 50));
// ❌ THIS IS NOT COOL!
// Can reserve 20 more (total 100 >= frozen 50, free would be 50 >= ED)
ensure_max_reserve(who, 20);
// Can hold up to 69 more (leaving 1 for ED)
ensure_max_hold(who, 69);
});
}
#[test]
fn locks_and_holds() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 60 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 60, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 60));
ensure_max_hold(who, 99);
ensure_max_reserve(who, 40);
// Hold 40 tokens
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 40));
assert_eq!(b(who), (60, 40, 60));
ensure_max_hold(who, 59);
ensure_max_reserve(who, 0);
});
}
#[test]
fn locks_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 40 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 40, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 40));
// Freeze 70 tokens
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 70));
// Frozen takes the max of lock (40) and freeze (70)
assert_eq!(b(who), (100, 0, 70));
ensure_max_hold(who, 99);
ensure_max_reserve(who, 30);
});
}
#[test]
fn reserves_and_holds() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Reserve 30 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 30));
assert_eq!(b(who), (70, 30, 0));
ensure_max_reserve(who, 69);
ensure_max_hold(who, 69);
// Hold 25 tokens (accumulates with reserve)
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 25));
assert_eq!(b(who), (45, 55, 0)); // reserved = 30 + 25 = 55
ensure_max_reserve(who, 44);
ensure_max_hold(who, 44);
});
}
#[test]
fn reserves_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Reserve 25 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 25));
assert_eq!(b(who), (75, 25, 0));
// Freeze 80 tokens
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 80));
assert_eq!(b(who), (75, 25, 80));
// The current implementation doesn't allow more reserves once freezes are involved
ensure_max_reserve(who, 0);
ensure_max_hold(who, 74);
});
}
#[test]
fn holds_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Hold 35 tokens
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 35));
assert_eq!(b(who), (65, 35, 0));
// Freeze 90 tokens
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 90));
assert_eq!(b(who), (65, 35, 90));
ensure_max_hold(who, 64);
ensure_max_reserve(who, 0);
});
}
// Three primitives combined
#[test]
fn locks_reserves_and_holds() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 60 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 60, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 60));
// Reserve 20 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 20));
assert_eq!(b(who), (80, 20, 60));
// Hold 15 tokens (accumulates with reserve)
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 15));
assert_eq!(b(who), (65, 35, 60)); // reserved = 20 + 15 = 35
// Can reserve up to 5 more (total 100 >= frozen 60, free would be 60 >= ED)
ensure_max_reserve(who, 5);
// Can hold up to 64 more (leaving 1 for ED)
ensure_max_hold(who, 64);
});
}
#[test]
fn locks_reserves_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 40 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 40, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 40));
// Reserve 25 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 25));
assert_eq!(b(who), (75, 25, 40));
// Freeze 80 tokens (max of lock 40 and freeze 80)
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 80));
assert_eq!(b(who), (75, 25, 80));
// The current implementation doesn't allow more reserves when freezes are involved
ensure_max_reserve(who, 0);
ensure_max_hold(who, 74);
});
}
#[test]
fn locks_holds_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 50 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 50, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 50));
// Hold 30 tokens
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 30));
assert_eq!(b(who), (70, 30, 50));
// Freeze 75 tokens (max of lock 50 and freeze 75)
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 75));
assert_eq!(b(who), (70, 30, 75));
// Can hold up to 69 more (leaving 1 for ED)
ensure_max_hold(who, 69);
// The current implementation doesn't allow more reserves when freezes are involved
ensure_max_reserve(who, 0);
});
}
#[test]
fn reserves_holds_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Reserve 20 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 20));
assert_eq!(b(who), (80, 20, 0));
// Hold 25 tokens (accumulates with reserve)
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 25));
assert_eq!(b(who), (55, 45, 0)); // reserved = 20 + 25 = 45
// Freeze 90 tokens
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 90));
assert_eq!(b(who), (55, 45, 90));
// Can hold up to 54 more (leaving 1 for ED)
ensure_max_hold(who, 54);
// The current implementation doesn't allow more reserves when freezes are involved
ensure_max_reserve(who, 0);
});
}
// All four primitives combined
#[test]
fn locks_reserves_holds_and_freezes() {
ExtBuilder::default()
.monied(false)
.existential_deposit(1)
.build_and_execute_with(|| {
let who = subject();
// Lock 40 tokens
<Balances as LockableCurrency<_>>::set_lock(ID, &who, 40, WithdrawReasons::all());
assert_eq!(b(who), (100, 0, 40));
// Reserve 20 tokens
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&who, 20));
assert_eq!(b(who), (80, 20, 40));
// Hold 15 tokens (accumulates with reserve)
assert_ok!(<Balances as MutateHold<_>>::hold(&TestId::Foo, &who, 15));
assert_eq!(b(who), (65, 35, 40)); // reserved = 20 + 15 = 35
// Freeze 85 tokens (max of lock 40 and freeze 85)
assert_ok!(<Balances as MutateFreeze<_>>::set_freeze(&TestId::Foo, &who, 85));
assert_eq!(b(who), (65, 35, 85));
// Can hold up to 64 more (leaving 1 for ED)
ensure_max_hold(who, 64);
// The current implementation doesn't allow more reserves when freezes are involved
ensure_max_reserve(who, 0);
// Since max_reserve is 0 when freezes are involved, we can't reserve more
// Test that we can't reserve more
assert!(!<Balances as ReservableCurrency<_>>::can_reserve(&who, 1));
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment