--- a/js-packages/.vscode/settings.json +++ b/js-packages/.vscode/settings.json @@ -4,7 +4,8 @@ "RUN_XCM_TESTS": "1" }, "mochaExplorer.files": "tests/**/*.test.ts", - "mochaExplorer.require": "ts-node/register", + "mochaExplorer.esmLoader": true, + "mochaExplorer.nodeArgv": ["--loader", "ts-node/esm"], "eslint.format.enable": true, "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" --- a/js-packages/test-utils/index.ts +++ b/js-packages/test-utils/index.ts @@ -214,6 +214,23 @@ })); }; + static FinCouncil = class extends EventSection('financialCouncil') { + static Proposed = this.Method('Proposed', data => ({ + account: eventHumanData(data, 0), + proposalIndex: eventJsonData(data, 1), + proposalHash: eventHumanData(data, 2), + threshold: eventJsonData(data, 3), + })); + static Closed = this.Method('Closed', data => ({ + proposalHash: eventHumanData(data, 0), + yes: eventJsonData(data, 1), + no: eventJsonData(data, 2), + })); + static Executed = this.Method('Executed', data => ({ + proposalHash: eventHumanData(data, 0), + })); + }; + static TechnicalCommittee = class extends EventSection('technicalCommittee') { static Proposed = this.Method('Proposed', data => ({ account: eventHumanData(data, 0), @@ -475,6 +492,7 @@ scheduler: SchedulerGroup; collatorSelection: CollatorSelectionGroup; council: ICollectiveGroup; + finCouncil: ICollectiveGroup; technicalCommittee: ICollectiveGroup; fellowship: IFellowshipGroup; democracy: DemocracyGroup; @@ -498,6 +516,10 @@ collective: new CollectiveGroup(this, 'council'), membership: new CollectiveMembershipGroup(this, 'councilMembership'), }; + this.finCouncil = { + collective: new CollectiveGroup(this, 'financialCouncil'), + membership: new CollectiveMembershipGroup(this, 'financialCouncilMembership'), + }; this.technicalCommittee = { collective: new CollectiveGroup(this, 'technicalCommittee'), membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'), --- a/js-packages/tests/pallet-presence.test.ts +++ b/js-packages/tests/pallet-presence.test.ts @@ -80,6 +80,8 @@ 'scheduler', 'technicalcommittee', 'technicalcommitteemembership', + 'financialcouncil', + 'financialcouncilmembership', 'identity', ]; const testUtils = 'testutils'; --- a/js-packages/tests/sub/governance/council.test.ts +++ b/js-packages/tests/sub/governance/council.test.ts @@ -2,7 +2,7 @@ import type {IKeyringPair} from '@polkadot/types/types'; import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js'; import {Event} from '@unique/test-utils'; -import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js'; +import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin, initFinCouncil} from './util.js'; import type {ICounselors} from './util.js'; describeGov('Governance: Council tests', () => { @@ -192,6 +192,25 @@ expect(techCommMembers).to.not.contains(techComm.andy.address); }); + itSub('Council can remove FinCouncil member', async ({helper}) => { + const finCouncil = await initFinCouncil(donor, sudoer); + const removeMemberPrpoposal = helper.finCouncil.membership.removeMemberCall(finCouncil.andy.address); + await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal); + + const finCouncilMembers = await helper.finCouncil.membership.getMembers(); + expect(finCouncilMembers).to.not.contains(finCouncil.andy.address); + }); + + itSub('Council can add FinCouncil member', async ({helper}) => { + await initFinCouncil(donor, sudoer); + const newFinCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberPrpoposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address); + await proposalFromMoreThanHalfCouncil(addMemberPrpoposal); + + const finCouncilMembers = await helper.finCouncil.membership.getMembers(); + expect(finCouncilMembers).to.contains(newFinCouncilMember.address); + }); + itSub.skip('Council member can add Fellowship member', async ({helper}) => { const newFellowshipMember = helper.arrange.createEmptyAccount(); await expect(helper.council.collective.execute( @@ -328,6 +347,22 @@ )).to.be.rejectedWith('BadOrigin'); }); + itSub('[Negative] Council member can\'t add FinCouncil member', async ({helper}) => { + const newFinCouncilMember = helper.arrange.createEmptyAccount(); + await expect(helper.council.collective.execute( + counselors.alex, + helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address), + )).rejectedWith('BadOrigin'); + }); + + itSub('[Negative] Council member can\'t remove FinCouncil member', async ({helper}) => { + const finCouncil = await initFinCouncil(donor, sudoer); + await expect(helper.council.collective.execute( + counselors.alex, + helper.finCouncil.membership.removeMemberCall(finCouncil.ildar.address), + )).rejectedWith('BadOrigin'); + }); + itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => { const fellowship = await initFellowship(donor, sudoer); const memberWithRankOne = fellowship[1][0]; @@ -454,4 +489,21 @@ await expect(helper.council.collective.close(counselors.filip, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly'); }); + itSub('[Negative] Council can\'t veto Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(proposalFromAllCouncil(helper.democracy.vetoExternalCall(preimageHash))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] Council member can\'t veto Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(helper.council.collective.execute( + counselors.charu, + helper.democracy.vetoExternalCall(preimageHash), + )).rejectedWith('BadOrigin'); + }); }); --- /dev/null +++ b/js-packages/tests/sub/governance/financialCouncil.test.ts @@ -0,0 +1,440 @@ +import type {IKeyringPair} from '@polkadot/types/types'; +import {usingPlaygrounds, itSub, expect, describeGov} from '@unique/test-utils/util.js'; +import {Event} from '@unique/test-utils'; +import {democracyFastTrackVotingPeriod, IFinCounselors, clearTechComm, dummyProposalCall, initFinCouncil, clearFinCouncil, democracyLaunchPeriod, initFellowship, dummyProposal, fellowshipPropositionOrigin, defaultEnactmentMoment, initCouncil, clearCouncil, clearFellowship} from './util.js'; + + +describeGov('Governance: Financial Council tests', () => { + let donor: IKeyringPair; + let finCounselors: IFinCounselors; + let sudoer: IKeyringPair; + + const moreThanHalfCouncilThreshold = 2; + + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + sudoer = await privateKey('//Alice'); + donor = await privateKey({url: import.meta.url}); + }); + }); + + beforeEach(async () => { + finCounselors = await initFinCouncil(donor, sudoer); + }); + + afterEach(async () => { + await clearFinCouncil(sudoer); + await clearTechComm(sudoer); + }); + + async function proposalFromMoreThanHalfFinCouncil(proposal: any) { + return await usingPlaygrounds(async (helper) => { + expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3); + const proposeResult = await helper.finCouncil.collective.propose( + finCounselors.ildar, + proposal, + moreThanHalfCouncilThreshold, + ); + + const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult); + const proposalIndex = councilProposedEvent.proposalIndex; + const proposalHash = councilProposedEvent.proposalHash; + + await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true); + await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true); + + return await helper.finCouncil.collective.close(finCounselors.ildar, proposalHash, proposalIndex); + }); + } + + async function proposalFromAllFinCouncil(proposal: any) { + return await usingPlaygrounds(async (helper) => { + expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3); + const proposeResult = await helper.finCouncil.collective.propose( + finCounselors.ildar, + proposal, + moreThanHalfCouncilThreshold, + ); + + const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult); + const proposalIndex = councilProposedEvent.proposalIndex; + const proposalHash = councilProposedEvent.proposalHash; + + await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true); + await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true); + await helper.finCouncil.collective.vote(finCounselors.andy, proposalHash, proposalIndex, true); + + return await helper.finCouncil.collective.close(finCounselors.andy, proposalHash, proposalIndex); + }); + } + + itSub('FinCouncil member can register foreign asset', async ({helper}) => { + const location = { + parents: 1, + interior: {X3: [ + { + Parachain: 1000, + }, + { + PalletInstance: 50, + }, + { + GeneralIndex: 1984, + }, + ]}, + }; + const assetId = {Concrete: location}; + + const registerForeignAssetCall = helper.constructApiCall( + 'api.tx.foreignAssets.forceRegisterForeignAsset', + [{V3: assetId}, helper.util.str2vec('New Asset'), 'NEW', {Fungible: 10}], + ); + + await helper.finCouncil.collective.execute(finCounselors.andy, registerForeignAssetCall); + + const asset = await helper.foreignAssets.foreignCollectionId(location); + expect(asset).not.null; + }); + + itSub('[Negative] FinCouncil can\'t fast-track Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.wait.parachainBlockMultiplesOf(35n); + + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(proposalFromAllFinCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot fast-track Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil can\'t cancel Democracy proposals', async ({helper}) => { + const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + + await expect(proposalFromAllFinCouncil(helper.democracy.cancelProposalCall(proposalIndex))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot cancel Democracy proposals', async ({helper}) => { + const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); + const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.democracy.cancelProposalCall(proposalIndex), + )) + .to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil can\'t cancel ongoing Democracy referendums', async ({helper}) => { + await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper)); + const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started); + const referendumIndex = startedEvent.referendumIndex; + + await expect(proposalFromAllFinCouncil(helper.democracy.emergencyCancelCall(referendumIndex))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot cancel ongoing Democracy referendums', async ({helper}) => { + await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper)); + const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started); + const referendumIndex = startedEvent.referendumIndex; + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.democracy.emergencyCancelCall(referendumIndex), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil can\'t veto Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(proposalFromAllFinCouncil(helper.democracy.vetoExternalCall(preimageHash))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member can\'t veto Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.democracy.vetoExternalCall(preimageHash), + )).rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot blacklist Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(proposalFromAllFinCouncil(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot blacklist Democracy proposals', async ({helper}) => { + const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); + await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.democracy.blacklistCall(preimageHash), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil can\'t cancel Fellowship referendums', async ({helper}) => { + const fellowship = await initFellowship(donor, sudoer); + const fellowshipProposer = fellowship[5][0]; + const proposal = dummyProposal(helper); + + const submitResult = await helper.fellowship.referenda.submit( + fellowshipProposer, + fellowshipPropositionOrigin, + proposal, + defaultEnactmentMoment, + ); + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + await expect(proposalFromAllFinCouncil(helper.fellowship.referenda.cancelCall(referendumIndex))) + .rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot cancel Fellowship referendums', async ({helper}) => { + const fellowship = await initFellowship(donor, sudoer); + const fellowshipProposer = fellowship[5][0]; + const proposal = dummyProposal(helper); + + const submitResult = await helper.fellowship.referenda.submit( + fellowshipProposer, + fellowshipPropositionOrigin, + proposal, + defaultEnactmentMoment, + ); + + const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.fellowship.referenda.cancelCall(referendumIndex), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot add a Fellowship member', async ({helper}) => { + const newFellowshipMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.fellowship.collective.addMemberCall(newFellowshipMember.address); + + await expect(proposalFromAllFinCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot add a Fellowship member', async ({helper}) => { + const newFellowshipMember = helper.arrange.createEmptyAccount(); + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.fellowship.collective.addMemberCall(newFellowshipMember.address), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot submit regular democracy proposal', async ({helper}) => { + const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n); + + await expect(proposalFromAllFinCouncil(councilProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot externally propose SuperMajorityAgainst', async ({helper}) => { + const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)); + + await expect(proposalFromAllFinCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot submit regular democracy proposal', async ({helper}) => { + const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + memberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot externally propose SimpleMajority', async ({helper}) => { + const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)); + + await expect(proposalFromAllFinCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot externally propose SuperMajorityApprove', async ({helper}) => { + const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper)); + + await expect(proposalFromAllFinCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityAgainst', async ({helper}) => { + const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + memberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot externally propose SimpleMajority', async ({helper}) => { + const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + memberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityApprove', async ({helper}) => { + const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper)); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + memberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot add/remove a Council member', async ({helper}) => { + const newCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address); + const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address); + + await expect(proposalFromAllFinCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin'); + await expect(proposalFromAllFinCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot add/remove a Council member', async ({helper}) => { + const newCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address); + const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + addMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + removeMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot add/remove a FinCouncil member', async ({helper}) => { + const newCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address); + const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address); + + await expect(proposalFromAllFinCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin'); + await expect(proposalFromAllFinCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot add/remove a FinCouncil prime member', async ({helper}) => { + const setPrimeCall = helper.finCouncil.membership.setPrimeCall(finCounselors.andy.address); + const clearPrimeCall = helper.finCouncil.membership.clearPrimeCall(); + + await expect(proposalFromAllFinCouncil(setPrimeCall)).to.be.rejectedWith('BadOrigin'); + await expect(proposalFromAllFinCouncil(clearPrimeCall)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot add/remove a FinCouncil prime member', async ({helper}) => { + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.finCouncil.membership.setPrimeCall(finCounselors.andy.address), + )).to.be.rejectedWith('BadOrigin'); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.finCouncil.membership.clearPrimeCall(), + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot add/remove a FinCouncil member', async ({helper}) => { + const newCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address); + const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + addMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + removeMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot set/clear Council prime member', async ({helper}) => { + const counselors = await initCouncil(donor, sudoer); + const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address); + const proposalForClear = await helper.council.membership.clearPrimeCall(); + + await expect(proposalFromAllFinCouncil(proposalForSet)).to.be.rejectedWith('BadOrigin'); + await expect(proposalFromAllFinCouncil(proposalForClear)).to.be.rejectedWith('BadOrigin'); + await clearCouncil(sudoer); + }); + + itSub('[Negative] FinCouncil member cannot set/clear Council prime member', async ({helper}) => { + const counselors = await initCouncil(donor, sudoer); + const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address); + const proposalForClear = await helper.council.membership.clearPrimeCall(); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + proposalForSet, + )).to.be.rejectedWith('BadOrigin'); + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + proposalForClear, + )).to.be.rejectedWith('BadOrigin'); + await clearCouncil(sudoer); + }); + + itSub('[Negative] FinCouncil cannot add/remove a TechComm member', async ({helper}) => { + const newCommMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newCommMember.address); + const removeMemberProposal = helper.technicalCommittee.membership.removeMemberCall(newCommMember.address); + + await expect(proposalFromAllFinCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin'); + await expect(proposalFromAllFinCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil member cannot add/remove a TechComm member', async ({helper}) => { + const newCommMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newCommMember.address); + const removeMemberProposal = helper.technicalCommittee.membership.removeMemberCall(newCommMember.address); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + addMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + removeMemberProposal, + )).to.be.rejectedWith('BadOrigin'); + }); + + itSub('[Negative] FinCouncil cannot remove a Fellowship member', async ({helper}) => { + const fellowship = await initFellowship(donor, sudoer); + + await expect(proposalFromAllFinCouncil(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin'); + await clearFellowship(sudoer); + }); + + itSub('[Negative] FinCouncil member cannot remove a Fellowship member', async ({helper}) => { + const fellowship = await initFellowship(donor, sudoer); + + await expect(helper.finCouncil.collective.execute( + finCounselors.andy, + helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5), + )).to.be.rejectedWith('BadOrigin'); + await clearFellowship(sudoer); + }); + + +}); --- a/js-packages/tests/sub/governance/technicalCommittee.test.ts +++ b/js-packages/tests/sub/governance/technicalCommittee.test.ts @@ -130,6 +130,50 @@ await clearFellowship(sudoer); }); + itSub('[Negative] TechComm can\'t add FinCouncil member', async ({helper}) => { + const newFinCouncilMember = helper.arrange.createEmptyAccount(); + const addMemberProposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address); + await expect(proposalFromAllCommittee(addMemberProposal)).rejectedWith('BadOrigin'); + + const finCouncilMembers = await helper.finCouncil.membership.getMembers(); + expect(finCouncilMembers).to.not.contains(newFinCouncilMember.address); + }); + + + itSub('[Negative] TechComm member can\'t add FinCouncil member', async ({helper}) => { + const newFinCouncilMember = helper.arrange.createEmptyAccount(); + await expect(helper.technicalCommittee.collective.execute( + techcomms.greg, + helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address), + )).rejectedWith('BadOrigin'); + }); + + itSub('[Negative] TechComm member cannot register foreign asset', async ({helper}) => { + const location = { + parents: 1, + interior: {X3: [ + { + Parachain: 1000, + }, + { + PalletInstance: 50, + }, + { + GeneralIndex: 1985, + }, + ]}, + }; + const assetId = {Concrete: location}; + + const foreignAssetProposal = helper.constructApiCall( + 'api.tx.foreignAssets.forceRegisterForeignAsset', + [{V3: assetId}, helper.util.str2vec('New Asset2'), 'NEW', {Fungible: 10}], + ); + + await expect(helper.technicalCommittee.collective.execute(techcomms.andy, foreignAssetProposal)) + .to.be.rejectedWith('BadOrigin'); + }); + itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => { const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n); --- a/js-packages/tests/sub/governance/util.ts +++ b/js-packages/tests/sub/governance/util.ts @@ -29,12 +29,62 @@ filip: IKeyringPair; irina: IKeyringPair; } + +export interface IFinCounselors { + greg: IKeyringPair; + ildar: IKeyringPair; + andy: IKeyringPair; +} + export interface ITechComms { greg: IKeyringPair; andy: IKeyringPair; constantine: IKeyringPair; } +export function initFinCouncil(donor: IKeyringPair, superuser: IKeyringPair): Promise { + return usingPlaygrounds(async (helper) => { + const [greg, ildar, andy] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n], donor); + const sudo = helper.getSudo(); + { + const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON() as []; + if(members.length != 0) { + await clearFinCouncil(superuser); + } + } + const expectedMembers = [greg, ildar, andy]; + for(const member of expectedMembers) { + await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.addMember', [member.address]); + } + await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.setPrime', [greg.address]); + { + const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON(); + expect(members).to.containSubset(expectedMembers.map((x: IKeyringPair) => x.address)); + expect(members.length).to.be.equal(expectedMembers.length); + } + + return { + greg, + ildar, + andy, + }; + }); +} + +export async function clearFinCouncil(superuser: IKeyringPair) { + await usingPlaygrounds(async (helper) => { + let members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON(); + if(members.length) { + const sudo = helper.getSudo(); + for(const address of members) { + await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.removeMember', [address]); + } + members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON(); + } + expect(members).to.be.deep.equal([]); + }); +} + export async function initCouncil(donor: IKeyringPair, superuser: IKeyringPair) { let counselors: IKeyringPair[] = []; --- a/pallets/foreign-assets/src/lib.rs +++ b/pallets/foreign-assets/src/lib.rs @@ -107,7 +107,7 @@ type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Origin for force registering of a foreign asset. - type ForceRegisterOrigin: EnsureOrigin; + type ManagerOrigin: EnsureOrigin; /// The ID of the foreign assets pallet. type PalletId: Get; @@ -197,7 +197,7 @@ token_prefix: CollectionTokenPrefix, mode: ForeignCollectionMode, ) -> DispatchResult { - T::ForceRegisterOrigin::ensure_origin(origin.clone())?; + T::ManagerOrigin::ensure_origin(origin.clone())?; let asset_id: AssetId = versioned_asset_id .as_ref() --- /dev/null +++ b/runtime/common/config/governance/financial_council.rs @@ -0,0 +1,56 @@ +use super::*; + +parameter_types! { + pub FinancialCouncilMaxProposals: u32 = 100; + pub FinancialCouncilMaxMembers: u32 = 100; +} + +#[cfg(not(feature = "gov-test-timings"))] +use crate::governance_timings::financial_council as financial_council_timings; + +#[cfg(feature = "gov-test-timings")] +pub mod financial_council_timings { + use super::*; + + parameter_types! { + pub FinancialCouncilMotionDuration: BlockNumber = 35; + } +} + +pub type FinancialCollective = pallet_collective::Instance3; +impl pallet_collective::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = financial_council_timings::FinancialCouncilMotionDuration; + type MaxProposals = FinancialCouncilMaxProposals; + type MaxMembers = FinancialCouncilMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureRoot; + type MaxProposalWeight = MaxCollectivesProposalWeight; +} + +pub type FinancialCollectiveMembership = pallet_membership::Instance3; +impl pallet_membership::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = RootOrMoreThanHalfCouncil; + type RemoveOrigin = RootOrMoreThanHalfCouncil; + type SwapOrigin = RootOrMoreThanHalfCouncil; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = RootOrMoreThanHalfCouncil; + type MembershipInitialized = FinancialCouncil; + type MembershipChanged = FinancialCouncil; + type MaxMembers = FinancialCouncilMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +pub type FinancialCouncilMember = pallet_collective::EnsureMember; + +pub type RootOrFinancialCouncilMember = + EitherOfDiverse, FinancialCouncilMember>; + +pub type AllFinancialCouncil = + pallet_collective::EnsureProportionAtLeast; + +pub type RootOrAllFinancialCouncil = EitherOfDiverse, AllFinancialCouncil>; --- a/runtime/common/config/governance/mod.rs +++ b/runtime/common/config/governance/mod.rs @@ -36,8 +36,8 @@ }; use crate::{ - Balances, Council, OriginCaller, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, - Scheduler, TechnicalCommittee, Treasury, + Balances, Council, FinancialCouncil, OriginCaller, Preimage, Runtime, RuntimeCall, + RuntimeEvent, RuntimeOrigin, Scheduler, TechnicalCommittee, Treasury, }; pub mod council; @@ -49,6 +49,9 @@ pub mod technical_committee; pub use technical_committee::*; +pub mod financial_council; +pub use financial_council::*; + pub mod fellowship; pub use fellowship::*; --- a/runtime/common/config/pallets/foreign_asset.rs +++ b/runtime/common/config/pallets/foreign_asset.rs @@ -40,10 +40,10 @@ type RuntimeEvent = RuntimeEvent; #[cfg(feature = "governance")] - type ForceRegisterOrigin = governance::RootOrTechnicalCommitteeMember; + type ManagerOrigin = governance::RootOrFinancialCouncilMember; #[cfg(not(feature = "governance"))] - type ForceRegisterOrigin = EnsureRoot; + type ManagerOrigin = EnsureRoot; type PalletId = ForeignAssetPalletId; type SelfLocation = SelfLocation; --- a/runtime/common/construct_runtime.rs +++ b/runtime/common/construct_runtime.rs @@ -81,6 +81,12 @@ Scheduler: pallet_scheduler = 49, #[cfg(feature = "governance")] + FinancialCouncil: pallet_collective:: = 97, + + #[cfg(feature = "governance")] + FinancialCouncilMembership: pallet_membership:: = 98, + + #[cfg(feature = "governance")] Origins: pallet_gov_origins = 99, // XCM helpers. --- a/runtime/opal/src/governance_timings.rs +++ b/runtime/opal/src/governance_timings.rs @@ -52,3 +52,11 @@ pub TechnicalMotionDuration: BlockNumber = 15 * MINUTES; } } + +pub mod financial_council { + use super::*; + + parameter_types! { + pub FinancialCouncilMotionDuration: BlockNumber = 15 * MINUTES; + } +} --- a/runtime/quartz/src/governance_timings.rs +++ b/runtime/quartz/src/governance_timings.rs @@ -52,3 +52,11 @@ pub TechnicalMotionDuration: BlockNumber = 3 * DAYS; } } + +pub mod financial_council { + use super::*; + + parameter_types! { + pub FinancialCouncilMotionDuration: BlockNumber = 3 * DAYS; + } +} --- a/runtime/unique/src/governance_timings.rs +++ b/runtime/unique/src/governance_timings.rs @@ -52,3 +52,11 @@ pub TechnicalMotionDuration: BlockNumber = 3 * DAYS; } } + +pub mod financial_council { + use super::*; + + parameter_types! { + pub FinancialCouncilMotionDuration: BlockNumber = 3 * DAYS; + } +}