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
--- 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);
 
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
before · pallets/foreign-assets/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Foreign Assets18//!19//! ## Overview20//!21//! The Foreign Assets is a proxy that maps XCM operations to the Unique Network's pallets logic.2223#![cfg_attr(not(feature = "std"), no_std)]24#![allow(clippy::unused_unit)]2526use core::ops::Deref;2728use derivative::Derivative;29use frame_support::{30	dispatch::DispatchResult, pallet_prelude::*, storage_alias, traits::EnsureOrigin, PalletId,31};32use frame_system::pallet_prelude::*;33use pallet_common::{34	dispatch::CollectionDispatch, erc::CrossAccountId, XcmExtensions, NATIVE_FUNGIBLE_COLLECTION_ID,35};36use sp_runtime::traits::AccountIdConversion;37use sp_std::{boxed::Box, vec, vec::Vec};38use staging_xcm::{v4::prelude::*, VersionedAssetId};39use staging_xcm_executor::{40	traits::{ConvertLocation, Error as XcmExecutorError, TransactAsset, WeightTrader},41	AssetsInHolding,42};43use up_data_structs::{44	budget::ZeroBudget, CollectionFlags, CollectionId, CollectionMode, CollectionName,45	CollectionTokenPrefix, CreateCollectionData, CreateFungibleData, CreateItemData, TokenId,46};4748pub mod weights;4950#[cfg(feature = "runtime-benchmarks")]51mod benchmarking;5253pub use module::*;54pub use weights::WeightInfo;5556/// Status of storage migration from an old XCM version to a new one.57#[derive(Clone, Copy, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]58pub enum MigrationStatus {59	V3ToV4(MigrationStatusV3ToV4),60}6162/// Status of storage migration from XCMv3 to XCMv4.63#[derive(Clone, Copy, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]64pub enum MigrationStatusV3ToV4 {65	/// The migration is completed.66	Done,6768	/// An asset is skipped during the migration69	/// due to its inconsistent state.70	SkippedInconsistentAssetData(staging_xcm::v3::AssetId),7172	/// An asset instance is skipped during the migration73	/// due to its inconsistent state.74	SkippedInconsistentAssetInstanceData {75		asset_id: staging_xcm::v3::AssetId,76		asset_instance: staging_xcm::v3::AssetInstance,77	},7879	/// An asset is skipped during the migration80	/// because it couldn't be converted to the new XCM version.81	SkippedNotConvertibleAssetId(staging_xcm::v3::AssetId),8283	/// An asset instance is skipped during the migration84	/// because it couldn't be converted to the new XCM version.85	SkippedNotConvertibleAssetInstance {86		asset_id: staging_xcm::v3::AssetId,87		asset_instance: staging_xcm::v3::AssetInstance,88	},89}9091#[frame_support::pallet]92pub mod module {93	use frame_support::traits::BuildGenesisConfig;94	use pallet_common::CollectionIssuer;95	use up_data_structs::CollectionDescription;9697	use super::*;9899	#[pallet::config]100	pub trait Config:101		frame_system::Config102		+ pallet_common::Config103		+ pallet_fungible::Config104		+ pallet_balances::Config105	{106		/// The overarching event type.107		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;108109		/// Origin for force registering of a foreign asset.110		type ForceRegisterOrigin: EnsureOrigin<Self::RuntimeOrigin>;111112		/// The ID of the foreign assets pallet.113		type PalletId: Get<PalletId>;114115		/// Self-location of this parachain.116		type SelfLocation: Get<Location>;117118		/// The converter from a Location to a CrossAccountId.119		type LocationToAccountId: ConvertLocation<Self::CrossAccountId>;120121		/// Weight information for the extrinsics in this module.122		type WeightInfo: WeightInfo;123	}124125	#[pallet::error]126	pub enum Error<T> {127		/// The foreign asset is already registered.128		ForeignAssetAlreadyRegistered,129130		/// The given asset ID could not be converted into the current XCM version.131		BadForeignAssetId,132	}133134	#[pallet::event]135	#[pallet::generate_deposit(pub(crate) fn deposit_event)]136	pub enum Event<T: Config> {137		/// The foreign asset registered.138		ForeignAssetRegistered {139			collection_id: CollectionId,140			asset_id: Box<VersionedAssetId>,141		},142143		/// The migration status.144		MigrationStatus(MigrationStatus),145	}146147	/// The corresponding collections of foreign assets.148	#[pallet::storage]149	#[pallet::getter(fn foreign_asset_to_collection)]150	pub type ForeignAssetToCollection<T: Config> =151		StorageMap<_, Blake2_128Concat, staging_xcm::v4::AssetId, CollectionId, OptionQuery>;152153	/// The corresponding foreign assets of collections.154	#[pallet::storage]155	#[pallet::getter(fn collection_to_foreign_asset)]156	pub type CollectionToForeignAsset<T: Config> =157		StorageMap<_, Blake2_128Concat, CollectionId, staging_xcm::v4::AssetId, OptionQuery>;158159	/// The correponding NFT token id of reserve NFTs160	#[pallet::storage]161	#[pallet::getter(fn foreign_reserve_asset_instance_to_token_id)]162	pub type ForeignReserveAssetInstanceToTokenId<T: Config> = StorageDoubleMap<163		Hasher1 = Blake2_128Concat,164		Key1 = CollectionId,165		Hasher2 = Blake2_128Concat,166		Key2 = staging_xcm::v4::AssetInstance,167		Value = TokenId,168		QueryKind = OptionQuery,169	>;170171	/// The correponding reserve NFT of a token ID172	#[pallet::storage]173	#[pallet::getter(fn token_id_to_foreign_reserve_asset_instance)]174	pub type TokenIdToForeignReserveAssetInstance<T: Config> = StorageDoubleMap<175		Hasher1 = Blake2_128Concat,176		Key1 = CollectionId,177		Hasher2 = Blake2_128Concat,178		Key2 = TokenId,179		Value = staging_xcm::v4::AssetInstance,180		QueryKind = OptionQuery,181	>;182183	const STORAGE_VERSION: StorageVersion = StorageVersion::new(staging_xcm::v4::VERSION as u16);184185	#[pallet::pallet]186	#[pallet::storage_version(STORAGE_VERSION)]187	pub struct Pallet<T>(_);188189	#[pallet::call]190	impl<T: Config> Pallet<T> {191		#[pallet::call_index(0)]192		#[pallet::weight(<T as Config>::WeightInfo::force_register_foreign_asset())]193		pub fn force_register_foreign_asset(194			origin: OriginFor<T>,195			versioned_asset_id: Box<VersionedAssetId>,196			name: CollectionName,197			token_prefix: CollectionTokenPrefix,198			mode: ForeignCollectionMode,199		) -> DispatchResult {200			T::ForceRegisterOrigin::ensure_origin(origin.clone())?;201202			let asset_id: AssetId = versioned_asset_id203				.as_ref()204				.clone()205				.try_into()206				.map_err(|()| Error::<T>::BadForeignAssetId)?;207208			ensure!(209				!<ForeignAssetToCollection<T>>::contains_key(&asset_id),210				<Error<T>>::ForeignAssetAlreadyRegistered,211			);212213			let foreign_collection_owner = Self::pallet_account();214215			let description: CollectionDescription = "Foreign Assets Collection"216				.encode_utf16()217				.collect::<Vec<_>>()218				.try_into()219				.expect("description length < max description length; qed");220221			let collection_id = T::CollectionDispatch::create(222				foreign_collection_owner,223				CollectionIssuer::Internals,224				CreateCollectionData {225					name,226					token_prefix,227					description,228					mode: mode.into(),229					flags: CollectionFlags {230						foreign: true,231						..Default::default()232					},233					..Default::default()234				},235			)?;236237			<ForeignAssetToCollection<T>>::insert(&asset_id, collection_id);238			<CollectionToForeignAsset<T>>::insert(collection_id, asset_id);239240			Self::deposit_event(Event::<T>::ForeignAssetRegistered {241				collection_id,242				asset_id: versioned_asset_id,243			});244245			Ok(())246		}247	}248249	#[pallet::genesis_config]250	#[derive(Derivative)]251	#[derivative(Default(bound = ""))]252	pub struct GenesisConfig<T: Config>(PhantomData<T>);253254	#[pallet::genesis_build]255	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {256		fn build(&self) {257			<Pallet<T>>::in_code_storage_version().put::<Pallet<T>>();258		}259	}260261	#[pallet::hooks]262	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {263		fn on_runtime_upgrade() -> Weight {264			if Self::on_chain_storage_version() < staging_xcm::v4::VERSION as u16 {265				let put_version_weight = T::DbWeight::get().writes(1);266				let fix_foreign_flag_weight = Self::fix_foreign_flag();267				let weight_v3_to_v4 = Self::migrate_v3_to_v4();268269				Self::in_code_storage_version().put::<Self>();270271				put_version_weight272					.saturating_add(fix_foreign_flag_weight)273					.saturating_add(weight_v3_to_v4)274			} else {275				Weight::zero()276			}277		}278	}279}280281mod v3_storage {282	use super::*;283284	#[storage_alias]285	pub type ForeignAssetToCollection<T: Config> =286		StorageMap<Pallet<T>, Twox64Concat, staging_xcm::v3::AssetId, CollectionId, OptionQuery>;287288	#[storage_alias]289	pub type CollectionToForeignAsset<T: Config> =290		StorageMap<Pallet<T>, Twox64Concat, CollectionId, staging_xcm::v3::AssetId, OptionQuery>;291292	#[storage_alias]293	pub type ForeignReserveAssetInstanceToTokenId<T: Config> = StorageDoubleMap<294		Pallet<T>,295		Twox64Concat,296		CollectionId,297		Blake2_128Concat,298		staging_xcm::v3::AssetInstance,299		TokenId,300		OptionQuery,301	>;302303	#[storage_alias]304	pub type TokenIdToForeignReserveAssetInstance<T: Config> = StorageDoubleMap<305		Pallet<T>,306		Twox64Concat,307		CollectionId,308		Blake2_128Concat,309		TokenId,310		staging_xcm::v3::AssetInstance,311		OptionQuery,312	>;313}314315impl<T: Config> Pallet<T> {316	fn fix_foreign_flag() -> Weight {317		let mut weight = Weight::zero();318319		for (_, collection_id) in v3_storage::ForeignAssetToCollection::<T>::iter() {320			pallet_common::CollectionById::<T>::mutate(collection_id, |collection| {321				if let Some(collection) = collection {322					collection.flags.foreign = true;323				}324			});325326			weight = weight.saturating_add(T::DbWeight::get().reads_writes(2, 1));327		}328329		weight330	}331332	fn migrate_v3_to_v4() -> Weight {333		let event_weight = T::DbWeight::get().writes(1);334		let collection_migration_weight = Self::migrate_collections();335336		Self::deposit_event(Event::<T>::MigrationStatus(MigrationStatus::V3ToV4(337			MigrationStatusV3ToV4::Done,338		)));339340		collection_migration_weight.saturating_add(event_weight)341	}342343	fn migrate_collections() -> Weight {344		use MigrationStatus::*;345		use MigrationStatusV3ToV4::*;346347		let mut weight = Weight::zero();348349		for (fwd_asset_id, collection_id) in v3_storage::ForeignAssetToCollection::<T>::drain() {350			let bwd_asset_id = v3_storage::CollectionToForeignAsset::<T>::take(collection_id);351			weight = weight.saturating_add(T::DbWeight::get().reads(2));352353			let Some(bwd_asset_id) = bwd_asset_id else {354				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(355					SkippedInconsistentAssetData(fwd_asset_id),356				)));357358				weight = weight.saturating_add(T::DbWeight::get().writes(1));359				continue;360			};361362			if fwd_asset_id != bwd_asset_id {363				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(364					SkippedInconsistentAssetData(fwd_asset_id),365				)));366367				weight = weight.saturating_add(T::DbWeight::get().writes(1));368				continue;369			}370371			let Ok(asset_id) = staging_xcm::v4::AssetId::try_from(fwd_asset_id) else {372				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(373					SkippedNotConvertibleAssetId(fwd_asset_id),374				)));375376				weight = weight.saturating_add(T::DbWeight::get().writes(1));377				continue;378			};379380			<ForeignAssetToCollection<T>>::insert(&asset_id, collection_id);381			<CollectionToForeignAsset<T>>::insert(collection_id, asset_id);382			weight = weight.saturating_add(T::DbWeight::get().writes(2));383384			let migrate_tokens_weight = Self::migrate_tokens(&fwd_asset_id, collection_id);385			weight = weight.saturating_add(migrate_tokens_weight);386		}387388		weight389	}390391	fn migrate_tokens(asset_id: &staging_xcm::v3::AssetId, collection_id: CollectionId) -> Weight {392		use MigrationStatus::*;393		use MigrationStatusV3ToV4::*;394395		let mut weight = Weight::zero();396397		for (fwd_asset_instance, token_id) in398			v3_storage::ForeignReserveAssetInstanceToTokenId::<T>::drain_prefix(collection_id)399		{400			let bwd_asset_instance = v3_storage::TokenIdToForeignReserveAssetInstance::<T>::take(401				collection_id,402				token_id,403			);404			weight = weight.saturating_add(T::DbWeight::get().reads(2));405406			let Some(bwd_asset_instance) = bwd_asset_instance else {407				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(408					SkippedInconsistentAssetInstanceData {409						asset_id: *asset_id,410						asset_instance: fwd_asset_instance,411					},412				)));413414				weight = weight.saturating_add(T::DbWeight::get().writes(1));415				continue;416			};417418			if fwd_asset_instance != bwd_asset_instance {419				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(420					SkippedInconsistentAssetInstanceData {421						asset_id: *asset_id,422						asset_instance: fwd_asset_instance,423					},424				)));425426				weight = weight.saturating_add(T::DbWeight::get().writes(1));427				continue;428			}429430			let Ok(asset_instance) = staging_xcm::v4::AssetInstance::try_from(fwd_asset_instance)431			else {432				Self::deposit_event(Event::<T>::MigrationStatus(V3ToV4(433					SkippedNotConvertibleAssetInstance {434						asset_id: *asset_id,435						asset_instance: fwd_asset_instance,436					},437				)));438439				weight = weight.saturating_add(T::DbWeight::get().writes(1));440				continue;441			};442443			<ForeignReserveAssetInstanceToTokenId<T>>::insert(444				collection_id,445				&asset_instance,446				token_id,447			);448			<TokenIdToForeignReserveAssetInstance<T>>::insert(449				collection_id,450				token_id,451				asset_instance,452			);453			weight = weight.saturating_add(T::DbWeight::get().writes(2));454		}455456		weight457	}458459	fn pallet_account() -> T::CrossAccountId {460		let owner: T::AccountId = T::PalletId::get().into_account_truncating();461		T::CrossAccountId::from_sub(owner)462	}463464	/// Converts a concrete asset ID (the asset multilocation) to a local collection on Unique Network.465	///466	/// The multilocation corresponds to a local collection if:467	/// * It is `Here` location that corresponds to the native token of this parachain.468	/// * It is `../Parachain(<Unique Network Para ID>)` that also corresponds to the native token of this parachain.469	/// * It is `../Parachain(<Unique Network Para ID>)/GeneralIndex(<Collection ID>)` that corresponds470	/// to the collection with the ID equal to `<Collection ID>`. The `<Collection ID>` must be in the valid range,471	/// otherwise `None` is returned.472	/// * It is `GeneralIndex(<Collection ID>)`. Same as the last one above.473	///474	/// If the multilocation doesn't match the patterns listed above,475	/// or the `<Collection ID>` points to a foreign collection,476	/// `None` is returned, identifying that the given multilocation doesn't correspond to a local collection.477	fn local_asset_id_to_collection(478		AssetId(asset_location): &AssetId,479	) -> Option<CollectionLocality> {480		let self_location = T::SelfLocation::get();481482		if *asset_location == Here.into() || *asset_location == self_location {483			return Some(CollectionLocality::Local(NATIVE_FUNGIBLE_COLLECTION_ID));484		}485486		let prefix = if asset_location.parents == 0 {487			&Here488		} else if asset_location.parents == self_location.parents {489			&self_location.interior490		} else {491			return None;492		};493494		let GeneralIndex(collection_id) = asset_location.interior.match_and_split(prefix)? else {495			return None;496		};497498		let collection_id = CollectionId((*collection_id).try_into().ok()?);499500		Self::collection_to_foreign_asset(collection_id)501			.is_none()502			.then_some(CollectionLocality::Local(collection_id))503	}504505	/// Converts an asset ID to a Unique Network's collection locality (either foreign or a local one).506	///507	/// The function will check if the asset's reserve location has the corresponding508	/// foreign collection on Unique Network,509	/// and will return the "foreign" locality containing the collection ID if found.510	///511	/// If no corresponding foreign collection is found, the function will check512	/// if the asset's reserve location corresponds to a local collection.513	/// If the local collection is found, the "local" locality with the collection ID is returned.514	///515	/// If all of the above have failed, the `AssetIdConversionFailed` error will be returned.516	fn asset_to_collection(asset_id: &AssetId) -> Result<CollectionLocality, XcmError> {517		Self::foreign_asset_to_collection(asset_id)518			.map(CollectionLocality::Foreign)519			.or_else(|| Self::local_asset_id_to_collection(asset_id))520			.ok_or_else(|| XcmExecutorError::AssetIdConversionFailed.into())521	}522523	/// Converts an XCM asset instance of local collection to the Unique Network's token ID.524	///525	/// The asset instance corresponds to the Unique Network's token ID if it is in the following format:526	/// `AssetInstance::Index(<token ID>)`.527	///528	/// If the asset instance is not in the valid format or the `<token ID>` can't fit into the valid token ID,529	/// `None` will be returned.530	///531	/// Note: this function can return `Some` containing the token ID of a non-existing NFT.532	/// It returns `None` when it failed to convert the `asset_instance` to a local ID.533	fn local_asset_instance_to_token_id(asset_instance: &AssetInstance) -> Option<TokenId> {534		match asset_instance {535			AssetInstance::Index(token_id) => Some(TokenId((*token_id).try_into().ok()?)),536			_ => None,537		}538	}539540	/// Obtains the token ID of the `asset_instance` in the collection.541	///542	/// Note: this function can return `Some` containing the token ID of a non-existing NFT.543	/// It returns `None` when it failed to convert the `asset_instance` to a local ID.544	fn asset_instance_to_token_id(545		collection_locality: CollectionLocality,546		asset_instance: &AssetInstance,547	) -> Option<TokenId> {548		match collection_locality {549			CollectionLocality::Local(_) => Self::local_asset_instance_to_token_id(asset_instance),550			CollectionLocality::Foreign(collection_id) => {551				Self::foreign_reserve_asset_instance_to_token_id(collection_id, asset_instance)552			}553		}554	}555556	/// Creates a foreign item in the the collection.557	fn create_foreign_asset_instance(558		xcm_ext: &dyn XcmExtensions<T>,559		collection_id: CollectionId,560		asset_instance: &AssetInstance,561		to: T::CrossAccountId,562	) -> DispatchResult {563		let derivative_token_id = xcm_ext.create_item(564			&Self::pallet_account(),565			to,566			CreateItemData::NFT(Default::default()),567			&ZeroBudget,568		)?;569570		<ForeignReserveAssetInstanceToTokenId<T>>::insert(571			collection_id,572			asset_instance,573			derivative_token_id,574		);575576		<TokenIdToForeignReserveAssetInstance<T>>::insert(577			collection_id,578			derivative_token_id,579			asset_instance,580		);581582		Ok(())583	}584585	/// Deposits an asset instance to the `to` account.586	///587	/// Either transfers an existing item from the pallet's account588	/// or creates a foreign item.589	fn deposit_asset_instance(590		xcm_ext: &dyn XcmExtensions<T>,591		collection_locality: CollectionLocality,592		asset_instance: &AssetInstance,593		to: T::CrossAccountId,594	) -> XcmResult {595		let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance);596597		let deposit_result = match (collection_locality, token_id) {598			(_, Some(token_id)) => {599				let depositor = &Self::pallet_account();600				let from = depositor;601				let amount = 1;602603				xcm_ext.transfer_item(depositor, from, &to, token_id, amount, &ZeroBudget)604			}605			(CollectionLocality::Foreign(collection_id), None) => {606				Self::create_foreign_asset_instance(xcm_ext, collection_id, asset_instance, to)607			}608			(CollectionLocality::Local(_), None) => {609				return Err(XcmExecutorError::InstanceConversionFailed.into());610			}611		};612613		deposit_result614			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item deposit failed"))615	}616617	/// Withdraws an asset instance from the `from` account.618	///619	/// Transfers the asset instance to the pallet's account.620	fn withdraw_asset_instance(621		xcm_ext: &dyn XcmExtensions<T>,622		collection_locality: CollectionLocality,623		asset_instance: &AssetInstance,624		from: T::CrossAccountId,625	) -> XcmResult {626		let token_id = Self::asset_instance_to_token_id(collection_locality, asset_instance)627			.ok_or(XcmExecutorError::InstanceConversionFailed)?;628629		let depositor = &from;630		let to = Self::pallet_account();631		let amount = 1;632		xcm_ext633			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)634			.map_err(|_| XcmError::FailedToTransactAsset("non-fungible item withdraw failed"))?;635636		Ok(())637	}638}639640// #[derive()]641// pub enum Migration {642643// }644645impl<T: Config> TransactAsset for Pallet<T> {646	fn can_check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {647		Err(XcmError::Unimplemented)648	}649650	fn check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) {}651652	fn can_check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {653		Err(XcmError::Unimplemented)654	}655656	fn check_out(_dest: &Location, _what: &Asset, _context: &XcmContext) {}657658	fn deposit_asset(what: &Asset, to: &Location, _context: Option<&XcmContext>) -> XcmResult {659		let to = T::LocationToAccountId::convert_location(to)660			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;661662		let collection_locality = Self::asset_to_collection(&what.id)?;663		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)664			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;665666		let collection = dispatch.as_dyn();667		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::Unimplemented)?;668669		match what.fun {670			Fungibility::Fungible(amount) => xcm_ext671				.create_item(672					&Self::pallet_account(),673					to,674					CreateItemData::Fungible(CreateFungibleData { value: amount }),675					&ZeroBudget,676				)677				.map(|_| ())678				.map_err(|_| XcmError::FailedToTransactAsset("fungible item deposit failed")),679680			Fungibility::NonFungible(asset_instance) => {681				Self::deposit_asset_instance(xcm_ext, collection_locality, &asset_instance, to)682			}683		}684	}685686	fn withdraw_asset(687		what: &Asset,688		from: &Location,689		_maybe_context: Option<&XcmContext>,690	) -> Result<AssetsInHolding, XcmError> {691		let from = T::LocationToAccountId::convert_location(from)692			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;693694		let collection_locality = Self::asset_to_collection(&what.id)?;695		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)696			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;697698		let collection = dispatch.as_dyn();699		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;700701		match what.fun {702			Fungibility::Fungible(amount) => xcm_ext703				.burn_item(from, TokenId::default(), amount)704				.map_err(|_| XcmError::FailedToTransactAsset("fungible item withdraw failed"))?,705706			Fungibility::NonFungible(asset_instance) => {707				Self::withdraw_asset_instance(xcm_ext, collection_locality, &asset_instance, from)?;708			}709		}710711		Ok(what.clone().into())712	}713714	fn internal_transfer_asset(715		what: &Asset,716		from: &Location,717		to: &Location,718		_context: &XcmContext,719	) -> Result<AssetsInHolding, XcmError> {720		let from = T::LocationToAccountId::convert_location(from)721			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;722723		let to = T::LocationToAccountId::convert_location(to)724			.ok_or(XcmExecutorError::AccountIdConversionFailed)?;725726		let collection_locality = Self::asset_to_collection(&what.id)?;727728		let dispatch = T::CollectionDispatch::dispatch(*collection_locality)729			.map_err(|_| XcmExecutorError::AssetIdConversionFailed)?;730		let collection = dispatch.as_dyn();731		let xcm_ext = collection.xcm_extensions().ok_or(XcmError::NoPermission)?;732733		let depositor = &from;734735		let token_id;736		let amount;737		let map_error: fn(DispatchError) -> XcmError;738739		match what.fun {740			Fungibility::Fungible(fungible_amount) => {741				token_id = TokenId::default();742				amount = fungible_amount;743				map_error = |_| XcmError::FailedToTransactAsset("fungible item transfer failed");744			}745746			Fungibility::NonFungible(asset_instance) => {747				token_id = Self::asset_instance_to_token_id(collection_locality, &asset_instance)748					.ok_or(XcmExecutorError::InstanceConversionFailed)?;749750				amount = 1;751				map_error = |_| XcmError::FailedToTransactAsset("non-fungible item transfer failed")752			}753		}754755		xcm_ext756			.transfer_item(depositor, &from, &to, token_id, amount, &ZeroBudget)757			.map_err(map_error)?;758759		Ok(what.clone().into())760	}761}762763#[derive(Clone, Copy)]764pub enum CollectionLocality {765	Local(CollectionId),766	Foreign(CollectionId),767}768769impl Deref for CollectionLocality {770	type Target = CollectionId;771772	fn deref(&self) -> &Self::Target {773		match self {774			Self::Local(id) => id,775			Self::Foreign(id) => id,776		}777	}778}779780pub struct CurrencyIdConvert<T: Config>(PhantomData<T>);781impl<T: Config> sp_runtime::traits::Convert<CollectionId, Option<Location>>782	for CurrencyIdConvert<T>783{784	fn convert(collection_id: CollectionId) -> Option<Location> {785		if collection_id == NATIVE_FUNGIBLE_COLLECTION_ID {786			Some(T::SelfLocation::get())787		} else {788			<Pallet<T>>::collection_to_foreign_asset(collection_id)789				.map(|AssetId(location)| location)790				.or_else(|| {791					T::SelfLocation::get()792						.pushed_with_interior(GeneralIndex(collection_id.0.into()))793						.ok()794				})795		}796	}797}798799#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]800pub enum ForeignCollectionMode {801	NFT,802	Fungible(u8),803}804805impl From<ForeignCollectionMode> for CollectionMode {806	fn from(value: ForeignCollectionMode) -> Self {807		match value {808			ForeignCollectionMode::NFT => Self::NFT,809			ForeignCollectionMode::Fungible(decimals) => Self::Fungible(decimals),810		}811	}812}813814pub struct FreeForAll;815816impl WeightTrader for FreeForAll {817	fn new() -> Self {818		Self819	}820821	fn buy_weight(822		&mut self,823		weight: Weight,824		payment: AssetsInHolding,825		_xcm: &XcmContext,826	) -> Result<AssetsInHolding, XcmError> {827		log::trace!(target: "fassets::weight", "buy_weight weight: {:?}, payment: {:?}", weight, payment);828		Ok(payment)829	}830}
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;
+	}
+}