pallet_permission0/ext/
emission_impl.rs

1use crate::{
2    generate_permission_id, get_total_allocated_percentage, pallet,
3    permission::{emission::*, *},
4    update_permission_indices, AccumulatedStreamAmounts, BalanceOf, Config, DistributionControl,
5    EmissionAllocation, EmissionScope, EnforcementTracking, Error, Event, NegativeImbalanceOf,
6    Pallet, PermissionContract, PermissionDuration, PermissionId, PermissionScope, Permissions,
7};
8
9use pallet_permission0_api::{
10    DistributionControl as ApiDistributionControl, EmissionAllocation as ApiEmissionAllocation,
11    EnforcementAuthority as ApiEnforcementAuthority, Permission0EmissionApi,
12    PermissionDuration as ApiPermissionDuration, RevocationTerms as ApiRevocationTerms, StreamId,
13};
14use polkadot_sdk::{
15    frame_support::{dispatch::DispatchResult, ensure, traits::ReservableCurrency},
16    frame_system::{self, ensure_signed, ensure_signed_or_root},
17    polkadot_sdk_frame::prelude::{BlockNumberFor, OriginFor},
18    sp_core::{Get, TryCollect},
19    sp_runtime::{
20        traits::{CheckedAdd, Saturating, Zero},
21        BoundedBTreeMap, DispatchError, Percent, Vec,
22    },
23};
24
25use pallet_torus0_api::Torus0Api;
26
27impl<T: Config>
28    Permission0EmissionApi<
29        T::AccountId,
30        OriginFor<T>,
31        BlockNumberFor<T>,
32        crate::BalanceOf<T>,
33        NegativeImbalanceOf<T>,
34    > for pallet::Pallet<T>
35{
36    fn grant_emission_permission(
37        grantor: T::AccountId,
38        grantee: T::AccountId,
39        allocation: ApiEmissionAllocation<crate::BalanceOf<T>>,
40        targets: Vec<(T::AccountId, u16)>,
41        distribution: ApiDistributionControl<crate::BalanceOf<T>, BlockNumberFor<T>>,
42        duration: ApiPermissionDuration<BlockNumberFor<T>>,
43        revocation: ApiRevocationTerms<T::AccountId, BlockNumberFor<T>>,
44        enforcement: ApiEnforcementAuthority<T::AccountId>,
45    ) -> Result<PermissionId, DispatchError> {
46        let internal_allocation = match allocation {
47            ApiEmissionAllocation::Streams(streams) => EmissionAllocation::Streams(
48                streams
49                    .try_into()
50                    .map_err(|_| crate::Error::<T>::TooManyStreams)?,
51            ),
52            ApiEmissionAllocation::FixedAmount(amount) => EmissionAllocation::FixedAmount(amount),
53        };
54
55        let internal_distribution = match distribution {
56            ApiDistributionControl::Manual => DistributionControl::Manual,
57            ApiDistributionControl::Automatic(threshold) => {
58                DistributionControl::Automatic(threshold)
59            }
60            ApiDistributionControl::AtBlock(block) => DistributionControl::AtBlock(block),
61            ApiDistributionControl::Interval(interval) => DistributionControl::Interval(interval),
62        };
63
64        let duration = super::translate_duration(duration)?;
65        let revocation = super::translate_revocation_terms(revocation)?;
66        let enforcement = super::translate_enforcement_authority(enforcement)?;
67
68        let targets = targets
69            .into_iter()
70            .try_collect()
71            .map_err(|_| crate::Error::<T>::TooManyTargets)?;
72
73        grant_emission_permission_impl::<T>(
74            grantor,
75            grantee,
76            internal_allocation,
77            targets,
78            internal_distribution,
79            duration,
80            revocation,
81            enforcement,
82            None, // No parent by default
83        )
84    }
85
86    fn accumulate_emissions(
87        agent: &T::AccountId,
88        stream: &StreamId,
89        amount: &mut NegativeImbalanceOf<T>,
90    ) {
91        crate::permission::emission::do_accumulate_emissions::<T>(agent, stream, amount);
92    }
93
94    fn process_auto_distributions(current_block: BlockNumberFor<T>) {
95        crate::permission::do_auto_permission_execution::<T>(current_block);
96    }
97
98    fn get_accumulated_amount(
99        permission_id: &PermissionId,
100        stream: &StreamId,
101    ) -> crate::BalanceOf<T> {
102        let Some(contract) = Permissions::<T>::get(permission_id) else {
103            return Zero::zero();
104        };
105
106        crate::AccumulatedStreamAmounts::<T>::get((contract.grantor, stream, permission_id))
107            .unwrap_or_default()
108    }
109}
110
111/// Grant a permission implementation
112#[allow(clippy::too_many_arguments)]
113pub(crate) fn grant_emission_permission_impl<T: Config>(
114    grantor: T::AccountId,
115    grantee: T::AccountId,
116    allocation: EmissionAllocation<T>,
117    targets: BoundedBTreeMap<T::AccountId, u16, T::MaxTargetsPerPermission>,
118    distribution: DistributionControl<T>,
119    duration: PermissionDuration<T>,
120    revocation: RevocationTerms<T>,
121    enforcement: EnforcementAuthority<T>,
122    parent_id: Option<PermissionId>,
123) -> Result<PermissionId, DispatchError> {
124    use polkadot_sdk::frame_support::ensure;
125
126    ensure!(
127        T::Torus::is_agent_registered(&grantor),
128        Error::<T>::NotRegisteredAgent
129    );
130    ensure!(
131        T::Torus::is_agent_registered(&grantee),
132        Error::<T>::NotRegisteredAgent
133    );
134
135    validate_emission_permission_target_weights::<T>(&targets)?;
136
137    match &allocation {
138        EmissionAllocation::Streams(streams) => {
139            validate_emission_permission_streams::<T>(streams, &grantor)?;
140        }
141        EmissionAllocation::FixedAmount(amount) => {
142            ensure!(*amount > BalanceOf::<T>::zero(), Error::<T>::InvalidAmount);
143            ensure!(
144                T::Currency::can_reserve(&grantor, *amount),
145                Error::<T>::InsufficientBalance
146            );
147            ensure!(
148                matches!(
149                    &distribution,
150                    DistributionControl::Manual | DistributionControl::AtBlock(_)
151                ),
152                Error::<T>::FixedAmountCanOnlyBeTriggeredOnce
153            );
154        }
155    }
156
157    validate_emission_permission_distribution::<T>(&distribution)?;
158
159    if let Some(parent) = parent_id {
160        let parent_contract =
161            Permissions::<T>::get(parent).ok_or(Error::<T>::ParentPermissionNotFound)?;
162
163        ensure!(
164            parent_contract.grantee == grantor,
165            Error::<T>::NotPermissionGrantee
166        );
167    }
168
169    let emission_scope = EmissionScope {
170        allocation: allocation.clone(),
171        distribution,
172        targets,
173        accumulating: true, // Start with accumulation enabled by default
174    };
175
176    let scope = PermissionScope::Emission(emission_scope);
177
178    let permission_id = generate_permission_id::<T>(&grantor, &grantee, &scope)?;
179
180    let contract = PermissionContract {
181        grantor: grantor.clone(),
182        grantee: grantee.clone(),
183        scope,
184        duration,
185        revocation,
186        enforcement,
187        last_execution: None,
188        execution_count: 0,
189        parent: parent_id,
190        created_at: <frame_system::Pallet<T>>::block_number(),
191    };
192
193    // Reserve funds if fixed amount allocation. We use the Balances API for this.
194    // This means total issuance is always correct.
195    match allocation {
196        EmissionAllocation::FixedAmount(amount) => {
197            T::Currency::reserve(&grantor, amount)?;
198        }
199        EmissionAllocation::Streams(streams) => {
200            for stream in streams.keys() {
201                AccumulatedStreamAmounts::<T>::set(
202                    (&grantor, stream, permission_id),
203                    Some(Zero::zero()),
204                )
205            }
206        }
207    }
208
209    Permissions::<T>::insert(permission_id, contract);
210
211    update_permission_indices::<T>(&grantor, &grantee, permission_id)?;
212
213    <Pallet<T>>::deposit_event(Event::PermissionGranted {
214        grantor,
215        grantee,
216        permission_id,
217    });
218
219    Ok(permission_id)
220}
221
222pub fn execute_permission_impl<T: Config>(
223    permission_id: &PermissionId,
224    contract: &PermissionContract<T>,
225    emission_scope: &EmissionScope<T>,
226) -> DispatchResult {
227    match &emission_scope.distribution {
228        DistributionControl::Manual => {
229            ensure!(
230                emission_scope.accumulating,
231                Error::<T>::UnsupportedPermissionType
232            );
233
234            let accumulated = match &emission_scope.allocation {
235                EmissionAllocation::Streams(streams) => streams
236                    .keys()
237                    .filter_map(|id| {
238                        AccumulatedStreamAmounts::<T>::get((&contract.grantor, id, permission_id))
239                    })
240                    .fold(BalanceOf::<T>::zero(), |acc, e| acc.saturating_add(e)), // The Balance AST does not enforce the Sum trait
241                EmissionAllocation::FixedAmount(amount) => *amount,
242            };
243
244            ensure!(!accumulated.is_zero(), Error::<T>::NoAccumulatedAmount);
245
246            crate::permission::emission::do_distribute_emission::<T>(
247                *permission_id,
248                contract,
249                DistributionReason::Manual,
250            );
251
252            Ok(())
253        }
254        _ => Err(Error::<T>::InvalidDistributionMethod.into()),
255    }
256}
257
258/// Toggle a permission's accumulation state
259pub fn toggle_permission_accumulation_impl<T: Config>(
260    origin: OriginFor<T>,
261    permission_id: PermissionId,
262    accumulating: bool,
263) -> DispatchResult {
264    let who = ensure_signed_or_root(origin)?;
265
266    let mut contract =
267        Permissions::<T>::get(permission_id).ok_or(Error::<T>::PermissionNotFound)?;
268
269    if let Some(who) = &who {
270        match &contract.enforcement {
271            _ if who == &contract.grantor => {}
272            EnforcementAuthority::None => {
273                return Err(Error::<T>::NotAuthorizedToToggle.into());
274            }
275            EnforcementAuthority::ControlledBy {
276                controllers,
277                required_votes,
278            } => {
279                ensure!(controllers.contains(who), Error::<T>::NotAuthorizedToToggle);
280
281                let referendum = EnforcementReferendum::EmissionAccumulation(accumulating);
282                let votes = EnforcementTracking::<T>::get(permission_id, &referendum)
283                    .into_iter()
284                    .filter(|id| id != who)
285                    .filter(|id| controllers.contains(id))
286                    .count();
287
288                if votes.saturating_add(1) < *required_votes as usize {
289                    return EnforcementTracking::<T>::mutate(
290                        permission_id,
291                        referendum.clone(),
292                        |votes| {
293                            votes
294                                .try_insert(who.clone())
295                                .map_err(|_| Error::<T>::TooManyControllers)?;
296
297                            <Pallet<T>>::deposit_event(Event::EnforcementVoteCast {
298                                permission_id,
299                                voter: who.clone(),
300                                referendum,
301                            });
302
303                            Ok(())
304                        },
305                    );
306                }
307            }
308        }
309    }
310
311    match &mut contract.scope {
312        PermissionScope::Emission(emission_scope) => emission_scope.accumulating = accumulating,
313        _ => return Err(Error::<T>::UnsupportedPermissionType.into()),
314    }
315
316    Permissions::<T>::insert(permission_id, contract);
317
318    // Clear any votes for this referendum
319    EnforcementTracking::<T>::remove(
320        permission_id,
321        EnforcementReferendum::EmissionAccumulation(accumulating),
322    );
323
324    <Pallet<T>>::deposit_event(Event::PermissionAccumulationToggled {
325        permission_id,
326        accumulating,
327        toggled_by: who,
328    });
329
330    Ok(())
331}
332
333pub(crate) fn update_emission_permission<T: Config>(
334    origin: OriginFor<T>,
335    permission_id: PermissionId,
336    new_targets: BoundedBTreeMap<T::AccountId, u16, T::MaxTargetsPerPermission>,
337    new_streams: Option<BoundedBTreeMap<StreamId, Percent, T::MaxStreamsPerPermission>>,
338    new_distribution_control: Option<DistributionControl<T>>,
339) -> DispatchResult {
340    let who = ensure_signed(origin)?;
341
342    let permission = Permissions::<T>::get(permission_id);
343
344    let Some(mut permission) = permission else {
345        return Err(Error::<T>::PermissionNotFound.into());
346    };
347
348    let allowed_grantor = permission.grantor == who && permission.is_updatable();
349    let allowed_grantee =
350        permission.grantee == who && (new_streams.is_none() && new_distribution_control.is_none());
351
352    if !allowed_grantor && !allowed_grantee {
353        return Err(Error::<T>::NotAuthorizedToEdit.into());
354    }
355
356    let mut scope = permission.scope.clone();
357    match &mut scope {
358        PermissionScope::Emission(emission_scope) => {
359            validate_emission_permission_target_weights::<T>(&new_targets)?;
360
361            emission_scope.targets = new_targets;
362
363            let EmissionAllocation::Streams(streams) = &mut emission_scope.allocation else {
364                return Err(Error::<T>::NotEditable.into());
365            };
366
367            if let Some(new_streams) = new_streams {
368                crate::permission::emission::do_distribute_emission::<T>(
369                    permission_id,
370                    &permission,
371                    DistributionReason::Manual,
372                );
373
374                for stream in streams.keys() {
375                    AccumulatedStreamAmounts::<T>::remove((
376                        &permission.grantor,
377                        stream,
378                        &permission_id,
379                    ));
380                }
381
382                validate_emission_permission_streams::<T>(&new_streams, &permission.grantor)?;
383
384                for stream in new_streams.keys() {
385                    AccumulatedStreamAmounts::<T>::set(
386                        (&permission.grantor, stream, permission_id),
387                        Some(Zero::zero()),
388                    )
389                }
390
391                *streams = new_streams;
392            }
393
394            if let Some(new_distribution_control) = new_distribution_control {
395                validate_emission_permission_distribution::<T>(&new_distribution_control)?;
396
397                emission_scope.distribution = new_distribution_control;
398            }
399        }
400        _ => return Err(Error::<T>::NotEditable.into()),
401    }
402
403    permission.scope = scope;
404    Permissions::<T>::set(permission_id, Some(permission));
405
406    Ok(())
407}
408
409fn validate_emission_permission_target_weights<T: Config>(
410    targets: &BoundedBTreeMap<T::AccountId, u16, T::MaxTargetsPerPermission>,
411) -> DispatchResult {
412    ensure!(!targets.is_empty(), Error::<T>::NoTargetsSpecified);
413
414    for (target, weight) in targets {
415        ensure!(*weight > 0, Error::<T>::InvalidTargetWeight);
416        ensure!(
417            T::Torus::is_agent_registered(target),
418            Error::<T>::NotRegisteredAgent
419        );
420    }
421
422    Ok(())
423}
424fn validate_emission_permission_streams<T: Config>(
425    streams: &BoundedBTreeMap<StreamId, Percent, T::MaxStreamsPerPermission>,
426    grantor: &T::AccountId,
427) -> DispatchResult {
428    for (stream, percentage) in streams {
429        ensure!(*percentage <= Percent::one(), Error::<T>::InvalidPercentage);
430
431        let total_allocated = get_total_allocated_percentage::<T>(grantor, stream);
432        let new_total_allocated = match total_allocated.checked_add(percentage) {
433            Some(new_total_allocated) => new_total_allocated,
434            None => return Err(Error::<T>::TotalAllocationExceeded.into()),
435        };
436
437        ensure!(
438            new_total_allocated <= Percent::one(),
439            Error::<T>::TotalAllocationExceeded
440        );
441    }
442
443    Ok(())
444}
445
446fn validate_emission_permission_distribution<T: Config>(
447    distribution: &DistributionControl<T>,
448) -> DispatchResult {
449    match distribution {
450        DistributionControl::Automatic(threshold) => {
451            ensure!(
452                *threshold >= T::MinAutoDistributionThreshold::get(),
453                Error::<T>::InvalidThreshold
454            );
455        }
456        DistributionControl::Interval(interval) => {
457            ensure!(!interval.is_zero(), Error::<T>::InvalidInterval);
458        }
459        DistributionControl::AtBlock(block) => {
460            let current_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
461            ensure!(*block > current_block, Error::<T>::InvalidInterval);
462        }
463        _ => {}
464    }
465
466    Ok(())
467}