pallet_dap/lib.rs
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//! # Dynamic Allocation Pool (DAP) Pallet
19//!
20//! Intercepts native token burns (staking slashes, transaction fees, dust removal, reward
21//! remainders, EVM gas rounding) on AssetHub and redirects them into a buffer account instead
22//! of destroying them.
23//! The buffer account must be pre-funded with at least ED (existential deposit), e.g., via
24//! balances genesis config or a transfer. If the buffer account is not pre-funded, deposits
25//! below ED will be silently burned.
26//!
27//! Incoming funds are deactivated to exclude them from governance voting.
28//! When DAP distributes funds (e.g., to validators, nominators, treasury, collators), those funds
29//! must be reactivated before transfer.
30//!
31//! - **Burns**: Use `Dap` as `OnUnbalanced` handler for any burn source (e.g., `type Slash = Dap`,
32//! `type DustRemoval = Dap`, `type OnBurn = Dap`)
33//! Note: Direct calls to `pallet_balances::Pallet::burn()` extrinsic are not redirected to
34//! the buffer — they still reduce total issuance directly.
35
36#![cfg_attr(not(feature = "std"), no_std)]
37
38#[cfg(test)]
39pub(crate) mod mock;
40#[cfg(test)]
41mod tests;
42
43use frame_support::{
44 defensive,
45 pallet_prelude::*,
46 traits::{
47 fungible::{Balanced, Credit, Inspect, Unbalanced},
48 Imbalance, OnUnbalanced,
49 },
50 PalletId,
51};
52
53pub use pallet::*;
54
55const LOG_TARGET: &str = "runtime::dap";
56
57/// Type alias for balance.
58pub type BalanceOf<T> =
59 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
60
61#[frame_support::pallet]
62pub mod pallet {
63 use super::*;
64 use frame_support::{sp_runtime::traits::AccountIdConversion, traits::StorageVersion};
65
66 /// The in-code storage version.
67 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
68
69 #[pallet::pallet]
70 #[pallet::storage_version(STORAGE_VERSION)]
71 pub struct Pallet<T>(_);
72
73 #[pallet::config]
74 pub trait Config: frame_system::Config {
75 /// The currency type (new fungible traits).
76 type Currency: Inspect<Self::AccountId>
77 + Unbalanced<Self::AccountId>
78 + Balanced<Self::AccountId>;
79
80 /// The pallet ID used to derive the buffer account.
81 ///
82 /// Each runtime should configure a unique ID to avoid collisions if multiple
83 /// DAP instances are used.
84 #[pallet::constant]
85 type PalletId: Get<PalletId>;
86 }
87
88 impl<T: Config> Pallet<T> {
89 /// Get the DAP buffer account
90 /// NOTE: We may need more accounts in the future, for instance, to manage the strategic
91 /// reserve. We will add them as necessary, generating them with additional seed.
92 pub fn buffer_account() -> T::AccountId {
93 T::PalletId::get().into_account_truncating()
94 }
95 }
96}
97
98/// Type alias for credit (negative imbalance - funds that were slashed/removed).
99/// This is for the `fungible::Balanced` trait as used by staking-async.
100pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
101
102/// Implementation of OnUnbalanced for the fungible::Balanced trait.
103/// Example: use as `type Slash = Dap` in staking-async config.
104///
105/// Only the new fungible `Credit` type is supported. An `OnUnbalanced<NegativeImbalance>` impl
106/// for the old `Currency` trait is not provided because there are no consumers.
107impl<T: Config> OnUnbalanced<CreditOf<T>> for Pallet<T> {
108 fn on_nonzero_unbalanced(amount: CreditOf<T>) {
109 let buffer = Self::buffer_account();
110 let numeric_amount = amount.peek();
111
112 // Resolve should never fail because:
113 // - can_deposit on destination succeeds since buffer exists (created with provider at
114 // genesis/runtime upgrade so no ED issue)
115 // - amount is guaranteed non-zero by the trait method signature
116 // The only failure would be overflow on destination.
117 let _ = T::Currency::resolve(&buffer, amount)
118 .inspect_err(|_| {
119 defensive!(
120 "🚨 Failed to deposit slash to DAP buffer - funds burned, it should never happen!"
121 );
122 })
123 .inspect(|_| {
124 // Mark funds as inactive so they don't participate in governance voting.
125 // Only deactivate on success; if resolve failed, tokens were burned.
126 <T::Currency as Unbalanced<T::AccountId>>::deactivate(numeric_amount);
127 log::debug!(
128 target: LOG_TARGET,
129 "💸 Deposited slash of {numeric_amount:?} to DAP buffer"
130 );
131 });
132 }
133}