Skip to content

Commit f7193fa

Browse files
ggwpezMorganamilo
authored andcommitted
[balances] Safeguard against consumer ref underflow (#3865)
There are some accounts that do not have a consumer ref while having a reserve. This adds a fail-safe mechanism to trigger in the case that `does_consume` is true, but the assumption of `consumer>0` is not. This should prevent those accounts from loosing balance and the TI from getting messed up even more, but is not an "ideal" fix. TBH an ideal fix is not possible, since on-chain data is in an invalid state. --------- Signed-off-by: Oliver Tale-Yazdi <[email protected]>
1 parent e8f8aac commit f7193fa

6 files changed

Lines changed: 150 additions & 2 deletions

File tree

prdoc/pr_3865.prdoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
title: "Balances: add failsafe for consumer ref underflow"
2+
3+
doc:
4+
- audience: Runtime Dev
5+
description: |
6+
Pallet balances now handles the case that historic accounts violate a invariant that they should have a consumer ref on `reserved > 0` balance.
7+
This disallows such accounts from reaping and should prevent TI from getting messed up even more.
8+
9+
crates:
10+
- name: pallet-balances
11+
bump: patch

substrate/frame/balances/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ docify = "0.2.8"
2828

2929
[dev-dependencies]
3030
pallet-transaction-payment = { path = "../transaction-payment" }
31+
frame-support = { path = "../support", features = ["experimental"] }
3132
sp-core = { path = "../../primitives/core" }
3233
sp-io = { path = "../../primitives/io" }
3334
paste = "1.0.12"

substrate/frame/balances/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,13 @@ pub mod pallet {
954954
if !did_consume && does_consume {
955955
frame_system::Pallet::<T>::inc_consumers(who)?;
956956
}
957+
if does_consume && frame_system::Pallet::<T>::consumers(who) == 0 {
958+
// NOTE: This is a failsafe and should not happen for normal accounts. A normal
959+
// account should have gotten a consumer ref in `!did_consume && does_consume`
960+
// at some point.
961+
log::error!(target: LOG_TARGET, "Defensively bumping a consumer ref.");
962+
frame_system::Pallet::<T>::inc_consumers(who)?;
963+
}
957964
if did_provide && !does_provide {
958965
// This could reap the account so must go last.
959966
frame_system::Pallet::<T>::dec_providers(who).map_err(|r| {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// This file is part of Substrate.
2+
3+
// Copyright (C) Parity Technologies (UK) Ltd.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
#![cfg(test)]
19+
20+
use crate::{
21+
system::AccountInfo,
22+
tests::{ensure_ti_valid, Balances, ExtBuilder, System, Test, TestId, UseSystem},
23+
AccountData, ExtraFlags, TotalIssuance,
24+
};
25+
use frame_support::{
26+
assert_noop, assert_ok, hypothetically,
27+
traits::{
28+
fungible::{Mutate, MutateHold},
29+
tokens::Precision,
30+
},
31+
};
32+
use sp_runtime::DispatchError;
33+
34+
/// There are some accounts that have one consumer ref too few. These accounts are at risk of losing
35+
/// their held (reserved) balance. They do not just lose it - it is also not accounted for in the
36+
/// Total Issuance. Here we test the case that the account does not reap in such a case, but gets
37+
/// one consumer ref for its reserved balance.
38+
#[test]
39+
fn regression_historic_acc_does_not_evaporate_reserve() {
40+
ExtBuilder::default().build_and_execute_with(|| {
41+
UseSystem::set(true);
42+
let (alice, bob) = (0, 1);
43+
// Alice is in a bad state with consumer == 0 && reserved > 0:
44+
Balances::set_balance(&alice, 100);
45+
TotalIssuance::<Test>::put(100);
46+
ensure_ti_valid();
47+
48+
assert_ok!(Balances::hold(&TestId::Foo, &alice, 10));
49+
// This is the issue of the account:
50+
System::dec_consumers(&alice);
51+
52+
assert_eq!(
53+
System::account(&alice),
54+
AccountInfo {
55+
data: AccountData {
56+
free: 90,
57+
reserved: 10,
58+
frozen: 0,
59+
flags: ExtraFlags(1u128 << 127),
60+
},
61+
nonce: 0,
62+
consumers: 0, // should be 1 on a good acc
63+
providers: 1,
64+
sufficients: 0,
65+
}
66+
);
67+
68+
ensure_ti_valid();
69+
70+
// Reaping the account is prevented by the new logic:
71+
assert_noop!(
72+
Balances::transfer_allow_death(Some(alice).into(), bob, 90),
73+
DispatchError::ConsumerRemaining
74+
);
75+
assert_noop!(
76+
Balances::transfer_all(Some(alice).into(), bob, false),
77+
DispatchError::ConsumerRemaining
78+
);
79+
80+
// normal transfers still work:
81+
hypothetically!({
82+
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
83+
// Alice got back her consumer ref:
84+
assert_eq!(System::consumers(&alice), 1);
85+
ensure_ti_valid();
86+
});
87+
hypothetically!({
88+
assert_ok!(Balances::transfer_all(Some(alice).into(), bob, true));
89+
// Alice got back her consumer ref:
90+
assert_eq!(System::consumers(&alice), 1);
91+
ensure_ti_valid();
92+
});
93+
94+
// un-reserving all does not add a consumer ref:
95+
hypothetically!({
96+
assert_ok!(Balances::release(&TestId::Foo, &alice, 10, Precision::Exact));
97+
assert_eq!(System::consumers(&alice), 0);
98+
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
99+
assert_eq!(System::consumers(&alice), 0);
100+
ensure_ti_valid();
101+
});
102+
// un-reserving some does add a consumer ref:
103+
hypothetically!({
104+
assert_ok!(Balances::release(&TestId::Foo, &alice, 5, Precision::Exact));
105+
assert_eq!(System::consumers(&alice), 1);
106+
assert_ok!(Balances::transfer_keep_alive(Some(alice).into(), bob, 40));
107+
assert_eq!(System::consumers(&alice), 1);
108+
ensure_ti_valid();
109+
});
110+
});
111+
}

substrate/frame/balances/src/tests/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
2020
#![cfg(test)]
2121

22-
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet};
22+
use crate::{self as pallet_balances, AccountData, Config, CreditOf, Error, Pallet, TotalIssuance};
2323
use codec::{Decode, Encode, MaxEncodedLen};
2424
use frame_support::{
2525
assert_err, assert_noop, assert_ok, assert_storage_noop, derive_impl,
@@ -47,6 +47,7 @@ mod currency_tests;
4747
mod dispatchable_tests;
4848
mod fungible_conformance_tests;
4949
mod fungible_tests;
50+
mod general_tests;
5051
mod reentrancy_tests;
5152

5253
type Block = frame_system::mocking::MockBlock<Test>;
@@ -278,6 +279,23 @@ pub fn info_from_weight(w: Weight) -> DispatchInfo {
278279
DispatchInfo { weight: w, ..Default::default() }
279280
}
280281

282+
/// Check that the total-issuance matches the sum of all accounts' total balances.
283+
pub fn ensure_ti_valid() {
284+
let mut sum = 0;
285+
286+
for acc in frame_system::Account::<Test>::iter_keys() {
287+
if UseSystem::get() {
288+
let data = frame_system::Pallet::<Test>::account(acc);
289+
sum += data.data.total();
290+
} else {
291+
let data = crate::Account::<Test>::get(acc);
292+
sum += data.total();
293+
}
294+
}
295+
296+
assert_eq!(TotalIssuance::<Test>::get(), sum, "Total Issuance wrong");
297+
}
298+
281299
#[test]
282300
fn weights_sane() {
283301
let info = crate::Call::<Test>::transfer_allow_death { dest: 10, value: 4 }.get_dispatch_info();

substrate/frame/balances/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub struct AccountData<Balance> {
111111
const IS_NEW_LOGIC: u128 = 0x80000000_00000000_00000000_00000000u128;
112112

113113
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
114-
pub struct ExtraFlags(u128);
114+
pub struct ExtraFlags(pub(crate) u128);
115115
impl Default for ExtraFlags {
116116
fn default() -> Self {
117117
Self(IS_NEW_LOGIC)

0 commit comments

Comments
 (0)