pallet_permission0/permission/
emission.rs1use polkadot_sdk::{
2 frame_support::traits::{
3 Currency, ExistenceRequirement, Imbalance, ReservableCurrency, WithdrawReasons,
4 },
5 frame_system,
6 sp_arithmetic::FixedU128,
7 sp_runtime::traits::{Saturating, Zero},
8};
9
10use super::*;
11
12pub type StreamId = H256;
14
15#[derive(Encode, Decode, CloneNoBound, PartialEq, TypeInfo, MaxEncodedLen, DebugNoBound)]
17#[scale_info(skip_type_params(T))]
18pub struct EmissionScope<T: Config> {
19 pub allocation: EmissionAllocation<T>,
21 pub distribution: DistributionControl<T>,
23 pub targets: BoundedBTreeMap<T::AccountId, u16, T::MaxTargetsPerPermission>,
25 pub accumulating: bool,
27}
28
29impl<T: Config> EmissionScope<T> {
30 pub(super) fn cleanup(
31 self,
32 permission_id: H256,
33 last_executed: &Option<BlockNumberFor<T>>,
34 grantor: &T::AccountId,
35 ) {
36 match self.allocation {
37 EmissionAllocation::Streams(streams) => {
38 for stream in streams.keys() {
39 AccumulatedStreamAmounts::<T>::remove((grantor, stream, &permission_id));
40 }
41 }
42 EmissionAllocation::FixedAmount(amount) if last_executed.is_none() => {
43 T::Currency::unreserve(grantor, amount);
44 }
45 _ => {}
46 }
47 }
48}
49
50#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, TypeInfo, MaxEncodedLen, DebugNoBound)]
52#[scale_info(skip_type_params(T))]
53pub enum EmissionAllocation<T: Config> {
54 Streams(BoundedBTreeMap<StreamId, Percent, T::MaxStreamsPerPermission>),
56 FixedAmount(BalanceOf<T>),
58}
59
60#[derive(Encode, Decode, CloneNoBound, PartialEq, TypeInfo, MaxEncodedLen, DebugNoBound)]
61#[scale_info(skip_type_params(T))]
62pub enum DistributionControl<T: Config> {
63 Manual,
65 Automatic(BalanceOf<T>),
67 AtBlock(BlockNumberFor<T>),
69 Interval(BlockNumberFor<T>),
71}
72
73pub(crate) fn do_accumulate_emissions<T: Config>(
75 agent: &T::AccountId,
76 stream: &StreamId,
77 imbalance: &mut NegativeImbalanceOf<T>,
78) {
79 let initial_balance = imbalance.peek();
80 let total_initial_amount =
81 FixedU128::from_inner(initial_balance.try_into().unwrap_or_default());
82 if total_initial_amount.is_zero() {
83 return;
84 }
85
86 let streams = AccumulatedStreamAmounts::<T>::iter_prefix((agent, stream));
87 for (permission_id, accumulated) in streams {
88 let Some(contract) = Permissions::<T>::get(permission_id) else {
89 continue;
90 };
91
92 let PermissionScope::Emission(EmissionScope {
95 allocation: EmissionAllocation::Streams(streams),
96 accumulating,
97 ..
98 }) = contract.scope
99 else {
100 continue;
101 };
102
103 if !accumulating {
104 continue;
105 }
106
107 let Some(percentage) = streams.get(stream) else {
108 continue;
109 };
110
111 let delegated_amount = percentage.mul_floor(total_initial_amount.into_inner());
112 if delegated_amount.is_zero() {
113 continue;
114 }
115
116 let delegated_amount = imbalance
117 .extract(delegated_amount.try_into().unwrap_or_default())
118 .peek();
119
120 AccumulatedStreamAmounts::<T>::set(
121 (agent, stream, &permission_id),
122 Some(accumulated.saturating_add(delegated_amount)),
123 );
124 }
125}
126
127pub(crate) fn do_auto_distribution<T: Config>(
128 emission_scope: &EmissionScope<T>,
129 permission_id: H256,
130 current_block: BlockNumberFor<T>,
131 contract: &PermissionContract<T>,
132) {
133 match emission_scope.distribution {
134 DistributionControl::Automatic(threshold) => {
135 let accumulated = match &emission_scope.allocation {
136 EmissionAllocation::Streams(streams) => streams
137 .keys()
138 .filter_map(|id| {
139 AccumulatedStreamAmounts::<T>::get((&contract.grantor, id, permission_id))
140 })
141 .fold(BalanceOf::<T>::zero(), |acc, e| acc.saturating_add(e)), EmissionAllocation::FixedAmount(amount) => *amount,
143 };
144
145 if accumulated >= threshold {
146 do_distribute_emission::<T>(permission_id, contract, DistributionReason::Automatic);
147 }
148 }
149
150 DistributionControl::AtBlock(target_block) if current_block > target_block => {
151 if contract
156 .last_execution
157 .is_some_and(|last_execution| last_execution >= target_block)
158 {
159 return;
160 }
161
162 do_distribute_emission::<T>(permission_id, contract, DistributionReason::Automatic);
163 }
164
165 DistributionControl::Interval(interval) => {
166 let last_execution = contract.last_execution.unwrap_or(contract.created_at);
167 if current_block.saturating_sub(last_execution) < interval {
168 return;
169 }
170
171 do_distribute_emission::<T>(permission_id, contract, DistributionReason::Automatic);
172 }
173
174 _ => {}
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
180pub(crate) enum DistributionReason {
181 Automatic,
182 Manual,
183}
184
185pub(crate) fn do_distribute_emission<T: Config>(
187 permission_id: PermissionId,
188 contract: &PermissionContract<T>,
189 reason: DistributionReason,
190) {
191 #[allow(irrefutable_let_patterns)]
192 let PermissionScope::Emission(emission_scope) = &contract.scope
193 else {
194 return;
195 };
196
197 let total_weight =
198 FixedU128::from_u32(emission_scope.targets.values().map(|w| *w as u32).sum());
199 if total_weight.is_zero() {
200 return;
201 }
202
203 match &emission_scope.allocation {
204 EmissionAllocation::Streams(streams) => {
205 let streams = streams.keys().filter_map(|id| {
206 let acc =
207 AccumulatedStreamAmounts::<T>::get((&contract.grantor, id, permission_id))?;
208
209 AccumulatedStreamAmounts::<T>::set(
212 (&contract.grantor, id, permission_id),
213 Some(Zero::zero()),
214 );
215
216 if acc.is_zero() {
217 None
218 } else {
219 Some((id, T::Currency::issue(acc)))
223 }
224 });
225
226 for (stream, mut imbalance) in streams {
227 do_distribute_to_targets(
228 &mut imbalance,
229 permission_id,
230 contract,
231 emission_scope,
232 Some(stream),
233 total_weight,
234 reason,
235 );
236
237 let remainder = imbalance.peek();
238 if !remainder.is_zero() {
239 AccumulatedStreamAmounts::<T>::mutate(
240 (&contract.grantor, stream, permission_id),
241 |acc| {
242 if let Some(acc_value) = acc {
243 *acc_value = acc_value.saturating_add(remainder);
244 } else {
245 *acc = Some(remainder)
246 }
247 },
248 );
249 }
250 }
251 }
252 EmissionAllocation::FixedAmount(amount) => {
253 if contract.last_execution.is_some() {
254 return;
256 }
257
258 let _ = T::Currency::unreserve(&contract.grantor, *amount);
260 let mut imbalance = T::Currency::withdraw(
261 &contract.grantor,
262 *amount,
263 WithdrawReasons::TRANSFER,
264 ExistenceRequirement::KeepAlive,
265 )
266 .unwrap_or_else(|_| NegativeImbalanceOf::<T>::zero());
267
268 do_distribute_to_targets(
269 &mut imbalance,
270 permission_id,
271 contract,
272 emission_scope,
273 None,
274 total_weight,
275 reason,
276 );
277 }
278 }
279
280 Permissions::<T>::mutate(permission_id, |maybe_contract| {
281 if let Some(c) = maybe_contract {
282 c.last_execution = Some(<frame_system::Pallet<T>>::block_number());
283 c.execution_count = c.execution_count.saturating_add(1);
284 }
285 });
286}
287
288fn do_distribute_to_targets<T: Config>(
289 imbalance: &mut NegativeImbalanceOf<T>,
290 permission_id: PermissionId,
291 contract: &PermissionContract<T>,
292 emission_scope: &EmissionScope<T>,
293 stream: Option<&StreamId>,
294 total_weight: FixedU128,
295 reason: DistributionReason,
296) {
297 let initial_balance = imbalance.peek();
298 let total_initial_amount =
299 FixedU128::from_inner(initial_balance.try_into().unwrap_or_default());
300 if total_initial_amount.is_zero() {
301 return;
302 }
303
304 for (target, weight) in emission_scope.targets.iter() {
305 let target_weight = FixedU128::from_u32(*weight as u32);
306 let target_amount = total_initial_amount
307 .saturating_mul(target_weight)
308 .const_checked_div(total_weight)
309 .unwrap_or_default();
310
311 if target_amount.is_zero() {
312 continue;
313 }
314
315 let target_amount =
316 BalanceOf::<T>::try_from(target_amount.into_inner()).unwrap_or_default();
317 let mut imbalance = imbalance.extract(target_amount);
318
319 if let Some(stream) = stream {
320 do_accumulate_emissions::<T>(target, stream, &mut imbalance);
322 }
323
324 T::Currency::resolve_creating(target, imbalance);
325 }
326
327 let amount = initial_balance.saturating_sub(imbalance.peek());
328 if !amount.is_zero() {
329 <Pallet<T>>::deposit_event(match reason {
330 DistributionReason::Automatic => Event::AutoDistributionExecuted {
331 grantor: contract.grantor.clone(),
332 grantee: contract.grantee.clone(),
333 permission_id,
334 stream_id: None,
335 amount,
336 },
337 DistributionReason::Manual => Event::PermissionExecuted {
338 grantor: contract.grantor.clone(),
339 grantee: contract.grantee.clone(),
340 permission_id,
341 stream_id: None,
342 amount,
343 },
344 });
345 }
346}