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