From 627e966195d14156dd100e3a9946e4f04a571cff Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 6 Feb 2024 17:28:02 -0800 Subject: [PATCH] test: prepare a3p test for vault upgrade --- golang/cosmos/app/app.go | 2 + .../builders/scripts/vats/upgradeVaults.js | 23 +++ .../src/contractGovernance/paramManager.js | 8 +- packages/governance/src/contractHelper.js | 25 ++- .../src/proposals/price-feed-proposal.js | 2 +- .../src/proposals/upgrade-vaults.js | 163 ++++++++++++++++++ .../inter-protocol/src/vaultFactory/params.js | 41 ++++- .../src/vaultFactory/vaultDirector.js | 11 +- .../src/vaultFactory/vaultFactory.js | 5 +- .../src/vaultFactory/vaultManager.js | 3 +- 10 files changed, 252 insertions(+), 31 deletions(-) create mode 100644 packages/builders/scripts/vats/upgradeVaults.js create mode 100644 packages/inter-protocol/src/proposals/upgrade-vaults.js diff --git a/golang/cosmos/app/app.go b/golang/cosmos/app/app.go index 36d1d66d85a..ebdd78232e3 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -911,6 +911,8 @@ func unreleasedUpgradeHandler(app *GaiaApp, targetUpgrade string) func(sdk.Conte ), // Add new auction contract. The old one will be retired shortly. vm.CoreProposalStepForModules( "@agoric/builders/scripts/vats/add-auction.js"), + // upgrade vaultFactory. + vm.CoreProposalStepForModules( "@agoric/builders/scripts/vats/upgradeVaults.js"), } } diff --git a/packages/builders/scripts/vats/upgradeVaults.js b/packages/builders/scripts/vats/upgradeVaults.js new file mode 100644 index 00000000000..ac69a81a37c --- /dev/null +++ b/packages/builders/scripts/vats/upgradeVaults.js @@ -0,0 +1,23 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/inter-protocol/src/proposals/upgrade-vaults.js', + getManifestCall: [ + 'getManifestForUpgradeVaults', + { + vaultsRef: publishRef( + install( + '@agoric/inter-protocol/src/vaultFactory/vaultFactory.js', + '../bundles/bundle-vaultFactory.js', + ), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('upgrade-vaults', defaultProposalBuilder); +}; diff --git a/packages/governance/src/contractGovernance/paramManager.js b/packages/governance/src/contractGovernance/paramManager.js index e300f67bfc1..e7472f8f8c8 100644 --- a/packages/governance/src/contractGovernance/paramManager.js +++ b/packages/governance/src/contractGovernance/paramManager.js @@ -220,10 +220,10 @@ const makeParamManagerBuilder = (publisherKit, zoe) => { if (!zoe) { throw Fail`zoe must be provided for governed Invitations ${zoe}`; } - const { instance, installation } = await E(zoe).getInvitationDetails(i); - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- the build config doesn't expect an error here - // @ts-ignore typedefs say they're guaranteed truthy but just to be safe - assert(instance && installation, 'must be an invitation'); + + // local check on isLive() gives better report than .getInvitationDetails() + const isLive = await E(E(zoe).getInvitationIssuer()).isLive(i); + isLive || Fail`Invitation passed to paramManager is not live ${i}`; }; /** diff --git a/packages/governance/src/contractHelper.js b/packages/governance/src/contractHelper.js index 894f6f041d0..d1ef46c3645 100644 --- a/packages/governance/src/contractHelper.js +++ b/packages/governance/src/contractHelper.js @@ -2,15 +2,15 @@ import { Far } from '@endo/marshal'; import { makeStoredPublisherKit } from '@agoric/notifier'; import { getMethodNames, objectMap } from '@agoric/internal'; import { ignoreContext, prepareExo } from '@agoric/vat-data'; -import { keyEQ, M } from '@agoric/store'; +import { M } from '@agoric/store'; import { AmountShape, BrandShape } from '@agoric/ertp'; import { RelativeTimeRecordShape, TimestampRecordShape } from '@agoric/time'; import { E } from '@endo/eventual-send'; -import { assertElectorateMatches } from './contractGovernance/paramManager.js'; import { makeParamManagerFromTerms } from './contractGovernance/typedParamManager.js'; import { GovernorFacetShape } from './typeGuards.js'; +import { CONTRACT_ELECTORATE } from './contractGovernance/governParam.js'; -const { Fail, quote: q } = assert; +const { Fail } = assert; export const GOVERNANCE_STORAGE_KEY = 'governance'; @@ -31,22 +31,17 @@ const publicMixinAPI = harden({ }); /** + * Verify that the electorate is represented by a live invitation. + * * @param {ZCF & {}>} zcf * @param {import('./contractGovernance/typedParamManager.js').TypedParamManager} paramManager */ export const validateElectorate = (zcf, paramManager) => { - const { governedParams } = zcf.getTerms(); - return E.when(paramManager.getParams(), finishedParams => { - try { - keyEQ(governedParams, finishedParams) || - Fail`The 'governedParams' term must be an object like ${q( - finishedParams, - )}, but was ${q(governedParams)}`; - assertElectorateMatches(paramManager, governedParams); - } catch (err) { - zcf.shutdownWithFailure(err); - } - }); + const invitation = paramManager.getInternalParamValue(CONTRACT_ELECTORATE); + return E.when( + E(zcf.getInvitationIssuer()).isLive(invitation), + isLive => isLive || Fail`Electorate invitation is not live.`, + ); }; harden(validateElectorate); diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 15aabdf6567..ef0808cea6e 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -147,7 +147,7 @@ export const createPriceFeed = async ( ), err => console.error( - `🚨 failed to update priceAggregator instance for ${AGORIC_INSTANCE_NAME}`, + `🚨 failed to update priceAggregator installation for ${AGORIC_INSTANCE_NAME}`, err, ), ); diff --git a/packages/inter-protocol/src/proposals/upgrade-vaults.js b/packages/inter-protocol/src/proposals/upgrade-vaults.js new file mode 100644 index 00000000000..2d54f8de6db --- /dev/null +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -0,0 +1,163 @@ +import { E } from '@endo/far'; +import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; +import { AmountMath } from '@agoric/ertp/src/index.js'; +import { makeScalarMapStore } from '@agoric/store/src/index.js'; + +// stand-in for Promise.any() which isn't available at this point. +const any = promises => + new Promise((resolve, reject) => { + for (const promise of promises) { + promise.then(resolve); + } + void Promise.allSettled(promises).then(results => { + const rejects = results.filter(({ status }) => status === 'rejected'); + if (rejects.length === results.length) { + // @ts-expect-error TypeScript doesn't know enough + const messages = rejects.map(({ message }) => message); + const aggregate = new Error(messages.join(';')); + // @ts-expect-error TypeScript doesn't know enough + aggregate.errors = rejects.map(({ reason }) => reason); + reject(aggregate); + } + }); + }); + +/** + * @param {import('../../src/proposals/econ-behaviors').EconomyBootstrapPowers} powers + * @param {{ options: { vaultsRef: { bundleID: string } } }} options + */ +export const upgradeVaults = async (powers, { options }) => { + const { + consume: { + agoricNamesAdmin, + auctioneerKit: auctioneerKitP, + priceAuthority, + vaultFactoryKit, + zoe, + economicCommitteeCreatorFacet: electorateCreatorFacet, + reserveKit, + }, + } = powers; + const { vaultsRef } = options; + const kit = await vaultFactoryKit; + const auctioneerKit = await auctioneerKitP; + const { instance: directorInstance } = kit; + const allBrands = await E(zoe).getBrands(directorInstance); + const { Minted: istBrand, ...vaultBrands } = allBrands; + + const bundleID = vaultsRef.bundleID; + console.log(`upgradeVaults: bundleId`, bundleID); + let installationP; + await null; + if (vaultsRef) { + if (bundleID) { + installationP = E(zoe).installBundleID(bundleID); + await E.when( + installationP, + installation => + E(E(agoricNamesAdmin).lookupAdmin('installation')).update( + 'vaultFactory', + installation, + ), + err => + console.error(`🚨 failed to update vaultFactory installation`, err), + ); + } + } + + const readManagerParams = async () => { + const { publicFacet: directorPF } = kit; + + await null; + + const params = {}; + for (const kwd of Object.keys(vaultBrands)) { + const b = vaultBrands[kwd]; + const subscription = E(directorPF).getSubscription({ + collateralBrand: b, + }); + const notifier = makeNotifierFromAsyncIterable(subscription); + const { value } = await notifier.getUpdateSince(); + params[kwd] = harden({ + brand: b, + debtLimit: value.current.DebtLimit.value, + interestRate: value.current.InterestRate.value, + liquidationMargin: value.current.LiquidationMargin.value, + liquidationPadding: value.current.LiquidationPadding.value, + liquidationPenalty: value.current.LiquidationPenalty.value, + mintFee: value.current.MintFee.value, + }); + } + return params; + }; + const managerParamValues = await readManagerParams(); + + // upgrade the vaultFactory + const upgradeVaultFactory = async () => { + // @ts-expect-error cast XXX privateArgs missing from type + const { privateArgs } = kit; + + const shortfallInvitation = await E( + E.get(reserveKit).creatorFacet, + ).makeShortfallReportingInvitation(); + + const poserInvitation = await E( + electorateCreatorFacet, + ).getPoserInvitation(); + /** @type {import('../../src/vaultFactory/vaultFactory').VaultFactoryContract['privateArgs']} */ + const newPrivateArgs = harden({ + ...privateArgs, + auctioneerInstance: auctioneerKit.instance, + initialPoserInvitation: poserInvitation, + initialShortfallInvitation: shortfallInvitation, + managerParams: managerParamValues, + }); + + const upgradeResult = await E(kit.adminFacet).upgradeContract( + bundleID, + newPrivateArgs, + ); + + console.log('upgraded vaultFactory.', upgradeResult); + }; + + // Wait for at least one new price feed to be ready before upgrading Vaults + void E.when( + any( + Object.values(vaultBrands).map(brand => + E(priceAuthority).quoteGiven(AmountMath.make(brand, 10n), istBrand), + ), + ), + () => upgradeVaultFactory(), + ); + + console.log(`upgradeVaults scheduled; waiting for priceFeeds`); +}; + +const t = 'upgradeVaults'; +/** + * Return the manifest, installations, and options for upgrading Vaults. + * + * @param {object} _ign + * @param {any} vaultUpgradeOptions + */ +export const getManifestForUpgradeVaults = async ( + _ign, + vaultUpgradeOptions, +) => ({ + manifest: { + [upgradeVaults.name]: { + consume: { + agoricNamesAdmin: t, + auctioneerKit: t, + economicCommitteeCreatorFacet: t, + priceAuthority: t, + reserveKit: t, + vaultFactoryKit: t, + board: t, + zoe: t, + }, + }, + }, + options: { ...vaultUpgradeOptions }, +}); diff --git a/packages/inter-protocol/src/vaultFactory/params.js b/packages/inter-protocol/src/vaultFactory/params.js index 3544a6e5000..8fd360ee7e9 100644 --- a/packages/inter-protocol/src/vaultFactory/params.js +++ b/packages/inter-protocol/src/vaultFactory/params.js @@ -163,6 +163,19 @@ export const makeGovernedTerms = ({ }); }; harden(makeGovernedTerms); + +// XXX Better to declare this as VaultManagerParamValues + brand. How? +/** + * @typedef {object} VaultManagerParams + * @property {Brand} brand + * @property {Ratio} liquidationMargin + * @property {Ratio} liquidationPenalty + * @property {Ratio} interestRate + * @property {Ratio} mintFee + * @property {Amount<'nat'>} debtLimit + * @property {Ratio} [liquidationPadding] + */ + /** * Stop-gap which restores initial param values UNTIL * https://github.com/Agoric/agoric-sdk/issues/5200 @@ -171,8 +184,14 @@ harden(makeGovernedTerms); * * @param {import('@agoric/vat-data').Baggage} baggage * @param {ERef} marshaller + * @param {Record} managerParamValues - sets of + * parameters (plus brand:) keyed by Keyword. override stored initial values */ -export const provideVaultParamManagers = (baggage, marshaller) => { +export const provideVaultParamManagers = ( + baggage, + marshaller, + managerParamValues, +) => { /** @type {MapStore} */ const managers = makeScalarMapStore(); @@ -197,10 +216,24 @@ export const provideVaultParamManagers = (baggage, marshaller) => { return manager; }; - // restore from baggage - // [...managerArgs.entries()].map(([brand, args]) => makeManager(brand, args)); + // restore from baggage, unless `managerParamValues` overrides. for (const [brand, args] of managerArgs.entries()) { - makeManager(brand, args); + let values; + for (const key of Object.keys(managerParamValues)) { + // For a couple of runs, changing to managerParamValues[+key] worked, + // but then that stopped working. Dunno why + // eslint-disable-next-line no-restricted-syntax + if (managerParamValues[key].brand === brand) { + values = managerParamValues[+key]; + break; + } + } + + if (values) { + makeManager(brand, { ...args, initialParamValues: values }); + } else { + makeManager(brand, args); + } } return { diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index e4a26c15e42..4b57e0e725d 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -102,6 +102,7 @@ const prepareVaultDirector = ( marshaller, makeRecorderKit, makeERecorderKit, + managerParams, ) => { /** @type {import('../reserve/assetReserve.js').ShortfallReporter} */ let shortfallReporter; @@ -120,7 +121,11 @@ const prepareVaultDirector = ( // Non-durable map because param managers aren't durable. // In the event they're needed they can be reconstructed from contract terms and off-chain data. /** a powerful object; can modify parameters */ - const vaultParamManagers = provideVaultParamManagers(baggage, marshaller); + const vaultParamManagers = provideVaultParamManagers( + baggage, + marshaller, + managerParams, + ); const metricsNode = E(storageNode).makeChildNode('metrics'); @@ -146,12 +151,10 @@ const prepareVaultDirector = ( const oldInvitation = baggage.has(shortfallInvitationKey) ? baggage.get(shortfallInvitationKey) : undefined; - console.log('@@@@@ Old Invitation', oldInvitation); const newInvitation = await directorParamManager.getInternalParamValue( SHORTFALL_INVITATION_KEY, ); - console.log('@@@@@ New Invitation', newInvitation); if (newInvitation === oldInvitation) { shortfallReporter || @@ -309,7 +312,7 @@ const prepareVaultDirector = ( SubscriberShape, ), getElectorateSubscription: M.call().returns(SubscriberShape), - getGovernedParams: M.call({ collateralBrand: BrandShape }).returns( + getGovernedParams: M.callWhen({ collateralBrand: BrandShape }).returns( M.record(), ), getInvitationAmount: M.call(M.string()).returns(AmountShape), diff --git a/packages/inter-protocol/src/vaultFactory/vaultFactory.js b/packages/inter-protocol/src/vaultFactory/vaultFactory.js index 1e22d2f4f9e..e714f268ee7 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultFactory.js +++ b/packages/inter-protocol/src/vaultFactory/vaultFactory.js @@ -70,6 +70,7 @@ harden(meta); * storageNode: ERef; * marshaller: ERef; * auctioneerInstance: Instance; + * managerParams: Record; * }} privateArgs * @param {import('@agoric/ertp').Baggage} baggage */ @@ -81,6 +82,7 @@ export const start = async (zcf, privateArgs, baggage) => { marshaller, storageNode, auctioneerInstance, + managerParams, } = privateArgs; trace('awaiting debtMint'); @@ -95,7 +97,7 @@ export const start = async (zcf, privateArgs, baggage) => { const { timerService } = zcf.getTerms(); const zoe = zcf.getZoeService(); - const auctioneerPublicFacet = await E(zoe).getPublicFacet(auctioneerInstance); + const auctioneerPublicFacet = E(zoe).getPublicFacet(auctioneerInstance); const { makeRecorderKit, makeERecorderKit } = prepareRecorderKitMakers( baggage, @@ -138,6 +140,7 @@ export const start = async (zcf, privateArgs, baggage) => { marshaller, makeRecorderKit, makeERecorderKit, + managerParams, ); // cannot await because it would make remote calls during vat restart diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index 715a52fbf2c..0306b816095 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -51,7 +51,6 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { PriceQuoteShape, SeatShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/eventual-send'; -import { AuctionPFShape } from '../auction/auctioneer.js'; import { checkDebtLimit, makeNatAmountShape, @@ -335,7 +334,7 @@ export const prepareVaultManagerKit = ( getCollateralQuote: M.call().returns(PriceQuoteShape), getPublicFacet: M.call().returns(M.remotable('publicFacet')), lockOraclePrices: M.call().returns(PriceQuoteShape), - liquidateVaults: M.call(AuctionPFShape).returns(M.promise()), + liquidateVaults: M.call(M.promise()).returns(M.promise()), }), }, initState,