difftreelog
Merge pull request #1059 from UniqueNetwork/feature/financial-council
in: master
15 files changed
js-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"
js-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'),
js-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';
js-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');
+ });
});
js-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);
+ });
+
+
+});
js-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);
js-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[] = [];
pallets/foreign-assets/src/lib.rsdiffbeforeafterboth1// 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}runtime/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>;
runtime/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::*;
runtime/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;
runtime/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.
runtime/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;
+ }
+}
runtime/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;
+ }
+}
runtime/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;
+ }
+}