diff --git a/packages/boot/test/bootstrapTests/ec-membership-update.test.ts b/packages/boot/test/bootstrapTests/ec-membership-update.test.ts new file mode 100644 index 00000000000..dd41e017f87 --- /dev/null +++ b/packages/boot/test/bootstrapTests/ec-membership-update.test.ts @@ -0,0 +1,407 @@ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { TestFn } from 'ava'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { + makeAgoricNamesRemotesFromFakeStorage, + slotToBoardRemote, + unmarshalFromVstorage, +} from '@agoric/vats/tools/board-utils.js'; +import { makeMarshal } from '@endo/marshal'; + +import { makeSwingsetTestKit } from '../../tools/supports.js'; +import { + makeGovernanceDriver, + makeWalletFactoryDriver, +} from '../../tools/drivers.js'; + +const wallets = [ + 'agoric1gx9uu7y6c90rqruhesae2t7c2vlw4uyyxlqxrx', + 'agoric1d4228cvelf8tj65f4h7n2td90sscavln2283h5', + 'agoric14543m33dr28x7qhwc558hzlj9szwhzwzpcmw6a', + 'agoric13p9adwk0na5npfq64g22l6xucvqdmu3xqe70wq', + 'agoric1el6zqs8ggctj5vwyukyk4fh50wcpdpwgugd5l5', + 'agoric1zayxg4e9vd0es9c9jlpt36qtth255txjp6a8yc', +]; + +const highPrioritySenderKey = 'highPrioritySenders'; + +const offerIds = { + propose: { outgoing: 'outgoing_propose' }, + vote: { outgoing: 'outgoing_vote', incoming: 'incoming_vote' }, +}; + +const getQuestionId = id => `propose-question-${id}`; +const getVoteId = id => `vote-${id}`; + +export const makeZoeTestContext = async t => { + console.time('ZoeTestContext'); + const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { + configSpecifier: '@agoric/vm-config/decentral-main-vaults-config.json', + }); + + const { runUtils, storage } = swingsetTestKit; + console.timeLog('DefaultTestContext', 'swingsetTestKit'); + const { EV } = runUtils; + + await eventLoopIteration(); + + // We don't need vaults, but this gets the brand, which is checked somewhere + // Wait for ATOM to make it into agoricNames + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); + console.timeLog('DefaultTestContext', 'vaultFactoryKit'); + + // has to be late enough for agoricNames data to have been published + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); + + console.timeEnd('DefaultTestContext'); + const walletFactoryDriver = await makeWalletFactoryDriver( + runUtils, + storage, + agoricNamesRemotes, + ); + + const { fromCapData } = makeMarshal(undefined, slotToBoardRemote); + + const getUpdatedDebtLimit = () => { + const atomGovernance = unmarshalFromVstorage( + storage.data, + 'published.vaultFactory.managers.manager0.governance', + fromCapData, + -1, + ); + return atomGovernance.current.DebtLimit.value.value; + }; + + const governanceDriver = await makeGovernanceDriver( + swingsetTestKit, + agoricNamesRemotes, + walletFactoryDriver, + wallets, + ); + + return { + ...swingsetTestKit, + storage, + getUpdatedDebtLimit, + governanceDriver, + }; +}; +const test = anyTest as TestFn>>; + +test.before(async t => { + t.context = await makeZoeTestContext(t); +}); + +test.serial('normal running of committee', async t => { + const { advanceTimeBy, storage, getUpdatedDebtLimit, governanceDriver } = + t.context; + + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + const { VaultFactory, economicCommittee } = agoricNamesRemotes.instance; + const { ATOM: collateralBrand, IST: debtBrand } = agoricNamesRemotes.brand; + + const committee = governanceDriver.ecMembers; + + t.log('Accepting all invitations for original committee'); + await null; + for (const member of committee) { + await member.acceptCharterInvitation(offerIds.propose.outgoing); + await member.acceptCommitteeInvitation(offerIds.vote.outgoing); + } + + t.log('Proposing a question using first wallet'); + await governanceDriver.proposeParams( + VaultFactory, + { DebtLimit: { brand: debtBrand, value: 100_000_000n } }, + { paramPath: { key: { collateralBrand } } }, + committee[0], + getQuestionId(1), + offerIds.propose.outgoing, + ); + + t.log('Checking if question proposal passed'); + t.like(committee[0].getLatestUpdateRecord(), { + status: { id: getQuestionId(1), numWantsSatisfied: 1 }, + }); + + t.log('Voting on question using first 3 wallets'); + await governanceDriver.enactLatestProposal( + committee, + getVoteId(1), + offerIds.vote.outgoing, + ); + + t.log('Checking if votes passed'); + for (const w of committee.slice(0, 3)) { + t.like(w.getLatestUpdateRecord(), { + status: { id: getVoteId(1), numWantsSatisfied: 1 }, + }); + } + + t.log('Waiting for period to end'); + await advanceTimeBy(1, 'minutes'); + + t.log('Verifying outcome'); + + const lastOutcome = await governanceDriver.getLatestOutcome(); + console.log(lastOutcome); + t.deepEqual(getUpdatedDebtLimit(), 100_000_000n); + t.assert(lastOutcome.outcome === 'win'); +}); + +test.serial( + 'check high priority senders before replacing committee', + async t => { + const { storage } = t.context; + + const data: any = storage.toStorage({ + method: 'children', + args: [highPrioritySenderKey], + }); + t.deepEqual(data.sort(), [...wallets].sort()); + }, +); + +test.serial('replace committee', async t => { + const { buildProposal, evalProposal, storage } = t.context; + + const preEvalAgoricNames = makeAgoricNamesRemotesFromFakeStorage(storage); + await evalProposal( + buildProposal( + '@agoric/builders/scripts/inter-protocol/replace-electorate-core.js', + ['BOOTSTRAP_TEST'], + ), + ); + await eventLoopIteration(); + + const postEvalAgoricNames = makeAgoricNamesRemotesFromFakeStorage(storage); + + t.not( + preEvalAgoricNames.instance.economicCommittee, + postEvalAgoricNames.instance.economicCommittee, + ); +}); + +test.serial( + 'check high priority senders after replacing committee', + async t => { + const { storage } = t.context; + + const data: any = storage.toStorage({ + method: 'children', + args: [highPrioritySenderKey], + }); + t.deepEqual(data.sort(), wallets.slice(0, 3).sort()); + }, +); + +test.serial('successful vote by 2 continuing members', async t => { + const { storage, advanceTimeBy, getUpdatedDebtLimit, governanceDriver } = + t.context; + const newCommittee = governanceDriver.ecMembers.slice(0, 3); + + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + const { economicCommittee, VaultFactory } = agoricNamesRemotes.instance; + const { ATOM: collateralBrand, IST: debtBrand } = agoricNamesRemotes.brand; + + t.log('Accepting all new invitations for voters'); + await null; + for (const member of newCommittee) { + await member.acceptCommitteeInvitation( + offerIds.vote.incoming, + economicCommittee, + ); + } + + t.log('Proposing question using old charter invitation'); + await governanceDriver.proposeParams( + VaultFactory, + { DebtLimit: { brand: debtBrand, value: 200_000_000n } }, + { paramPath: { key: { collateralBrand } } }, + newCommittee[0], + getQuestionId(2), + offerIds.propose.outgoing, + ); + + t.like(newCommittee[0].getLatestUpdateRecord(), { + status: { id: getQuestionId(2), numWantsSatisfied: 1 }, + }); + + t.log('Voting on question using first 2 wallets'); + await governanceDriver.enactLatestProposal( + newCommittee.slice(0, 2), + getVoteId(2), + offerIds.vote.incoming, + ); + for (const w of newCommittee.slice(0, 2)) { + t.like(w.getLatestUpdateRecord(), { + status: { id: getVoteId(2), numWantsSatisfied: 1 }, + }); + } + + t.log('Waiting for period to end'); + await advanceTimeBy(1, 'minutes'); + + t.log('Verifying outcome'); + const lastOutcome = await governanceDriver.getLatestOutcome(); + t.deepEqual(getUpdatedDebtLimit(), 200_000_000n); + t.assert(lastOutcome.outcome === 'win'); +}); + +test.serial('unsuccessful vote by 2 outgoing members', async t => { + const { governanceDriver, storage, advanceTimeBy, getUpdatedDebtLimit } = + t.context; + const outgoingCommittee = governanceDriver.ecMembers.slice(3); + + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + const { VaultFactory } = agoricNamesRemotes.instance; + const { ATOM: collateralBrand, IST: debtBrand } = agoricNamesRemotes.brand; + + t.log('Proposing question using old charter invitation'); + await governanceDriver.proposeParams( + VaultFactory, + { DebtLimit: { brand: debtBrand, value: 300_000_000n } }, + { paramPath: { key: { collateralBrand } } }, + outgoingCommittee[0], + getQuestionId(3), + offerIds.propose.outgoing, + ); + t.like(outgoingCommittee[0].getLatestUpdateRecord(), { + status: { id: getQuestionId(3), numWantsSatisfied: 1 }, + }); + + t.log('Voting on question using first 2 wallets'); + t.log('voting is done by invitations already present and should fail'); + const votePromises = outgoingCommittee + .slice(0, 2) + .map(member => + member.voteOnLatestProposal(getVoteId(3), offerIds.vote.outgoing), + ); + + await t.throwsAsync(votePromises[0]); + await t.throwsAsync(votePromises[1]); + + for (const w of outgoingCommittee.slice(0, 2)) { + t.like(w.getLatestUpdateRecord(), { + status: { id: getVoteId(3), numWantsSatisfied: 1 }, + }); + } + + t.log('Waiting for period to end'); + await advanceTimeBy(1, 'minutes'); + + const lastOutcome = await governanceDriver.getLatestOutcome(); + t.notDeepEqual(getUpdatedDebtLimit(), 300_000_000n); + t.assert(lastOutcome.outcome === 'fail'); +}); + +test.serial( + 'successful vote by 2 continuing and 1 outgoing members', + async t => { + const { storage, advanceTimeBy, getUpdatedDebtLimit, governanceDriver } = + t.context; + const committee = [ + ...governanceDriver.ecMembers.slice(0, 2), + governanceDriver.ecMembers[3], + ]; + + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + const { VaultFactory } = agoricNamesRemotes.instance; + const { ATOM: collateralBrand, IST: debtBrand } = agoricNamesRemotes.brand; + + t.log('Proposing question using old charter invitation'); + await governanceDriver.proposeParams( + VaultFactory, + { DebtLimit: { brand: debtBrand, value: 400_000_000n } }, + { paramPath: { key: { collateralBrand } } }, + committee[0], + getQuestionId(4), + offerIds.propose.outgoing, + ); + t.like(committee[0].getLatestUpdateRecord(), { + status: { id: getQuestionId(4), numWantsSatisfied: 1 }, + }); + + t.log('Voting on question using first all wallets'); + t.log('first 2 should pass, last should fail'); + const votePromises = committee.map((member, index) => + member.voteOnLatestProposal( + getVoteId(4), + index === 2 ? offerIds.vote.outgoing : offerIds.vote.incoming, + ), + ); + + await votePromises[0]; + await votePromises[1]; + await t.throwsAsync(votePromises[2]); + + for (const w of committee) { + t.like(w.getLatestUpdateRecord(), { + status: { id: getVoteId(4), numWantsSatisfied: 1 }, + }); + } + + t.log('Waiting for period to end'); + await advanceTimeBy(1, 'minutes'); + + const lastOutcome = await governanceDriver.getLatestOutcome(); + t.deepEqual(getUpdatedDebtLimit(), 400_000_000n); + t.assert(lastOutcome.outcome === 'win'); + }, +); + +test.serial( + 'unsuccessful vote by 1 continuing and 2 outgoing members', + async t => { + const { storage, advanceTimeBy, getUpdatedDebtLimit, governanceDriver } = + t.context; + const committee = [ + governanceDriver.ecMembers[0], + ...governanceDriver.ecMembers.slice(3, 5), + ]; + + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); + const { VaultFactory } = agoricNamesRemotes.instance; + const { ATOM: collateralBrand, IST: debtBrand } = agoricNamesRemotes.brand; + + t.log('Proposing question using old charter invitation'); + await governanceDriver.proposeParams( + VaultFactory, + { DebtLimit: { brand: debtBrand, value: 500_000_000n } }, + { paramPath: { key: { collateralBrand } } }, + committee[0], + getQuestionId(5), + offerIds.propose.outgoing, + ); + t.like(committee[0].getLatestUpdateRecord(), { + status: { id: getQuestionId(5), numWantsSatisfied: 1 }, + }); + + t.log('Voting on question using first all wallets'); + t.log('first 2 should fail, last should pass'); + const votePromises = committee.map((member, index) => + member.voteOnLatestProposal( + getVoteId(5), + index === 0 ? offerIds.vote.incoming : offerIds.vote.outgoing, + ), + ); + + await votePromises[0]; + await t.throwsAsync(votePromises[1]); + await t.throwsAsync(votePromises[2]); + + for (const w of committee) { + t.like(w.getLatestUpdateRecord(), { + status: { id: getVoteId(5), numWantsSatisfied: 1 }, + }); + } + + t.log('Waiting for period to end'); + await advanceTimeBy(1, 'minutes'); + + const lastOutcome = await governanceDriver.getLatestOutcome(); + t.notDeepEqual(getUpdatedDebtLimit(), 500_000_000n); + t.assert(lastOutcome.outcome === 'fail'); + }, +); diff --git a/packages/boot/tools/drivers.ts b/packages/boot/tools/drivers.ts index 6f36e56aeb7..b1877c0a31c 100644 --- a/packages/boot/tools/drivers.ts +++ b/packages/boot/tools/drivers.ts @@ -3,7 +3,10 @@ import { Fail } from '@endo/errors'; import { NonNullish } from '@agoric/internal'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { SECONDS_PER_MINUTE } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; -import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; +import { + slotToBoardRemote, + unmarshalFromVstorage, +} from '@agoric/internal/src/marshal.js'; import { FakeStorageKit, slotToRemotable, @@ -24,6 +27,7 @@ import type { OfferSpec } from '@agoric/smart-wallet/src/offers.js'; import type { TimerService } from '@agoric/time'; import type { OfferMaker } from '@agoric/smart-wallet/src/types.js'; import type { RunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; +import { makeMarshal } from '@endo/marshal'; import type { SwingsetTestKit } from './supports.js'; export const makeWalletFactoryDriver = async ( @@ -207,67 +211,105 @@ export const makeGovernanceDriver = async ( const charterMembershipId = 'charterMembership'; const committeeMembershipId = 'committeeMembership'; + const { fromCapData } = makeMarshal(undefined, slotToBoardRemote); + const chainTimerService: ERef = await EV.vat('bootstrap').consumeItem('chainTimerService'); let invitationsAccepted = false; - const ecMembers = await Promise.all( + const smartWallets = await Promise.all( committeeAddresses.map(address => walletFactoryDriver.provideSmartWallet(address), ), ); + const ecMembers = smartWallets.map(w => ({ + ...w, + acceptCharterInvitation: async ( + charterOfferId = charterMembershipId, + instance = agoricNamesRemotes.instance.econCommitteeCharter, + ) => { + await w.executeOffer({ + id: charterOfferId, + invitationSpec: { + source: 'purse', + instance, + description: 'charter member invitation', + }, + proposal: {}, + }); + }, + acceptCommitteeInvitation: async ( + committeeOfferId = committeeMembershipId, + instance = agoricNamesRemotes.instance.economicCommittee, + ) => { + const description = + w.getCurrentWalletRecord().purses[0].balance.value[0].description; + await w.executeOffer({ + id: committeeOfferId, + invitationSpec: { + source: 'purse', + instance, + description, + }, + proposal: {}, + }); + }, + voteOnLatestProposal: async ( + voteId = 'voteInNewLimit', + committeeId = committeeMembershipId, + ) => { + const latestQuestionRecord = testKit.readLatest( + 'published.committees.Economic_Committee.latestQuestion', + ) as any; + + const chosenPositions = [latestQuestionRecord.positions[0]]; + + await w.executeOffer({ + id: voteId, + invitationSpec: { + source: 'continuing', + previousOffer: committeeId, + invitationMakerName: 'makeVoteInvitation', + // (positionList, questionHandle) + invitationArgs: harden([ + chosenPositions, + latestQuestionRecord.questionHandle, + ]), + }, + proposal: {}, + }); + }, + })); + const ensureInvitationsAccepted = async () => { if (invitationsAccepted) { return; } - // accept charter invitations - { - const instance = agoricNamesRemotes.instance.econCommitteeCharter; - const promises = ecMembers.map(member => - member.executeOffer({ - id: charterMembershipId, - invitationSpec: { - source: 'purse', - instance, - description: 'charter member invitation', - }, - proposal: {}, - }), - ); - await Promise.all(promises); - } - // accept committee invitations - { - const instance = agoricNamesRemotes.instance.economicCommittee; - const promises = ecMembers.map(member => { - const description = - member.getCurrentWalletRecord().purses[0].balance.value[0] - .description; - return member.executeOffer({ - id: committeeMembershipId, - invitationSpec: { - source: 'purse', - instance, - description, - }, - proposal: {}, - }); - }); - await Promise.all(promises); + await null; + for (const member of ecMembers) { + await member.acceptCharterInvitation(); + await member.acceptCommitteeInvitation(); } invitationsAccepted = true; }; - const proposeParams = async (instance, params, path) => { + const proposeParams = async ( + instance, + params, + path, + ecMember: (typeof ecMembers)[0] | null = null, + questionId = 'propose', + charterOfferId = charterMembershipId, + ) => { const now = await EV(chainTimerService).getCurrentTimestamp(); - await ecMembers[0].executeOffer({ - id: 'propose', + await (ecMember || ecMembers[0]).executeOffer({ + id: questionId, invitationSpec: { invitationMakerName: 'VoteOnParamChange', - previousOffer: charterMembershipId, + previousOffer: charterOfferId, source: 'continuing', }, offerArgs: { @@ -280,33 +322,30 @@ export const makeGovernanceDriver = async ( }); }; - const enactLatestProposal = async () => { - const latestQuestionRecord = testKit.readLatest( - 'published.committees.Economic_Committee.latestQuestion', - ) as any; - - const chosenPositions = [latestQuestionRecord.positions[0]]; - - const promises = ecMembers.map(member => - member.executeOffer({ - id: 'voteInNewLimit', - invitationSpec: { - source: 'continuing', - previousOffer: committeeMembershipId, - invitationMakerName: 'makeVoteInvitation', - // (positionList, questionHandle) - invitationArgs: harden([ - chosenPositions, - latestQuestionRecord.questionHandle, - ]), - }, - proposal: {}, - }), + const enactLatestProposal = async ( + members = ecMembers, + voteId = 'voteInNewLimit', + committeeId = committeeMembershipId, + ) => { + const promises = members.map(member => + member.voteOnLatestProposal(voteId, committeeId), ); await Promise.all(promises); }; + const getLatestOutcome = async () => { + return unmarshalFromVstorage( + testKit.storage.data, + 'published.committees.Economic_Committee.latestOutcome', + fromCapData, + -1, + ); + }; + return { + proposeParams, + enactLatestProposal, + getLatestOutcome, async changeParams(instance: Instance, params: Object, path?: object) { instance || Fail`missing instance`; await ensureInvitationsAccepted(); diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 9f6a89f7cdc..384d382e580 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -163,6 +163,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { outputDir: string, scriptPath: string, env: NodeJS.ProcessEnv, + cliArgs: string[] = [], ) => { console.info('running package script:', scriptPath); const out = childProcess.execFileSync('yarn', ['bin', 'agoric'], { @@ -171,7 +172,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { }); return childProcess.execFileSync( out.toString().trim(), - ['run', scriptPath], + ['run', scriptPath, ...cliArgs], { cwd: outputDir, env, @@ -202,7 +203,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { return { evals, bundles }; }; - const buildAndExtract = async (builderPath: string) => { + const buildAndExtract = async (builderPath: string, args: string[] = []) => { const tmpDir = await fsAmbientPromises.mkdtemp( join(getPkgPath('builders'), 'proposal-'), ); @@ -212,6 +213,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { tmpDir, await importSpec(builderPath), process.env, + args, ).toString(), ); diff --git a/packages/builders/scripts/inter-protocol/replace-electorate-core.js b/packages/builders/scripts/inter-protocol/replace-electorate-core.js new file mode 100644 index 00000000000..a856177f7d0 --- /dev/null +++ b/packages/builders/scripts/inter-protocol/replace-electorate-core.js @@ -0,0 +1,113 @@ +/** + * @file build core eval script to replace EC committee and charter + * Usage: + * To run this script, use the following command format in the CLI: + * agoric run replace-electorate-core.js [ENVIRONMENT] + * where [ENVIRONMENT] is one of the following: + * - MAINNET + * - DEVNET + * - A3P_INTEGRATION + * - BOOTSTRAP_TEST + * + * Example: + * agoric run replace-electorate-core.js MAINNET + */ +/* global process */ +import { makeHelpers } from '@agoric/deploy-script-support'; +import { getManifestForReplaceAllElectorates } from '@agoric/inter-protocol/src/proposals/replaceElectorate.js'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }, opts) => { + return harden({ + sourceSpec: '@agoric/inter-protocol/src/proposals/replaceElectorate.js', + getManifestCall: [ + getManifestForReplaceAllElectorates.name, + { + ...opts, + economicCommitteeRef: publishRef( + install( + '@agoric/governance/src/committee.js', + '../bundles/bundle-committee.js', + ), + ), + }, + ], + }); +}; + +const configurations = { + MAINNET: { + committeeName: 'Economic Committee', + // UNTIL https://github.com/Agoric/agoric-sdk/issues/10194 + voterAddresses: { + gov1: 'agoric1gx9uu7y6c90rqruhesae2t7c2vlw4uyyxlqxrx', + gov2: 'agoric1d4228cvelf8tj65f4h7n2td90sscavln2283h5', + gov3: 'agoric14543m33dr28x7qhwc558hzlj9szwhzwzpcmw6a', + }, + highPrioritySendersConfig: { + addressesToAdd: [], + addressesToRemove: [ + 'agoric13p9adwk0na5npfq64g22l6xucvqdmu3xqe70wq', + 'agoric1el6zqs8ggctj5vwyukyk4fh50wcpdpwgugd5l5', + 'agoric1zayxg4e9vd0es9c9jlpt36qtth255txjp6a8yc', + ], + }, + }, + DEVNET: { + committeeName: 'Economic Committee', + // TODO: Update the addresses after confirmation + voterAddresses: { + gov1: 'agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce', + gov2: 'agoric140dmkrz2e42ergjj7gyvejhzmjzurvqeq82ang', + }, + highPrioritySendersConfig: { + addressesToAdd: [], + addressesToRemove: ['agoric1w8wktaur4zf8qmmtn3n7x3r0jhsjkjntcm3u6h'], + }, + }, + A3P_INTEGRATION: { + committeeName: 'Economic Committee', + voterAddresses: { + gov1: 'agoric1ee9hr0jyrxhy999y755mp862ljgycmwyp4pl7q', + }, + highPrioritySendersConfig: { + addressesToAdd: [], + addressesToRemove: ['agoric1wrfh296eu2z34p6pah7q04jjuyj3mxu9v98277'], + }, + }, + BOOTSTRAP_TEST: { + committeeName: 'Economic Committee', + voterAddresses: { + gov1: 'agoric1gx9uu7y6c90rqruhesae2t7c2vlw4uyyxlqxrx', + gov2: 'agoric1d4228cvelf8tj65f4h7n2td90sscavln2283h5', + gov3: 'agoric14543m33dr28x7qhwc558hzlj9szwhzwzpcmw6a', + }, + highPrioritySendersConfig: { + addressesToAdd: [], + addressesToRemove: [ + 'agoric13p9adwk0na5npfq64g22l6xucvqdmu3xqe70wq', + 'agoric1el6zqs8ggctj5vwyukyk4fh50wcpdpwgugd5l5', + 'agoric1zayxg4e9vd0es9c9jlpt36qtth255txjp6a8yc', + ], + }, + }, +}; + +const { keys } = Object; +const Usage = `agoric run replace-electorate-core.js ${keys(configurations).join(' | ')}`; +export default async (homeP, endowments) => { + const { scriptArgs } = endowments; + const variant = scriptArgs?.[0]; + const config = configurations[variant]; + if (!config) { + console.error(Usage); + process.exit(1); + } + console.log('replace-committee', scriptArgs, config); + + const { writeCoreEval } = await makeHelpers(homeP, endowments); + + await writeCoreEval(`replace-committee-${variant}`, (utils, opts) => + defaultProposalBuilder(utils, { ...opts, ...config }), + ); +}; diff --git a/packages/inter-protocol/src/proposals/replaceElectorate.js b/packages/inter-protocol/src/proposals/replaceElectorate.js new file mode 100644 index 00000000000..1eacc1bb832 --- /dev/null +++ b/packages/inter-protocol/src/proposals/replaceElectorate.js @@ -0,0 +1,336 @@ +/** + * @file A proposal to replace the EC committee and charter. + * + * This script manages configuration updates, distributes invitations, and + * establishes committees using specified voter addresses and related + * parameters. + * + * See `@agoric/builders/scripts/inter-protocol/replace-electorate-core.js` for + * the proposal builder. + */ + +// @ts-check +import { E } from '@endo/eventual-send'; +import { + assertPathSegment, + makeStorageNodeChild, +} from '@agoric/internal/src/lib-chainStorage.js'; +import { reserveThenDeposit } from './utils.js'; + +/** @import {EconomyBootstrapPowers} from './econ-behaviors.js' */ +/** @import {CommitteeElectorateCreatorFacet} from '@agoric/governance/src/committee.js'; */ + +const trace = (...args) => console.log('GovReplaceCommiteeAndCharter', ...args); + +const traced = (label, x) => { + trace(label, x); + return x; +}; + +const { values } = Object; + +/** @type {(xs: X[], ys: Y[]) => [X, Y][]} */ +const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]); + +/** @type {(name: string) => string} */ +const sanitizePathSegment = name => { + const candidate = name.replace(/[ ,]/g, '_'); + assertPathSegment(candidate); + return candidate; +}; + +/** + * Handles the configuration updates for high-priority senders list by adding or + * removing addresses. + * + * @param {EconomyBootstrapPowers} powers - The bootstrap powers required for + * economic operations. + * @param {{ + * options: { + * highPrioritySendersConfig: { + * addressesToAdd: string[]; + * addressesToRemove: string[]; + * }; + * }; + * }} config + * - The configuration object containing lists of addresses to add or remove. + */ +const handlehighPrioritySendersList = async ( + { consume: { highPrioritySendersManager: highPrioritySendersManagerP } }, + { options: { highPrioritySendersConfig } }, +) => { + const HIGH_PRIORITY_SENDERS_NAMESPACE = 'economicCommittee'; + const highPrioritySendersManager = await highPrioritySendersManagerP; + + if (!highPrioritySendersManager) { + throw assert.error(`highPrioritySendersManager is not defined`); + } + + const { addressesToAdd, addressesToRemove } = highPrioritySendersConfig; + + await Promise.all( + addressesToAdd.map(addr => + E(highPrioritySendersManager).add( + HIGH_PRIORITY_SENDERS_NAMESPACE, + traced('High Priority Senders: adding', addr), + ), + ), + ); + + await Promise.all( + addressesToRemove.map(addr => + E(highPrioritySendersManager).remove( + HIGH_PRIORITY_SENDERS_NAMESPACE, + traced('High Priority Senders: removing', addr), + ), + ), + ); +}; + +/** + * Invites Economic Committee (EC) members by distributing voting invitations to + * the specified addresses. + * + * @param {EconomyBootstrapPowers} powers - The bootstrap powers required for + * economic operations, including `namesByAddressAdmin` used for managing + * names. + * @param {{ + * options: { + * voterAddresses: Record; + * economicCommitteeCreatorFacet: CommitteeElectorateCreatorFacet; + * }; + * }} config + * - The configuration object containing voter addresses and the economic + * committee facet to create voter invitations. + * + * @returns {Promise} A promise that resolves once the invitations have + * been distributed. + */ +const inviteECMembers = async ( + { consume: { namesByAddressAdmin } }, + { options: { voterAddresses = {}, economicCommitteeCreatorFacet } }, +) => { + trace('Create invitations for new committee'); + + const invitations = await E( + economicCommitteeCreatorFacet, + ).getVoterInvitations(); + assert.equal(invitations.length, values(voterAddresses).length); + + trace('Distribute invitations'); + /** @param {[string, Promise][]} addrInvitations */ + const distributeInvitations = async addrInvitations => { + await Promise.all( + addrInvitations.map(async ([addr, invitationP]) => { + const [voterInvitation] = await Promise.all([invitationP]); + trace('Sending voting invitations to', addr); + await reserveThenDeposit( + `econ committee member ${addr}`, + namesByAddressAdmin, + addr, + [voterInvitation], + ); + }), + ); + }; + + await distributeInvitations(zip(values(voterAddresses), invitations)); +}; + +/** + * Starts a new Economic Committee (EC) by creating an instance with the + * provided committee specifications. + * + * @param {EconomyBootstrapPowers} powers - The resources and capabilities + * required to start the committee. + * @param {{ + * options: { + * committeeName: string; + * committeeSize: number; + * }; + * }} config + * - Configuration object containing the name and size of the committee. + * + * @returns {Promise} A promise that resolves + * to the creator facet of the newly created EC instance. + */ +const startNewEconomicCommittee = async ( + { + consume: { board, chainStorage, startUpgradable }, + produce: { economicCommitteeKit, economicCommitteeCreatorFacet }, + installation: { + consume: { committee }, + }, + instance: { + produce: { economicCommittee }, + }, + }, + { options: { committeeName, committeeSize } }, +) => { + const COMMITTEES_ROOT = 'committees'; + + trace('startNewEconomicCommittee'); + + trace(`committeeName ${committeeName}`); + trace(`committeeSize ${committeeSize}`); + + const committeesNode = await makeStorageNodeChild( + chainStorage, + COMMITTEES_ROOT, + ); + const storageNode = await E(committeesNode).makeChildNode( + sanitizePathSegment(committeeName), + ); + + const marshaller = await E(board).getPublishingMarshaller(); + + trace('Starting new EC Committee Instance'); + + const privateArgs = { + storageNode, + marshaller, + }; + + const terms = { + committeeName, + committeeSize, + }; + + const startResult = await E(startUpgradable)({ + label: 'economicCommittee', + installation: committee, + privateArgs, + terms, + }); + + const { instance, creatorFacet } = startResult; + + trace('Started new EC Committee Instance Successfully'); + + economicCommitteeKit.reset(); + economicCommitteeKit.resolve( + harden({ ...startResult, label: 'economicCommittee' }), + ); + + economicCommittee.reset(); + economicCommittee.resolve(instance); + + economicCommitteeCreatorFacet.reset(); + economicCommitteeCreatorFacet.resolve(creatorFacet); + + return creatorFacet; +}; + +/** + * Replaces the electorate for governance contracts by creating a new Economic + * Committee and updating contracts with the new electorate's creator facet. + * + * @param {EconomyBootstrapPowers} permittedPowers - The resources and + * capabilities needed for operations, including access to governance + * contracts and the PSM kit. + * @param {{ + * options: { + * committeeName: string; + * voterAddresses: Record; + * highPrioritySendersConfig: { + * addressesToAdd: string[]; + * addressesToRemove: string[]; + * }; + * }; + * }} config + * - Configuration object containing the committee details and governance options. + * + * @returns {Promise} A promise that resolves when the electorate has been + * replaced. + */ +export const replaceAllElectorates = async (permittedPowers, config) => { + const { committeeName, voterAddresses, highPrioritySendersConfig } = + config.options; + + const economicCommitteeCreatorFacet = await startNewEconomicCommittee( + permittedPowers, + { + options: { + committeeName, + committeeSize: values(voterAddresses).length, + }, + }, + ); + + const governedContractKitsMap = + await permittedPowers.consume.governedContractKits; + const psmKitMap = await permittedPowers.consume.psmKit; + + const governanceDetails = [ + ...[...governedContractKitsMap.values()].map(governedContractKit => ({ + creatorFacet: governedContractKit.governorCreatorFacet, + label: governedContractKit.label, + })), + ...[...psmKitMap.values()].map(psmKit => ({ + creatorFacet: psmKit.psmGovernorCreatorFacet, + label: psmKit.label, + })), + ]; + + await Promise.all( + governanceDetails.map(async ({ creatorFacet, label }) => { + trace(`Getting PoserInvitation for ${label}...`); + const newElectoratePoser = await E( + economicCommitteeCreatorFacet, + ).getPoserInvitation(); + trace(`Successfully received newElectoratePoser for ${label}`); + + trace(`Replacing electorate for ${label}`); + await E(creatorFacet).replaceElectorate(newElectoratePoser); + trace(`Successfully replaced electorate for ${label}`); + }), + ); + + await inviteECMembers(permittedPowers, { + options: { + voterAddresses, + economicCommitteeCreatorFacet, + }, + }); + + await handlehighPrioritySendersList(permittedPowers, { + options: { + highPrioritySendersConfig, + }, + }); + + trace('Installed New Economic Committee'); +}; + +harden(replaceAllElectorates); + +export const getManifestForReplaceAllElectorates = async ( + { economicCommitteeRef: _economicCommitteeRef }, + options, +) => ({ + manifest: { + [replaceAllElectorates.name]: { + consume: { + psmKit: true, + governedContractKits: true, + chainStorage: true, + highPrioritySendersManager: true, + namesByAddressAdmin: true, + // Rest of these are designed to be widely shared + board: true, + startUpgradable: true, + }, + produce: { + economicCommitteeKit: true, + economicCommitteeCreatorFacet: 'economicCommittee', + }, + installation: { + consume: { committee: 'zoe' }, + }, + instance: { + produce: { economicCommittee: 'economicCommittee' }, + }, + }, + }, + options: { ...options }, +});