pallet_permission0/ext/
emission_impl.rs

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