--- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ pallet-state-trie-migration = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } pallet-sudo = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } pallet-timestamp = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } +pallet-utility = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } pallet-transaction-payment = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } pallet-transaction-payment-rpc-runtime-api = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43" } --- a/runtime/common/config/governance/technical_committee.rs +++ b/runtime/common/config/governance/technical_committee.rs @@ -38,7 +38,7 @@ type RemoveOrigin = RootOrMoreThanHalfCouncil; type SwapOrigin = RootOrMoreThanHalfCouncil; type ResetOrigin = EnsureRoot; - type PrimeOrigin = EnsureRoot; + type PrimeOrigin = RootOrMoreThanHalfCouncil; type MembershipInitialized = TechnicalCommittee; type MembershipChanged = TechnicalCommittee; type MaxMembers = TechnicalMaxMembers; --- a/runtime/common/config/substrate.rs +++ b/runtime/common/config/substrate.rs @@ -35,8 +35,8 @@ }; use pallet_transaction_payment::{Multiplier, ConstFeeMultiplier}; use crate::{ - runtime_common::DealWithFees, Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, PalletInfo, - System, Balances, SS58Prefix, Version, + runtime_common::DealWithFees, Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, OriginCaller, + PalletInfo, System, Balances, SS58Prefix, Version, }; use up_common::{types::*, constants::*}; use sp_std::vec; @@ -248,3 +248,10 @@ type DisabledValidators = (); type MaxAuthorities = MaxAuthorities; } + +impl pallet_utility::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = pallet_utility::weights::SubstrateWeight; +} --- a/runtime/common/construct_runtime.rs +++ b/runtime/common/construct_runtime.rs @@ -136,6 +136,8 @@ BalancesAdapter: pallet_balances_adapter = 155, + Utility: pallet_utility = 156, + #[cfg(feature = "pallet-test-utils")] TestUtils: pallet_test_utils = 255, } --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -58,6 +58,7 @@ 'pallet-refungible/runtime-benchmarks', 'pallet-structure/runtime-benchmarks', 'pallet-timestamp/runtime-benchmarks', + 'pallet-utility/runtime-benchmarks', 'pallet-unique-scheduler-v2/runtime-benchmarks', 'pallet-unique/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', @@ -120,6 +121,7 @@ 'pallet-structure/std', 'pallet-sudo/std', 'pallet-timestamp/std', + 'pallet-utility/std', 'pallet-transaction-payment-rpc-runtime-api/std', 'pallet-transaction-payment/std', 'pallet-treasury/std', @@ -213,6 +215,7 @@ 'pallet-sudo/try-runtime', 'pallet-test-utils?/try-runtime', 'pallet-timestamp/try-runtime', + 'pallet-utility/try-runtime', 'pallet-transaction-payment/try-runtime', 'pallet-treasury/try-runtime', 'pallet-unique-scheduler-v2/try-runtime', @@ -263,6 +266,7 @@ pallet-state-trie-migration = { workspace = true } pallet-sudo = { workspace = true } pallet-timestamp = { workspace = true } +pallet-utility = { workspace = true } pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-treasury = { workspace = true } --- a/runtime/quartz/Cargo.toml +++ b/runtime/quartz/Cargo.toml @@ -55,6 +55,7 @@ 'pallet-scheduler/runtime-benchmarks', 'pallet-structure/runtime-benchmarks', 'pallet-timestamp/runtime-benchmarks', + 'pallet-utility/runtime-benchmarks', 'pallet-unique/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', @@ -120,6 +121,7 @@ 'pallet-structure/std', 'pallet-sudo/std', 'pallet-timestamp/std', + 'pallet-utility/std', 'pallet-transaction-payment-rpc-runtime-api/std', 'pallet-transaction-payment/std', 'pallet-treasury/std', @@ -203,6 +205,7 @@ 'pallet-structure/try-runtime', 'pallet-sudo/try-runtime', 'pallet-timestamp/try-runtime', + 'pallet-utility/try-runtime', 'pallet-transaction-payment/try-runtime', 'pallet-treasury/try-runtime', 'pallet-unique/try-runtime', @@ -252,6 +255,7 @@ pallet-state-trie-migration = { workspace = true } pallet-sudo = { workspace = true } pallet-timestamp = { workspace = true } +pallet-utility = { workspace = true } pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-treasury = { workspace = true } --- a/runtime/unique/Cargo.toml +++ b/runtime/unique/Cargo.toml @@ -52,6 +52,7 @@ 'pallet-scheduler/runtime-benchmarks', 'pallet-structure/runtime-benchmarks', 'pallet-timestamp/runtime-benchmarks', + 'pallet-utility/runtime-benchmarks', 'pallet-unique/runtime-benchmarks', 'pallet-xcm/runtime-benchmarks', 'sp-runtime/runtime-benchmarks', @@ -118,6 +119,7 @@ 'pallet-structure/std', 'pallet-sudo/std', 'pallet-timestamp/std', + 'pallet-utility/std', 'pallet-transaction-payment-rpc-runtime-api/std', 'pallet-transaction-payment/std', 'pallet-treasury/std', @@ -205,6 +207,7 @@ 'pallet-structure/try-runtime', 'pallet-sudo/try-runtime', 'pallet-timestamp/try-runtime', + 'pallet-utility/try-runtime', 'pallet-transaction-payment/try-runtime', 'pallet-treasury/try-runtime', 'pallet-unique/try-runtime', @@ -255,6 +258,7 @@ pallet-state-trie-migration = { workspace = true } pallet-sudo = { workspace = true } pallet-timestamp = { workspace = true } +pallet-utility = { workspace = true } pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-treasury = { workspace = true } --- /dev/null +++ b/tests/src/governance/init.test.ts @@ -0,0 +1,192 @@ +import {IKeyringPair} from '@polkadot/types/types'; +import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util'; +import {Event} from '../util/playgrounds/unique.dev'; +import {ICounselors, democracyLaunchPeriod, democracyVotingPeriod, ITechComms, democracyEnactmentPeriod, clearCouncil, clearTechComm, clearFellowship} from './util'; + +describeGov('Governance: Initialization', () => { + let donor: IKeyringPair; + let sudoer: IKeyringPair; + let counselors: ICounselors; + let techcomms: ITechComms; + let coreDevs: any; + + const expectedAlexFellowRank = 7; + const expectedFellowRank = 6; + + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Democracy, Pallets.Council, Pallets.TechnicalCommittee]); + + const councilMembers = await helper.council.membership.getMembers(); + const techcommMembers = await helper.technicalCommittee.membership.getMembers(); + expect(councilMembers.length == 0, 'The Council must be empty before the Gov Init'); + expect(techcommMembers.length == 0, 'The Technical Commettee must be empty before the Gov Init'); + + donor = await privateKey({url: import.meta.url}); + sudoer = await privateKey('//Alice'); + + const counselorsNum = 5; + const techCommsNum = 3; + const coreDevsNum = 2; + const [ + alex, + ildar, + charu, + filip, + irina, + + greg, + andy, + constantine, + + yaroslav, + daniel, + ] = await helper.arrange.createAccounts(new Array(counselorsNum + techCommsNum + coreDevsNum).fill(10_000n), donor); + + counselors = { + alex, + ildar, + charu, + filip, + irina, + }; + + techcomms = { + greg, + andy, + constantine, + }; + + coreDevs = { + yaroslav: yaroslav, + daniel: daniel, + }; + }); + }); + + itSub('Initialize Governance', async ({helper}) => { + const promoteFellow = (fellow: string, promotionsNum: number) => new Array(promotionsNum).fill(helper.fellowship.collective.promoteCall(fellow)); + + const expectFellowRank = async (fellow: string, expectedRank: number) => { + expect(await helper.fellowship.collective.getMemberRank(fellow)).to.be.equal(expectedRank); + }; + + console.log('\t- Setup the Prime of the Council via sudo'); + await helper.getSudo().utility.batchAll(sudoer, [ + helper.council.membership.addMemberCall(counselors.alex.address), + helper.council.membership.setPrimeCall(counselors.alex.address), + + helper.fellowship.collective.addMemberCall(counselors.alex.address), + ...promoteFellow(counselors.alex.address, expectedAlexFellowRank), + ]); + + let councilMembers = await helper.council.membership.getMembers(); + const councilPrime = await helper.council.collective.getPrimeMember(); + const alexFellowRank = await helper.fellowship.collective.getMemberRank(counselors.alex.address); + expect(councilMembers).to.be.deep.equal([counselors.alex.address]); + expect(councilPrime).to.be.equal(counselors.alex.address); + expect(alexFellowRank).to.be.equal(expectedAlexFellowRank); + + console.log('\t- The Council Prime initializes the Technical Commettee'); + const councilProposalThreshold = 1; + + await helper.council.collective.propose( + counselors.alex, + helper.utility.batchAllCall([ + helper.technicalCommittee.membership.addMemberCall(techcomms.greg.address), + helper.technicalCommittee.membership.addMemberCall(techcomms.andy.address), + helper.technicalCommittee.membership.addMemberCall(techcomms.constantine.address), + + helper.technicalCommittee.membership.setPrimeCall(techcomms.greg.address), + ]), + councilProposalThreshold, + ); + + const techCommMembers = await helper.technicalCommittee.membership.getMembers(); + const techCommPrime = await helper.technicalCommittee.membership.getPrimeMember(); + const expectedTechComms = [techcomms.greg.address, techcomms.andy.address, techcomms.constantine.address]; + expect(techCommMembers.length).to.be.equal(expectedTechComms.length); + expect(techCommMembers).to.containSubset(expectedTechComms); + expect(techCommPrime).to.be.equal(techcomms.greg.address); + + console.log('\t- The Council Prime initiates a referendum to add counselors'); + const returnPreimageHash = true; + const preimageHash = await helper.preimage.notePreimageFromCall(counselors.alex, helper.utility.batchAllCall([ + helper.council.membership.addMemberCall(counselors.ildar.address), + helper.council.membership.addMemberCall(counselors.charu.address), + helper.council.membership.addMemberCall(counselors.filip.address), + helper.council.membership.addMemberCall(counselors.irina.address), + + helper.fellowship.collective.addMemberCall(counselors.charu.address), + helper.fellowship.collective.addMemberCall(counselors.ildar.address), + helper.fellowship.collective.addMemberCall(counselors.irina.address), + helper.fellowship.collective.addMemberCall(counselors.filip.address), + helper.fellowship.collective.addMemberCall(techcomms.greg.address), + helper.fellowship.collective.addMemberCall(techcomms.andy.address), + helper.fellowship.collective.addMemberCall(techcomms.constantine.address), + helper.fellowship.collective.addMemberCall(coreDevs.yaroslav.address), + helper.fellowship.collective.addMemberCall(coreDevs.daniel.address), + + ...promoteFellow(counselors.charu.address, expectedFellowRank), + ...promoteFellow(counselors.ildar.address, expectedFellowRank), + ...promoteFellow(counselors.irina.address, expectedFellowRank), + ...promoteFellow(counselors.filip.address, expectedFellowRank), + ...promoteFellow(techcomms.greg.address, expectedFellowRank), + ...promoteFellow(techcomms.andy.address, expectedFellowRank), + ...promoteFellow(techcomms.constantine.address, expectedFellowRank), + ...promoteFellow(coreDevs.yaroslav.address, expectedFellowRank), + ...promoteFellow(coreDevs.daniel.address, expectedFellowRank), + ]), returnPreimageHash); + + await helper.council.collective.propose( + counselors.alex, + helper.democracy.externalProposeDefaultWithPreimageCall(preimageHash), + councilProposalThreshold, + ); + + console.log('\t- The referendum is being decided'); + const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started); + + await helper.democracy.vote(counselors.filip, startedEvent.referendumIndex, { + Standard: { + vote: { + aye: true, + conviction: 1, + }, + balance: 10_000n, + }, + }); + + const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed); + expect(passedReferendumEvent.referendumIndex).to.be.equal(startedEvent.referendumIndex); + + await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched); + + councilMembers = await helper.council.membership.getMembers(); + const expectedCounselors = [ + counselors.alex.address, + counselors.ildar.address, + counselors.charu.address, + counselors.filip.address, + counselors.irina.address, + ]; + expect(councilMembers.length).to.be.equal(expectedCounselors.length); + expect(councilMembers).to.containSubset(expectedCounselors); + + await expectFellowRank(counselors.ildar.address, expectedFellowRank); + await expectFellowRank(counselors.charu.address, expectedFellowRank); + await expectFellowRank(counselors.filip.address, expectedFellowRank); + await expectFellowRank(counselors.irina.address, expectedFellowRank); + await expectFellowRank(techcomms.greg.address, expectedFellowRank); + await expectFellowRank(techcomms.andy.address, expectedFellowRank); + await expectFellowRank(techcomms.constantine.address, expectedFellowRank); + await expectFellowRank(coreDevs.yaroslav.address, expectedFellowRank); + await expectFellowRank(coreDevs.daniel.address, expectedFellowRank); + }); + + after(async function() { + await clearFellowship(sudoer); + await clearTechComm(sudoer); + await clearCouncil(sudoer); + }); +}); --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -27,6 +27,7 @@ 'statetriemigration', 'structure', 'system', + 'utility', 'vesting', 'parachainsystem', 'parachaininfo', --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -3255,8 +3255,8 @@ return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.promoteMember`, [member]); } - promoteCall(newMember: string) { - return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [newMember]); + promoteCall(member: string) { + return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [member]); } demote(signer: TSigner, member: string) { @@ -3275,6 +3275,10 @@ return (await this.helper.getApi().query.fellowshipCollective.members.keys()) .map((key) => key.args[0].toString()); } + + async getMemberRank(member: string) { + return (await this.helper.callRpc('api.query.fellowshipCollective.members', [member])).toJSON().rank; + } } class ReferendaGroup extends HelperGroup { @@ -3381,6 +3385,10 @@ return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]); } + externalProposeDefaultWithPreimageCall(preimage: string) { + return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]); + } + // ... and blacklist external proposal hash. vetoExternal(signer: TSigner, proposalHash: string) { return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vetoExternal', [proposalHash]); @@ -3733,6 +3741,20 @@ } } +class UtilityGroup extends HelperGroup { + async batch(signer: TSigner, txs: any[]) { + return await this.helper.executeExtrinsic(signer, 'api.tx.utility.batch', [txs]); + } + + async batchAll(signer: TSigner, txs: any[]) { + return await this.helper.executeExtrinsic(signer, 'api.tx.utility.batchAll', [txs]); + } + + batchAllCall(txs: any[]) { + return this.helper.constructApiCall('api.tx.utility.batchAll', [txs]); + } +} + class AcalaAssetRegistryGroup extends HelperGroup { async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) { await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true); @@ -3835,6 +3857,7 @@ xcm: XcmGroup; xTokens: XTokensGroup; tokens: TokensGroup; + utility: UtilityGroup; constructor(logger?: ILogger, options: { [key: string]: any } = {}) { super(logger, options.helperBase ?? UniqueHelper); @@ -3865,6 +3888,7 @@ this.xcm = new XcmGroup(this, 'polkadotXcm'); this.xTokens = new XTokensGroup(this); this.tokens = new TokensGroup(this); + this.utility = new UtilityGroup(this); } getSudo() {