1#![cfg_attr(not(feature = "std"), no_std)]
35
36pub mod migrations;
37pub mod weights;
38
39#[cfg(feature = "runtime-benchmarks")]
40pub mod benchmarking;
41
42#[cfg(test)]
43pub(crate) mod mock;
44#[cfg(test)]
45mod tests;
46
47extern crate alloc;
48
49use alloc::vec::Vec;
50use codec::DecodeWithMemTracking;
51use frame_support::{
52 defensive,
53 pallet_prelude::*,
54 traits::{
55 fungible::{Balanced, Credit, Inspect, Mutate, Unbalanced},
56 tokens::{Fortitude, Preservation},
57 Currency, Imbalance, OnUnbalanced, Time,
58 },
59 weights::WeightMeter,
60 PalletId,
61};
62use sp_runtime::{traits::Zero, BoundedBTreeMap, Perbill, SaturatedConversion, Saturating};
63use sp_staking::budget::{BudgetKey, BudgetRecipientList, IssuanceCurve};
64
65pub use pallet::*;
66
67pub use sp_dap::DAP_PALLET_ID;
68
69const LOG_TARGET: &str = "runtime::dap";
70
71pub const MAX_BUDGET_RECIPIENTS: u32 = 16;
73
74pub type BalanceOf<T> =
76 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
77
78pub type BudgetAllocationMap = BoundedBTreeMap<BudgetKey, Perbill, ConstU32<MAX_BUDGET_RECIPIENTS>>;
80
81#[frame_support::pallet]
82pub mod pallet {
83 use super::*;
84 use crate::weights::WeightInfo;
85 use frame_support::{sp_runtime::traits::AccountIdConversion, traits::StorageVersion};
86 use frame_system::pallet_prelude::*;
87
88 const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
90
91 #[pallet::pallet]
92 #[pallet::storage_version(STORAGE_VERSION)]
93 pub struct Pallet<T>(_);
94
95 #[pallet::config]
96 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
97 type Currency: Inspect<Self::AccountId>
99 + Mutate<Self::AccountId>
100 + Unbalanced<Self::AccountId>
101 + Balanced<Self::AccountId>;
102
103 #[pallet::constant]
105 type PalletId: Get<PalletId>;
106
107 type IssuanceCurve: IssuanceCurve<BalanceOf<Self>>;
109
110 type BudgetRecipients: BudgetRecipientList<Self::AccountId>;
117
118 type Time: Time;
122
123 #[pallet::constant]
130 type IssuanceCadence: Get<u64>;
131
132 #[pallet::constant]
138 type MaxElapsedPerDrip: Get<u64>;
139
140 type BudgetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
142
143 type WeightInfo: crate::weights::WeightInfo;
145 }
146
147 #[pallet::event]
148 #[pallet::generate_deposit(pub(super) fn deposit_event)]
149 pub enum Event<T: Config> {
150 IssuanceMinted {
152 total_minted: BalanceOf<T>,
154 elapsed_millis: u64,
156 },
157 BudgetAllocationUpdated {
159 allocations: BudgetAllocationMap,
161 },
162 StagingDrained {
164 amount: BalanceOf<T>,
166 },
167 Unexpected(UnexpectedKind),
169 }
170
171 #[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, TypeInfo, DebugNoBound)]
173 pub enum UnexpectedKind {
174 MintFailed,
176 ElapsedClamped {
178 actual_elapsed: u64,
180 ceiling: u64,
182 },
183 }
184
185 #[pallet::storage]
190 pub type BudgetAllocation<T> = StorageValue<_, BudgetAllocationMap, ValueQuery>;
191
192 #[pallet::storage]
197 pub type LastIssuanceTimestamp<T> = StorageValue<_, u64, ValueQuery>;
198
199 #[pallet::error]
200 pub enum Error<T> {
201 UnknownBudgetKey,
203 BudgetNotExact,
205 }
206
207 #[pallet::hooks]
208 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
209 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
210 Self::drip_issuance()
211 }
212
213 fn on_idle(_block: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
214 let mut meter = WeightMeter::with_limit(remaining_weight);
215
216 if meter.try_consume(T::DbWeight::get().reads(1)).is_err() {
218 return meter.consumed();
219 }
220
221 let staging_account = Self::staging_account();
222 let available = T::Currency::reducible_balance(
223 &staging_account,
224 Preservation::Preserve,
225 Fortitude::Polite,
226 );
227
228 if available.is_zero() {
229 return meter.consumed();
230 }
231
232 if meter.try_consume(T::DbWeight::get().reads_writes(3, 3)).is_err() {
235 return meter.consumed();
236 }
237
238 let buffer = Self::buffer_account();
239 if T::Currency::transfer(&staging_account, &buffer, available, Preservation::Preserve)
240 .is_err()
241 {
242 defensive!("DAP: staging account transfer to buffer failed");
243 return meter.consumed();
244 }
245
246 Self::deactivate_buffer_funds(available);
247 Self::deposit_event(Event::StagingDrained { amount: available });
248
249 log::debug!(
250 target: LOG_TARGET,
251 "DAP: drained {available:?} from staging account to DAP buffer"
252 );
253
254 meter.consumed()
255 }
256
257 fn integrity_test() {
258 assert!(
259 T::MaxElapsedPerDrip::get() > T::IssuanceCadence::get(),
260 "MaxElapsedPerDrip must be greater than IssuanceCadence, \
261 otherwise every drip would be clamped below the cadence threshold."
262 );
263
264 let mut keys: Vec<_> =
266 T::BudgetRecipients::recipients().into_iter().map(|(k, _)| k).collect();
267 keys.sort();
268 assert!(
269 keys.windows(2).all(|w| w[0] != w[1]),
270 "Duplicate BudgetRecipient key detected"
271 );
272 }
273
274 #[cfg(feature = "try-runtime")]
275 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
276 Ok(())
279 }
280 }
281
282 #[pallet::call]
283 impl<T: Config> Pallet<T> {
284 #[pallet::call_index(0)]
289 #[pallet::weight(T::WeightInfo::set_budget_allocation())]
290 pub fn set_budget_allocation(
291 origin: OriginFor<T>,
292 new_allocations: BudgetAllocationMap,
293 ) -> DispatchResult {
294 T::BudgetOrigin::ensure_origin(origin)?;
295
296 let registered: Vec<_> =
298 T::BudgetRecipients::recipients().into_iter().map(|(k, _)| k).collect();
299 for key in new_allocations.keys() {
300 ensure!(registered.contains(key), Error::<T>::UnknownBudgetKey);
301 }
302
303 let total_parts: u64 = new_allocations.values().map(|p| p.deconstruct() as u64).sum();
305 ensure!(total_parts == Perbill::one().deconstruct() as u64, Error::<T>::BudgetNotExact);
306
307 BudgetAllocation::<T>::put(new_allocations.clone());
308 Self::deposit_event(Event::BudgetAllocationUpdated { allocations: new_allocations });
309
310 Ok(())
311 }
312 }
313
314 #[pallet::view_functions]
315 impl<T: Config> Pallet<T> {
316 pub fn budget_recipients() -> Vec<(BudgetKey, T::AccountId, Perbill)> {
321 let allocation = BudgetAllocation::<T>::get();
322
323 T::BudgetRecipients::recipients()
324 .into_iter()
325 .map(|(key, account)| {
326 let share = allocation.get(&key).copied().unwrap_or(Perbill::zero());
327
328 (key, account, share)
329 })
330 .collect()
331 }
332
333 pub fn staging() -> T::AccountId {
337 Self::staging_account()
338 }
339 }
340
341 impl<T: Config> Pallet<T> {
342 pub fn buffer_account() -> T::AccountId {
347 T::PalletId::get().into_account_truncating()
348 }
349
350 pub fn staging_account() -> T::AccountId {
355 sp_dap::DAP_PALLET_ID.into_sub_account_truncating(sp_dap::DAP_STAGING_ACCOUNT_ID)
356 }
357
358 pub(crate) fn deactivate_buffer_funds(amount: BalanceOf<T>) {
360 <T::Currency as Unbalanced<T::AccountId>>::deactivate(amount);
361 }
362
363 pub(crate) fn drip_issuance() -> Weight {
365 let now_moment = T::Time::now();
366 let now: u64 = now_moment.saturated_into();
367 let last = LastIssuanceTimestamp::<T>::get();
368 let mut elapsed = now.saturating_sub(last);
369
370 let cadence = T::IssuanceCadence::get();
371 if cadence > 0 && elapsed < cadence {
372 return T::DbWeight::get().reads(2);
373 }
374
375 if last == 0 {
379 LastIssuanceTimestamp::<T>::put(now);
380 return T::DbWeight::get().reads_writes(2, 2);
381 }
382
383 let max_elapsed = T::MaxElapsedPerDrip::get();
385 if elapsed > max_elapsed {
386 Self::deposit_event(Event::Unexpected(UnexpectedKind::ElapsedClamped {
387 actual_elapsed: elapsed,
388 ceiling: max_elapsed,
389 }));
390 elapsed = max_elapsed;
391 }
392
393 LastIssuanceTimestamp::<T>::put(now);
395
396 let _ = Self::mint_and_distribute(elapsed);
397 T::WeightInfo::drip_issuance()
398 }
399
400 pub(crate) fn mint_and_distribute(elapsed: u64) -> BalanceOf<T> {
411 let total_issuance = T::Currency::total_issuance();
412 let issuance = T::IssuanceCurve::issue(total_issuance, elapsed);
413
414 if issuance.is_zero() {
415 return BalanceOf::<T>::zero();
416 }
417
418 let budget = BudgetAllocation::<T>::get();
419 if budget.is_empty() {
420 log::warn!(
422 target: LOG_TARGET,
423 "BudgetAllocation is empty โ no issuance will be distributed"
424 );
425 return BalanceOf::<T>::zero();
426 }
427 let recipients = T::BudgetRecipients::recipients();
428 let mut total_minted = BalanceOf::<T>::zero();
429
430 let buffer = Self::buffer_account();
431 for (key, account) in &recipients {
432 let perbill = budget.get(key).copied().unwrap_or(Perbill::zero());
433 let amount = perbill.mul_floor(issuance);
434 if !amount.is_zero() {
435 if let Err(_) = T::Currency::mint_into(account, amount) {
436 Self::deposit_event(Event::Unexpected(UnexpectedKind::MintFailed));
437 defensive!("Issuance mint should not fail");
438 } else {
439 total_minted = total_minted.saturating_add(amount);
440 if *account == buffer {
441 Self::deactivate_buffer_funds(amount);
442 }
443 }
444 }
445 }
446
447 Self::deposit_event(Event::IssuanceMinted { total_minted, elapsed_millis: elapsed });
450
451 log::debug!(
452 target: LOG_TARGET,
453 "Issuance drip: issued={issuance:?}, minted={total_minted:?}, elapsed={elapsed}ms"
454 );
455
456 debug_assert!(
457 !total_minted.is_zero(),
458 "mint_and_distribute: issuance was non-zero but nothing was minted"
459 );
460
461 total_minted
462 }
463 }
464
465 #[cfg(any(test, feature = "try-runtime"))]
466 impl<T: Config> Pallet<T> {
467 #[allow(dead_code)]
468 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
469 Self::check_budget_allocation()
470 }
471
472 fn check_budget_allocation() -> Result<(), sp_runtime::TryRuntimeError> {
476 let allocation = BudgetAllocation::<T>::get();
477
478 ensure!(!allocation.is_empty(), "BudgetAllocation is empty");
479
480 let registered: Vec<BudgetKey> =
481 T::BudgetRecipients::recipients().into_iter().map(|(k, _)| k).collect();
482
483 for key in allocation.keys() {
485 ensure!(
486 registered.contains(key),
487 "BudgetAllocation contains key not in BudgetRecipients"
488 );
489 }
490
491 let total_parts: u64 = allocation.values().map(|p| p.deconstruct() as u64).sum();
493 ensure!(
494 total_parts == Perbill::one().deconstruct() as u64,
495 "BudgetAllocation does not sum to 100%"
496 );
497
498 Ok(())
499 }
500 }
501}
502
503pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
505
506impl<T: Config> OnUnbalanced<CreditOf<T>> for Pallet<T> {
512 fn on_nonzero_unbalanced(amount: CreditOf<T>) {
513 let staging = Self::staging_account();
514 let numeric_amount = amount.peek();
515
516 let _ = T::Currency::resolve(&staging, amount).inspect_err(|_| {
520 defensive!(
521 "๐จ Failed to deposit slash to DAP staging account - funds burned, it should never happen!"
522 );
523 });
524 log::debug!(
525 target: LOG_TARGET,
526 "๐ธ Deposited {numeric_amount:?} to DAP staging account"
527 );
528 }
529}
530
531type LegacyNegativeImbalance<A, C> = <C as Currency<A>>::NegativeImbalance;
533
534pub struct DapLegacyAdapter<T, C>(core::marker::PhantomData<(T, C)>);
547
548impl<T: Config, C> OnUnbalanced<LegacyNegativeImbalance<T::AccountId, C>> for DapLegacyAdapter<T, C>
549where
550 C: Currency<T::AccountId, Balance = BalanceOf<T>>,
551{
552 fn on_nonzero_unbalanced(amount: LegacyNegativeImbalance<T::AccountId, C>) {
553 let staging = Pallet::<T>::staging_account();
554 let numeric_amount = amount.peek();
555 C::resolve_creating(&staging, amount);
557 log::debug!(
558 target: LOG_TARGET,
559 "๐ธ Deposited (legacy) {numeric_amount:?} to DAP staging account"
560 );
561 }
562}
563
564impl<T: Config> sp_staking::budget::BudgetRecipient<T::AccountId> for Pallet<T> {
567 fn budget_key() -> BudgetKey {
568 BudgetKey::truncate_from(b"buffer".to_vec())
569 }
570
571 fn pot_account() -> T::AccountId {
572 Self::buffer_account()
573 }
574}