diff --git a/a3p-integration/proposals/a:upgrade-next/agd-tools.js b/a3p-integration/proposals/a:upgrade-next/agd-tools.js new file mode 100644 index 00000000000..90cf71587ab --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/agd-tools.js @@ -0,0 +1,139 @@ +import { + agd, + agops, + agopsLocation, + executeCommand, + VALIDATORADDR, + executeOffer, + GOV1ADDR, + GOV2ADDR, + GOV3ADDR, + newOfferId, + CHAINID, +} from '@agoric/synthetic-chain'; + +const ORACLE_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; + +export const BID_OFFER_ID = 'bid-vaultUpgrade-test3'; + +const queryVstorage = path => + agd.query('vstorage', 'data', '--output', 'json', path); + +// XXX use endo/marshal? +const getQuoteBody = async path => { + const queryOut = await queryVstorage(path); + + const body = JSON.parse(JSON.parse(queryOut.value).values[0]); + return JSON.parse(body.body.substring(1)); +}; + +export const getOracleInstance = async price => { + const instanceRec = await queryVstorage(`published.agoricNames.instance`); + + const value = JSON.parse(instanceRec.value); + const body = JSON.parse(value.values.at(-1)); + + const feeds = JSON.parse(body.body.substring(1)); + const feedName = `${price}-USD price feed`; + + const key = Object.keys(feeds).find(k => feeds[k][0] === feedName); + if (key) { + return body.slots[key]; + } + return null; +}; + +export const checkForOracle = async (t, name) => { + const instance = await getOracleInstance(name); + t.truthy(instance); +}; + +export const addOraclesForBrand = async (brandIn, oraclesByBrand) => { + await null; + const promiseArray = []; + + const oraclesWithID = []; + // newOfferId() waits 1 second + const offerIdBase = await newOfferId(); + for (let i = 0; i < ORACLE_ADDRESSES.length; i += 1) { + const oracleAddress = ORACLE_ADDRESSES[i]; + const offerId = `${offerIdBase}.${i}`; + oraclesWithID.push({ address: oracleAddress, offerId }); + + promiseArray.push( + executeOffer( + oracleAddress, + agops.oracle('accept', '--offerId', offerId, `--pair ${brandIn}.USD`), + ), + ); + } + oraclesByBrand.set(brandIn, oraclesWithID); + + return Promise.all(promiseArray); +}; + +export const pushPrices = (price, brandIn, oraclesByBrand) => { + const promiseArray = []; + + for (const oracle of oraclesByBrand.get(brandIn)) { + promiseArray.push( + executeOffer( + oracle.address, + agops.oracle( + 'pushPriceRound', + '--price', + price, + '--oracleAdminAcceptOfferId', + oracle.offerId, + ), + ), + ); + } + + return Promise.all(promiseArray); +}; + +export const getPriceQuote = async price => { + const path = `published.priceFeed.${price}-USD_price_feed`; + const body = await getQuoteBody(path); + return body.amountOut.value; +}; + +export const agopsInter = (...params) => { + const newParams = ['inter', ...params]; + return executeCommand(agopsLocation, newParams); +}; + +export const createBid = (price, addr, offerId) => { + return agopsInter( + 'bid', + 'by-price', + `--price ${price}`, + `--give 1.0IST`, + '--from', + addr, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); +}; + +export const getLiveOffers = async addr => { + const path = `published.wallet.${addr}.current`; + const body = await getQuoteBody(path); + return body.liveOffers; +}; + +export const getAuctionCollateral = async index => { + const path = `published.auction.book${index}`; + const body = await getQuoteBody(path); + return body.collateralAvailable.value; +}; + +export const bankSend = (addr, wanted) => { + const chain = ['--chain-id', CHAINID]; + const from = ['--from', VALIDATORADDR]; + const testKeyring = ['--keyring-backend', 'test']; + const noise = [...from, ...chain, ...testKeyring, '--yes']; + + return agd.tx('bank', 'send', VALIDATORADDR, addr, wanted, ...noise); +}; diff --git a/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js b/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js deleted file mode 100644 index 5dba0181116..00000000000 --- a/a3p-integration/proposals/a:upgrade-next/priceFeed.test.js +++ /dev/null @@ -1,150 +0,0 @@ -import test from 'ava'; - -import { - agd, - agops, - executeOffer, - getVatDetails, - GOV1ADDR, - GOV2ADDR, - GOV3ADDR, - newOfferId, -} from '@agoric/synthetic-chain'; - -const ORACLE_ADDRESSES = [GOV1ADDR, GOV2ADDR, GOV3ADDR]; - -const getOracleInstance = async price => { - const instanceRec = await agd.query( - 'vstorage', - 'data', - '--output', - 'json', - `published.agoricNames.instance`, - ); - - // agd query -o json vstorage data published.agoricNames.instance - // |& jq '.value | fromjson | .values[-1] | fromjson | .body[1:] - // | fromjson | .[-2] ' - - const value = JSON.parse(instanceRec.value); - const body = JSON.parse(value.values.at(-1)); - - const feeds = JSON.parse(body.body.substring(1)); - const feedName = `${price}-USD price feed`; - - const key = Object.keys(feeds).find(k => feeds[k][0] === feedName); - if (key) { - return body.slots[key]; - } - return null; -}; - -const checkForOracle = async (t, name) => { - const instance = await getOracleInstance(name); - t.truthy(instance); -}; - -test.serial('check all priceFeed vats updated', async t => { - const atomDetails = await getVatDetails('ATOM-USD_price_feed'); - // both the original and the new ATOM vault are incarnation 0 - t.is(atomDetails.incarnation, 0); - const stAtomDetails = await getVatDetails('stATOM'); - t.is(stAtomDetails.incarnation, 0); - const stOsmoDetails = await getVatDetails('stOSMO'); - t.is(stOsmoDetails.incarnation, 0); - const stTiaDetails = await getVatDetails('stTIA'); - t.is(stTiaDetails.incarnation, 0); - await checkForOracle(t, 'ATOM'); - await checkForOracle(t, 'stATOM'); - await checkForOracle(t, 'stTIA'); - await checkForOracle(t, 'stOSMO'); -}); - -const oraclesByBrand = new Map(); - -const addOraclesForBrand = async brandIn => { - await null; - const promiseArray = []; - - const oraclesWithID = []; - for (const oracleAddress of ORACLE_ADDRESSES) { - const offerId = await newOfferId(); - oraclesWithID.push({ address: oracleAddress, offerId }); - - promiseArray.push( - executeOffer( - oracleAddress, - agops.oracle('accept', '--offerId', offerId, `--pair ${brandIn}.USD`), - ), - ); - } - oraclesByBrand.set(brandIn, oraclesWithID); - - return Promise.all(promiseArray); -}; - -const pushPrices = (price = 10.0, brandIn) => { - const promiseArray = []; - - for (const oracle of oraclesByBrand.get(brandIn)) { - promiseArray.push( - executeOffer( - oracle.address, - agops.oracle( - 'pushPriceRound', - '--price', - price, - '--oracleAdminAcceptOfferId', - oracle.offerId, - ), - ), - ); - } - - return Promise.all(promiseArray); -}; - -const getPriceQuote = async price => { - const priceQuote = await agd.query( - 'vstorage', - 'data', - '--output', - 'json', - `published.priceFeed.${price}-USD_price_feed`, - ); - - const body = JSON.parse(JSON.parse(priceQuote.value).values[0]); - const bodyTruncated = JSON.parse(body.body.substring(1)); - return bodyTruncated.amountOut.value; -}; - -test.serial('push prices', async t => { - // There are no old prices for the other currencies. - t.log('awaiting ATOM price pre'); - const atomOutPre = await getPriceQuote('ATOM'); - t.is(atomOutPre, '+12010000'); - - t.log('adding oracle for each brand'); - await addOraclesForBrand('ATOM'); - await addOraclesForBrand('stATOM'); - await addOraclesForBrand('stTIA'); - await addOraclesForBrand('stOSMO'); - - t.log('pushing new prices'); - await pushPrices(11.2, 'ATOM'); - await pushPrices(11.3, 'stTIA'); - await pushPrices(11.4, 'stATOM'); - await pushPrices(11.5, 'stOSMO'); - - t.log('awaiting new quotes'); - // agd query -o json vstorage data published.priceFeed.stOSMO-USD_price_feed |& - // jq '.value | fromjson | .values[0] | fromjson | .body[1:] | fromjson | .amountOut.value' - const atomOut = await getPriceQuote('ATOM'); - t.is(atomOut, '+11200000'); - const tiaOut = await getPriceQuote('stTIA'); - t.is(tiaOut, '+11300000'); - const stAtomOut = await getPriceQuote('stATOM'); - t.is(stAtomOut, '+11400000'); - const osmoOut = await getPriceQuote('stOSMO'); - t.is(osmoOut, '+11500000'); -}); diff --git a/a3p-integration/proposals/a:upgrade-next/test.sh b/a3p-integration/proposals/a:upgrade-next/test.sh index 6c8533d07f0..215f30f6aaf 100755 --- a/a3p-integration/proposals/a:upgrade-next/test.sh +++ b/a3p-integration/proposals/a:upgrade-next/test.sh @@ -3,9 +3,10 @@ # Place here any test that should be executed using the executed proposal. # The effects of this step are not persisted in further proposal layers. +GLOBIGNORE=initial.test.js + # test the state right after upgrade yarn ava initial.test.js # test more, in ways that changes system state -GLOBIGNORE=initial.test.js yarn ava ./*.test.js diff --git a/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js b/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js new file mode 100644 index 00000000000..46b179b7b65 --- /dev/null +++ b/a3p-integration/proposals/a:upgrade-next/upgradeVaults.test.js @@ -0,0 +1,129 @@ +import test from 'ava'; + +import { + agops, + ATOM_DENOM, + getISTBalance, + getVatDetails, + openVault, + USER1ADDR, +} from '@agoric/synthetic-chain'; + +import { + addOraclesForBrand, + bankSend, + BID_OFFER_ID, + checkForOracle, + createBid, + getLiveOffers, + getPriceQuote, + pushPrices, +} from './agd-tools.js'; +import { getDetailsMatchingVats } from './vatDetails.js'; + +const checkPriceFeedVatsUpdated = async t => { + const atomDetails = await getVatDetails('ATOM-USD_price_feed'); + // both the original and the new ATOM vault are incarnation 0 + t.is(atomDetails.incarnation, 0); + const stAtomDetails = await getVatDetails('stATOM'); + t.is(stAtomDetails.incarnation, 0); + const stOsmoDetails = await getVatDetails('stOSMO'); + t.is(stOsmoDetails.incarnation, 0); + const stTiaDetails = await getVatDetails('stTIA'); + t.is(stTiaDetails.incarnation, 0); + await Promise.all([ + checkForOracle(t, 'ATOM'), + checkForOracle(t, 'stATOM'), + checkForOracle(t, 'stTIA'), + checkForOracle(t, 'stOSMO'), + ]); +}; + +const oraclesByBrand = new Map(); + +const tryPushPrices = async t => { + // There are no old prices for the other currencies. + const atomOutPre = await getPriceQuote('ATOM'); + t.is(atomOutPre, '+12010000'); + + t.log('adding oracle for each brand'); + await addOraclesForBrand('ATOM', oraclesByBrand); + await addOraclesForBrand('stATOM', oraclesByBrand); + await addOraclesForBrand('stTIA', oraclesByBrand); + await addOraclesForBrand('stOSMO', oraclesByBrand); + + t.log('pushing new prices'); + await pushPrices(11.2, 'ATOM', oraclesByBrand); + await pushPrices(11.3, 'stTIA', oraclesByBrand); + await pushPrices(11.4, 'stATOM', oraclesByBrand); + await pushPrices(11.5, 'stOSMO', oraclesByBrand); + + t.log('awaiting new quotes'); + const atomOut = await getPriceQuote('ATOM'); + t.is(atomOut, '+11200000'); + const tiaOut = await getPriceQuote('stTIA'); + t.is(tiaOut, '+11300000'); + const stAtomOut = await getPriceQuote('stATOM'); + t.is(stAtomOut, '+11400000'); + const osmoOut = await getPriceQuote('stOSMO'); + t.is(osmoOut, '+11500000'); +}; + +const createNewBid = async t => { + await createBid('20', USER1ADDR, BID_OFFER_ID); + const liveOffer = await getLiveOffers(USER1ADDR); + t.true(liveOffer[0].includes(BID_OFFER_ID)); +}; + +const openMarginalVault = async t => { + let user1IST = await getISTBalance(USER1ADDR); + await bankSend(USER1ADDR, `20000000${ATOM_DENOM}`); + const currentVaults = await agops.vaults('list', '--from', USER1ADDR); + + t.log('opening a vault'); + await openVault(USER1ADDR, 5, 10); + user1IST += 5; + const istBalanceAfterVaultOpen = await getISTBalance(USER1ADDR); + t.is(istBalanceAfterVaultOpen, user1IST); + + const activeVaultsAfter = await agops.vaults('list', '--from', USER1ADDR); + t.log(currentVaults, activeVaultsAfter); + t.true( + activeVaultsAfter.length > currentVaults.length, + `vaults count should increase, ${activeVaultsAfter.length}, ${currentVaults.length}`, + ); +}; + +const triggerAuction = async t => { + await pushPrices(5.2, 'ATOM', oraclesByBrand); + + const atomOut = await getPriceQuote('ATOM'); + t.is(atomOut, '+5200000'); +}; + +const makeNewAuctionVat = async t => { + const details = await getDetailsMatchingVats('auctioneer'); + // This query matches both the auction and its governor, so double the count + t.true(Object.keys(details).length > 2); +}; + +// test.serial() isn't guaranteed to run tests in order, so we run the intended tests here +test('liquidation post upgrade', async t => { + t.log('starting upgrade vaults test'); + await checkPriceFeedVatsUpdated(t); + + t.log('starting pushPrices'); + await tryPushPrices(t); + + t.log('create a new Bid for the auction'); + await createNewBid(t); + + t.log('open a marginal vault'); + await openMarginalVault(t); + + t.log('trigger Auction'); + await triggerAuction(t); + + t.log('make new auction'); + await makeNewAuctionVat(t); +}); diff --git a/a3p-integration/scripts/generate-a3p-submissions.sh b/a3p-integration/scripts/generate-a3p-submissions.sh index 527e1755429..1b01b7cc85f 100755 --- a/a3p-integration/scripts/generate-a3p-submissions.sh +++ b/a3p-integration/scripts/generate-a3p-submissions.sh @@ -1,7 +1,8 @@ #!/bin/bash set -ueo pipefail -SCRIPT_DIR=$( cd ${0%/*} && pwd -P ) +# cd prints its target, so without the redirect, we get two copies +SCRIPT_DIR=$( cd ${0%/*} > /dev/null && pwd -P ) IFS=$'\n' diff --git a/golang/cosmos/app/app.go b/golang/cosmos/app/app.go index 1d456668cb2..d2794e32717 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -916,6 +916,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/boot/test/bootstrapTests/test-vaults-upgrade.ts b/packages/boot/test/bootstrapTests/test-vaults-upgrade.ts index a580a039fbf..cb7cf1233b7 100644 --- a/packages/boot/test/bootstrapTests/test-vaults-upgrade.ts +++ b/packages/boot/test/bootstrapTests/test-vaults-upgrade.ts @@ -287,8 +287,25 @@ test.serial('restart vaultFactory', async t => { 'vaultFactoryKit', ) as EconomyBootstrapSpace['consume']['vaultFactoryKit']); - // @ts-expect-error cast XXX missing from type - const { privateArgs } = vaultFactoryKit; + const reserveKit = await (EV.vat('bootstrap').consumeItem( + 'reserveKit', + ) as EconomyBootstrapSpace['consume']['reserveKit']); + const bootstrapVat = EV.vat('bootstrap'); + const electorateCreatorFacet = await (bootstrapVat.consumeItem( + 'economicCommitteeCreatorFacet', + ) as EconomyBootstrapSpace['consume']['economicCommitteeCreatorFacet']); + + const poserInvitation = await EV(electorateCreatorFacet).getPoserInvitation(); + const creatorFacet1 = await EV.get(reserveKit).creatorFacet; + const shortfallInvitation = + await EV(creatorFacet1).makeShortfallReportingInvitation(); + + const privateArgs = { + // @ts-expect-error cast XXX missing from type + ...vaultFactoryKit.privateArgs, + initialPoserInvitation: poserInvitation, + initialShortfallInvitation: shortfallInvitation, + }; console.log('reused privateArgs', privateArgs, vaultFactoryKit); const vfAdminFacet = await EV( 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 515e7579f5a..ed96f7cb3df 100644 --- a/packages/governance/src/contractGovernance/paramManager.js +++ b/packages/governance/src/contractGovernance/paramManager.js @@ -224,10 +224,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); - // @ts-expect-error 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 4b4486e693e..f03112e29ab 100644 --- a/packages/governance/src/contractHelper.js +++ b/packages/governance/src/contractHelper.js @@ -2,19 +2,19 @@ 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'; /** * @import {VoteCounterCreatorFacet, VoteCounterPublicFacet, QuestionSpec, OutcomeRecord, AddQuestion, AddQuestionReturn, GovernanceSubscriptionState, GovernanceTerms, GovernedApis, GovernedCreatorFacet, GovernedPublicFacet} from './types.js'; */ -const { Fail, quote: q } = assert; +const { Fail } = assert; export const GOVERNANCE_STORAGE_KEY = 'governance'; @@ -35,22 +35,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/governance/test/swingsetTests/contractGovernor/test-governor.js b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js index 944f419e7f1..d1bb1317bc5 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js +++ b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js @@ -162,8 +162,8 @@ test.serial('brokenUpdateStart', async t => { // TODO: allow either message // 'vote rejected outcome: Error: (an object) was not a live payment for brand (an object). It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', // 'update failed: Error: (an object) was not a live payment for brand (an object). It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', - 'vote rejected outcome: Error: A Zoe invitation is required, not (an object)', - 'update failed: Error: A Zoe invitation is required, not (an object)', + 'vote rejected outcome: Error: (an object) was not a live payment for brand "[Alleged: Zoe Invitation brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', + 'update failed: Error: (an object) was not a live payment for brand "[Alleged: Zoe Invitation brand]". It could be a used-up payment, a payment for another brand, or it might not be a payment at all.', ]); }); diff --git a/packages/governance/test/unitTests/test-puppetContractGovernor.js b/packages/governance/test/unitTests/test-puppetContractGovernor.js index 161cefe6a22..1664e28e867 100644 --- a/packages/governance/test/unitTests/test-puppetContractGovernor.js +++ b/packages/governance/test/unitTests/test-puppetContractGovernor.js @@ -59,7 +59,7 @@ test('multiple params bad change', async t => { () => E(governorFacets.creatorFacet).changeParams(paramChangesSpec), { message: - 'In "getInvitationDetails" method of (ZoeService): arg 0: "[13n]" - Must match one of ["[match:remotable]","[match:kind]"]', + 'In "getAmountOf" method of (Zoe Invitation issuer): arg 0: "[13n]" - Must be a remotable Payment, not bigint', }, ); }); diff --git a/packages/inter-protocol/src/proposals/add-auction.js b/packages/inter-protocol/src/proposals/add-auction.js index 9bc60bcdb5f..1eafb2e747f 100644 --- a/packages/inter-protocol/src/proposals/add-auction.js +++ b/packages/inter-protocol/src/proposals/add-auction.js @@ -125,7 +125,6 @@ export const addAuction = async ({ ), ); - // don't overwrite auctioneerKit yet newAuctioneerKit.resolve( harden({ label: 'auctioneer', @@ -139,7 +138,8 @@ export const addAuction = async ({ governorAdminFacet: governorStartResult.adminFacet, }), ); - // don't replace auction instance yet. + // don't overwrite auctioneerKit or auction instance yet. Wait until + // upgrade-vault.js }; export const ADD_AUCTION_MANIFEST = harden({ diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index 2ffaf3ddb14..c5fec37281f 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -64,7 +64,7 @@ export const SECONDS_PER_WEEK = 7n * SECONDS_PER_DAY; * >; * vaultFactoryKit: GovernanceFacetKit; * auctioneerKit: AuctioneerKit; - * newAuctioneerKit: AuctioneerKit; + * newAuctioneerKit: AuctioneerKit | undefined; * minInitialDebt: NatValue; * }>} EconomyBootstrapSpace */ @@ -232,14 +232,14 @@ export const setupVaultFactoryArguments = async ( initialShortfallInvitation, shortfallInvitationAmount, feeMintAccess, - auctioneerPublicFacet, + auctioneerInstance, ] = await Promise.all([ poserInvitationP, E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), shortfallInvitationP, E(E(zoe).getInvitationIssuer()).getAmountOf(shortfallInvitationP), feeMintAccessP, - E.get(auctioneerKit).publicFacet, + E.get(auctioneerKit).instance, ]); const reservePublicFacet = await E.get(reserveKit).publicFacet; @@ -248,7 +248,6 @@ export const setupVaultFactoryArguments = async ( const vaultFactoryTerms = makeGovernedVFTerms({ priceAuthority, - auctioneerPublicFacet, reservePublicFacet, interestTiming, timer: chainTimerService, @@ -260,6 +259,7 @@ export const setupVaultFactoryArguments = async ( }); const vaultFactoryPrivateArgs = { + auctioneerInstance, feeMintAccess, initialPoserInvitation, initialShortfallInvitation, 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..0c7ee64caaf --- /dev/null +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -0,0 +1,202 @@ +import { E } from '@endo/far'; +import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; +import { AmountMath } from '@agoric/ertp/src/index.js'; +import { makeTracer } from '@agoric/internal/src/index.js'; + +const trace = makeTracer('upgrade Vaults proposal'); + +// 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, + newAuctioneerKit: auctioneerKitP, + priceAuthority, + vaultFactoryKit, + zoe, + economicCommitteeCreatorFacet: electorateCreatorFacet, + reserveKit, + }, + produce: { + auctioneerKit: auctioneerKitProducer, + newAuctioneerKit: tempAuctioneerKit, + }, + instance: { + produce: { auctioneer: auctioneerProducer }, + }, + } = 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); + /** + * @type {Promise< + * Installation + * >} + */ + 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 collateralBrand = vaultBrands[kwd]; + const subscription = E(directorPF).getSubscription({ + collateralBrand, + }); + const notifier = makeNotifierFromAsyncIterable(subscription); + let { value, updateCount } = await notifier.getUpdateSince(0n); + // @ts-expect-error It's an amount. + while (AmountMath.isEmpty(value.current.DebtLimit.value)) { + ({ value, updateCount } = await notifier.getUpdateSince(updateCount)); + trace(`debtLimit was empty, retried`, value.current.DebtLimit.value); + } + trace(kwd, 'params at', updateCount, 'are', value.current); + params[kwd] = harden({ + brand: collateralBrand, + 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, + // @ts-expect-error It has a value until reset after the upgrade + 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), + ), + ), + async price => { + trace(`upgrading after delay`, price); + await upgradeVaultFactory(); + auctioneerKitProducer.reset(); + // @ts-expect-error auctioneerKit is non-null except between auctioneerKitProducer.reset() and auctioneerKitProducer.resolve() + auctioneerKitProducer.resolve(auctioneerKit); + auctioneerProducer.reset(); + // @ts-expect-error auctioneerKit is non-null except between auctioneerKitProducer.reset() and auctioneerKitProducer.resolve() + auctioneerProducer.resolve(auctioneerKit.instance); + // We wanted it to be valid for only a short while. + tempAuctioneerKit.reset(); + await E(E(agoricNamesAdmin).lookupAdmin('instance')).update( + 'auctioneer', + // @ts-expect-error auctioneerKit is non-null except between auctioneerKitProducer.reset() and auctioneerKitProducer.resolve() + auctioneerKit.instance, + ); + }, + ); + + console.log(`upgradeVaults scheduled; waiting for priceFeeds`); +}; + +const uV = '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: uV, + newAuctioneerKit: uV, + economicCommitteeCreatorFacet: uV, + priceAuthority: uV, + reserveKit: uV, + vaultFactoryKit: uV, + board: uV, + zoe: uV, + }, + produce: { auctioneerKit: uV, newAuctioneerKit: uV }, + instance: { produce: { auctioneer: uV, newAuctioneerKit: uV } }, + }, + }, + options: { ...vaultUpgradeOptions }, +}); diff --git a/packages/inter-protocol/src/vaultFactory/params.js b/packages/inter-protocol/src/vaultFactory/params.js index 37a14d6c713..b232f5cb1bf 100644 --- a/packages/inter-protocol/src/vaultFactory/params.js +++ b/packages/inter-protocol/src/vaultFactory/params.js @@ -12,6 +12,7 @@ import { M, makeScalarMapStore } from '@agoric/store'; import { TimeMath } from '@agoric/time'; import { provideDurableMapStore } from '@agoric/vat-data'; import { subtractRatios } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { makeTracer } from '@agoric/internal/src/index.js'; import { amountPattern, ratioPattern } from '../contractSupport.js'; /** @import {PriceAuthority, PriceDescription, PriceQuote, PriceQuoteValue, PriceQuery,} from '@agoric/zoe/tools/types.js'; */ @@ -37,6 +38,8 @@ export const vaultDirectorParamTypes = { }; harden(vaultDirectorParamTypes); +const trace = makeTracer('Vault Params'); + /** * @param {Amount<'set'>} electorateInvitationAmount * @param {Amount<'nat'>} minInitialDebt @@ -128,7 +131,6 @@ export const vaultParamPattern = M.splitRecord( /** * @param {{ - * auctioneerPublicFacet: ERef; * electorateInvitationAmount: Amount<'set'>; * minInitialDebt: Amount<'nat'>; * bootstrapPaymentValue: bigint; @@ -141,7 +143,6 @@ export const vaultParamPattern = M.splitRecord( * }} opts */ export const makeGovernedTerms = ({ - auctioneerPublicFacet, bootstrapPaymentValue, electorateInvitationAmount, interestTiming, @@ -153,7 +154,6 @@ export const makeGovernedTerms = ({ referencedUi = 'NO REFERENCE', }) => { return harden({ - auctioneerPublicFacet, priceAuthority, reservePublicFacet, timerService: timer, @@ -168,6 +168,9 @@ export const makeGovernedTerms = ({ }); }; harden(makeGovernedTerms); + +/** @typedef {VaultManagerParamValues & { brand: Brand }} VaultManagerParamOverrides */ + /** * Stop-gap which restores initial param values UNTIL * https://github.com/Agoric/agoric-sdk/issues/5200 @@ -176,8 +179,13 @@ harden(makeGovernedTerms); * * @param {import('@agoric/vat-data').Baggage} baggage * @param {ERef} marshaller + * @param {Record} managerParamOverrides */ -export const provideVaultParamManagers = (baggage, marshaller) => { +export const provideVaultParamManagers = ( + baggage, + marshaller, + managerParamOverrides, +) => { /** @type {MapStore} */ const managers = makeScalarMapStore(); @@ -202,8 +210,20 @@ export const provideVaultParamManagers = (baggage, marshaller) => { return manager; }; - // restore from baggage - [...managerArgs.entries()].map(([brand, args]) => makeManager(brand, args)); + // restore from baggage, unless `managerParamOverrides` overrides. + for (const [brand, args] of managerArgs.entries()) { + const newInitial = managerParamOverrides + ? Object.values(managerParamOverrides).find(e => e.brand === brand) + : undefined; + + if (newInitial) { + trace(`reviving params, override`, brand, newInitial); + makeManager(brand, { ...args, initialParamValues: newInitial }); + } else { + trace(`reviving params, keeping`, brand, args.initialParamValues); + makeManager(brand, args); + } + } return { /** diff --git a/packages/inter-protocol/src/vaultFactory/vaultDirector.js b/packages/inter-protocol/src/vaultFactory/vaultDirector.js index e2aef962a78..701dc0ac80c 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultDirector.js +++ b/packages/inter-protocol/src/vaultFactory/vaultDirector.js @@ -4,7 +4,10 @@ import '@agoric/zoe/src/contracts/exported.js'; import '@agoric/governance/exported.js'; import { AmountMath, AmountShape, BrandShape, IssuerShape } from '@agoric/ertp'; -import { GovernorFacetShape } from '@agoric/governance/src/typeGuards.js'; +import { + GovernorFacetShape, + InvitationShape, +} from '@agoric/governance/src/typeGuards.js'; import { makeTracer } from '@agoric/internal'; import { M, mustMatch } from '@agoric/store'; import { @@ -99,6 +102,7 @@ const prepareVaultDirector = ( marshaller, makeRecorderKit, makeERecorderKit, + managerParams, ) => { /** @type {import('../reserve/assetReserve.js').ShortfallReporter} */ let shortfallReporter; @@ -117,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'); @@ -143,6 +151,7 @@ const prepareVaultDirector = ( const oldInvitation = baggage.has(shortfallInvitationKey) ? baggage.get(shortfallInvitationKey) : undefined; + const newInvitation = await directorParamManager.getInternalParamValue( SHORTFALL_INVITATION_KEY, ); @@ -294,6 +303,7 @@ const prepareVaultDirector = ( makePriceLockWaker: M.call().returns(M.remotable('TimerWaker')), makeLiquidationWaker: M.call().returns(M.remotable('TimerWaker')), makeReschedulerWaker: M.call().returns(M.remotable('TimerWaker')), + setShortfallReporter: M.call(InvitationShape).returns(M.promise()), }), public: M.interface('public', { getCollateralManager: M.call(BrandShape).returns(M.remotable()), @@ -437,6 +447,12 @@ const prepareVaultDirector = ( allManagersDo(vm => vm.lockOraclePrices()); }); }, + async setShortfallReporter(newInvitation) { + const zoe = zcf.getZoeService(); + shortfallReporter = await E( + E(zoe).offer(newInvitation), + ).getOfferResult(); + }, }, public: { /** @param {Brand} brandIn */ diff --git a/packages/inter-protocol/src/vaultFactory/vaultFactory.js b/packages/inter-protocol/src/vaultFactory/vaultFactory.js index cce581eef8d..2ee4f2001ae 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultFactory.js +++ b/packages/inter-protocol/src/vaultFactory/vaultFactory.js @@ -38,8 +38,7 @@ const trace = makeTracer('VF', true); /** * @typedef {ZCF< - * GovernanceTerms & { - * auctioneerPublicFacet: import('../auction/auctioneer.js').AuctioneerPublicFacet; + * GovernanceTerms & { * priceAuthority: ERef; * reservePublicFacet: AssetReservePublicFacet; * timerService: import('@agoric/time').TimerService; @@ -73,6 +72,11 @@ harden(meta); * initialShortfallInvitation: Invitation; * storageNode: ERef; * marshaller: ERef; + * auctioneerInstance: Instance; + * managerParams: Record< + * string, + * import('./params.js').VaultManagerParamOverrides + * >; * }} privateArgs * @param {import('@agoric/swingset-liveslots').Baggage} baggage */ @@ -83,6 +87,8 @@ export const start = async (zcf, privateArgs, baggage) => { initialShortfallInvitation, marshaller, storageNode, + auctioneerInstance, + managerParams, } = privateArgs; trace('awaiting debtMint'); @@ -94,7 +100,10 @@ export const start = async (zcf, privateArgs, baggage) => { mintedIssuerRecord: debtMint.getIssuerRecord(), })); - const { timerService, auctioneerPublicFacet } = zcf.getTerms(); + const { timerService } = zcf.getTerms(); + + const zoe = zcf.getZoeService(); + const auctioneerPublicFacet = E(zoe).getPublicFacet(auctioneerInstance); const { makeRecorderKit, makeERecorderKit } = prepareRecorderKitMakers( baggage, @@ -137,6 +146,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 d4e39a39055..fbbde1eb542 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, @@ -63,6 +62,7 @@ import { calculateMinimumCollateralization, minimumPrice } from './math.js'; import { makePrioritizedVaults } from './prioritizedVaults.js'; import { Phase, prepareVault } from './vault.js'; import { calculateDistributionPlan } from './proceeds.js'; +import { AuctionPFShape } from '../auction/auctioneer.js'; /** * @import {Baggage} from '@agoric/vat-data'; @@ -340,7 +340,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.eref(AuctionPFShape)).returns(M.promise()), }), }, initState,