12import type {IKeyringPair} from '@polkadot/types/types';3import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';4import {Event} from '@unique/playgrounds/src/unique.dev.js';5import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js';6import type {ICounselors} from './util.js';78describeGov('Governance: Council tests', () => {9 let donor: IKeyringPair;10 let counselors: ICounselors;11 let sudoer: IKeyringPair;1213 const moreThanHalfCouncilThreshold = 3;1415 before(async function() {16 await usingPlaygrounds(async (helper, privateKey) => {17 requirePalletsOrSkip(this, helper, [Pallets.Council]);1819 donor = await privateKey({url: import.meta.url});20 sudoer = await privateKey('//Alice');21 });22 });2324 beforeEach(async () => {25 counselors = await initCouncil(donor, sudoer);26 });2728 afterEach(async () => {29 await clearCouncil(sudoer);30 await clearTechComm(sudoer);31 });3233 async function proposalFromMoreThanHalfCouncil(proposal: any) {34 return await usingPlaygrounds(async (helper) => {35 expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);36 const proposeResult = await helper.council.collective.propose(37 counselors.filip,38 proposal,39 moreThanHalfCouncilThreshold,40 );4142 const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);43 const proposalIndex = councilProposedEvent.proposalIndex;44 const proposalHash = councilProposedEvent.proposalHash;454647 await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);48 await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);49 await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);5051 return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);52 });53 }5455 async function proposalFromAllCouncil(proposal: any) {56 return await usingPlaygrounds(async (helper) => {57 expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);58 const proposeResult = await helper.council.collective.propose(59 counselors.filip,60 proposal,61 moreThanHalfCouncilThreshold,62 );6364 const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);65 const proposalIndex = councilProposedEvent.proposalIndex;66 const proposalHash = councilProposedEvent.proposalHash;676869 await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);70 await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);71 await helper.council.collective.vote(counselors.ildar, proposalHash, proposalIndex, true);72 await helper.council.collective.vote(counselors.irina, proposalHash, proposalIndex, true);73 await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);7475 return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);76 });77 }7879 itSub('>50% of Council can externally propose SuperMajorityAgainst', async ({helper}) => {80 const forceSetBalanceReceiver = helper.arrange.createEmptyAccount();81 const forceSetBalanceTestValue = 20n * 10n ** 25n;8283 const democracyProposal = await helper.constructApiCall('api.tx.balances.forceSetBalance', [84 forceSetBalanceReceiver.address, forceSetBalanceTestValue,85 ]);8687 const councilProposal = await helper.democracy.externalProposeDefaultCall(democracyProposal);8889 const proposeResult = await helper.council.collective.propose(90 counselors.filip,91 councilProposal,92 moreThanHalfCouncilThreshold,93 );9495 const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);96 const proposalIndex = councilProposedEvent.proposalIndex;97 const proposalHash = councilProposedEvent.proposalHash;9899 await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);100 await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);101 await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);102103 await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);104105 const democracyStartedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);106 const democracyReferendumIndex = democracyStartedEvent.referendumIndex;107 const democracyThreshold = democracyStartedEvent.threshold;108109 expect(democracyThreshold).to.be.equal('SuperMajorityAgainst');110111 await helper.democracy.vote(counselors.filip, democracyReferendumIndex, {112 Standard: {113 vote: {114 aye: true,115 conviction: 1,116 },117 balance: 10_000n,118 },119 });120121 await helper.democracy.vote(counselors.charu, democracyReferendumIndex, {122 Standard: {123 vote: {124 aye: false,125 conviction: 1,126 },127 balance: 50_000n,128 },129 });130131 const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed);132 expect(passedReferendumEvent.referendumIndex).to.be.equal(democracyReferendumIndex);133134 await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched);135 const receiverBalance = await helper.balance.getSubstrate(forceSetBalanceReceiver.address);136 expect(receiverBalance).to.be.equal(forceSetBalanceTestValue);137 });138139 itSub('Council prime member vote is the default', async ({helper}) => {140 const newTechCommMember = helper.arrange.createEmptyAccount();141 const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);142 const proposeResult = await helper.council.collective.propose(143 counselors.filip,144 addMemberProposal,145 moreThanHalfCouncilThreshold,146 );147148 const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);149 const proposalIndex = councilProposedEvent.proposalIndex;150 const proposalHash = councilProposedEvent.proposalHash;151152 await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);153154 await helper.wait.newBlocks(councilMotionDuration);155 const closeResult = await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);156 const closeEvent = Event.Council.Closed.expect(closeResult);157 const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as string[];158 expect(closeEvent.yes).to.be.equal(members.length);159 });160161 itSub('Superuser can add a member', async ({helper}) => {162 const newMember = helper.arrange.createEmptyAccount();163 await expect(helper.getSudo().council.membership.addMember(sudoer, newMember.address)).to.be.fulfilled;164165 const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();166 expect(members).to.contains(newMember.address);167 });168169 itSub('Superuser can remove a member', async ({helper}) => {170 await expect(helper.getSudo().council.membership.removeMember(sudoer, counselors.alex.address)).to.be.fulfilled;171172 const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();173 expect(members).to.not.contains(counselors.alex.address);174 });175176 itSub('>50% Council can add TechComm member', async ({helper}) => {177 const newTechCommMember = helper.arrange.createEmptyAccount();178 const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);179180 await proposalFromMoreThanHalfCouncil(addMemberProposal);181182 const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();183 expect(techCommMembers).to.contains(newTechCommMember.address);184 });185186 itSub('Council can remove TechComm member', async ({helper}) => {187 const techComm = await initTechComm(donor, sudoer);188 const removeMemberPrpoposal = helper.technicalCommittee.membership.removeMemberCall(techComm.andy.address);189 await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal);190191 const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();192 expect(techCommMembers).to.not.contains(techComm.andy.address);193 });194195 itSub.skip('Council member can add Fellowship member', async ({helper}) => {196 const newFellowshipMember = helper.arrange.createEmptyAccount();197 await expect(helper.council.collective.execute(198 counselors.alex,199 helper.fellowship.collective.addMemberCall(newFellowshipMember.address),200 )).to.be.fulfilled;201 const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();202 expect(fellowshipMembers).to.contains(newFellowshipMember.address);203 });204205 itSub('>50% Council can promote Fellowship member', async ({helper}) => {206 const fellowship = await initFellowship(donor, sudoer);207 const memberWithZeroRank = fellowship[0][0];208209 const proposal = helper.fellowship.collective.promoteCall(memberWithZeroRank.address);210 await proposalFromMoreThanHalfCouncil(proposal);211 const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithZeroRank.address])).toJSON();212 expect(record).to.be.deep.equal({rank: 1});213214 await clearFellowship(sudoer);215 });216217 itSub('>50% Council can demote Fellowship member', async ({helper}) => {218 const fellowship = await initFellowship(donor, sudoer);219 const memberWithRankOne = fellowship[1][0];220221 const proposal = helper.fellowship.collective.demoteCall(memberWithRankOne.address);222 await proposalFromMoreThanHalfCouncil(proposal);223224 const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithRankOne.address])).toJSON();225 expect(record).to.be.deep.equal({rank: 0});226227 await clearFellowship(sudoer);228 });229230 itSub('>50% Council can add\remove Fellowship member', async ({helper}) => {231 try {232 const newMember = helper.arrange.createEmptyAccount();233234 const proposalAdd = helper.fellowship.collective.addMemberCall(newMember.address);235 const proposalRemove = helper.fellowship.collective.removeMemberCall(newMember.address, fellowshipRankLimit);236 await expect(proposalFromMoreThanHalfCouncil(proposalAdd)).to.be.fulfilled;237 expect(await helper.fellowship.collective.getMembers()).to.be.deep.contain(newMember.address);238 await expect(proposalFromMoreThanHalfCouncil(proposalRemove)).to.be.fulfilled;239 expect(await helper.fellowship.collective.getMembers()).to.be.not.deep.contain(newMember.address);240 }241 finally {242 await clearFellowship(sudoer);243 }244 });245246 itSub('Council can blacklist Democracy proposals', async ({helper}) => {247 const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);248 await expect(proposalFromAllCouncil(helper.democracy.blacklistCall(preimageHash, null))).to.be.fulfilled;249 });250251 itSub('Sudo can blacklist Democracy proposals', async ({helper}) => {252 const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);253 await expect(helper.getSudo().democracy.blacklist(sudoer, preimageHash)).to.be.fulfilled;254 });255256 itSub('[Negative] Council cannot add Council member', async ({helper}) => {257 const newCouncilMember = helper.arrange.createEmptyAccount();258 const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);259260 await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejected;261 });262263 itSub('[Negative] Council cannot remove Council member', async ({helper}) => {264 const removeMemberProposal = helper.council.membership.removeMemberCall(counselors.alex.address);265266 await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejected;267 });268269 itSub('[Negative] Council cannot submit regular democracy proposal', async ({helper}) => {270 const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);271272 await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);273 });274275 itSub('[Negative] Council cannot externally propose SimpleMajority', async ({helper}) => {276 const councilProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));277278 await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);279 });280281 itSub('[Negative] Council cannot externally propose SuperMajorityApprove', async ({helper}) => {282 const councilProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));283284 await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);285 });286287 itSub('[Negative] Council member cannot add/remove a Council member', async ({helper}) => {288 const newCouncilMember = helper.arrange.createEmptyAccount();289 await expect(helper.council.collective.execute(290 counselors.alex,291 helper.council.membership.addMemberCall(newCouncilMember.address),292 )).to.be.rejectedWith('BadOrigin');293 await expect(helper.council.collective.execute(294 counselors.alex,295 helper.council.membership.removeMemberCall(counselors.charu.address),296 )).to.be.rejectedWith('BadOrigin');297 });298299 itSub('[Negative] Council cannot set/clear Council prime member', async ({helper}) => {300 const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);301 const proposalForClear = await helper.council.membership.clearPrimeCall();302303 await expect(proposalFromAllCouncil(proposalForSet)).to.be.rejectedWith(/BadOrigin/);304 await expect(proposalFromAllCouncil(proposalForClear)).to.be.rejectedWith(/BadOrigin/);305306 });307308 itSub('[Negative] Council member cannot set/clear Council prime member', async ({helper}) => {309 await expect(helper.council.collective.execute(310 counselors.alex,311 helper.council.membership.setPrimeCall(counselors.charu.address),312 )).to.be.rejectedWith('BadOrigin');313 await expect(helper.council.collective.execute(314 counselors.alex,315 helper.council.membership.clearPrimeCall(),316 )).to.be.rejectedWith('BadOrigin');317 });318319 itSub('[Negative] Council member cannot add/remove a TechComm member', async ({helper}) => {320 const newTechCommMember = helper.arrange.createEmptyAccount();321 await expect(helper.council.collective.execute(322 counselors.alex,323 helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address),324 )).to.be.rejectedWith('BadOrigin');325 await expect(helper.council.collective.execute(326 counselors.alex,327 helper.technicalCommittee.membership.removeMemberCall(newTechCommMember.address),328 )).to.be.rejectedWith('BadOrigin');329 });330331 itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => {332 const fellowship = await initFellowship(donor, sudoer);333 const memberWithRankOne = fellowship[1][0];334335 await expect(helper.council.collective.execute(336 counselors.alex,337 helper.fellowship.collective.promoteCall(memberWithRankOne.address),338 )).to.be.rejectedWith('BadOrigin');339 await expect(helper.council.collective.execute(340 counselors.alex,341 helper.fellowship.collective.demoteCall(memberWithRankOne.address),342 )).to.be.rejectedWith('BadOrigin');343 await clearFellowship(sudoer);344 });345346 itSub('[Negative] Council cannot fast-track Democracy proposals', async ({helper}) => {347 const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);348 await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);349350 await expect(proposalFromAllCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)))351 .to.be.rejectedWith(/BadOrigin/);352 });353354 itSub('[Negative] Council member cannot fast-track Democracy proposals', async ({helper}) => {355 const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);356 await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);357358 await expect(helper.council.collective.execute(359 counselors.alex,360 helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),361 )).to.be.rejectedWith('BadOrigin');362 });363364 itSub('[Negative] Council cannot cancel Democracy proposals', async ({helper}) => {365 const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);366 const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;367368 await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex)))369 .to.be.rejectedWith(/BadOrigin/);370 });371372 itSub('[Negative] Council member cannot cancel Democracy proposals', async ({helper}) => {373374 const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);375 const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;376377 await expect(helper.council.collective.execute(378 counselors.alex,379 helper.democracy.cancelProposalCall(proposalIndex),380 )).to.be.rejectedWith('BadOrigin');381 });382383 itSub('[Negative] Council cannot cancel ongoing Democracy referendums', async ({helper}) => {384 await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));385 const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);386 const referendumIndex = startedEvent.referendumIndex;387388 await expect(proposalFromAllCouncil(helper.democracy.emergencyCancelCall(referendumIndex)))389 .to.be.rejectedWith(/BadOrigin/);390 });391392 itSub('[Negative] Council member cannot cancel ongoing Democracy referendums', async ({helper}) => {393 await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));394 const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);395 const referendumIndex = startedEvent.referendumIndex;396397 await expect(helper.council.collective.execute(398 counselors.alex,399 helper.democracy.emergencyCancelCall(referendumIndex),400 )).to.be.rejectedWith('BadOrigin');401 });402403 itSub('[Negative] Council cannot cancel Fellowship referendums', async ({helper}) => {404 const fellowship = await initFellowship(donor, sudoer);405 const fellowshipProposer = fellowship[5][0];406 const proposal = dummyProposal(helper);407408 const submitResult = await helper.fellowship.referenda.submit(409 fellowshipProposer,410 fellowshipPropositionOrigin,411 proposal,412 defaultEnactmentMoment,413 );414415 const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;416417 await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex)))418 .to.be.rejectedWith(/BadOrigin/);419 });420421 itSub('[Negative] Council member cannot cancel Fellowship referendums', async ({helper}) => {422 const fellowship = await initFellowship(donor, sudoer);423 const fellowshipProposer = fellowship[5][0];424 const proposal = dummyProposal(helper);425426 const submitResult = await helper.fellowship.referenda.submit(427 fellowshipProposer,428 fellowshipPropositionOrigin,429 proposal,430 defaultEnactmentMoment,431 );432 const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;433 await expect(helper.council.collective.execute(434 counselors.alex,435 helper.fellowship.referenda.cancelCall(referendumIndex),436 )).to.be.rejectedWith('BadOrigin');437 });438439 itSub('[Negative] Council referendum cannot be closed until the voting threshold is met', async ({helper}) => {440 const councilSize = (await helper.callRpc('api.query.councilMembership.members')).toJSON().length as any as number;441 expect(councilSize).is.greaterThan(1);442 const proposeResult = await helper.council.collective.propose(443 counselors.filip,444 dummyProposalCall(helper),445 councilSize,446 );447448 const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);449 const proposalIndex = councilProposedEvent.proposalIndex;450 const proposalHash = councilProposedEvent.proposalHash;451452453 await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);454 await expect(helper.council.collective.close(counselors.filip, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');455 });456457});