git.delta.rocks / unique-network / refs/commits / 7012a1c2007b

difftreelog

Merge pull request #1059 from UniqueNetwork/feature/financial-council

Yaroslav Bolyukin2024-06-13parents: #d32fbd9 #3360555.patch.diff
in: master

15 files changed

modifiedjs-packages/.vscode/settings.jsondiffbeforeafterboth
--- 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"
modifiedjs-packages/test-utils/index.tsdiffbeforeafterboth
--- 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<number>(data, 1),
+      proposalHash: eventHumanData(data, 2),
+      threshold: eventJsonData<number>(data, 3),
+    }));
+    static Closed = this.Method('Closed', data => ({
+      proposalHash: eventHumanData(data, 0),
+      yes: eventJsonData<number>(data, 1),
+      no: eventJsonData<number>(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'),
modifiedjs-packages/tests/pallet-presence.test.tsdiffbeforeafterboth
--- 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';
modifiedjs-packages/tests/sub/governance/council.test.tsdiffbeforeafterboth
--- 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');
+  });
 });
addedjs-packages/tests/sub/governance/financialCouncil.test.tsdiffbeforeafterboth
--- /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);
+  });
+
+
+});
modifiedjs-packages/tests/sub/governance/technicalCommittee.test.tsdiffbeforeafterboth
after · js-packages/tests/sub/governance/technicalCommittee.test.ts
1import type {IKeyringPair} from '@polkadot/types/types';2import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';3import {Event} from '@unique/test-utils';4import {initCouncil, democracyLaunchPeriod, democracyFastTrackVotingPeriod, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';5import type {ITechComms} from './util.js';67describeGov('Governance: Technical Committee tests', () => {8  let sudoer: IKeyringPair;9  let techcomms: ITechComms;10  let donor: IKeyringPair;11  let preImageHash: string;121314  const allTechCommitteeThreshold = 3;1516  before(async function() {17    await usingPlaygrounds(async (helper, privateKey) => {18      requirePalletsOrSkip(this, helper, [Pallets.TechnicalCommittee]);19      sudoer = await privateKey('//Alice');20      donor = await privateKey({url: import.meta.url});2122      techcomms = await initTechComm(donor, sudoer);2324      const proposalCall = await helper.constructApiCall('api.tx.balances.forceSetBalance', [donor.address, 20n * 10n ** 25n]);25      preImageHash = await helper.preimage.notePreimageFromCall(sudoer, proposalCall, true);26    });27  });2829  after(async () => {30    await usingPlaygrounds(async (helper) => {31      await clearTechComm(sudoer);3233      await helper.preimage.unnotePreimage(sudoer, preImageHash);34      await hardResetFellowshipReferenda(sudoer);35      await hardResetDemocracy(sudoer);36      await hardResetGovScheduler(sudoer);37    });38  });3940  function proposalFromAllCommittee(proposal: any) {41    return usingPlaygrounds(async (helper) => {42      expect((await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length).to.be.equal(allTechCommitteeThreshold);43      const proposeResult = await helper.technicalCommittee.collective.propose(44        techcomms.andy,45        proposal,46        allTechCommitteeThreshold,47      );4849      const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);50      const proposalIndex = commiteeProposedEvent.proposalIndex;51      const proposalHash = commiteeProposedEvent.proposalHash;525354      await helper.technicalCommittee.collective.vote(techcomms.andy, proposalHash, proposalIndex, true);55      await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);56      await helper.technicalCommittee.collective.vote(techcomms.greg, proposalHash, proposalIndex, true);5758      const closeResult = await helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex);59      Event.TechnicalCommittee.Closed.expect(closeResult);60      Event.TechnicalCommittee.Approved.expect(closeResult);61      const {result} = Event.TechnicalCommittee.Executed.expect(closeResult);62      expect(result).to.eq('Ok');6364      return closeResult;65    });66  }6768  itSub('TechComm can fast-track Democracy proposals', async ({helper}) => {69    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);70    await helper.wait.parachainBlockMultiplesOf(35n);7172    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);7374    const fastTrackProposal = await proposalFromAllCommittee(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0));75    Event.Democracy.Started.expect(fastTrackProposal);76  });7778  itSub('TechComm can cancel Democracy proposals', async ({helper}) => {79    const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);80    const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;8182    const cancelProposal = await proposalFromAllCommittee(helper.democracy.cancelProposalCall(proposalIndex));83    Event.Democracy.ProposalCanceled.expect(cancelProposal);84  });8586  itSub('TechComm can cancel ongoing Democracy referendums', async ({helper}) => {87    await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));88    const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);89    const referendumIndex = startedEvent.referendumIndex;9091    const emergencyCancelProposal = await proposalFromAllCommittee(helper.democracy.emergencyCancelCall(referendumIndex));92    Event.Democracy.Cancelled.expect(emergencyCancelProposal);93  });9495  itSub('TechComm member can veto Democracy proposals', async ({helper}) => {96    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);97    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);9899    const vetoExternalCall = await helper.technicalCommittee.collective.execute(100      techcomms.andy,101      helper.democracy.vetoExternalCall(preimageHash),102    );103    Event.Democracy.Vetoed.expect(vetoExternalCall);104  });105106  itSub('TechComm can cancel Fellowship referendums', async ({helper}) => {107    const fellowship = await initFellowship(donor, sudoer);108    const fellowshipProposer = fellowship[5][0];109    const proposal = dummyProposal(helper);110111    const submitResult = await helper.fellowship.referenda.submit(112      fellowshipProposer,113      fellowshipPropositionOrigin,114      proposal,115      defaultEnactmentMoment,116    );117    const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;118    const cancelProposal = await proposalFromAllCommittee(helper.fellowship.referenda.cancelCall(referendumIndex));119    Event.FellowshipReferenda.Cancelled.expect(cancelProposal);120  });121122  itSub.skip('TechComm member can add a Fellowship member', async ({helper}) => {123    const newFellowshipMember = helper.arrange.createEmptyAccount();124    await expect(helper.technicalCommittee.collective.execute(125      techcomms.andy,126      helper.fellowship.collective.addMemberCall(newFellowshipMember.address),127    )).to.be.fulfilled;128    const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();129    expect(fellowshipMembers).to.contains(newFellowshipMember.address);130    await clearFellowship(sudoer);131  });132133  itSub('[Negative] TechComm can\'t add FinCouncil member', async ({helper}) => {134    const newFinCouncilMember = helper.arrange.createEmptyAccount();135    const addMemberProposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address);136    await expect(proposalFromAllCommittee(addMemberProposal)).rejectedWith('BadOrigin');137138    const finCouncilMembers = await helper.finCouncil.membership.getMembers();139    expect(finCouncilMembers).to.not.contains(newFinCouncilMember.address);140  });141142143  itSub('[Negative] TechComm member can\'t add FinCouncil member', async ({helper}) => {144    const newFinCouncilMember = helper.arrange.createEmptyAccount();145    await expect(helper.technicalCommittee.collective.execute(146      techcomms.greg,147      helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address),148    )).rejectedWith('BadOrigin');149  });150151  itSub('[Negative] TechComm member cannot register foreign asset', async ({helper}) => {152    const location = {153      parents: 1,154      interior: {X3: [155        {156          Parachain: 1000,157        },158        {159          PalletInstance: 50,160        },161        {162          GeneralIndex: 1985,163        },164      ]},165    };166    const assetId = {Concrete: location};167168    const foreignAssetProposal = helper.constructApiCall(169      'api.tx.foreignAssets.forceRegisterForeignAsset',170      [{V3: assetId}, helper.util.str2vec('New Asset2'), 'NEW', {Fungible: 10}],171    );172173    await expect(helper.technicalCommittee.collective.execute(techcomms.andy, foreignAssetProposal))174      .to.be.rejectedWith('BadOrigin');175  });176177  itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => {178    const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);179180    await expect(proposalFromAllCommittee(councilProposal)).to.be.rejectedWith('BadOrigin');181  });182183  itSub('[Negative] TechComm cannot externally propose SuperMajorityAgainst', async ({helper}) => {184    const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));185186    await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');187  });188189  itSub('[Negative] TechComm cannot externally propose SimpleMajority', async ({helper}) => {190    const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));191192    await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');193  });194195  itSub('[Negative] TechComm cannot externally propose SuperMajorityApprove', async ({helper}) => {196    const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));197198    await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');199  });200201  itSub('[Negative] TechComm member cannot submit regular democracy proposal', async ({helper}) => {202    const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);203204    await expect(helper.technicalCommittee.collective.execute(205      techcomms.andy,206      memberProposal,207    )).to.be.rejectedWith('BadOrigin');208  });209210  itSub('[Negative] TechComm member cannot externally propose SuperMajorityAgainst', async ({helper}) => {211    const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));212213    await expect(helper.technicalCommittee.collective.execute(214      techcomms.andy,215      memberProposal,216    )).to.be.rejectedWith('BadOrigin');217  });218219  itSub('[Negative] TechComm member cannot externally propose SimpleMajority', async ({helper}) => {220    const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));221222    await expect(helper.technicalCommittee.collective.execute(223      techcomms.andy,224      memberProposal,225    )).to.be.rejectedWith('BadOrigin');226  });227228  itSub('[Negative] TechComm member cannot externally propose SuperMajorityApprove', async ({helper}) => {229    const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));230231    await expect(helper.technicalCommittee.collective.execute(232      techcomms.andy,233      memberProposal,234    )).to.be.rejectedWith('BadOrigin');235  });236237238  itSub.skip('[Negative] TechComm cannot promote/demote Fellowship member', async () => {239240  });241242  itSub.skip('[Negative] TechComm member cannot promote/demote Fellowship member', async () => {243244  });245246  itSub('[Negative] TechComm cannot add/remove a Council member', async ({helper}) => {247    const newCouncilMember = helper.arrange.createEmptyAccount();248    const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);249    const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);250251    await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');252    await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');253  });254255  itSub('[Negative] TechComm member cannot add/remove a Council member', async ({helper}) => {256    const newCouncilMember = helper.arrange.createEmptyAccount();257    const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);258    const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);259260    await expect(helper.technicalCommittee.collective.execute(261      techcomms.andy,262      addMemberProposal,263    )).to.be.rejectedWith('BadOrigin');264    await expect(helper.technicalCommittee.collective.execute(265      techcomms.andy,266      removeMemberProposal,267    )).to.be.rejectedWith('BadOrigin');268  });269270  itSub('[Negative] TechComm cannot set/clear Council prime member', async ({helper}) => {271    const counselors = await initCouncil(donor, sudoer);272    const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);273    const proposalForClear = await helper.council.membership.clearPrimeCall();274275    await expect(proposalFromAllCommittee(proposalForSet)).to.be.rejectedWith('BadOrigin');276    await expect(proposalFromAllCommittee(proposalForClear)).to.be.rejectedWith('BadOrigin');277    await clearCouncil(sudoer);278  });279280  itSub('[Negative] TechComm member cannot set/clear Council prime member', async ({helper}) => {281    const counselors = await initCouncil(donor, sudoer);282    const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);283    const proposalForClear = await helper.council.membership.clearPrimeCall();284285    await expect(helper.technicalCommittee.collective.execute(286      techcomms.andy,287      proposalForSet,288    )).to.be.rejectedWith('BadOrigin');289    await expect(helper.technicalCommittee.collective.execute(290      techcomms.andy,291      proposalForClear,292    )).to.be.rejectedWith('BadOrigin');293    await clearCouncil(sudoer);294  });295296  itSub('[Negative] TechComm cannot add/remove a TechComm member', async ({helper}) => {297    const newCommMember = helper.arrange.createEmptyAccount();298    const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);299    const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);300301    await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');302    await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');303  });304305  itSub('[Negative] TechComm member cannot add/remove a TechComm member', async ({helper}) => {306    const newCommMember = helper.arrange.createEmptyAccount();307    const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);308    const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);309310    await expect(helper.technicalCommittee.collective.execute(311      techcomms.andy,312      addMemberProposal,313    )).to.be.rejectedWith('BadOrigin');314    await expect(helper.technicalCommittee.collective.execute(315      techcomms.andy,316      removeMemberProposal,317    )).to.be.rejectedWith('BadOrigin');318  });319320  itSub('[Negative] TechComm cannot remove a Fellowship member', async ({helper}) => {321    const fellowship = await initFellowship(donor, sudoer);322323    await expect(proposalFromAllCommittee(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin');324    await clearFellowship(sudoer);325  });326327  itSub('[Negative] TechComm member cannot remove a Fellowship member', async ({helper}) => {328    const fellowship = await initFellowship(donor, sudoer);329330    await expect(helper.technicalCommittee.collective.execute(331      techcomms.andy,332      helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5),333    )).to.be.rejectedWith('BadOrigin');334    await clearFellowship(sudoer);335  });336337  itSub('[Negative] TechComm member cannot fast-track Democracy proposals', async ({helper}) => {338    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);339    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);340341    await expect(helper.technicalCommittee.collective.execute(342      techcomms.andy,343      helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),344    )).to.be.rejectedWith('BadOrigin');345  });346347  itSub('[Negative] TechComm member cannot cancel Democracy proposals', async ({helper}) => {348    const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);349    const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;350351    await expect(helper.technicalCommittee.collective.execute(352      techcomms.andy,353      helper.democracy.cancelProposalCall(proposalIndex),354    ))355      .to.be.rejectedWith('BadOrigin');356  });357358  itSub('[Negative] TechComm member cannot cancel ongoing Democracy referendums', async ({helper}) => {359    await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));360    const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);361    const referendumIndex = startedEvent.referendumIndex;362363    await expect(helper.technicalCommittee.collective.execute(364      techcomms.andy,365      helper.democracy.emergencyCancelCall(referendumIndex),366    )).to.be.rejectedWith('BadOrigin');367  });368369  itSub('[Negative] TechComm cannot blacklist Democracy proposals', async ({helper}) => {370    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);371    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);372373    await expect(proposalFromAllCommittee(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin');374  });375376  itSub('[Negative] TechComm member cannot blacklist Democracy proposals', async ({helper}) => {377    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);378    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);379380    await expect(helper.technicalCommittee.collective.execute(381      techcomms.andy,382      helper.democracy.blacklistCall(preimageHash),383    )).to.be.rejectedWith('BadOrigin');384  });385386  itSub.skip('[Negative] TechComm member cannot veto external Democracy proposals until the cool-off period pass', async () => {387388  });389390  itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => {391    const fellowship = await initFellowship(donor, sudoer);392    const fellowshipProposer = fellowship[5][0];393    const proposal = dummyProposal(helper);394395    const submitResult = await helper.fellowship.referenda.submit(396      fellowshipProposer,397      fellowshipPropositionOrigin,398      proposal,399      defaultEnactmentMoment,400    );401402    const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;403404    await expect(helper.technicalCommittee.collective.execute(405      techcomms.andy,406      helper.fellowship.referenda.cancelCall(referendumIndex),407    )).to.be.rejectedWith('BadOrigin');408  });409410  itSub('[Negative] TechComm referendum cannot be closed until the voting threshold is met', async ({helper}) => {411    const committeeSize = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length as any as number;412    expect(committeeSize).is.greaterThan(1);413    const proposeResult = await helper.technicalCommittee.collective.propose(414      techcomms.andy,415      dummyProposalCall(helper),416      committeeSize,417    );418419    const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);420    const proposalIndex = committeeProposedEvent.proposalIndex;421    const proposalHash = committeeProposedEvent.proposalHash;422423    await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);424425    await expect(helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');426  });427});
modifiedjs-packages/tests/sub/governance/util.tsdiffbeforeafterboth
--- 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<IFinCounselors> {
+  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[] = [];
 
modifiedpallets/foreign-assets/src/lib.rsdiffbeforeafterboth
--- a/pallets/foreign-assets/src/lib.rs
+++ b/pallets/foreign-assets/src/lib.rs
@@ -107,7 +107,7 @@
 		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
 
 		/// Origin for force registering of a foreign asset.
-		type ForceRegisterOrigin: EnsureOrigin<Self::RuntimeOrigin>;
+		type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
 
 		/// The ID of the foreign assets pallet.
 		type PalletId: Get<PalletId>;
@@ -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()
addedruntime/common/config/governance/financial_council.rsdiffbeforeafterboth
--- /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<FinancialCollective> 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<Runtime>;
+	type SetMembersOrigin = EnsureRoot<AccountId>;
+	type MaxProposalWeight = MaxCollectivesProposalWeight;
+}
+
+pub type FinancialCollectiveMembership = pallet_membership::Instance3;
+impl pallet_membership::Config<FinancialCollectiveMembership> for Runtime {
+	type RuntimeEvent = RuntimeEvent;
+	type AddOrigin = RootOrMoreThanHalfCouncil;
+	type RemoveOrigin = RootOrMoreThanHalfCouncil;
+	type SwapOrigin = RootOrMoreThanHalfCouncil;
+	type ResetOrigin = EnsureRoot<AccountId>;
+	type PrimeOrigin = RootOrMoreThanHalfCouncil;
+	type MembershipInitialized = FinancialCouncil;
+	type MembershipChanged = FinancialCouncil;
+	type MaxMembers = FinancialCouncilMaxMembers;
+	type WeightInfo = pallet_membership::weights::SubstrateWeight<Runtime>;
+}
+
+pub type FinancialCouncilMember = pallet_collective::EnsureMember<AccountId, FinancialCollective>;
+
+pub type RootOrFinancialCouncilMember =
+	EitherOfDiverse<EnsureRoot<AccountId>, FinancialCouncilMember>;
+
+pub type AllFinancialCouncil =
+	pallet_collective::EnsureProportionAtLeast<AccountId, FinancialCollective, 1, 1>;
+
+pub type RootOrAllFinancialCouncil = EitherOfDiverse<EnsureRoot<AccountId>, AllFinancialCouncil>;
modifiedruntime/common/config/governance/mod.rsdiffbeforeafterboth
--- 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::*;
 
modifiedruntime/common/config/pallets/foreign_asset.rsdiffbeforeafterboth
--- 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<Self::AccountId>;
+	type ManagerOrigin = EnsureRoot<Self::AccountId>;
 
 	type PalletId = ForeignAssetPalletId;
 	type SelfLocation = SelfLocation;
modifiedruntime/common/construct_runtime.rsdiffbeforeafterboth
--- 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::<Instance3> = 97,
+
+				#[cfg(feature = "governance")]
+				FinancialCouncilMembership: pallet_membership::<Instance3> = 98,
+
+				#[cfg(feature = "governance")]
 				Origins: pallet_gov_origins = 99,
 
 				// XCM helpers.
modifiedruntime/opal/src/governance_timings.rsdiffbeforeafterboth
--- 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;
+	}
+}
modifiedruntime/quartz/src/governance_timings.rsdiffbeforeafterboth
--- 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;
+	}
+}
modifiedruntime/unique/src/governance_timings.rsdiffbeforeafterboth
--- 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;
+	}
+}