diff --git a/Cargo.lock b/Cargo.lock index 5e2bcf8..c2b02bf 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -8364,6 +8364,7 @@ dependencies = [ "parity-scale-codec", "polkadot-sdk", "scale-info", + "substrate-fixed", ] [[package]] @@ -16292,6 +16293,17 @@ name = "substrate-build-script-utils" version = "11.0.0" source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=stable2409#981d6c0fa87a00b72bb3b6211d1e71deed21f0cc" +[[package]] +name = "substrate-fixed" +version = "0.5.9" +source = "git+https://github.com/encointer/substrate-fixed#ddaa922892d1565f02c9c5702f0aacd17da53ce2" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "substrate-typenum", +] + [[package]] name = "substrate-frame-rpc-support" version = "40.0.0" @@ -16369,6 +16381,16 @@ dependencies = [ "trie-db", ] +[[package]] +name = "substrate-typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f0091e93c2c75b233ae39424c52cb8a662c0811fb68add149e20e5d7e8a788" +dependencies = [ + "parity-scale-codec", + "scale-info", +] + [[package]] name = "substrate-wasm-builder" version = "24.0.1" diff --git a/Cargo.toml b/Cargo.toml index 0d2c227..1c17589 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ test-utils.path = "./test-utils" # Substrate codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false } scale-info = { version = "2.11.1", default-features = false } + polkadot-sdk = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "stable2409", default-features = false } # Frontier @@ -43,6 +44,8 @@ pallet-evm-precompile-modexp = { git = "https://github.com/paritytech/frontier.g pallet-evm-precompile-sha3fips = { git = "https://github.com/paritytech/frontier.git", rev = "b9b1c620c8b418bdeeadc79725f9cfa4703c0333", default-features = false } pallet-evm-precompile-simple = { git = "https://github.com/paritytech/frontier.git", rev = "b9b1c620c8b418bdeeadc79725f9cfa4703c0333", default-features = false } +substrate-fixed = { git = "https://github.com/encointer/substrate-fixed", default-features = false } + # CLI-specific dependencies clap = { version = "4.5.22", features = ["derive"] } serde_json = { version = "1.0", default-features = false } diff --git a/pallets/governance/Cargo.toml b/pallets/governance/Cargo.toml index d868322..238a132 100644 --- a/pallets/governance/Cargo.toml +++ b/pallets/governance/Cargo.toml @@ -8,12 +8,18 @@ edition.workspace = true [features] default = ["std"] -std = ["codec/std", "polkadot-sdk/std", "scale-info/std"] +std = ["codec/std", "polkadot-sdk/std", "scale-info/std", "substrate-fixed/std"] runtime-benchmarks = ["polkadot-sdk/runtime-benchmarks"] try-runtime = ["polkadot-sdk/try-runtime"] [dependencies] codec = { workspace = true, features = ["derive"] } pallet-torus0 = { workspace = true } -polkadot-sdk = { workspace = true, features = ["experimental", "runtime"] } +polkadot-sdk = { workspace = true, features = [ + "experimental", + "runtime", + "pallet-sudo", + "sc-telemetry", +] } scale-info = { workspace = true, features = ["derive"] } +substrate-fixed = { workspace = true } diff --git a/pallets/governance/src/application.rs b/pallets/governance/src/application.rs index 630df6e..7516fd0 100644 --- a/pallets/governance/src/application.rs +++ b/pallets/governance/src/application.rs @@ -1,43 +1,114 @@ +use crate::frame::traits::ExistenceRequirement; +use crate::{whitelist, AccountIdOf, BalanceOf, Block}; use codec::{Decode, Encode, MaxEncodedLen}; +use polkadot_sdk::frame_election_provider_support::Get; use polkadot_sdk::frame_support::dispatch::DispatchResult; +use polkadot_sdk::frame_support::traits::Currency; +use polkadot_sdk::frame_support::traits::WithdrawReasons; use polkadot_sdk::frame_support::DebugNoBound; -use polkadot_sdk::polkadot_sdk_frame::prelude::OriginFor; -use polkadot_sdk::sp_core::ConstU32; use polkadot_sdk::sp_runtime::BoundedVec; use polkadot_sdk::sp_std::vec::Vec; use scale_info::TypeInfo; -use crate::{AccountIdOf, BalanceOf, Block}; - #[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct AgentApplication { pub id: u32, pub payer_key: AccountIdOf, pub agent_key: AccountIdOf, - pub data: BoundedVec>, + pub data: BoundedVec, pub cost: BalanceOf, pub expires_at: Block, } pub fn submit_application( - _origin: OriginFor, - _agent_key: AccountIdOf, - _data: Vec, + payer: AccountIdOf, + agent_key: AccountIdOf, + data: Vec, ) -> DispatchResult { - todo!() + if whitelist::is_whitelisted::(&agent_key) { + return Err(crate::Error::::AlreadyWhitelisted.into()); + } + + let config = crate::GlobalGovernanceConfig::::get(); + let cost = config.agent_application_cost; + + let _ = ::Currency::withdraw( + &payer, + cost, + WithdrawReasons::except(WithdrawReasons::TIP), + ExistenceRequirement::KeepAlive, + ) + .map_err(|_| crate::Error::::NotEnoughBalanceToApply)?; + + let data_len: u32 = data + .len() + .try_into() + .map_err(|_| crate::Error::::InvalidApplicationDataLength)?; + + let data_range = T::MinApplicationDataLength::get()..T::MaxApplicationDataLength::get(); + if !data_range.contains(&data_len) { + return Err(crate::Error::::InvalidApplicationDataLength.into()); + } + + let current_block: u64 = + TryInto::try_into(>::block_number()) + .ok() + .expect("blockchain will not exceed 2^64 blocks; QED."); + + let expires_at = current_block + config.agent_application_expiration; + + let application_id: u32 = crate::AgentApplicationId::::mutate(|id| { + let last_id = *id; + *id = id.saturating_add(1); + last_id + }); + + let application = AgentApplication:: { + id: application_id, + payer_key: payer, + agent_key, + data: BoundedVec::truncate_from(data), + cost, + expires_at, + }; + + crate::AgentApplications::::insert(application_id, application); + crate::Pallet::::deposit_event(crate::Event::::ApplicationCreated(application_id)); + + Ok(()) } -pub fn accept_application( - _origin: OriginFor, - _application_id: u32, -) -> DispatchResult { - todo!() +pub fn accept_application(application_id: u32) -> DispatchResult { + let application = crate::AgentApplications::::get(application_id) + .ok_or(crate::Error::::ApplicationNotFound)?; + + crate::AgentApplications::::remove(application.id); + + whitelist::add_to_whitelist::(application.agent_key.clone())?; + crate::Pallet::::deposit_event(crate::Event::::ApplicationAccepted(application.id)); + crate::Pallet::::deposit_event(crate::Event::::WhitelistAdded(application.agent_key)); + + Ok(()) } -pub fn deny_application( - _origin: OriginFor, - _application_id: u32, -) -> DispatchResult { - todo!() +pub fn deny_application(application_id: u32) -> DispatchResult { + let application = crate::AgentApplications::::get(application_id) + .ok_or(crate::Error::::ApplicationNotFound)?; + + crate::AgentApplications::::remove(application.id); + crate::Pallet::::deposit_event(crate::Event::::ApplicationDenied(application.id)); + + Ok(()) +} + +pub(crate) fn remove_expired_applications(current_block: Block) { + for application in crate::AgentApplications::::iter_values() { + if current_block < application.expires_at { + continue; + } + + crate::AgentApplications::::remove(application.id); + crate::Pallet::::deposit_event(crate::Event::::ApplicationExpired(application.id)); + } } diff --git a/pallets/governance/src/config.rs b/pallets/governance/src/config.rs index 8687fcf..919c264 100644 --- a/pallets/governance/src/config.rs +++ b/pallets/governance/src/config.rs @@ -10,6 +10,7 @@ pub struct GovernanceConfiguration { pub proposal_cost: BalanceOf, pub proposal_expiration: BlockAmount, pub agent_application_cost: BalanceOf, + pub agent_application_expiration: BlockAmount, pub proposal_reward_treasury_allocation: Percent, pub max_proposal_reward_treasury_allocation: BalanceOf, pub proposal_reward_interval: BlockAmount, @@ -21,6 +22,7 @@ impl Default for GovernanceConfiguration { proposal_cost: 10_000_000_000_000, proposal_expiration: 130_000, agent_application_cost: 1_000_000_000_000, + agent_application_expiration: 2_000, proposal_reward_treasury_allocation: Percent::from_percent(2), max_proposal_reward_treasury_allocation: 10_000_000_000_000, proposal_reward_interval: 75_600, diff --git a/pallets/governance/src/curator.rs b/pallets/governance/src/curator.rs index d93e88a..8329b77 100644 --- a/pallets/governance/src/curator.rs +++ b/pallets/governance/src/curator.rs @@ -1,19 +1,55 @@ +use crate::AccountIdOf; +use polkadot_sdk::frame_election_provider_support::Get; +use polkadot_sdk::sp_runtime::{DispatchError, Percent}; use polkadot_sdk::{ - frame_support::dispatch::DispatchResult, polkadot_sdk_frame::prelude::OriginFor, + frame_support::dispatch::DispatchResult, frame_system::ensure_signed, + polkadot_sdk_frame::prelude::OriginFor, }; -use crate::AccountIdOf; +pub fn add_curator(key: AccountIdOf) -> DispatchResult { + if crate::Curators::::contains_key(&key) { + return Err(crate::Error::::AlreadyCurator.into()); + } -pub fn add_curator( - _origin: OriginFor, - _key: AccountIdOf, -) -> DispatchResult { - todo!() + crate::Curators::::insert(key, ()); + Ok(()) } -pub fn remove_curator( - _origin: OriginFor, - _key: AccountIdOf, +pub fn remove_curator(key: AccountIdOf) -> DispatchResult { + if !crate::Curators::::contains_key(&key) { + return Err(crate::Error::::NotCurator.into()); + } + + crate::Curators::::remove(&key); + Ok(()) +} + +pub fn penalize_agent( + agent_key: AccountIdOf, + percentage: u8, ) -> DispatchResult { - todo!() + if percentage > T::MaxPenaltyPercentage::get() { + return Err(crate::Error::::InvalidPenaltyPercentage.into()); + } + + pallet_torus0::Agents::::try_mutate(&agent_key, |agent| { + let Some(agent) = agent else { + return Err(crate::Error::::AgentNotFound.into()); + }; + + agent.weight_factor = Percent::from_percent(100u8.saturating_sub(percentage)); + + Ok::<(), DispatchError>(()) + })?; + + Ok(()) +} + +pub fn ensure_curator(origin: OriginFor) -> DispatchResult { + let key: AccountIdOf = ensure_signed(origin)?; + if !crate::Curators::::contains_key(key) { + return Err(crate::Error::::NotCurator.into()); + } + + Ok(()) } diff --git a/pallets/governance/src/lib.rs b/pallets/governance/src/lib.rs index 30444d5..0f735b9 100644 --- a/pallets/governance/src/lib.rs +++ b/pallets/governance/src/lib.rs @@ -10,17 +10,28 @@ mod whitelist; use crate::application::AgentApplication; use crate::config::GovernanceConfiguration; -use crate::proposal::{Proposal, ProposalId, UnrewardedProposal}; +use crate::proposal::Proposal; +use crate::proposal::ProposalId; +use crate::proposal::UnrewardedProposal; pub(crate) use ext::*; +use frame::prelude::ensure_root; pub use pallet::*; -use polkadot_sdk::frame_support::Identity; -use polkadot_sdk::frame_support::{pallet_prelude::*, PalletId}; -use polkadot_sdk::polkadot_sdk_frame::{self as frame, prelude::OriginFor, traits::Currency}; -use polkadot_sdk::sp_runtime::traits::AccountIdConversion; +use polkadot_sdk::frame_support::{ + dispatch::DispatchResult, + pallet_prelude::{ValueQuery, *}, + traits::Currency, + Identity, PalletId, +}; +use polkadot_sdk::frame_system::pallet_prelude::{ensure_signed, BlockNumberFor, OriginFor}; +use polkadot_sdk::polkadot_sdk_frame::traits::AccountIdConversion; +use polkadot_sdk::polkadot_sdk_frame::{self as frame}; use polkadot_sdk::sp_std::vec::Vec; #[frame::pallet(dev_mode)] pub mod pallet { + #![allow(clippy::too_many_arguments)] + + use proposal::GlobalParamsData; use super::*; @@ -50,8 +61,10 @@ pub mod pallet { StorageValue<_, AccountIdOf, ValueQuery, DefaultDaoTreasuryAddress>; #[pallet::storage] - pub type AgentApplications = - StorageMap<_, Identity, BalanceOf, AgentApplication>; + pub type AgentApplications = StorageMap<_, Identity, u32, AgentApplication>; + + #[pallet::storage] + pub type AgentApplicationId = StorageValue<_, u32, ValueQuery>; #[pallet::storage] pub type Whitelist = StorageMap<_, Identity, AccountIdOf, ()>; @@ -59,132 +72,213 @@ pub mod pallet { #[pallet::storage] pub type Curators = StorageMap<_, Identity, AccountIdOf, ()>; - #[pallet::config] - pub trait Config: polkadot_sdk::frame_system::Config { + #[pallet::config(with_default)] + pub trait Config: polkadot_sdk::frame_system::Config + pallet_torus0::Config { #[pallet::constant] type PalletId: Get; + #[pallet::constant] + type MinApplicationDataLength: Get; + + #[pallet::constant] + type MaxApplicationDataLength: Get; + + #[pallet::constant] + type ApplicationExpiration: Get; + + #[pallet::constant] + type MaxPenaltyPercentage: Get; + + #[pallet::no_default_bounds] + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + type Currency: Currency + Send + Sync; } #[pallet::pallet] pub struct Pallet(_); + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(block_number: BlockNumberFor) -> Weight { + let current_block: u64 = block_number + .try_into() + .ok() + .expect("blockchain won't pass 2 ^ 64 blocks"); + + application::remove_expired_applications::(current_block); + proposal::tick_proposals::(current_block); + proposal::tick_proposal_rewards::(current_block); + + Weight::zero() + } + } + #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(0)] - pub fn add_curator_extrinsic(origin: OriginFor, key: AccountIdOf) -> DispatchResult { - curator::add_curator::(origin, key) + pub fn add_curator(origin: OriginFor, key: AccountIdOf) -> DispatchResult { + ensure_root(origin)?; + curator::add_curator::(key) } #[pallet::call_index(1)] #[pallet::weight(0)] - pub fn remove_curator_extrinsic( - origin: OriginFor, - key: AccountIdOf, - ) -> DispatchResult { - curator::remove_curator::(origin, key) + pub fn remove_curator(origin: OriginFor, key: AccountIdOf) -> DispatchResult { + ensure_root(origin)?; + curator::remove_curator::(key) } #[pallet::call_index(2)] #[pallet::weight(0)] - pub fn add_to_whitelist_extrinsic( - origin: OriginFor, - key: AccountIdOf, - ) -> DispatchResult { - whitelist::add_to_whitelist::(origin, key) + pub fn add_to_whitelist(origin: OriginFor, key: AccountIdOf) -> DispatchResult { + curator::ensure_curator::(origin)?; + whitelist::add_to_whitelist::(key) } #[pallet::call_index(3)] #[pallet::weight(0)] - pub fn remove_from_whitelist_extrinsic( - origin: OriginFor, - key: AccountIdOf, - ) -> DispatchResult { - whitelist::remove_from_whitelist::(origin, key) + pub fn remove_from_whitelist(origin: OriginFor, key: AccountIdOf) -> DispatchResult { + curator::ensure_curator::(origin)?; + whitelist::remove_from_whitelist::(key) } #[pallet::call_index(4)] #[pallet::weight(0)] - pub fn submit_application_extrinsic( + pub fn accept_application(origin: OriginFor, application_id: u32) -> DispatchResult { + curator::ensure_curator::(origin)?; + application::accept_application::(application_id) + } + + #[pallet::call_index(5)] + #[pallet::weight(0)] + pub fn deny_application(origin: OriginFor, application_id: u32) -> DispatchResult { + curator::ensure_curator::(origin)?; + application::deny_application::(application_id) + } + + #[pallet::call_index(6)] + #[pallet::weight(0)] + pub fn penalize_agent( origin: OriginFor, agent_key: AccountIdOf, - data: Vec, + percentage: u8, ) -> DispatchResult { - application::submit_application::(origin, agent_key, data) + curator::ensure_curator::(origin)?; + curator::penalize_agent::(agent_key, percentage) } - #[pallet::call_index(5)] + #[pallet::call_index(7)] #[pallet::weight(0)] - pub fn accept_application_extrinsic( + pub fn submit_application( origin: OriginFor, - application_id: u32, + agent_key: AccountIdOf, + metadata: Vec, ) -> DispatchResult { - application::accept_application::(origin, application_id) + let payer = ensure_signed(origin)?; + application::submit_application::(payer, agent_key, metadata) } - #[pallet::call_index(6)] + #[pallet::call_index(8)] #[pallet::weight(0)] - pub fn deny_application_extrinsic( + pub fn add_global_params_proposal( origin: OriginFor, - application_id: u32, + data: GlobalParamsData, + metadata: Vec, ) -> DispatchResult { - application::deny_application::(origin, application_id) + let proposer = ensure_signed(origin)?; + proposal::add_global_params_proposal::(proposer, data, metadata) } - #[pallet::call_index(7)] + #[pallet::call_index(9)] #[pallet::weight(0)] - pub fn add_global_custom_proposal_extrinsic( + pub fn add_global_custom_proposal( origin: OriginFor, - data: Vec, + metadata: Vec, ) -> DispatchResult { - proposal::add_global_custom_proposal::(origin, data) + let proposer = ensure_signed(origin)?; + proposal::add_global_custom_proposal::(proposer, metadata) } - #[pallet::call_index(8)] + #[pallet::call_index(10)] #[pallet::weight(0)] - pub fn add_dao_treasury_transfer_proposal_extrinsic( + pub fn add_dao_treasury_transfer_proposal( origin: OriginFor, value: BalanceOf, destination_key: AccountIdOf, data: Vec, ) -> DispatchResult { - proposal::add_dao_treasury_transfer_proposal::(origin, value, destination_key, data) + let proposer = ensure_signed(origin)?; + proposal::add_dao_treasury_transfer_proposal::( + proposer, + value, + destination_key, + data, + ) } - #[pallet::call_index(9)] + #[pallet::call_index(11)] #[pallet::weight(0)] - pub fn vote_proposal_extrinsic( + pub fn vote_proposal( origin: OriginFor, proposal_id: u64, agree: bool, ) -> DispatchResult { - voting::add_vote::(origin, proposal_id, agree) + let voter = ensure_signed(origin)?; + voting::add_vote::(voter, proposal_id, agree) } - #[pallet::call_index(10)] + #[pallet::call_index(12)] #[pallet::weight(0)] - pub fn remove_vote_proposal_extrinsic( - origin: OriginFor, - proposal_id: u64, - ) -> DispatchResult { - voting::remove_vote::(origin, proposal_id) + pub fn remove_vote_proposal(origin: OriginFor, proposal_id: u64) -> DispatchResult { + let voter = ensure_signed(origin)?; + voting::remove_vote::(voter, proposal_id) } - #[pallet::call_index(11)] + #[pallet::call_index(13)] #[pallet::weight(0)] - pub fn enable_vote_delegation_extrinsic(origin: OriginFor) -> DispatchResult { + pub fn enable_vote_delegation(origin: OriginFor) -> DispatchResult { voting::enable_delegation::(origin) } - #[pallet::call_index(12)] + #[pallet::call_index(14)] #[pallet::weight(0)] - pub fn disable_vote_delegation_extrinsic(origin: OriginFor) -> DispatchResult { + pub fn disable_vote_delegation(origin: OriginFor) -> DispatchResult { voting::disable_delegation::(origin) } } + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A new proposal has been created. + ProposalCreated(ProposalId), + /// A proposal has been accepted. + ProposalAccepted(ProposalId), + /// A proposal has been refused. + ProposalRefused(ProposalId), + /// A proposal has expired. + ProposalExpired(ProposalId), + /// A vote has been cast on a proposal. + ProposalVoted(u64, T::AccountId, bool), + /// A vote has been unregistered from a proposal. + ProposalVoteUnregistered(u64, T::AccountId), + /// An agent account has been added to the whitelist. + WhitelistAdded(T::AccountId), + /// An agent account has been removed from the whitelist. + WhitelistRemoved(T::AccountId), + /// A new application has been created. + ApplicationCreated(u32), + /// An application has been accepted. + ApplicationAccepted(u32), + /// An application has been denied. + ApplicationDenied(u32), + /// An application has expired. + ApplicationExpired(u32), + } + #[pallet::error] pub enum Error { /// The proposal is already finished. Do not retry. @@ -224,8 +318,6 @@ pub mod pallet { /// The voter is delegating its voting power to their staked modules. Disable voting power /// delegation. VoterIsDelegatingVotingPower, - /// The network vote mode must be authority for changes to be imposed. - VoteModeIsNotAuthority, /// An internal error occurred, probably relating to the size of the bounded sets. InternalError, /// The application data is too small or empty. @@ -250,5 +342,25 @@ pub mod pallet { NotWhitelisted, /// Failed to convert the given value to a balance. CouldNotConvertToBalance, + /// The application data provided does not meet the length requirement + InvalidApplicationDataLength, + /// The penalty percentage provided does not meet the maximum requirement + InvalidAgentPenaltyPercentage, + /// The key is already a curator. + AlreadyCurator, + /// Agent not found + AgentNotFound, + /// Invalid agent penalty percentage + InvalidPenaltyPercentage, + /// Invalid minimum name length in proposal + InvalidMinNameLength, + /// Invalid maximum name length in proposal + InvalidMaxNameLength, + /// Invalid maximum allowed agents in proposal + InvalidMaxAllowedAgents, + /// Invalid maximum allowed weights in proposal + InvalidMaxAllowedWeights, + /// Invalid minimum weight control fee in proposal + InvalidMinWeightControlFee, } } diff --git a/pallets/governance/src/proposal.rs b/pallets/governance/src/proposal.rs index e0cd59a..3139ba5 100644 --- a/pallets/governance/src/proposal.rs +++ b/pallets/governance/src/proposal.rs @@ -1,14 +1,26 @@ +use crate::frame::traits::ExistenceRequirement; +use crate::BoundedBTreeSet; +use crate::BoundedVec; +use crate::DebugNoBound; +use crate::TypeInfo; +use crate::{ + AccountIdOf, BalanceOf, Block, DaoTreasuryAddress, Error, GlobalGovernanceConfig, Proposals, + UnrewardedProposals, +}; +use crate::{GovernanceConfiguration, NotDelegatingVotingPower}; use codec::{Decode, Encode, MaxEncodedLen}; +use polkadot_sdk::frame_election_provider_support::Get; +use polkadot_sdk::frame_support::traits::Currency; +use polkadot_sdk::frame_support::traits::WithdrawReasons; +use polkadot_sdk::sp_runtime::SaturatedConversion; +use polkadot_sdk::sp_std::{collections::btree_set::BTreeSet, vec::Vec}; use polkadot_sdk::{ - frame_support::{dispatch::DispatchResult, DebugNoBound}, - polkadot_sdk_frame::prelude::OriginFor, + frame_support::{dispatch::DispatchResult, ensure, storage::with_storage_layer}, + sc_telemetry::log, sp_core::ConstU32, - sp_runtime::{BoundedBTreeMap, BoundedBTreeSet, BoundedVec}, - sp_std::vec::Vec, + sp_runtime::{BoundedBTreeMap, DispatchError, Percent}, }; -use scale_info::TypeInfo; - -use crate::{AccountIdOf, BalanceOf, Block}; +use substrate_fixed::types::I92F36; pub type ProposalId = u64; @@ -25,6 +37,117 @@ pub struct Proposal { pub creation_block: Block, } +impl Proposal { + /// Whether the proposal is still active. + #[must_use] + pub fn is_active(&self) -> bool { + matches!(self.status, ProposalStatus::Open { .. }) + } + + /// Marks a proposal as accepted and overrides the storage value. + pub fn accept( + mut self, + block: Block, + stake_for: BalanceOf, + stake_against: BalanceOf, + ) -> DispatchResult { + ensure!(self.is_active(), crate::Error::::ProposalIsFinished); + + self.status = ProposalStatus::Accepted { + block, + stake_for, + stake_against, + }; + + Proposals::::insert(self.id, &self); + crate::Pallet::::deposit_event(crate::Event::ProposalAccepted(self.id)); + + self.execute_proposal()?; + + Ok(()) + } + + fn execute_proposal(self) -> DispatchResult { + let _ = + ::Currency::deposit_creating(&self.proposer, self.proposal_cost); + + match self.data { + ProposalData::GlobalParams(data) => { + let GlobalParamsData { + min_name_length, + max_name_length, + max_allowed_agents, + max_allowed_weights, + min_weight_stake, + min_weight_control_fee, + min_staking_fee, + } = data; + + pallet_torus0::MinNameLength::::set(min_name_length); + pallet_torus0::MaxNameLength::::set(max_name_length); + pallet_torus0::MaxAllowedAgents::::set(max_allowed_agents); + pallet_torus0::MaxAllowedWeights::::set(max_allowed_weights); + pallet_torus0::MinWeightStake::::set(min_weight_stake); + pallet_torus0::FeeConstraints::::mutate(|constraints| { + constraints.min_weight_control_fee = + Percent::from_percent(min_weight_control_fee); + constraints.min_staking_fee = Percent::from_percent(min_staking_fee); + }); + } + ProposalData::TransferDaoTreasury { account, amount } => { + ::Currency::transfer( + &DaoTreasuryAddress::::get(), + &account, + amount, + ExistenceRequirement::KeepAlive, + ) + .map_err(|_| crate::Error::::InternalError)?; + } + + _ => {} + } + + Ok(()) + } + + /// Marks a proposal as refused and overrides the storage value. + pub fn refuse( + mut self, + block: Block, + stake_for: BalanceOf, + stake_against: BalanceOf, + ) -> DispatchResult { + ensure!(self.is_active(), crate::Error::::ProposalIsFinished); + + self.status = ProposalStatus::Refused { + block, + stake_for, + stake_against, + }; + + Proposals::::insert(self.id, &self); + crate::Pallet::::deposit_event(crate::Event::ProposalRefused(self.id)); + + Ok(()) + } + + /// Marks a proposal as expired and overrides the storage value. + pub fn expire(mut self, block_number: u64) -> DispatchResult { + ensure!(self.is_active(), crate::Error::::ProposalIsFinished); + ensure!( + block_number >= self.expiration_block, + crate::Error::::InvalidProposalFinalizationParameters + ); + + self.status = ProposalStatus::Expired; + + Proposals::::insert(self.id, &self); + crate::Pallet::::deposit_event(crate::Event::ProposalExpired(self.id)); + + Ok(()) + } +} + #[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] #[scale_info(skip_type_params(T))] pub enum ProposalStatus { @@ -47,9 +170,49 @@ pub enum ProposalStatus { Expired, } +#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] +#[scale_info(skip_type_params(T))] +pub struct GlobalParamsData { + pub min_name_length: u16, + pub max_name_length: u16, + pub max_allowed_agents: u16, + pub max_allowed_weights: u16, + pub min_weight_stake: BalanceOf, + pub min_weight_control_fee: u8, + pub min_staking_fee: u8, +} + +impl GlobalParamsData { + pub fn validate(&self) -> DispatchResult { + ensure!( + self.min_name_length > 1, + crate::Error::::InvalidMinNameLength + ); + ensure!( + (self.max_name_length as u32) < T::MaxAgentNameLengthConstraint::get(), + crate::Error::::InvalidMaxNameLength + ); + ensure!( + self.max_allowed_agents < 2000, + crate::Error::::InvalidMaxAllowedAgents + ); + ensure!( + self.max_allowed_weights < 2000, + crate::Error::::InvalidMaxAllowedWeights + ); + ensure!( + self.min_weight_control_fee > 10, + crate::Error::::InvalidMaxAllowedWeights + ); + + Ok(()) + } +} + #[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] #[scale_info(skip_type_params(T))] pub enum ProposalData { + GlobalParams(GlobalParamsData), GlobalCustom, TransferDaoTreasury { account: AccountIdOf, @@ -57,26 +220,382 @@ pub enum ProposalData { }, } +impl ProposalData { + #[must_use] + pub fn required_stake(&self) -> Percent { + match self { + Self::GlobalCustom | Self::TransferDaoTreasury { .. } => Percent::from_parts(50), + Self::GlobalParams { .. } => Percent::from_parts(40), + } + } +} + #[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)] #[scale_info(skip_type_params(T))] pub struct UnrewardedProposal { pub block: Block, - pub votes_for: BoundedBTreeMap, u64, ConstU32<{ u32::MAX }>>, - pub votes_against: BoundedBTreeMap, u64, ConstU32<{ u32::MAX }>>, + pub votes_for: BoundedBTreeMap, BalanceOf, ConstU32<{ u32::MAX }>>, + pub votes_against: BoundedBTreeMap, BalanceOf, ConstU32<{ u32::MAX }>>, +} + +#[allow(clippy::too_many_arguments)] +pub fn add_global_params_proposal( + proposer: AccountIdOf, + data: GlobalParamsData, + metadata: Vec, +) -> DispatchResult { + let data = ProposalData::::GlobalParams(data); + + add_proposal::(proposer, data, metadata) } pub fn add_global_custom_proposal( - _origin: OriginFor, - _data: Vec, + proposer: AccountIdOf, + metadata: Vec, ) -> DispatchResult { - todo!() + add_proposal(proposer, ProposalData::::GlobalCustom, metadata) } pub fn add_dao_treasury_transfer_proposal( - _origin: OriginFor, - _value: BalanceOf, - _destination_key: AccountIdOf, - _data: Vec, + proposer: AccountIdOf, + value: BalanceOf, + destination_key: AccountIdOf, + metadata: Vec, +) -> DispatchResult { + let data = ProposalData::::TransferDaoTreasury { + account: destination_key, + amount: value, + }; + + add_proposal::(proposer, data, metadata) +} + +fn add_proposal( + proposer: AccountIdOf, + data: ProposalData, + metadata: Vec, +) -> DispatchResult { + ensure!( + !metadata.is_empty(), + crate::Error::::ProposalDataTooSmall + ); + ensure!( + metadata.len() <= 256, + crate::Error::::ProposalDataTooLarge + ); + + let config = GlobalGovernanceConfig::::get(); + + let cost = config.proposal_cost; + let _ = ::Currency::withdraw( + &proposer, + cost, + WithdrawReasons::except(WithdrawReasons::TIP), + ExistenceRequirement::KeepAlive, + ) + .map_err(|_| crate::Error::::NotEnoughBalanceToPropose)?; + + let proposal_id: u64 = crate::Proposals::::iter() + .count() + .try_into() + .map_err(|_| crate::Error::::InternalError)?; + + let current_block: u64 = + TryInto::try_into(>::block_number()) + .ok() + .expect("blockchain will not exceed 2^64 blocks; QED."); + + let proposal = Proposal:: { + id: proposal_id, + proposer, + expiration_block: current_block + config.proposal_expiration, + data, + status: ProposalStatus::Open { + votes_for: BoundedBTreeSet::new(), + votes_against: BoundedBTreeSet::new(), + stake_for: 0, + stake_against: 0, + }, + metadata: BoundedVec::truncate_from(metadata), + proposal_cost: cost, + creation_block: current_block, + }; + + crate::Proposals::::insert(proposal_id, proposal); + + Ok(()) +} + +pub fn tick_proposals(block_number: Block) { + let not_delegating = NotDelegatingVotingPower::::get().into_inner(); + + let proposals = Proposals::::iter().filter(|(_, p)| p.is_active()); + + if block_number % 100 != 0 { + return; + } + + for (id, proposal) in proposals { + let res = with_storage_layer(|| tick_proposal(¬_delegating, block_number, proposal)); + if let Err(err) = res { + log::error!("failed to tick proposal {id}: {err:?}, skipping..."); + } + } +} + +pub fn get_minimal_stake_to_execute_with_percentage( + threshold: Percent, +) -> BalanceOf { + let stake = pallet_torus0::TotalStake::::get(); + + stake + .saturated_into::>() + .checked_mul(threshold.deconstruct() as u128) + .unwrap_or_default() + .checked_div(100) + .unwrap_or_default() +} + +fn tick_proposal( + not_delegating: &BTreeSet, + block_number: u64, + mut proposal: Proposal, ) -> DispatchResult { - todo!() + let ProposalStatus::Open { + votes_for, + votes_against, + .. + } = &proposal.status + else { + return Err(Error::::ProposalIsFinished.into()); + }; + + let votes_for: Vec<(AccountIdOf, BalanceOf)> = votes_for + .iter() + .cloned() + .map(|id| { + let stake = calc_stake::(not_delegating, &id); + (id, stake) + }) + .collect(); + let votes_against: Vec<(AccountIdOf, BalanceOf)> = votes_against + .iter() + .cloned() + .map(|id| { + let stake = calc_stake::(not_delegating, &id); + (id, stake) + }) + .collect(); + + let stake_for_sum: BalanceOf = votes_for.iter().map(|(_, stake)| stake).sum(); + let stake_against_sum: BalanceOf = votes_against.iter().map(|(_, stake)| stake).sum(); + + if block_number < proposal.expiration_block { + if let ProposalStatus::Open { + stake_for, + stake_against, + .. + } = &mut proposal.status + { + *stake_for = stake_for_sum; + *stake_against = stake_against_sum; + } + Proposals::::set(proposal.id, Some(proposal)); + return Ok(()); + } + + let total_stake = stake_for_sum.saturating_add(stake_against_sum); + let minimal_stake_to_execute = + get_minimal_stake_to_execute_with_percentage::(proposal.data.required_stake()); + + let mut reward_votes_for = BoundedBTreeMap::new(); + for (key, value) in votes_for { + reward_votes_for + .try_insert(key, value) + .expect("this wont exceed u32::MAX"); + } + + let mut reward_votes_against: BoundedBTreeMap< + T::AccountId, + BalanceOf, + ConstU32<{ u32::MAX }>, + > = BoundedBTreeMap::new(); + for (key, value) in votes_against { + reward_votes_against + .try_insert(key, value) + .expect("this probably wont exceed u32::MAX"); + } + + UnrewardedProposals::::insert( + proposal.id, + UnrewardedProposal:: { + block: block_number, + votes_for: reward_votes_for, + votes_against: reward_votes_against, + }, + ); + + if total_stake >= minimal_stake_to_execute { + if stake_against_sum > stake_for_sum { + proposal.refuse(block_number, stake_for_sum, stake_against_sum) + } else { + proposal.accept(block_number, stake_for_sum, stake_against_sum) + } + } else { + proposal.expire(block_number) + } +} + +#[inline] +fn calc_stake( + not_delegating: &BTreeSet, + voter: &T::AccountId, +) -> BalanceOf { + let own_stake: BalanceOf = if !not_delegating.contains(voter) { + 0 + } else { + pallet_torus0::stake::sum_staking_to::(voter) + }; + + let delegated_stake = pallet_torus0::stake::get_staking_to_vector::(voter) + .into_iter() + .filter(|(staker, _)| !not_delegating.contains(staker)) + .map(|(_, stake)| stake) + .sum(); + + own_stake.saturating_add(delegated_stake) +} + +pub fn tick_proposal_rewards(block_number: u64) { + let governance_config = crate::GlobalGovernanceConfig::::get(); + let reached_interval = block_number + .checked_rem(governance_config.proposal_reward_interval) + .is_some_and(|r| r == 0); + if !reached_interval { + return; + } + + let mut n: u16 = 0; + let mut account_stakes: BoundedBTreeMap, ConstU32<{ u32::MAX }>> = + BoundedBTreeMap::new(); + let mut total_allocation: I92F36 = I92F36::from_num(0); + for (proposal_id, unrewarded_proposal) in UnrewardedProposals::::iter() { + if unrewarded_proposal.block + < block_number.saturating_sub(governance_config.proposal_reward_interval) + { + continue; + } + + for (acc_id, stake) in unrewarded_proposal + .votes_for + .into_iter() + .chain(unrewarded_proposal.votes_against.into_iter()) + { + let curr_stake = *account_stakes.get(&acc_id).unwrap_or(&0u128); + account_stakes + .try_insert(acc_id, curr_stake.saturating_add(stake)) + .expect("infallible"); + } + + match get_reward_allocation::(&governance_config, n) { + Ok(allocation) => { + total_allocation = total_allocation.saturating_add(allocation); + } + Err(err) => { + log::error!("could not get reward allocation for proposal {proposal_id}: {err:?}"); + continue; + } + } + + UnrewardedProposals::::remove(proposal_id); + n = n.saturating_add(1); + } + + distribute_proposal_rewards::( + account_stakes, + total_allocation, + governance_config.max_proposal_reward_treasury_allocation, + ); +} + +fn get_reward_allocation( + governance_config: &GovernanceConfiguration, + n: u16, +) -> Result { + let treasury_address = DaoTreasuryAddress::::get(); + let treasury_balance = ::Currency::free_balance(&treasury_address); + let treasury_balance = I92F36::from_num(treasury_balance); + + let allocation_percentage = I92F36::from_num( + governance_config + .proposal_reward_treasury_allocation + .deconstruct(), + ); + let max_allocation = + I92F36::from_num(governance_config.max_proposal_reward_treasury_allocation); + + let mut allocation = treasury_balance + .checked_mul(allocation_percentage) + .unwrap_or_default() + .min(max_allocation); + if n > 0 { + let mut base = I92F36::from_num(1.5); + let mut result = I92F36::from_num(1); + let mut remaining = n; + + while remaining > 0 { + if remaining % 2 == 1 { + result = result.checked_mul(base).unwrap_or(result); + } + base = base.checked_mul(base).unwrap_or_default(); + remaining /= 2; + } + + allocation = allocation.checked_div(result).unwrap_or(allocation); + } + Ok(allocation) +} + +fn distribute_proposal_rewards( + account_stakes: BoundedBTreeMap, ConstU32<{ u32::MAX }>>, + total_allocation: I92F36, + max_proposal_reward_treasury_allocation: BalanceOf, +) { + // This is just a sanity check, making sure we can never allocate more than the max + if total_allocation > I92F36::from_num(max_proposal_reward_treasury_allocation) { + log::error!("total allocation exceeds max proposal reward treasury allocation"); + return; + } + + use polkadot_sdk::frame_support::sp_runtime::traits::IntegerSquareRoot; + + let dao_treasury_address = DaoTreasuryAddress::::get(); + let account_sqrt_stakes: Vec<_> = account_stakes + .into_iter() + .map(|(acc_id, stake)| (acc_id, stake.integer_sqrt())) + .collect(); + + let total_stake: BalanceOf = account_sqrt_stakes.iter().map(|(_, stake)| *stake).sum(); + let total_stake = I92F36::from_num(total_stake); + + for (acc_id, stake) in account_sqrt_stakes.into_iter() { + let percentage = I92F36::from_num(stake) + .checked_div(total_stake) + .unwrap_or_default(); + + let reward: BalanceOf = total_allocation + .checked_mul(percentage) + .unwrap_or_default() + .to_num(); + + // Transfer the proposal reward to the accounts from treasury + if let Err(err) = ::Currency::transfer( + &dao_treasury_address, + &acc_id, + reward, + ExistenceRequirement::KeepAlive, + ) { + log::error!("could not transfer proposal reward: {err:?}") + } + } } diff --git a/pallets/governance/src/voting.rs b/pallets/governance/src/voting.rs index 08cfcc9..a07a28b 100644 --- a/pallets/governance/src/voting.rs +++ b/pallets/governance/src/voting.rs @@ -1,17 +1,82 @@ +use crate::{proposal::ProposalStatus, AccountIdOf, Error, Event, Proposals}; use polkadot_sdk::{ - frame_support::dispatch::DispatchResult, polkadot_sdk_frame::prelude::OriginFor, + frame_support::{dispatch::DispatchResult, ensure}, + polkadot_sdk_frame::prelude::OriginFor, }; pub fn add_vote( - _origin: OriginFor, - _proposal_id: u64, - _agree: bool, + voter: AccountIdOf, + proposal_id: u64, + agree: bool, ) -> DispatchResult { - todo!() + let Some(mut proposal) = Proposals::::get(proposal_id) else { + return Err(Error::::ProposalNotFound.into()); + }; + + let crate::proposal::ProposalStatus::Open { + votes_for, + votes_against, + .. + } = &mut proposal.status + else { + return Err(Error::::ProposalClosed.into()); + }; + + ensure!( + !votes_for.contains(&voter) && !votes_against.contains(&voter), + crate::Error::::AlreadyVoted + ); + + let voter_delegated_stake = pallet_torus0::stake::sum_staked_by::(&voter); + let voter_owned_stake = pallet_torus0::stake::sum_staking_to::(&voter); + + ensure!( + voter_delegated_stake > 0 || voter_owned_stake > 0, + crate::Error::::InsufficientStake + ); + + if !crate::NotDelegatingVotingPower::::get().contains(&voter) && voter_delegated_stake == 0 { + return Err(Error::::VoterIsDelegatingVotingPower.into()); + } + + if agree { + votes_for + .try_insert(voter.clone()) + .map_err(|_| Error::::InternalError)?; + } else { + votes_against + .try_insert(voter.clone()) + .map_err(|_| Error::::InternalError)?; + } + + Proposals::::insert(proposal.id, proposal); + crate::Pallet::::deposit_event(Event::::ProposalVoted(proposal_id, voter, agree)); + Ok(()) } -pub fn remove_vote(_origin: OriginFor, _proposal_id: u64) -> DispatchResult { - todo!() +pub fn remove_vote(voter: AccountIdOf, proposal_id: u64) -> DispatchResult { + let Ok(mut proposal) = Proposals::::try_get(proposal_id) else { + return Err(Error::::ProposalNotFound.into()); + }; + + let ProposalStatus::Open { + votes_for, + votes_against, + .. + } = &mut proposal.status + else { + return Err(Error::::ProposalClosed.into()); + }; + + let removed = votes_for.remove(&voter) || votes_against.remove(&voter); + + // Check if the voter has actually voted on the proposal + ensure!(removed, crate::Error::::NotVoted); + + // Update the proposal in storage + Proposals::::insert(proposal.id, proposal); + crate::Pallet::::deposit_event(Event::::ProposalVoteUnregistered(proposal_id, voter)); + Ok(()) } pub fn enable_delegation(_origin: OriginFor) -> DispatchResult { diff --git a/pallets/governance/src/whitelist.rs b/pallets/governance/src/whitelist.rs index e48504b..fe0353c 100644 --- a/pallets/governance/src/whitelist.rs +++ b/pallets/governance/src/whitelist.rs @@ -1,19 +1,27 @@ -use polkadot_sdk::{ - frame_support::dispatch::DispatchResult, polkadot_sdk_frame::prelude::OriginFor, -}; +use polkadot_sdk::frame_support::dispatch::DispatchResult; use crate::AccountIdOf; -pub fn add_to_whitelist( - _origin: OriginFor, - _key: AccountIdOf, -) -> DispatchResult { - todo!() +pub fn add_to_whitelist(key: AccountIdOf) -> DispatchResult { + if is_whitelisted::(&key) { + return Err(crate::Error::::AlreadyWhitelisted.into()); + } + + crate::Whitelist::::insert(key.clone(), ()); + crate::Pallet::::deposit_event(crate::Event::::WhitelistAdded(key)); + Ok(()) +} + +pub fn remove_from_whitelist(key: AccountIdOf) -> DispatchResult { + if !is_whitelisted::(&key) { + return Err(crate::Error::::NotWhitelisted.into()); + } + + crate::Whitelist::::remove(&key); + crate::Pallet::::deposit_event(crate::Event::::WhitelistRemoved(key)); + Ok(()) } -pub fn remove_from_whitelist( - _origin: OriginFor, - _key: AccountIdOf, -) -> DispatchResult { - todo!() +pub fn is_whitelisted(key: &AccountIdOf) -> bool { + crate::Whitelist::::contains_key(key) } diff --git a/pallets/torus0/src/fee.rs b/pallets/torus0/src/fee.rs index 634633c..7faa800 100644 --- a/pallets/torus0/src/fee.rs +++ b/pallets/torus0/src/fee.rs @@ -8,7 +8,7 @@ use scale_info::TypeInfo; #[derive(DebugNoBound, Decode, Encode, MaxEncodedLen, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct ValidatorFeeConstraints { - pub min_stake_delegation_fee: Percent, + pub min_staking_fee: Percent, pub min_weight_control_fee: Percent, pub _pd: PhantomData, } @@ -16,7 +16,7 @@ pub struct ValidatorFeeConstraints { impl Default for ValidatorFeeConstraints { fn default() -> Self { Self { - min_stake_delegation_fee: Percent::from_percent(T::DefaultMinStakingFee::get()), + min_staking_fee: Percent::from_percent(T::DefaultMinStakingFee::get()), min_weight_control_fee: Percent::from_percent(T::DefaultMinWeightControlFee::get()), _pd: PhantomData, } @@ -26,7 +26,7 @@ impl Default for ValidatorFeeConstraints { #[derive(DebugNoBound, Decode, Encode, MaxEncodedLen, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct ValidatorFee { - pub stake_delegation_fee: Percent, + pub staking_fee: Percent, pub weight_control_fee: Percent, pub _pd: PhantomData, } @@ -34,7 +34,7 @@ pub struct ValidatorFee { impl Default for ValidatorFee { fn default() -> Self { Self { - stake_delegation_fee: Percent::from_percent(T::DefaultMinStakingFee::get()), + staking_fee: Percent::from_percent(T::DefaultMinStakingFee::get()), weight_control_fee: Percent::from_percent(T::DefaultMinWeightControlFee::get()), _pd: PhantomData, } diff --git a/pallets/torus0/src/lib.rs b/pallets/torus0/src/lib.rs index 0a45429..950a9dd 100644 --- a/pallets/torus0/src/lib.rs +++ b/pallets/torus0/src/lib.rs @@ -4,7 +4,7 @@ mod agent; mod balance; mod ext; mod fee; -mod stake; +pub mod stake; use crate::agent::Agent; use crate::fee::ValidatorFee; diff --git a/pallets/torus0/src/stake.rs b/pallets/torus0/src/stake.rs index fb38064..1547285 100644 --- a/pallets/torus0/src/stake.rs +++ b/pallets/torus0/src/stake.rs @@ -1,3 +1,5 @@ +use polkadot_sdk::sp_std::collections::btree_map::BTreeMap; + use polkadot_sdk::{ frame_support::dispatch::DispatchResult, polkadot_sdk_frame::prelude::OriginFor, }; @@ -28,3 +30,20 @@ pub fn transfer_stake( ) -> DispatchResult { todo!() } + +#[inline] +pub fn sum_staking_to(staker: &AccountIdOf) -> BalanceOf { + crate::StakingTo::::iter_prefix_values(staker).sum() +} + +#[inline] +pub fn get_staking_to_vector( + staker: &AccountIdOf, +) -> BTreeMap> { + crate::StakingTo::::iter_prefix(staker).collect() +} + +#[inline] +pub fn sum_staked_by(staked: &AccountIdOf) -> BalanceOf { + crate::StakedBy::::iter_prefix_values(staked).sum() +} diff --git a/runtime/src/configs.rs b/runtime/src/configs.rs index 59e3265..af804cb 100644 --- a/runtime/src/configs.rs +++ b/runtime/src/configs.rs @@ -333,5 +333,15 @@ parameter_types! { impl pallet_governance::Config for Runtime { type PalletId = GovernancePalletId; + type MinApplicationDataLength = ConstU32<2>; + + type MaxApplicationDataLength = ConstU32<256>; + + type ApplicationExpiration = ConstU64<2000>; + + type MaxPenaltyPercentage = ConstU8<20>; + + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ba8bd73..a06547f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -143,6 +143,9 @@ mod runtime { #[runtime::pallet_index(11)] pub type Torus0 = pallet_torus0::Pallet; + + #[runtime::pallet_index(12)] + pub type Governance = pallet_governance::Pallet; } parameter_types! {