Skip to content

Commit 44f90b8

Browse files
committed
feat(permission0): implement multi-sig controllers
1 parent 3309fa6 commit 44f90b8

File tree

11 files changed

+896
-14
lines changed

11 files changed

+896
-14
lines changed

pallets/permission0/api/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,25 @@ pub enum RevocationTerms<AccountId, BlockNumber> {
8383
RevocableAfter(BlockNumber),
8484
}
8585

86+
/// Types of enforcement actions that can be voted on
87+
#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)]
88+
pub enum EnforcementAuthority<AccountId> {
89+
/// No special enforcement (standard permission execution)
90+
None,
91+
/// Permission can be toggled active/inactive by controllers
92+
ControlledBy {
93+
controllers: Vec<AccountId>,
94+
required_votes: u32,
95+
},
96+
}
97+
8698
/// The Permission0 API trait
8799
pub trait Permission0Api<AccountId, Origin, BlockNumber, Balance, NegativeImbalance> {
88100
/// Check if a permission exists
89101
fn permission_exists(id: &PermissionId) -> bool;
90102

91103
/// Grant a permission for emission delegation
104+
#[allow(clippy::too_many_arguments)]
92105
fn grant_emission_permission(
93106
grantor: AccountId,
94107
grantee: AccountId,
@@ -97,6 +110,7 @@ pub trait Permission0Api<AccountId, Origin, BlockNumber, Balance, NegativeImbala
97110
distribution: DistributionControl<Balance, BlockNumber>,
98111
duration: PermissionDuration<BlockNumber>,
99112
revocation: RevocationTerms<AccountId, BlockNumber>,
113+
enforcement: EnforcementAuthority<AccountId>,
100114
) -> Result<PermissionId, DispatchError>;
101115

102116
/// Revoke a permission

pallets/permission0/src/ext.rs

Lines changed: 221 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
use crate::permission::EnforcementReferendum;
12
use crate::{
23
generate_permission_id, get_total_allocated_percentage, pallet,
34
permission::emission::DistributionReason, update_permission_indices, AccumulatedStreamAmounts,
4-
BalanceOf, Config, DistributionControl, EmissionAllocation, EmissionScope, Error, Event,
5-
Pallet, PermissionContract, PermissionDuration, PermissionId, PermissionScope, Permissions,
6-
RevocationTerms,
5+
BalanceOf, Config, DistributionControl, EmissionAllocation, EmissionScope,
6+
EnforcementAuthority, EnforcementTracking, Error, Event, Pallet, PermissionContract,
7+
PermissionDuration, PermissionId, PermissionScope, Permissions, RevocationTerms,
78
};
89
use pallet_permission0_api::{
910
DistributionControl as ApiDistributionControl, EmissionAllocation as ApiEmissionAllocation,
10-
Permission0Api, PermissionDuration as ApiPermissionDuration,
11-
RevocationTerms as ApiRevocationTerms, StreamId,
11+
EnforcementAuthority as ApiEnforcementAuthority, Permission0Api,
12+
PermissionDuration as ApiPermissionDuration, RevocationTerms as ApiRevocationTerms, StreamId,
1213
};
1314
use pallet_torus0_api::Torus0Api;
1415
use polkadot_sdk::polkadot_sdk_frame::traits::CheckedAdd;
@@ -45,6 +46,7 @@ impl<T: Config>
4546
distribution: ApiDistributionControl<crate::BalanceOf<T>, BlockNumberFor<T>>,
4647
duration: ApiPermissionDuration<BlockNumberFor<T>>,
4748
revocation: ApiRevocationTerms<T::AccountId, BlockNumberFor<T>>,
49+
enforcement: ApiEnforcementAuthority<T::AccountId>,
4850
) -> Result<PermissionId, DispatchError> {
4951
let internal_allocation = match allocation {
5052
ApiEmissionAllocation::Streams(streams) => EmissionAllocation::Streams(
@@ -84,6 +86,19 @@ impl<T: Config>
8486
ApiRevocationTerms::RevocableAfter(blocks) => RevocationTerms::RevocableAfter(blocks),
8587
};
8688

89+
let internal_enforcement = match enforcement {
90+
ApiEnforcementAuthority::None => EnforcementAuthority::None,
91+
ApiEnforcementAuthority::ControlledBy {
92+
controllers,
93+
required_votes,
94+
} => EnforcementAuthority::ControlledBy {
95+
controllers: controllers
96+
.try_into()
97+
.map_err(|_| crate::Error::<T>::TooManyControllers)?,
98+
required_votes,
99+
},
100+
};
101+
87102
grant_permission_impl::<T>(
88103
grantor,
89104
grantee,
@@ -92,7 +107,8 @@ impl<T: Config>
92107
internal_distribution,
93108
internal_duration,
94109
internal_revocation,
95-
None,
110+
internal_enforcement,
111+
None, // No parent by default
96112
)
97113
}
98114

@@ -139,6 +155,7 @@ pub(crate) fn grant_permission_impl<T: Config>(
139155
distribution: DistributionControl<T>,
140156
duration: PermissionDuration<T>,
141157
revocation: RevocationTerms<T>,
158+
enforcement: EnforcementAuthority<T>,
142159
parent_id: Option<PermissionId>,
143160
) -> Result<PermissionId, DispatchError> {
144161
use polkadot_sdk::frame_support::ensure;
@@ -217,7 +234,7 @@ pub(crate) fn grant_permission_impl<T: Config>(
217234
required_votes,
218235
} => {
219236
ensure!(*required_votes > 0, Error::<T>::InvalidNumberOfRevokers);
220-
ensure!(accounts.len() > 0, Error::<T>::InvalidNumberOfRevokers);
237+
ensure!(!accounts.is_empty(), Error::<T>::InvalidNumberOfRevokers);
221238

222239
ensure!(
223240
*required_votes as usize <= accounts.len(),
@@ -232,6 +249,25 @@ pub(crate) fn grant_permission_impl<T: Config>(
232249
_ => {}
233250
}
234251

252+
match &enforcement {
253+
EnforcementAuthority::None => {}
254+
EnforcementAuthority::ControlledBy {
255+
controllers,
256+
required_votes,
257+
} => {
258+
ensure!(*required_votes > 0, Error::<T>::InvalidNumberOfControllers);
259+
ensure!(
260+
!controllers.is_empty(),
261+
Error::<T>::InvalidNumberOfControllers
262+
);
263+
264+
ensure!(
265+
*required_votes as usize <= controllers.len(),
266+
Error::<T>::InvalidNumberOfControllers
267+
);
268+
}
269+
}
270+
235271
// TODO: develop the idea
236272
if let Some(parent) = parent_id {
237273
let parent_contract =
@@ -254,6 +290,7 @@ pub(crate) fn grant_permission_impl<T: Config>(
254290
allocation: allocation.clone(),
255291
distribution,
256292
targets,
293+
accumulating: true, // Start with accumulation enabled by default
257294
};
258295

259296
let scope = PermissionScope::Emission(emission_scope);
@@ -266,6 +303,7 @@ pub(crate) fn grant_permission_impl<T: Config>(
266303
scope,
267304
duration,
268305
revocation,
306+
enforcement,
269307
last_execution: None,
270308
execution_count: 0,
271309
parent: parent_id,
@@ -329,6 +367,11 @@ pub(crate) fn execute_permission_impl<T: Config>(
329367
match &contract.scope {
330368
PermissionScope::Emission(emission_scope) => match emission_scope.distribution {
331369
DistributionControl::Manual => {
370+
ensure!(
371+
emission_scope.accumulating,
372+
Error::<T>::UnsupportedPermissionType
373+
);
374+
332375
let accumulated = match &emission_scope.allocation {
333376
EmissionAllocation::Streams(streams) => streams
334377
.keys()
@@ -359,3 +402,174 @@ pub(crate) fn execute_permission_impl<T: Config>(
359402

360403
Ok(())
361404
}
405+
406+
/// Toggle a permission's accumulation state
407+
pub fn toggle_permission_accumulation_impl<T: Config>(
408+
origin: OriginFor<T>,
409+
permission_id: PermissionId,
410+
accumulating: bool,
411+
) -> DispatchResult {
412+
let who = ensure_signed_or_root(origin)?;
413+
414+
let mut contract =
415+
Permissions::<T>::get(permission_id).ok_or(Error::<T>::PermissionNotFound)?;
416+
417+
if let Some(who) = &who {
418+
match &contract.enforcement {
419+
_ if who == &contract.grantor => {}
420+
EnforcementAuthority::None => {
421+
return Err(Error::<T>::NotAuthorizedToToggle.into());
422+
}
423+
EnforcementAuthority::ControlledBy {
424+
controllers,
425+
required_votes,
426+
} => {
427+
ensure!(controllers.contains(who), Error::<T>::NotAuthorizedToToggle);
428+
429+
let referendum = EnforcementReferendum::EmissionAccumulation(accumulating);
430+
let votes = EnforcementTracking::<T>::get(permission_id, &referendum)
431+
.into_iter()
432+
.filter(|id| id != who)
433+
.filter(|id| controllers.contains(id))
434+
.count();
435+
436+
if votes + 1 < *required_votes as usize {
437+
return EnforcementTracking::<T>::mutate(
438+
permission_id,
439+
referendum.clone(),
440+
|votes| {
441+
votes
442+
.try_insert(who.clone())
443+
.map_err(|_| Error::<T>::TooManyControllers)?;
444+
445+
<Pallet<T>>::deposit_event(Event::EnforcementVoteCast {
446+
permission_id,
447+
voter: who.clone(),
448+
referendum,
449+
});
450+
451+
Ok(())
452+
},
453+
);
454+
}
455+
}
456+
}
457+
}
458+
459+
match &mut contract.scope {
460+
PermissionScope::Emission(emission_scope) => emission_scope.accumulating = accumulating,
461+
}
462+
463+
Permissions::<T>::insert(permission_id, contract);
464+
465+
// Clear any votes for this referendum
466+
EnforcementTracking::<T>::remove(
467+
permission_id,
468+
EnforcementReferendum::EmissionAccumulation(accumulating),
469+
);
470+
471+
<Pallet<T>>::deposit_event(Event::PermissionAccumulationToggled {
472+
permission_id,
473+
accumulating,
474+
toggled_by: who,
475+
});
476+
477+
Ok(())
478+
}
479+
480+
/// Execute a permission through enforcement authority
481+
pub fn enforcement_execute_permission_impl<T: Config>(
482+
origin: OriginFor<T>,
483+
permission_id: PermissionId,
484+
) -> DispatchResult {
485+
let who = ensure_signed_or_root(origin)?;
486+
487+
let contract = Permissions::<T>::get(permission_id).ok_or(Error::<T>::PermissionNotFound)?;
488+
489+
// If not root, check enforcement authority
490+
if let Some(who) = &who {
491+
match &contract.enforcement {
492+
EnforcementAuthority::None => {
493+
return Err(Error::<T>::NotAuthorizedToToggle.into());
494+
}
495+
EnforcementAuthority::ControlledBy {
496+
controllers,
497+
required_votes,
498+
} => {
499+
ensure!(controllers.contains(who), Error::<T>::NotAuthorizedToToggle);
500+
501+
let referendum = EnforcementReferendum::Execution;
502+
let votes = EnforcementTracking::<T>::get(permission_id, &referendum)
503+
.into_iter()
504+
.filter(|id| id != who)
505+
.filter(|id| controllers.contains(id))
506+
.count();
507+
508+
if votes + 1 < *required_votes as usize {
509+
return EnforcementTracking::<T>::mutate(
510+
permission_id,
511+
referendum.clone(),
512+
|votes| {
513+
votes
514+
.try_insert(who.clone())
515+
.map_err(|_| Error::<T>::TooManyControllers)?;
516+
517+
<Pallet<T>>::deposit_event(Event::EnforcementVoteCast {
518+
permission_id,
519+
voter: who.clone(),
520+
referendum,
521+
});
522+
523+
Ok(())
524+
},
525+
);
526+
}
527+
}
528+
}
529+
}
530+
531+
match &contract.scope {
532+
PermissionScope::Emission(emission_scope) => {
533+
ensure!(
534+
emission_scope.accumulating,
535+
Error::<T>::UnsupportedPermissionType
536+
);
537+
538+
match emission_scope.distribution {
539+
DistributionControl::Manual => {
540+
let accumulated = match &emission_scope.allocation {
541+
EmissionAllocation::Streams(streams) => streams
542+
.keys()
543+
.filter_map(|id| {
544+
AccumulatedStreamAmounts::<T>::get((
545+
&contract.grantor,
546+
id,
547+
permission_id,
548+
))
549+
})
550+
.fold(BalanceOf::<T>::zero(), |acc, e| acc + e),
551+
EmissionAllocation::FixedAmount(amount) => *amount,
552+
};
553+
554+
ensure!(!accumulated.is_zero(), Error::<T>::NoAccumulatedAmount);
555+
556+
crate::permission::emission::do_distribute_emission::<T>(
557+
permission_id,
558+
&contract,
559+
DistributionReason::Manual,
560+
);
561+
}
562+
_ => return Err(Error::<T>::InvalidDistributionMethod.into()),
563+
}
564+
}
565+
}
566+
567+
EnforcementTracking::<T>::remove(permission_id, EnforcementReferendum::Execution);
568+
569+
<Pallet<T>>::deposit_event(Event::PermissionEnforcementExecuted {
570+
permission_id,
571+
executed_by: who,
572+
});
573+
574+
Ok(())
575+
}

0 commit comments

Comments
 (0)