-
Notifications
You must be signed in to change notification settings - Fork 709
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
[balances] Safeguard against consumer ref underflow #3865
Changes from all commits
7286ae0
b43039c
d3f25e6
340ca02
2fdf2fc
c5738ef
aeff9e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
title: "Balances: add failsafe for consumer ref underflow" | ||
|
||
doc: | ||
- audience: Runtime Dev | ||
description: | | ||
Pallet balances now handles the case that historic accounts violate a invariant that they should have a consumer ref on `reserved > 0` balance. | ||
This disallows such accounts from reaping and should prevent TI from getting messed up even more. | ||
|
||
crates: | ||
- name: pallet-balances | ||
bump: patch |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
// 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. | ||
|
||
#![cfg(test)] | ||
|
||
use crate::{ | ||
system::AccountInfo, | ||
tests::{ensure_ti_valid, Balances, ExtBuilder, System, Test, TestId, UseSystem}, | ||
AccountData, ExtraFlags, TotalIssuance, | ||
}; | ||
use frame_support::{ | ||
assert_noop, assert_ok, hypothetically, | ||
traits::{ | ||
fungible::{Mutate, MutateHold}, | ||
tokens::Precision, | ||
}, | ||
}; | ||
use sp_runtime::DispatchError; | ||
|
||
/// There are some accounts that have one consumer ref too few. These accounts are at risk of losing | ||
/// their held (reserved) balance. They do not just lose it - it is also not accounted for in the | ||
/// Total Issuance. Here we test the case that the account does not reap in such a case, but gets | ||
/// one consumer ref for its reserved balance. | ||
#[test] | ||
fn regression_historic_acc_does_not_evaporate_reserve() { | ||
ExtBuilder::default().build_and_execute_with(|| { | ||
UseSystem::set(true); | ||
let (alice, bob) = (0, 1); | ||
// Alice is in a bad state with consumer == 0 && reserved > 0: | ||
Balances::set_balance(&alice, 100); | ||
TotalIssuance::<Test>::put(100); | ||
ensure_ti_valid(); | ||
|
||
assert_ok!(Balances::hold(&TestId::Foo, &alice, 10)); | ||
// This is the issue of the account: | ||
System::dec_consumers(&alice); | ||
|
||
assert_eq!( | ||
System::account(&alice), | ||
AccountInfo { | ||
data: AccountData { | ||
free: 90, | ||
reserved: 10, | ||
frozen: 0, | ||
flags: ExtraFlags(1u128 << 127), | ||
}, | ||
nonce: 0, | ||
consumers: 0, // should be 1 on a good acc | ||
providers: 1, | ||
sufficients: 0, | ||
} | ||
); | ||
|
||
ensure_ti_valid(); | ||
|
||
// Reaping the account is prevented by the new logic: | ||
assert_noop!( | ||
Balances::transfer_allow_death(Some(alice).into(), bob, 90), | ||
DispatchError::ConsumerRemaining | ||
); | ||
assert_noop!( | ||
Balances::transfer_all(Some(alice).into(), bob, false), | ||
DispatchError::ConsumerRemaining | ||
); | ||
|
||
// normal transfers still work: | ||
hypothetically!({ | ||
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40)); | ||
// Alice got back her consumer ref: | ||
assert_eq!(System::consumers(&alice), 1); | ||
ensure_ti_valid(); | ||
}); | ||
hypothetically!({ | ||
assert_ok!(Balances::transfer_all(Some(alice).into(), bob, true)); | ||
// Alice got back her consumer ref: | ||
assert_eq!(System::consumers(&alice), 1); | ||
ensure_ti_valid(); | ||
}); | ||
|
||
// un-reserving all does not add a consumer ref: | ||
hypothetically!({ | ||
assert_ok!(Balances::release(&TestId::Foo, &alice, 10, Precision::Exact)); | ||
assert_eq!(System::consumers(&alice), 0); | ||
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40)); | ||
assert_eq!(System::consumers(&alice), 0); | ||
ensure_ti_valid(); | ||
}); | ||
// un-reserving some does add a consumer ref: | ||
hypothetically!({ | ||
assert_ok!(Balances::release(&TestId::Foo, &alice, 5, Precision::Exact)); | ||
assert_eq!(System::consumers(&alice), 1); | ||
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40)); | ||
assert_eq!(System::consumers(&alice), 1); | ||
ensure_ti_valid(); | ||
}); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ | |
|
||
#![cfg(test)] | ||
|
||
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet}; | ||
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance}; | ||
use codec::{Decode, Encode, MaxEncodedLen}; | ||
use frame_support::{ | ||
assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl, | ||
|
@@ -47,6 +47,7 @@ mod currency_tests; | |
mod dispatchable_tests; | ||
mod fungible_conformance_tests; | ||
mod fungible_tests; | ||
mod general_tests; | ||
mod reentrancy_tests; | ||
|
||
type Block = frame_system::mocking::MockBlock<Test>; | ||
|
@@ -278,6 +279,23 @@ pub fn info_from_weight(w: Weight) -> DispatchInfo { | |
DispatchInfo { weight: w, ..Default::default() } | ||
} | ||
|
||
/// Check that the total-issuance matches the sum of all accounts' total balances. | ||
pub fn ensure_ti_valid() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we easily inject this at the end of all tests in this crate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes. I though about adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be curious to also know if it's feasible as a try-state hook There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah this is what most pallets do, and I generally find it easier.
Ideally it should be, but yeah iterating all accounts will ruin everything else 🙈 We need to think of a system to separate try-state hooks that we always run in a place like CI vs. those that we want to run every month etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be really interested to bench it. We have a lot of really heavy staking hooks today without much issue, maybe as long as it's O(n) it's OK. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looking forward to adding it :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ran these checks in remote-externalities to find some issues with this: https://github.com/ggwpez/wtfwt There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3-5s is acceptable imo |
||
let mut sum = 0; | ||
|
||
for acc in frame_system::Account::<Test>::iter_keys() { | ||
if UseSystem::get() { | ||
let data = frame_system::Pallet::<Test>::account(acc); | ||
sum += data.data.total(); | ||
} else { | ||
let data = crate::Account::<Test>::get(acc); | ||
sum += data.total(); | ||
} | ||
} | ||
|
||
assert_eq!(TotalIssuance::<Test>::get(), sum, "Total Issuance wrong"); | ||
} | ||
|
||
#[test] | ||
fn weights_sane() { | ||
let info = crate::Call::<Test>::transfer_allow_death { dest: 10, value: 4 }.get_dispatch_info(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh this is a great macro, I always manually used a transactional layer!