Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FRAME: Conformance tests for fungible API #225

Open
2 of 5 tasks
Tracked by #327
gavofyork opened this issue Jan 27, 2023 · 8 comments
Open
2 of 5 tasks
Tracked by #327

FRAME: Conformance tests for fungible API #225

gavofyork opened this issue Jan 27, 2023 · 8 comments
Assignees
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework. T10-tests This PR/Issue is related to tests.

Comments

@gavofyork
Copy link
Member

gavofyork commented Jan 27, 2023

Generic acceptance tests for the fungible::* API, so any implementation can check it does the right thing.

Fungible API

Fungibles API

  • Write a new task list here describing TODOs to test fungibles. Should reuse as much code from the fungible api tests as makes sense (see fungible/item_of.rs)
@gavofyork gavofyork changed the title FRAME: Confirmance tests for fungible API FRAME: Conformance tests for fungible API Jan 27, 2023
@liamaharon liamaharon self-assigned this Mar 18, 2023
@liamaharon
Copy link
Contributor

liamaharon commented Mar 18, 2023

I'll pick this one up, planning to start on it next week.

Plan:

  1. Review fungible documentation and code, and if required clear up any ambiguity in the specification
  2. Create a candidate list of conformance tests and scaffold some code, get it looked over by another member of the FRAME team
  3. Implement conformance tests for the balances pallet
  4. Abstract balances pallet conformance tests into a generalised macro which can be used something like conformance_tests!(Implementor)

@liamaharon
Copy link
Contributor

liamaharon commented Mar 25, 2023

My current thinking:

  • We should segment tests by trait (e.g. one for Mutate, one for Balanced, one for MutateHold, etc.), so they can be selectively applied only for traits implemented by the pallet
  • Each trait test should begin by thoroughly unit testing each trait method, and finish with any integration tests deemed necessary to achieve full coverage
  • The tests should live somewhere in frame/support/src/traits/tokens/fungible, perhaps
frame/support/src/traits/tokens/fungible/
├── freeze.rs
├── hold.rs
├── item_of.rs
├── mod.rs
├── regular.rs
└── conformance_tests/
    ├── balanced_hold.rs
    ├── balanced.rs
    ├── handle_imbalance_drop.rs
    ├── inspect_freeze.rs
    ├── inspect_hold.rs
    ├── inspect.rs
    ├── mod.rs
    ├── mutate_freeze.rs
    ├── mutate_hold.rs
    └── mutate.rs
  • The tests can be applied either
    • one macro per-trait, e.g. fungible_inspect_conformance_tests!(PalletName) or
    • using a single meta-macro that inspects the pallet to determine which trait-specific macros are appropriate and auto-implements them, e.g. fungible_conformance_tests!(PalletName)

In terms of implementation steps, I'm thinking to methodically walk through each trait and test each method, initially coupling things to balances. Once the test surface is solid, begin the process of de-coupling & converting the tests into macros. edit: implement Inspect and Mutate tests directly on the Balances pallet, check with the team my testing approach is sensible, then if OK abstract the tests into macros and proceed with writing tests for the remaining traits.

Open Questions

  1. Does the pattern of segmenting tests (and macros) by fungible trait make sense?

    • As I write this I'm becoming unsure about this approach, because some traits seem to depend on another. Example: how is it possible to test total_balance from the Inspect trait if the pallet doesn't also implement Mutate allowing us to use set_balance in the test?
    • Edit: I just spoke about this with Keith. We agreed that extreme edge cases where a pallet does something like implementing Inspect without Mutate are not worth supporting (at least initially). In most cases we are covered by supertraits anyway, e.g. if a pallet implements Mutate we know that Inspect + Unbalanced are also implemented.
  2. Do we need to add conformance tests for Unbalanced traits? Based on the documentation I would think no, because it is not advised for pallets to use them (they should use Balanced instead). However I noticed that the balances pallet implements Unbalanced for some operations, so am unsure.

  3. How could the de-coupling of conformance tests & abstraction into macros be done? (this doesn't need to be addressed immediately, but when I do get to it I'd be grateful for some help, probably from @sam0x17 and/or @KiChjang).

@kianenigma
Copy link
Contributor

TODO: What does it mean for some issuance to be 'controlled by the system'?

eg. locked and controlled by the crowdloan system.

The tests should live somewhere in frame/support/src/traits/tokens/fungible, perhaps

The provided branch puts them under frame/balances though.

@liamaharon
Copy link
Contributor

I've made progress on Inspect and Mutate tests, and refactored them into macros exported from frame_support on this very WIP, PoC branch.

I have a bunch of questions such as (non-exhaustive)

  • Where do we draw the line on assuming pallet implementation details? e.g. existential deposit is only mentioned in passing in some comments in the fungibles crate, but it (or something like it) is required if we'd like to test parameters on some functions such as preservation on Mutate::decrease_balance
  • Best practices on Rust testing patterns, e.g. testing that done_burn_from is called after burn_from
  • Naming (modules, files, macros, etc)
  • Usage of use in macros
  • Is my general approach here making sense, is there anything I am missing?

I think they'd be best covered over a call. @kianenigma if you have time this week perhaps we could schedule 30 minutes in?

Once these questions have been cleared up and the PoC smoothed over, I think I'm ready to create a draft PR from my branch tracking TODOs and progress.

@xlc
Copy link
Contributor

xlc commented Mar 28, 2023

I will suggest don't try to implement everything in one go. Focus on a few traits and push it. Then we can also try to integrate them in ORML to check the implementation of orml-tokens. We will likely have feedbacks based on real usages.

@liamaharon
Copy link
Contributor

liamaharon commented Mar 31, 2023

Spoke with Kian about this. Going to:

  • Refactor the macros into simple functions which take a struct implementing the trait/s they will test
  • Remove assumptions about an 'existential deposit', just the existing minimum_balance (I had missed that they were the same)
  • Begin progressively shipping trait tests, as also suggested by @xlc

@juangirini
Copy link
Contributor

@liamaharon I see a close PR, does that closes this one?

@liamaharon
Copy link
Contributor

@juangirini there're still some traits that need coverage, I'll update the issue description to track them

@gavofyork gavofyork mentioned this issue Aug 24, 2023
20 tasks
@juangirini juangirini transferred this issue from paritytech/substrate Aug 24, 2023
@the-right-joyce the-right-joyce added T10-tests This PR/Issue is related to tests. T1-FRAME This PR/Issue is related to core FRAME, the framework. and removed I5-tests labels Aug 25, 2023
github-merge-queue bot pushed a commit that referenced this issue Jan 15, 2024
#1296)

Original PR paritytech/substrate#14655

---

Partial #225

- [x] Adds conformance tests for Unbalanced
- [x] Adds conformance tests for Balanced
- Several minor fixes to fungible default implementations and the
Balances pallet
- [x] `Unbalanced::decrease_balance` can reap account when
`Preservation` is `Preserve`
- [x] `Balanced::pair` can return pairs of imbalances which do not
cancel each other out
   - [x] Balances pallet `active_issuance` 'underflow'
- [x] Refactors the conformance test file structure to match the
fungible file structure: tests for traits in regular.rs go into a test
file named regular.rs, tests for traits in freezes.rs go into a test
file named freezes.rs, etc.
 - [x] Improve doc comments
 - [x] Simplify macros

## Fixes

### `Unbalanced::decrease_balance` can reap account when called with
`Preservation::Preserve`
There is a potential issue in the default implementation of
`Unbalanced::decrease_balance`. The implementation can delete an account
even when it is called with `preservation: Preservation::Preserve`. This
seems to contradict the documentation of `Preservation::Preserve`:

```rust
	/// The account may not be killed and our provider reference must remain (in the context of
	/// tokens, this means that the account may not be dusted).
	Preserve,
```

I updated `Unbalanced::decrease_balance` to return
`Err(TokenError::BelowMinimum)` when a withdrawal would cause the
account to be reaped and `preservation: Preservation::Preserve`.

- [ ] TODO Confirm with @gavofyork that this is correct behavior

Test for this behavior:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L912-L937

### `Balanced::pair` returning non-canceling pairs

`Balanced::pair` is supposed to create a pair of imbalances that cancel
each other out. However this is not the case when the method is called
with an amount greater than the total supply.

In the existing default implementation, `Balanced::pair` creates a pair
by first rescinding the balance, creating `Debt`, and then issuing the
balance, creating `Credit`.

When creating `Debt`, if the amount to create exceeds the
`total_supply`, `total_supply` units of `Debt` are created *instead* of
`amount` units of `Debt`. This can lead to non-canceling amount of
`Credit` and `Debt` being created.

To address this, I create the credit and debt directly in the method
instead of calling `issue` and `rescind`.

Test for this behavior:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1323-L1346

### `Balances` pallet `active_issuance` 'underflow'

This PR resolves an issue in the `Balances` pallet that can lead to odd
behavior of `active_issuance`.

Currently, the Balances pallet doesn't check if `InactiveIssuance`
remains less than or equal to `TotalIssuance` when supply is
deactivated. This allows `InactiveIssuance` to be greater than
`TotalIssuance`, which can result in unexpected behavior from the
perspective of the fungible API.

`active_issuance` is derived from
`TotalIssuance.saturating_sub(InactiveIssuance)`.

If an `amount` is deactivated that causes `InactiveIssuance` to become
greater TotalIssuance, `active_issuance` will return 0. However once in
that state, reactivating an amount will not increase `active_issuance`
by the reactivated `amount` as expected.

Consider this test where the last assertion would fail due to this
issue:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1036-L1071

To address this, I've modified the `deactivate` function to ensure
`InactiveIssuance` never surpasses `TotalIssuance`.

---------

Co-authored-by: Muharem <ismailov.m.h@gmail.com>
ahmadkaouk pushed a commit to moonbeam-foundation/polkadot-sdk that referenced this issue Jan 21, 2024
paritytech#1296)

Original PR paritytech/substrate#14655

---

Partial paritytech#225

- [x] Adds conformance tests for Unbalanced
- [x] Adds conformance tests for Balanced
- Several minor fixes to fungible default implementations and the
Balances pallet
- [x] `Unbalanced::decrease_balance` can reap account when
`Preservation` is `Preserve`
- [x] `Balanced::pair` can return pairs of imbalances which do not
cancel each other out
   - [x] Balances pallet `active_issuance` 'underflow'
- [x] Refactors the conformance test file structure to match the
fungible file structure: tests for traits in regular.rs go into a test
file named regular.rs, tests for traits in freezes.rs go into a test
file named freezes.rs, etc.
 - [x] Improve doc comments
 - [x] Simplify macros
`Preservation::Preserve`
There is a potential issue in the default implementation of
`Unbalanced::decrease_balance`. The implementation can delete an account
even when it is called with `preservation: Preservation::Preserve`. This
seems to contradict the documentation of `Preservation::Preserve`:

```rust
	/// The account may not be killed and our provider reference must remain (in the context of
	/// tokens, this means that the account may not be dusted).
	Preserve,
```

I updated `Unbalanced::decrease_balance` to return
`Err(TokenError::BelowMinimum)` when a withdrawal would cause the
account to be reaped and `preservation: Preservation::Preserve`.

- [ ] TODO Confirm with @gavofyork that this is correct behavior

Test for this behavior:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L912-L937

`Balanced::pair` is supposed to create a pair of imbalances that cancel
each other out. However this is not the case when the method is called
with an amount greater than the total supply.

In the existing default implementation, `Balanced::pair` creates a pair
by first rescinding the balance, creating `Debt`, and then issuing the
balance, creating `Credit`.

When creating `Debt`, if the amount to create exceeds the
`total_supply`, `total_supply` units of `Debt` are created *instead* of
`amount` units of `Debt`. This can lead to non-canceling amount of
`Credit` and `Debt` being created.

To address this, I create the credit and debt directly in the method
instead of calling `issue` and `rescind`.

Test for this behavior:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1323-L1346

This PR resolves an issue in the `Balances` pallet that can lead to odd
behavior of `active_issuance`.

Currently, the Balances pallet doesn't check if `InactiveIssuance`
remains less than or equal to `TotalIssuance` when supply is
deactivated. This allows `InactiveIssuance` to be greater than
`TotalIssuance`, which can result in unexpected behavior from the
perspective of the fungible API.

`active_issuance` is derived from
`TotalIssuance.saturating_sub(InactiveIssuance)`.

If an `amount` is deactivated that causes `InactiveIssuance` to become
greater TotalIssuance, `active_issuance` will return 0. However once in
that state, reactivating an amount will not increase `active_issuance`
by the reactivated `amount` as expected.

Consider this test where the last assertion would fail due to this
issue:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1036-L1071

To address this, I've modified the `deactivate` function to ensure
`InactiveIssuance` never surpasses `TotalIssuance`.

---------

Co-authored-by: Muharem <ismailov.m.h@gmail.com>
ahmadkaouk pushed a commit to moonbeam-foundation/polkadot-sdk that referenced this issue Jan 29, 2024
paritytech#1296)

Original PR paritytech/substrate#14655

---

Partial paritytech#225

- [x] Adds conformance tests for Unbalanced
- [x] Adds conformance tests for Balanced
- Several minor fixes to fungible default implementations and the
Balances pallet
- [x] `Unbalanced::decrease_balance` can reap account when
`Preservation` is `Preserve`
- [x] `Balanced::pair` can return pairs of imbalances which do not
cancel each other out
   - [x] Balances pallet `active_issuance` 'underflow'
- [x] Refactors the conformance test file structure to match the
fungible file structure: tests for traits in regular.rs go into a test
file named regular.rs, tests for traits in freezes.rs go into a test
file named freezes.rs, etc.
 - [x] Improve doc comments
 - [x] Simplify macros

## Fixes

### `Unbalanced::decrease_balance` can reap account when called with
`Preservation::Preserve`
There is a potential issue in the default implementation of
`Unbalanced::decrease_balance`. The implementation can delete an account
even when it is called with `preservation: Preservation::Preserve`. This
seems to contradict the documentation of `Preservation::Preserve`:

```rust
	/// The account may not be killed and our provider reference must remain (in the context of
	/// tokens, this means that the account may not be dusted).
	Preserve,
```

I updated `Unbalanced::decrease_balance` to return
`Err(TokenError::BelowMinimum)` when a withdrawal would cause the
account to be reaped and `preservation: Preservation::Preserve`.

- [ ] TODO Confirm with @gavofyork that this is correct behavior

Test for this behavior:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L912-L937

### `Balanced::pair` returning non-canceling pairs

`Balanced::pair` is supposed to create a pair of imbalances that cancel
each other out. However this is not the case when the method is called
with an amount greater than the total supply.

In the existing default implementation, `Balanced::pair` creates a pair
by first rescinding the balance, creating `Debt`, and then issuing the
balance, creating `Credit`.

When creating `Debt`, if the amount to create exceeds the
`total_supply`, `total_supply` units of `Debt` are created *instead* of
`amount` units of `Debt`. This can lead to non-canceling amount of
`Credit` and `Debt` being created.

To address this, I create the credit and debt directly in the method
instead of calling `issue` and `rescind`.

Test for this behavior:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1323-L1346

### `Balances` pallet `active_issuance` 'underflow'

This PR resolves an issue in the `Balances` pallet that can lead to odd
behavior of `active_issuance`.

Currently, the Balances pallet doesn't check if `InactiveIssuance`
remains less than or equal to `TotalIssuance` when supply is
deactivated. This allows `InactiveIssuance` to be greater than
`TotalIssuance`, which can result in unexpected behavior from the
perspective of the fungible API.

`active_issuance` is derived from
`TotalIssuance.saturating_sub(InactiveIssuance)`.

If an `amount` is deactivated that causes `InactiveIssuance` to become
greater TotalIssuance, `active_issuance` will return 0. However once in
that state, reactivating an amount will not increase `active_issuance`
by the reactivated `amount` as expected.

Consider this test where the last assertion would fail due to this
issue:

https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1036-L1071

To address this, I've modified the `deactivate` function to ensure
`InactiveIssuance` never surpasses `TotalIssuance`.

---------

Co-authored-by: Muharem <ismailov.m.h@gmail.com>
(cherry picked from commit 46090ff)
bgallois pushed a commit to duniter/duniter-polkadot-sdk that referenced this issue Mar 25, 2024
paritytech#1296)

Original PR paritytech/substrate#14655

---

Partial paritytech#225

- [x] Adds conformance tests for Unbalanced
- [x] Adds conformance tests for Balanced
- Several minor fixes to fungible default implementations and the
Balances pallet
- [x] `Unbalanced::decrease_balance` can reap account when
`Preservation` is `Preserve`
- [x] `Balanced::pair` can return pairs of imbalances which do not
cancel each other out
   - [x] Balances pallet `active_issuance` 'underflow'
- [x] Refactors the conformance test file structure to match the
fungible file structure: tests for traits in regular.rs go into a test
file named regular.rs, tests for traits in freezes.rs go into a test
file named freezes.rs, etc.
 - [x] Improve doc comments
 - [x] Simplify macros

## Fixes

### `Unbalanced::decrease_balance` can reap account when called with
`Preservation::Preserve`
There is a potential issue in the default implementation of
`Unbalanced::decrease_balance`. The implementation can delete an account
even when it is called with `preservation: Preservation::Preserve`. This
seems to contradict the documentation of `Preservation::Preserve`:

```rust
	/// The account may not be killed and our provider reference must remain (in the context of
	/// tokens, this means that the account may not be dusted).
	Preserve,
```

I updated `Unbalanced::decrease_balance` to return
`Err(TokenError::BelowMinimum)` when a withdrawal would cause the
account to be reaped and `preservation: Preservation::Preserve`.

- [ ] TODO Confirm with @gavofyork that this is correct behavior

Test for this behavior:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L912-L937

### `Balanced::pair` returning non-canceling pairs

`Balanced::pair` is supposed to create a pair of imbalances that cancel
each other out. However this is not the case when the method is called
with an amount greater than the total supply.

In the existing default implementation, `Balanced::pair` creates a pair
by first rescinding the balance, creating `Debt`, and then issuing the
balance, creating `Credit`.

When creating `Debt`, if the amount to create exceeds the
`total_supply`, `total_supply` units of `Debt` are created *instead* of
`amount` units of `Debt`. This can lead to non-canceling amount of
`Credit` and `Debt` being created.

To address this, I create the credit and debt directly in the method
instead of calling `issue` and `rescind`.

Test for this behavior:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1323-L1346

### `Balances` pallet `active_issuance` 'underflow'

This PR resolves an issue in the `Balances` pallet that can lead to odd
behavior of `active_issuance`.

Currently, the Balances pallet doesn't check if `InactiveIssuance`
remains less than or equal to `TotalIssuance` when supply is
deactivated. This allows `InactiveIssuance` to be greater than
`TotalIssuance`, which can result in unexpected behavior from the
perspective of the fungible API.

`active_issuance` is derived from
`TotalIssuance.saturating_sub(InactiveIssuance)`.

If an `amount` is deactivated that causes `InactiveIssuance` to become
greater TotalIssuance, `active_issuance` will return 0. However once in
that state, reactivating an amount will not increase `active_issuance`
by the reactivated `amount` as expected.

Consider this test where the last assertion would fail due to this
issue:


https://github.com/paritytech/polkadot-sdk/blob/e5c876dd6b59e2b7dbacaa4538cb42c802db3730/substrate/frame/support/src/traits/tokens/fungible/conformance_tests/regular.rs#L1036-L1071

To address this, I've modified the `deactivate` function to ensure
`InactiveIssuance` never surpasses `TotalIssuance`.

---------

Co-authored-by: Muharem <ismailov.m.h@gmail.com>
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 8, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 8, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 9, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 10, 2024
serban300 pushed a commit to serban300/polkadot-sdk that referenced this issue Apr 10, 2024
jonathanudd pushed a commit to jonathanudd/polkadot-sdk that referenced this issue Apr 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework. T10-tests This PR/Issue is related to tests.
Projects
Status: In Progress
Development

No branches or pull requests

6 participants