pallet-treasury: Ensure we respect max_amount for spend across batch calls (#13468)

* `pallet-treasury`: Ensure we respect `max_amount` for spend across batch calls

When calling `spend` the origin defines the `max_amount` of tokens it is allowed to spend. The
problem is that someone can send a `batch(spend, spend)` to circumvent this restriction as we don't
check across different calls that the `max_amount` is respected. This pull request fixes this
behavior by introducing a so-called dispatch context. This dispatch context is created once per
outer most `dispatch` call. For more information see the docs in this pr. The treasury then uses
this dispatch context to attach information about already spent funds per `max_amount` (we assume
that each origin has a different `max_amount` configured). So, a `batch(spend, spend)` is now
checked to stay inside the allowed spending bounds.

Fixes: https://github.com/paritytech/substrate/issues/13167

* Import `Box` for wasm

* FMT
This commit is contained in:
Bastian Köcher
2023-02-27 18:49:16 +01:00
committed by GitHub
parent 85a5a5db13
commit 6aa4127a74
9 changed files with 353 additions and 24 deletions
+30 -5
View File
@@ -67,10 +67,10 @@ use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{AccountIdConversion, Saturating, StaticLookup, Zero},
traits::{AccountIdConversion, CheckedAdd, Saturating, StaticLookup, Zero},
Permill, RuntimeDebug,
};
use sp_std::prelude::*;
use sp_std::{collections::btree_map::BTreeMap, prelude::*};
use frame_support::{
print,
@@ -136,7 +136,7 @@ pub struct Proposal<AccountId, Balance> {
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::{dispatch_context::with_context, pallet_prelude::*};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
@@ -339,6 +339,11 @@ pub mod pallet {
}
}
#[derive(Default)]
struct SpendContext<Balance> {
spend_in_context: BTreeMap<Balance, Balance>,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Put forward a suggestion for spending. A deposit proportional to the value
@@ -433,9 +438,29 @@ pub mod pallet {
beneficiary: AccountIdLookupOf<T>,
) -> DispatchResult {
let max_amount = T::SpendOrigin::ensure_origin(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
ensure!(amount <= max_amount, Error::<T, I>::InsufficientPermission);
with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| {
let context = v.or_default();
// We group based on `max_amount`, to dinstinguish between different kind of
// origins. (assumes that all origins have different `max_amount`)
//
// Worst case is that we reject some "valid" request.
let spend = context.spend_in_context.entry(max_amount).or_default();
// Ensure that we don't overflow nor use more than `max_amount`
if spend.checked_add(&amount).map(|s| s > max_amount).unwrap_or(true) {
Err(Error::<T, I>::InsufficientPermission)
} else {
*spend = spend.saturating_add(amount);
Ok(())
}
})
.unwrap_or(Ok(()))?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
let proposal_index = Self::proposal_count();
Approvals::<T, I>::try_append(proposal_index)
.map_err(|_| Error::<T, I>::TooManyApprovals)?;