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 delegator: &T::AccountId,
35 ) {
36 match self.allocation {
37 EmissionAllocation::Streams(streams) => {
38 for stream in streams.keys() {
39 AccumulatedStreamAmounts::<T>::remove((delegator, stream, &permission_id));
40 }
41 }
42 EmissionAllocation::FixedAmount(amount) if last_executed.is_none() => {
43 T::Currency::unreserve(delegator, 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_id: &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_id));
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_id) 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_id, &permission_id),
122 Some(accumulated.saturating_add(delegated_amount)),
123 );
124
125 Pallet::<T>::deposit_event(Event::AccumulatedEmission {
126 permission_id,
127 stream_id: *stream_id,
128 amount: delegated_amount,
129 });
130 }
131}
132
133pub(crate) fn do_auto_distribution<T: Config>(
134 emission_scope: &EmissionScope<T>,
135 permission_id: H256,
136 current_block: BlockNumberFor<T>,
137 contract: &PermissionContract<T>,
138) -> DispatchResult {
139 match emission_scope.distribution {
140 DistributionControl::Automatic(threshold) => {
141 let accumulated = match &emission_scope.allocation {
142 EmissionAllocation::Streams(streams) => streams
143 .keys()
144 .filter_map(|id| {
145 AccumulatedStreamAmounts::<T>::get((&contract.delegator, id, permission_id))
146 })
147 .fold(BalanceOf::<T>::zero(), |acc, e| acc.saturating_add(e)), EmissionAllocation::FixedAmount(amount) => *amount,
149 };
150
151 if accumulated >= threshold {
152 do_distribute_emission::<T>(
153 permission_id,
154 contract,
155 DistributionReason::Automatic,
156 )?;
157 }
158 }
159
160 DistributionControl::AtBlock(target_block) if current_block > target_block => {
161 if contract
166 .last_execution()
167 .is_some_and(|last_execution| last_execution >= target_block)
168 {
169 return Ok(());
170 }
171
172 do_distribute_emission::<T>(permission_id, contract, DistributionReason::Automatic)?;
173 }
174
175 DistributionControl::Interval(interval) => {
176 let last_execution = contract.last_execution.unwrap_or(contract.created_at);
177 if current_block.saturating_sub(last_execution) < interval {
178 return Ok(());
179 }
180
181 do_distribute_emission::<T>(permission_id, contract, DistributionReason::Automatic)?;
182 }
183
184 _ => {}
186 }
187
188 Ok(())
189}
190
191#[derive(
192 Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen,
193)]
194pub enum DistributionReason {
195 Automatic,
196 Manual,
197}
198
199pub(crate) fn do_distribute_emission<T: Config>(
201 permission_id: PermissionId,
202 contract: &PermissionContract<T>,
203 reason: DistributionReason,
204) -> DispatchResult {
205 let PermissionScope::Emission(emission_scope) = &contract.scope else {
206 return Ok(());
207 };
208
209 let total_weight =
210 FixedU128::from_u32(emission_scope.targets.values().map(|w| *w as u32).sum());
211 if total_weight.is_zero() {
212 trace!("permission {permission_id:?} does not have enough target weight");
213 return Ok(());
214 }
215
216 match &emission_scope.allocation {
217 EmissionAllocation::Streams(streams) => {
218 let streams = streams.keys().filter_map(|stream_id| {
219 let acc = AccumulatedStreamAmounts::<T>::get((
220 &contract.delegator,
221 stream_id,
222 permission_id,
223 ))?;
224
225 AccumulatedStreamAmounts::<T>::set(
228 (&contract.delegator, stream_id, permission_id),
229 Some(Zero::zero()),
230 );
231
232 if acc.is_zero() {
233 None
234 } else {
235 Some((stream_id, T::Currency::issue(acc)))
239 }
240 });
241
242 for (stream, mut imbalance) in streams {
243 do_distribute_to_targets(
244 &mut imbalance,
245 permission_id,
246 emission_scope,
247 Some(stream),
248 total_weight,
249 reason,
250 );
251
252 let remainder = imbalance.peek();
253 if !remainder.is_zero() {
254 AccumulatedStreamAmounts::<T>::mutate(
255 (&contract.delegator, stream, permission_id),
256 |acc| *acc = Some(acc.unwrap_or_default().saturating_add(remainder)),
257 );
258 }
259 }
260 }
261 EmissionAllocation::FixedAmount(amount) => {
262 if contract.last_execution().is_some() {
263 return Ok(());
265 }
266
267 let _ = T::Currency::unreserve(&contract.delegator, *amount);
269 let mut imbalance = T::Currency::withdraw(
270 &contract.delegator,
271 *amount,
272 WithdrawReasons::TRANSFER,
273 ExistenceRequirement::KeepAlive,
274 )
275 .unwrap_or_else(|_| NegativeImbalanceOf::<T>::zero());
276
277 do_distribute_to_targets(
278 &mut imbalance,
279 permission_id,
280 emission_scope,
281 None,
282 total_weight,
283 reason,
284 );
285 }
286 }
287
288 if let Some(mut contract) = Permissions::<T>::get(permission_id) {
289 contract.tick_execution(<frame_system::Pallet<T>>::block_number())?;
290 Permissions::<T>::set(permission_id, Some(contract));
291 }
292
293 Ok(())
294}
295
296fn do_distribute_to_targets<T: Config>(
297 imbalance: &mut NegativeImbalanceOf<T>,
298 permission_id: PermissionId,
299 emission_scope: &EmissionScope<T>,
300 stream: Option<&StreamId>,
301 total_weight: FixedU128,
302 reason: DistributionReason,
303) {
304 let initial_balance = imbalance.peek();
305 let total_initial_amount =
306 FixedU128::from_inner(initial_balance.try_into().unwrap_or_default());
307 if total_initial_amount.is_zero() {
308 trace!("no amount to distribute for permission {permission_id:?} and stream {stream:?}");
309 return;
310 }
311
312 for (target, weight) in emission_scope.targets.iter() {
313 let target_weight = FixedU128::from_u32(*weight as u32);
314 let target_amount = total_initial_amount
315 .saturating_mul(target_weight)
316 .const_checked_div(total_weight)
317 .unwrap_or_default();
318
319 if target_amount.is_zero() {
320 continue;
321 }
322
323 let target_amount =
324 BalanceOf::<T>::try_from(target_amount.into_inner()).unwrap_or_default();
325 let mut imbalance = imbalance.extract(target_amount);
326
327 if let Some(stream) = stream {
328 do_accumulate_emissions::<T>(target, stream, &mut imbalance);
330 }
331
332 T::Currency::resolve_creating(target, imbalance);
333
334 Pallet::<T>::deposit_event(Event::EmissionDistribution {
335 permission_id,
336 stream_id: stream.cloned(),
337 target: target.clone(),
338 amount: target_amount,
339 reason,
340 });
341 }
342}