diff --git a/golang/cosmos/app/upgrade.go b/golang/cosmos/app/upgrade.go index da3407ecd40..f842acb4a13 100644 --- a/golang/cosmos/app/upgrade.go +++ b/golang/cosmos/app/upgrade.go @@ -192,6 +192,27 @@ func terminateGovernorCoreProposal(upgradeName string) (vm.CoreProposalStep, err ) } +func terminateAuctioneersCoreProposal(upgradeName string) (vm.CoreProposalStep, error) { + // targets is a slice of "$boardID:$instanceKitLabel" strings. + var targets []string + switch getVariantFromUpgradeName(upgradeName) { + case "EMERYNET": + targets = []string{ + "board04149:auctioneer", // auctioneer v39, governor v38 + "board0501277:auctioneer", // auctioneer v665, governor v664 + } + default: + return nil, nil + } + + return buildProposalStepWithArgs( + "@agoric/builders/scripts/vats/terminate-governed-instance.js", + // Request `defaultProposalBuilder(powers, targets)`. + "defaultProposalBuilder", + []any{targets}, + ) +} + // func upgradeMintHolderCoreProposal(upgradeName string) (vm.CoreProposalStep, error) { // variant := getVariantFromUpgradeName(upgradeName) @@ -313,6 +334,13 @@ func upgrade18Handler(app *GaiaApp, targetUpgrade string) func(sdk.Context, upgr } else if terminateOldGovernor != nil { CoreProposalSteps = append(CoreProposalSteps, terminateOldGovernor) } + + terminateAuctioneers, err := terminateAuctioneersCoreProposal(targetUpgrade) + if err != nil { + return nil, err + } else if terminateAuctioneers != nil { + CoreProposalSteps = append(CoreProposalSteps, terminateAuctioneers) + } } app.upgradeDetails = &upgradeDetails{ diff --git a/packages/boot/test/bootstrapTests/terminate-governed.test.ts b/packages/boot/test/bootstrapTests/terminate-governed.test.ts new file mode 100644 index 00000000000..3a023de31ea --- /dev/null +++ b/packages/boot/test/bootstrapTests/terminate-governed.test.ts @@ -0,0 +1,69 @@ +import { test as anyTest } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +import type { TestFn } from 'ava'; +import { makeSwingsetTestKit } from '../../tools/supports.js'; + +// A more minimal set would be better. We need governance, but not econ vats. +const PLATFORM_CONFIG = '@agoric/vm-config/decentral-main-vaults-config.json'; + +const makeDefaultTestContext = async t => { + const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { + configSpecifier: PLATFORM_CONFIG, + }); + const { runUtils } = swingsetTestKit; + const { EV } = runUtils; + + // We need to poke at bootstrap vat and wait for results to allow + // SwingSet to finish its boot process before we start the test + const zoe: ZoeService = await EV.vat('bootstrap').consumeItem('zoe'); + + return { ...swingsetTestKit }; +}; + +const test = anyTest as TestFn< + Awaited> +>; + +test.before(async t => { + t.context = await makeDefaultTestContext(t); +}); + +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +test(`Create a contract via core-eval and kill it via core-eval by boardID `, async t => { + const TEST_CONTRACT_LABEL = 'testContractLabel'; + const { runUtils, buildProposal, evalProposal } = t.context; + const { EV } = runUtils; + + // create a contract via core-eval + const testContractProposalArgs = [TEST_CONTRACT_LABEL]; + const creatorProposalMaterials = buildProposal( + '@agoric/governance/test/swingsetTests/contractGovernor/add-governedContract.js', + testContractProposalArgs, + ); + await evalProposal(creatorProposalMaterials); + + const { boardID, publicFacet } = + await EV.vat('bootstrap').consumeItem(TEST_CONTRACT_LABEL); + console.log({ boardID }); + + // confirming the contract actually works + const num = await EV(publicFacet).getNum(); + t.is(num, 602214090000000000000000n); + + // killing via terminate-governed-instance + const targets = [`${boardID}:${TEST_CONTRACT_LABEL}`]; + console.log({ targets }); + + const terminatorProposalMaterials = buildProposal( + '@agoric/builders/scripts/vats/terminate-governed-instance.js', + targets, + ); + await evalProposal(terminatorProposalMaterials); + + // confirm the contract is no longer there + await t.throwsAsync(() => EV(publicFacet).getNum(), { + message: 'vat terminated', + }); +}); diff --git a/packages/builders/scripts/vats/terminate-governed-instance.js b/packages/builders/scripts/vats/terminate-governed-instance.js new file mode 100644 index 00000000000..52a806f1b9a --- /dev/null +++ b/packages/builders/scripts/vats/terminate-governed-instance.js @@ -0,0 +1,147 @@ +/** + * @file Terminate price-feed governor instances such as mainnet v110. + * Functions as both an off-chain builder and an on-chain core-eval. + */ + +/// + +import { E } from '@endo/far'; + +const SELF = '@agoric/builders/scripts/vats/terminate-governed-instance.js'; +const USAGE = `Usage: agoric run /path/to/terminate-governed-instance.js \\ + <$governorInstanceHandleBoardID:$instanceKitLabel>...`; + +const repr = val => + typeof val === 'string' || (typeof val === 'object' && val !== null) + ? JSON.stringify(val) + : String(val); +const defaultMakeError = (strings, ...subs) => + Error( + strings.map((s, i) => `${i === 0 ? '' : repr(subs[i - 1])}${s}`).join(''), + ); +const makeUsageError = (strings, ...subs) => { + const err = defaultMakeError(strings, ...subs); + console.error(err.message); + console.error(USAGE); + return err; +}; + +const rtarget = /^(?board[0-9]+):(?.+)$/; +/** + * @param {string[]} args + * @param {(strings: TemplateStringsArray | string[], ...subs: unknown[]) => Error} [makeError] + * @returns {Array<{boardID: string, instanceKitLabel: string}>} + */ +const parseTargets = (args = [], makeError = defaultMakeError) => { + if (!Array.isArray(args)) throw makeError`invalid targets: ${args}`; + /** @type {Array<{boardID: string, instanceKitLabel: string}>} */ + const targets = []; + const badTargets = []; + for (const arg of args) { + const m = typeof arg === 'string' && arg.match(rtarget); + if (!m) { + badTargets.push(arg); + } else { + // @ts-expect-error cast + targets.push(m.groups); + } + } + if (badTargets.length !== 0) { + throw makeError`malformed target(s): ${badTargets}`; + } else if (targets.length === 0) { + throw makeError`no target(s)`; + } + return targets; +}; + +/** + * @param {BootstrapPowers} powers + * @param {{ options: { targetSpecifiers: string[] } }} config + */ +export const terminateGoverned = async ( + { consume: { board, governedContractKits } }, + { options: { targetSpecifiers } }, +) => { + const { Fail, quote: q } = assert; + const targets = parseTargets(targetSpecifiers, Fail); + const doneP = Promise.allSettled( + targets.map(async ({ boardID, instanceKitLabel }) => { + const logLabel = [boardID, instanceKitLabel]; + const contractInstanceHandle = await E(board).getValue(boardID); + const instanceKit = await E(governedContractKits).get( + // @ts-expect-error TS2345 Property '[tag]' is missing + contractInstanceHandle, + ); + console.log( + `${q(logLabel)} alleged governor contract instance kit`, + instanceKit, + ); + + const instanceAdminFacet = await E( + instanceKit.governorCreatorFacet, + ).getAdminFacet(); + + const { label, governorAdminFacet, adminFacet } = instanceKit; + label === instanceKitLabel || + Fail`${q(logLabel)} unexpected instanceKit label, got ${label} but wanted ${q(instanceKitLabel)}`; + (adminFacet && adminFacet !== governorAdminFacet) || + Fail`${q(logLabel)} instanceKit adminFacet should have been present and different from governorAdminFacet but was ${adminFacet}`; + + console.log({ instanceAdminFacet }); + + await E(instanceAdminFacet).terminateContract( + harden(Error('Governed contract terminated by core-eval')), + ); + console.log('Governed contract terminated by core-eval.'); + + const reason = harden(Error(`core-eval terminating ${label} governor`)); + await E(governorAdminFacet).terminateContract(reason); + console.log(`${q(logLabel)} terminated governor`); + }), + ); + const results = await doneP; + const problems = targets.flatMap(({ boardID, instanceKitLabel }, i) => { + if (results[i].status === 'fulfilled') return []; + return [[boardID, instanceKitLabel, results[i].reason]]; + }); + if (problems.length !== 0) { + console.error('governor termination(s) failed', problems); + Fail`governor termination(s) failed: ${problems}`; + } +}; +harden(terminateGoverned); + +export const getManifest = (_powers, targetSpecifiers) => { + parseTargets(targetSpecifiers); + return { + manifest: { + [terminateGoverned.name]: { + consume: { board: true, governedContractKits: true }, + }, + }, + // Provide `terminateGovernors` a second argument like + // `{ options: { targetSpecifiers } }`. + options: { targetSpecifiers }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async (_utils, targetSpecifiers) => { + parseTargets(targetSpecifiers); + return harden({ + sourceSpec: SELF, + getManifestCall: ['getManifest', targetSpecifiers], + }); +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */ +export default async (homeP, endowments) => { + const { scriptArgs } = endowments; + parseTargets(scriptArgs, makeUsageError); + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(terminateGoverned.name, utils => + defaultProposalBuilder(utils, scriptArgs), + ); +}; diff --git a/packages/governance/package.json b/packages/governance/package.json index cd793427660..06af6074bd2 100644 --- a/packages/governance/package.json +++ b/packages/governance/package.json @@ -78,4 +78,4 @@ "typeCoverage": { "atLeast": 89.49 } -} +} \ No newline at end of file diff --git a/packages/governance/test/swingsetTests/contractGovernor/add-governedContract.js b/packages/governance/test/swingsetTests/contractGovernor/add-governedContract.js new file mode 100644 index 00000000000..a7c210076bc --- /dev/null +++ b/packages/governance/test/swingsetTests/contractGovernor/add-governedContract.js @@ -0,0 +1,207 @@ +/** + * @file Source for a core-eval to start a new governed instance of + * ./governedContract.js. + * Also functions as an off-chain builder script for such core-evals: + * agoric run /path/to/$0 [ [