pallet_governance/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3pub mod application;
4pub mod config;
5pub mod ext;
6pub mod migrations;
7pub mod proposal;
8pub mod roles;
9pub mod voting;
10pub mod whitelist;
11
12pub mod benchmarking;
13pub mod weights;
14
15pub(crate) use ext::*;
16use frame::prelude::ensure_root;
17pub use pallet::*;
18use polkadot_sdk::{
19    frame_support::{
20        dispatch::DispatchResult,
21        pallet_prelude::{ValueQuery, *},
22        sp_runtime::Percent,
23        traits::Currency,
24        Identity, PalletId,
25    },
26    frame_system::pallet_prelude::{ensure_signed, BlockNumberFor, OriginFor},
27    polkadot_sdk_frame::{
28        traits::AccountIdConversion,
29        {self as frame},
30    },
31    sp_std::vec::Vec,
32};
33
34use crate::{
35    application::AgentApplication,
36    config::GovernanceConfiguration,
37    proposal::{Proposal, ProposalId, UnrewardedProposal},
38};
39
40#[frame::pallet]
41pub mod pallet {
42    #![allow(clippy::too_many_arguments)]
43
44    const STORAGE_VERSION: StorageVersion = StorageVersion::new(5);
45
46    use pallet_permission0_api::{CuratorPermissions, Permission0Api, Permission0CuratorApi};
47    use polkadot_sdk::sp_core::ConstBool;
48    use proposal::GlobalParamsData;
49    use weights::WeightInfo;
50
51    use super::*;
52
53    /// Map of past and present proposals indexed by their incrementing ID.
54    #[pallet::storage]
55    pub type Proposals<T: Config> = StorageMap<_, Identity, ProposalId, Proposal<T>>;
56
57    /// Queue of proposals to be rewarded after closing.
58    #[pallet::storage]
59    pub type UnrewardedProposals<T: Config> =
60        StorageMap<_, Identity, ProposalId, UnrewardedProposal<T>>;
61
62    /// List of keys that are NOT delegating their voting power. By default, all
63    /// keys delegate their voting power.
64    #[pallet::storage]
65    pub type NotDelegatingVotingPower<T: Config> =
66        StorageValue<_, BoundedBTreeSet<AccountIdOf<T>, ConstU32<{ u32::MAX }>>, ValueQuery>;
67
68    /// Global governance configuration files.
69    #[pallet::storage]
70    pub type GlobalGovernanceConfig<T: Config> =
71        StorageValue<_, GovernanceConfiguration<T>, ValueQuery>;
72
73    // This has to be different than DefaultKey, so we are not conflicting in tests.
74    #[pallet::type_value]
75    pub fn DefaultDaoTreasuryAddress<T: Config>() -> AccountIdOf<T> {
76        <T as Config>::PalletId::get().into_account_truncating()
77    }
78
79    /// The treasury address to which the treasury emission percentages and
80    /// other funds go to. A proposal can be created withdrawing the funds to a
81    /// key.
82    #[pallet::storage]
83    pub type DaoTreasuryAddress<T: Config> =
84        StorageValue<_, AccountIdOf<T>, ValueQuery, DefaultDaoTreasuryAddress<T>>;
85
86    /// A map of agent applications, past and present.
87    #[pallet::storage]
88    pub type AgentApplications<T: Config> = StorageMap<_, Identity, u32, AgentApplication<T>>;
89
90    /// List of whitelisted keys. Keys listed here are allowed to register
91    /// agents.
92    #[pallet::storage]
93    pub type Whitelist<T: Config> = StorageMap<_, Identity, AccountIdOf<T>, ()>;
94
95    /// List of allocator keys, which are the default validators on the network.
96    #[pallet::storage]
97    pub type Allocators<T: Config> = StorageMap<_, Identity, AccountIdOf<T>, ()>;
98
99    /// Fee taken from emission distribution and deposited into
100    /// [`DaoTreasuryAddress`].
101    #[pallet::storage]
102    pub type TreasuryEmissionFee<T: Config> =
103        StorageValue<_, Percent, ValueQuery, T::DefaultTreasuryEmissionFee>;
104
105    /// Determines if new agents can be registered on the chain.
106    #[pallet::storage]
107    pub type AgentsFrozen<T: Config> = StorageValue<_, bool, ValueQuery, ConstBool<false>>;
108
109    /// Determines if new namespaces can be created on the chain.
110    #[pallet::storage]
111    pub type NamespacesFrozen<T: Config> = StorageValue<_, bool, ValueQuery, ConstBool<false>>;
112
113    #[pallet::config]
114    pub trait Config:
115        polkadot_sdk::frame_system::Config + pallet_torus0::Config + pallet_emission0::Config
116    {
117        type RuntimeEvent: From<Event<Self>>
118            + IsType<<Self as polkadot_sdk::frame_system::Config>::RuntimeEvent>;
119
120        #[pallet::constant]
121        type PalletId: Get<PalletId>;
122
123        #[pallet::constant]
124        type MinApplicationDataLength: Get<u32>;
125
126        #[pallet::constant]
127        type MaxApplicationDataLength: Get<u32>;
128
129        #[pallet::constant]
130        type ApplicationExpiration: Get<BlockNumberFor<Self>>;
131
132        #[pallet::constant]
133        type MaxPenaltyPercentage: Get<Percent>;
134
135        #[pallet::constant]
136        type DefaultTreasuryEmissionFee: Get<Percent>;
137
138        #[pallet::constant]
139        type DefaultProposalCost: Get<BalanceOf<Self>>;
140
141        #[pallet::constant]
142        type DefaultProposalExpiration: Get<BlockNumberFor<Self>>;
143
144        #[pallet::constant]
145        type DefaultAgentApplicationCost: Get<BalanceOf<Self>>;
146
147        #[pallet::constant]
148        type DefaultAgentApplicationExpiration: Get<BlockNumberFor<Self>>;
149
150        #[pallet::constant]
151        type DefaultProposalRewardTreasuryAllocation: Get<Percent>;
152
153        #[pallet::constant]
154        type DefaultMaxProposalRewardTreasuryAllocation: Get<BalanceOf<Self>>;
155
156        #[pallet::constant]
157        type DefaultProposalRewardInterval: Get<BlockNumberFor<Self>>;
158
159        type Currency: Currency<Self::AccountId, Balance = u128> + Send + Sync;
160
161        type Permission0: Permission0Api<OriginFor<Self>>
162            + Permission0CuratorApi<Self::AccountId, OriginFor<Self>, BlockNumberFor<Self>>;
163
164        type WeightInfo: WeightInfo;
165    }
166
167    #[pallet::pallet]
168    #[pallet::storage_version(STORAGE_VERSION)]
169    pub struct Pallet<T>(_);
170
171    #[pallet::hooks]
172    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
173        fn on_initialize(block_number: BlockNumberFor<T>) -> Weight {
174            application::resolve_expired_applications::<T>(block_number);
175
176            proposal::tick_proposals::<T>(block_number);
177            proposal::tick_proposal_rewards::<T>(block_number);
178
179            Weight::zero()
180        }
181    }
182
183    #[pallet::call]
184    impl<T: Config> Pallet<T> {
185        /// Adds a new allocator to the list. Only available for the root key.
186        #[pallet::call_index(2)]
187        #[pallet::weight((<T as Config>::WeightInfo::add_allocator(), DispatchClass::Normal, Pays::Yes))]
188        pub fn add_allocator(origin: OriginFor<T>, key: AccountIdOf<T>) -> DispatchResult {
189            ensure_root(origin)?;
190            roles::add_allocator::<T>(key)
191        }
192
193        /// Removes an existing allocator from the list. Only available for the
194        /// root key.
195        #[pallet::call_index(3)]
196        #[pallet::weight((<T as Config>::WeightInfo::remove_allocator(), DispatchClass::Normal, Pays::Yes))]
197        pub fn remove_allocator(origin: OriginFor<T>, key: AccountIdOf<T>) -> DispatchResult {
198            ensure_root(origin)?;
199            roles::remove_allocator::<T>(key)
200        }
201
202        /// Forcefully adds a new agent to the whitelist. Only available for the
203        /// root key or curators.
204        #[pallet::call_index(4)]
205        #[pallet::weight((<T as Config>::WeightInfo::add_to_whitelist(), DispatchClass::Normal, Pays::Yes))]
206        pub fn add_to_whitelist(origin: OriginFor<T>, key: AccountIdOf<T>) -> DispatchResult {
207            <T as Config>::Permission0::ensure_curator_permission(
208                origin,
209                CuratorPermissions::WHITELIST_MANAGE,
210            )?;
211            whitelist::add_to_whitelist::<T>(key)
212        }
213
214        /// Forcefully removes an agent from the whitelist. Only available for
215        /// the root key or curators.
216        #[pallet::call_index(5)]
217        #[pallet::weight((<T as Config>::WeightInfo::remove_from_whitelist(), DispatchClass::Normal, Pays::Yes))]
218        pub fn remove_from_whitelist(origin: OriginFor<T>, key: AccountIdOf<T>) -> DispatchResult {
219            <T as Config>::Permission0::ensure_curator_permission(
220                origin,
221                CuratorPermissions::WHITELIST_MANAGE,
222            )?;
223            whitelist::remove_from_whitelist::<T>(key)
224        }
225
226        /// Accepts an agent application. Only available for the root key or
227        /// curators.
228        #[pallet::call_index(6)]
229        #[pallet::weight((<T as Config>::WeightInfo::accept_application(), DispatchClass::Normal, Pays::Yes))]
230        pub fn accept_application(origin: OriginFor<T>, application_id: u32) -> DispatchResult {
231            <T as Config>::Permission0::ensure_curator_permission(
232                origin,
233                CuratorPermissions::APPLICATION_REVIEW,
234            )?;
235            application::accept_application::<T>(application_id)
236        }
237
238        /// Denies an agent application. Only available for the root key or
239        /// curators.
240        #[pallet::call_index(7)]
241        #[pallet::weight((<T as Config>::WeightInfo::deny_application(), DispatchClass::Normal, Pays::Yes))]
242        pub fn deny_application(origin: OriginFor<T>, application_id: u32) -> DispatchResult {
243            <T as Config>::Permission0::ensure_curator_permission(
244                origin,
245                CuratorPermissions::APPLICATION_REVIEW,
246            )?;
247            application::deny_application::<T>(application_id)
248        }
249
250        /// Sets a penalty factor to the given agent emissions. Only available
251        /// for the root key or curators.
252        #[pallet::call_index(8)]
253        #[pallet::weight((<T as Config>::WeightInfo::penalize_agent(), DispatchClass::Normal, Pays::Yes))]
254        pub fn penalize_agent(
255            origin: OriginFor<T>,
256            agent_key: AccountIdOf<T>,
257            percentage: u8,
258        ) -> DispatchResult {
259            roles::penalize_agent::<T>(origin, agent_key, percentage)
260        }
261
262        /// Submits a new agent application on behalf of a given key.
263        #[pallet::call_index(9)]
264        #[pallet::weight((<T as Config>::WeightInfo::submit_application(), DispatchClass::Normal, Pays::Yes))]
265        pub fn submit_application(
266            origin: OriginFor<T>,
267            agent_key: AccountIdOf<T>,
268            metadata: Vec<u8>,
269            removing: bool,
270        ) -> DispatchResult {
271            let payer = ensure_signed(origin)?;
272            application::submit_application::<T>(payer, agent_key, metadata, removing)
273        }
274
275        /// Creates a new global parameters proposal.
276        #[pallet::call_index(10)]
277        #[pallet::weight((<T as Config>::WeightInfo::add_global_params_proposal(), DispatchClass::Normal, Pays::Yes))]
278        pub fn add_global_params_proposal(
279            origin: OriginFor<T>,
280            data: GlobalParamsData<T>,
281            metadata: Vec<u8>,
282        ) -> DispatchResult {
283            let proposer = ensure_signed(origin)?;
284            proposal::add_global_params_proposal::<T>(proposer, data, metadata)
285        }
286
287        /// Creates a new custom global proposal.
288        #[pallet::call_index(11)]
289        #[pallet::weight((<T as Config>::WeightInfo::add_global_custom_proposal(), DispatchClass::Normal, Pays::Yes))]
290        pub fn add_global_custom_proposal(
291            origin: OriginFor<T>,
292            metadata: Vec<u8>,
293        ) -> DispatchResult {
294            let proposer = ensure_signed(origin)?;
295            proposal::add_global_custom_proposal::<T>(proposer, metadata)
296        }
297
298        /// Creates a proposal moving funds from the treasury account to the
299        /// given key.
300        #[pallet::call_index(12)]
301        #[pallet::weight((<T as Config>::WeightInfo::add_dao_treasury_transfer_proposal(), DispatchClass::Normal, Pays::Yes))]
302        pub fn add_dao_treasury_transfer_proposal(
303            origin: OriginFor<T>,
304            value: BalanceOf<T>,
305            destination_key: AccountIdOf<T>,
306            data: Vec<u8>,
307        ) -> DispatchResult {
308            let proposer = ensure_signed(origin)?;
309            proposal::add_dao_treasury_transfer_proposal::<T>(
310                proposer,
311                value,
312                destination_key,
313                data,
314            )
315        }
316
317        /// Casts a vote for an open proposal.
318        #[pallet::call_index(13)]
319        #[pallet::weight((<T as Config>::WeightInfo::vote_proposal(), DispatchClass::Normal, Pays::Yes))]
320        pub fn vote_proposal(
321            origin: OriginFor<T>,
322            proposal_id: u64,
323            agree: bool,
324        ) -> DispatchResult {
325            let voter = ensure_signed(origin)?;
326            voting::add_vote::<T>(voter, proposal_id, agree)
327        }
328
329        /// Removes a casted vote for an open proposal.
330        #[pallet::call_index(14)]
331        #[pallet::weight((<T as Config>::WeightInfo::remove_vote_proposal(), DispatchClass::Normal, Pays::Yes))]
332        pub fn remove_vote_proposal(origin: OriginFor<T>, proposal_id: u64) -> DispatchResult {
333            let voter = ensure_signed(origin)?;
334            voting::remove_vote::<T>(voter, proposal_id)
335        }
336
337        /// Enables vote power delegation.
338        #[pallet::call_index(15)]
339        #[pallet::weight((<T as Config>::WeightInfo::enable_vote_delegation(), DispatchClass::Normal, Pays::Yes))]
340        pub fn enable_vote_delegation(origin: OriginFor<T>) -> DispatchResult {
341            let delegator = ensure_signed(origin)?;
342            voting::enable_delegation::<T>(delegator)
343        }
344
345        /// Disables vote power delegation.
346        #[pallet::call_index(16)]
347        #[pallet::weight((<T as Config>::WeightInfo::disable_vote_delegation(), DispatchClass::Normal, Pays::Yes))]
348        pub fn disable_vote_delegation(origin: OriginFor<T>) -> DispatchResult {
349            let delegator = ensure_signed(origin)?;
350            voting::disable_delegation::<T>(delegator)
351        }
352
353        /// Creates a new emission percentage proposal.
354        #[pallet::call_index(17)]
355        #[pallet::weight((<T as Config>::WeightInfo::add_emission_proposal(), DispatchClass::Normal, Pays::Yes))]
356        pub fn add_emission_proposal(
357            origin: OriginFor<T>,
358            recycling_percentage: Percent,
359            treasury_percentage: Percent,
360            incentives_ratio: Percent,
361            data: Vec<u8>,
362        ) -> DispatchResult {
363            let proposer = ensure_signed(origin)?;
364            proposal::add_emission_proposal::<T>(
365                proposer,
366                recycling_percentage,
367                treasury_percentage,
368                incentives_ratio,
369                data,
370            )
371        }
372
373        /// Forcefully sets emission percentages. Only available for the root
374        /// key.
375        #[pallet::call_index(18)]
376        #[pallet::weight((<T as Config>::WeightInfo::add_emission_proposal(), DispatchClass::Normal, Pays::No))]
377        pub fn set_emission_params(
378            origin: OriginFor<T>,
379            recycling_percentage: Percent,
380            treasury_percentage: Percent,
381        ) -> DispatchResult {
382            // ensure root
383            ensure_root(origin)?;
384
385            pallet_emission0::EmissionRecyclingPercentage::<T>::set(recycling_percentage);
386            crate::TreasuryEmissionFee::<T>::set(treasury_percentage);
387
388            Ok(())
389        }
390
391        #[pallet::call_index(19)]
392        #[pallet::weight((<T as Config>::WeightInfo::toggle_agent_freezing(), DispatchClass::Normal, Pays::No))]
393        pub fn toggle_agent_freezing(origin: OriginFor<T>) -> DispatchResult {
394            let curator = <T as pallet::Config>::Permission0::ensure_curator_permission(
395                origin,
396                CuratorPermissions::AGENT_FREEZING_TOGGLING,
397            )?;
398
399            let new_state = !crate::AgentsFrozen::<T>::get();
400            AgentsFrozen::<T>::set(new_state);
401
402            crate::Pallet::<T>::deposit_event(crate::Event::AgentFreezingToggled {
403                curator,
404                new_state,
405            });
406
407            Ok(())
408        }
409
410        #[pallet::call_index(20)]
411        #[pallet::weight((<T as Config>::WeightInfo::toggle_namespace_freezing(), DispatchClass::Normal, Pays::No))]
412        pub fn toggle_namespace_freezing(origin: OriginFor<T>) -> DispatchResult {
413            let curator = <T as pallet::Config>::Permission0::ensure_curator_permission(
414                origin,
415                CuratorPermissions::NAMESPACE_FREEZING_TOGGLING,
416            )?;
417
418            let new_state = !crate::NamespacesFrozen::<T>::get();
419            NamespacesFrozen::<T>::set(new_state);
420
421            crate::Pallet::<T>::deposit_event(crate::Event::NamespaceFreezingToggled {
422                curator,
423                new_state,
424            });
425
426            Ok(())
427        }
428    }
429
430    #[pallet::event]
431    #[pallet::generate_deposit(pub(crate) fn deposit_event)]
432    pub enum Event<T: Config> {
433        /// A new proposal has been created.
434        ProposalCreated(ProposalId),
435        /// A proposal has been accepted.
436        ProposalAccepted(ProposalId),
437        /// A proposal has been refused.
438        ProposalRefused(ProposalId),
439        /// A proposal has expired.
440        ProposalExpired(ProposalId),
441        /// A vote has been cast on a proposal.
442        ProposalVoted(u64, T::AccountId, bool),
443        /// A vote has been unregistered from a proposal.
444        ProposalVoteUnregistered(u64, T::AccountId),
445        /// An agent account has been added to the whitelist.
446        WhitelistAdded(T::AccountId),
447        /// An agent account has been removed from the whitelist.
448        WhitelistRemoved(T::AccountId),
449        /// A new application has been created.
450        ApplicationCreated(u32),
451        /// An application has been accepted.
452        ApplicationAccepted(u32),
453        /// An application has been denied.
454        ApplicationDenied(u32),
455        /// An application has expired.
456        ApplicationExpired(u32),
457        /// A penalty was applied to an agent.
458        PenaltyApplied {
459            curator: T::AccountId,
460            agent: T::AccountId,
461            penalty: Percent,
462        },
463        /// The agent freezing feature was toggled by a curator.
464        AgentFreezingToggled {
465            curator: T::AccountId,
466            new_state: bool,
467        },
468        /// The namespace freezing feature was toggled by a curator.
469        NamespaceFreezingToggled {
470            curator: T::AccountId,
471            new_state: bool,
472        },
473    }
474
475    #[pallet::error]
476    pub enum Error<T> {
477        /// The proposal is already finished. Do not retry.
478        ProposalIsFinished,
479        /// Invalid parameters were provided to the finalization process.
480        InvalidProposalFinalizationParameters,
481        /// Invalid parameters were provided to the voting process.
482        InvalidProposalVotingParameters,
483        /// Negative proposal cost when setting global or subnet governance
484        /// configuration.
485        InvalidProposalCost,
486        /// Negative expiration when setting global or subnet governance
487        /// configuration.
488        InvalidProposalExpiration,
489        /// Key doesn't have enough tokens to create a proposal.
490        NotEnoughBalanceToPropose,
491        /// Proposal data is empty.
492        ProposalDataTooSmall,
493        /// Proposal data is bigger than 256 characters.
494        ProposalDataTooLarge,
495        /// The staked module is already delegating for 2 ^ 32 keys.
496        ModuleDelegatingForMaxStakers,
497        /// Proposal with given id doesn't exist.
498        ProposalNotFound,
499        /// Proposal was either accepted, refused or expired and cannot accept
500        /// votes.
501        ProposalClosed,
502        /// Proposal data isn't composed by valid UTF-8 characters.
503        InvalidProposalData,
504        /// Invalid value given when transforming a u64 into T::Currency.
505        InvalidCurrencyConversionValue,
506        /// Dao Treasury doesn't have enough funds to be transferred.
507        InsufficientDaoTreasuryFunds,
508        /// Key has already voted on given Proposal.
509        AlreadyVoted,
510        /// Key hasn't voted on given Proposal.
511        NotVoted,
512        /// Key doesn't have enough stake to vote.
513        InsufficientStake,
514        /// The voter is delegating its voting power to their staked modules.
515        /// Disable voting power delegation.
516        VoterIsDelegatingVotingPower,
517        /// An internal error occurred, probably relating to the size of the
518        /// bounded sets.
519        InternalError,
520        /// The application is not in a pending state.
521        ApplicationNotOpen,
522        /// The application key is already used in another application.
523        ApplicationKeyAlreadyUsed,
524        /// The account doesn't have enough balance to submit an application.
525        NotEnoughBalanceToApply,
526        /// The operation can only be performed by the curator.
527        NotCurator,
528        /// The application with the given ID was not found.
529        ApplicationNotFound,
530        /// The account is already whitelisted and cannot be added again.
531        AlreadyWhitelisted,
532        /// The account is not whitelisted and cannot be removed from the
533        /// whitelist.
534        NotWhitelisted,
535        /// Failed to convert the given value to a balance.
536        CouldNotConvertToBalance,
537        /// The application data provided does not meet the length requirement
538        InvalidApplicationDataLength,
539        /// The key is already a curator.
540        AlreadyCurator,
541        /// The key is already an allocator.
542        AlreadyAllocator,
543        /// The key is not an allocator.
544        NotAllocator,
545        /// Agent not found
546        AgentNotFound,
547        /// Invalid agent penalty percentage
548        InvalidPenaltyPercentage,
549        /// Invalid minimum name length in proposal
550        InvalidMinNameLength,
551        /// Invalid maximum name length in proposal
552        InvalidMaxNameLength,
553        /// Invalid maximum allowed weights in proposal
554        InvalidMaxAllowedWeights,
555        /// Invalid minimum weight control fee in proposal
556        InvalidMinWeightControlFee,
557        /// Invalid minimum staking fee in proposal
558        InvalidMinStakingFee,
559        /// Invalid params given to Emission proposal
560        InvalidEmissionProposalData,
561    }
562}
563
564impl<T: Config> pallet_governance_api::GovernanceApi<T::AccountId> for Pallet<T> {
565    fn dao_treasury_address() -> T::AccountId {
566        DaoTreasuryAddress::<T>::get()
567    }
568
569    fn treasury_emission_fee() -> Percent {
570        TreasuryEmissionFee::<T>::get()
571    }
572
573    fn is_whitelisted(key: &T::AccountId) -> bool {
574        whitelist::is_whitelisted::<T>(key)
575    }
576
577    fn ensure_allocator(key: &T::AccountId) -> DispatchResult {
578        crate::roles::ensure_allocator::<T>(key)
579    }
580
581    fn get_allocators() -> impl Iterator<Item = T::AccountId> {
582        Allocators::<T>::iter_keys()
583    }
584
585    fn set_allocator(key: &T::AccountId) {
586        Allocators::<T>::insert(key, ());
587    }
588
589    fn can_create_namespace(key: &T::AccountId) -> bool {
590        !NamespacesFrozen::<T>::get() || Self::is_whitelisted(key)
591    }
592
593    fn can_register_agent(key: &T::AccountId) -> bool {
594        !AgentsFrozen::<T>::get() || Self::is_whitelisted(key)
595    }
596
597    #[cfg(feature = "runtime-benchmarks")]
598    fn force_set_whitelisted(key: &T::AccountId) {
599        Whitelist::<T>::insert(key, ());
600    }
601}