From eae60bea7ffba9506ca35ab31274ffc534c4b87a Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Tue, 23 Apr 2024 17:33:41 -0700 Subject: [PATCH] feat: upgrade vaults and add a new auction --- .../scripts/generate-a3p-submissions.sh | 3 +- golang/cosmos/app/app.go | 4 + packages/builders/scripts/vats/add-auction.js | 14 ++ .../builders/scripts/vats/upgradeVaults.js | 23 ++ .../src/proposals/add-auction.js | 177 ++++++++++++++++ .../src/proposals/upgrade-vaults.js | 196 ++++++++++++++++++ 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 packages/builders/scripts/vats/add-auction.js create mode 100644 packages/builders/scripts/vats/upgradeVaults.js create mode 100644 packages/inter-protocol/src/proposals/add-auction.js create mode 100644 packages/inter-protocol/src/proposals/upgrade-vaults.js diff --git a/a3p-integration/scripts/generate-a3p-submissions.sh b/a3p-integration/scripts/generate-a3p-submissions.sh index 527e1755429..9e14163bd9e 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 on some platforms. Without the redirect, we get 2 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 5f0c0cb70dd..d2794e32717 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -914,6 +914,10 @@ func unreleasedUpgradeHandler(app *GaiaApp, targetUpgrade string) func(sdk.Conte "@agoric/builders/scripts/vats/updateStOsmoPriceFeed.js", "@agoric/builders/scripts/vats/updateStTiaPriceFeed.js", ), + // 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/add-auction.js b/packages/builders/scripts/vats/add-auction.js new file mode 100644 index 00000000000..8a248f17d17 --- /dev/null +++ b/packages/builders/scripts/vats/add-auction.js @@ -0,0 +1,14 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async () => { + return harden({ + sourceSpec: '@agoric/inter-protocol/src/proposals/add-auction.js', + getManifestCall: ['getManifestForAddAuction'], + }); +}; + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('add-auction', defaultProposalBuilder); +}; 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/inter-protocol/src/proposals/add-auction.js b/packages/inter-protocol/src/proposals/add-auction.js new file mode 100644 index 00000000000..1eafb2e747f --- /dev/null +++ b/packages/inter-protocol/src/proposals/add-auction.js @@ -0,0 +1,177 @@ +import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; +import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { E } from '@endo/far'; +import { Stable } from '@agoric/internal/src/tokens.js'; +import { makeGovernedTerms as makeGovernedATerms } from '../auction/params.js'; + +const trace = makeTracer('NewAuction', true); + +/** @param {import('./econ-behaviors.js').EconomyBootstrapPowers} powers */ +export const addAuction = async ({ + consume: { + zoe, + board, + chainTimerService, + priceAuthority, + chainStorage, + economicCommitteeCreatorFacet: electorateCreatorFacet, + auctioneerKit: legacyKitP, + }, + produce: { newAuctioneerKit }, + instance: { + consume: { reserve: reserveInstance }, + }, + installation: { + consume: { + auctioneer: auctionInstallation, + contractGovernor: contractGovernorInstallation, + }, + }, + issuer: { + consume: { [Stable.symbol]: stableIssuerP }, + }, +}) => { + trace('addAuction start'); + const STORAGE_PATH = 'auction'; + + const poserInvitationP = E(electorateCreatorFacet).getPoserInvitation(); + + const [ + initialPoserInvitation, + electorateInvitationAmount, + stableIssuer, + legacyKit, + ] = await Promise.all([ + poserInvitationP, + E(E(zoe).getInvitationIssuer()).getAmountOf(poserInvitationP), + stableIssuerP, + legacyKitP, + ]); + + // Each field has an extra layer of type + value: + // AuctionStartDelay: { type: 'relativeTime', value: { relValue: 2n, timerBrand: Object [Alleged: timerBrand] {} } } + /** @type {any} */ + const paramValues = await E(legacyKit.publicFacet).getGovernedParams(); + const params = harden({ + StartFrequency: paramValues.StartFrequency.value, + ClockStep: paramValues.ClockStep.value, + StartingRate: paramValues.StartingRate.value, + LowestRate: paramValues.LowestRate.value, + DiscountStep: paramValues.DiscountStep.value, + AuctionStartDelay: paramValues.AuctionStartDelay.value, + PriceLockPeriod: paramValues.PriceLockPeriod.value, + }); + const timerBrand = await E(chainTimerService).getTimerBrand(); + + const storageNode = await makeStorageNodeChild(chainStorage, STORAGE_PATH); + const marshaller = await E(board).getReadonlyMarshaller(); + + const reservePublicFacet = await E(zoe).getPublicFacet(reserveInstance); + + const auctionTerms = makeGovernedATerms( + { storageNode, marshaller }, + chainTimerService, + priceAuthority, + reservePublicFacet, + { + ...params, + ElectorateInvitationAmount: electorateInvitationAmount, + TimerBrand: timerBrand, + }, + ); + + const governorTerms = await deeplyFulfilledObject( + harden({ + timer: chainTimerService, + governedContractInstallation: auctionInstallation, + governed: { + terms: auctionTerms, + issuerKeywordRecord: { Bid: stableIssuer }, + storageNode, + marshaller, + label: 'auctioneer', + }, + }), + ); + + /** @type {GovernorStartedInstallationKit} */ + const governorStartResult = await E(zoe).startInstance( + contractGovernorInstallation, + undefined, + governorTerms, + harden({ + electorateCreatorFacet, + governed: { + initialPoserInvitation, + storageNode, + marshaller, + }, + }), + 'auctioneer.governor', + ); + + const [governedInstance, governedCreatorFacet, governedPublicFacet] = + await Promise.all([ + E(governorStartResult.creatorFacet).getInstance(), + E(governorStartResult.creatorFacet).getCreatorFacet(), + E(governorStartResult.creatorFacet).getPublicFacet(), + ]); + + const allIssuers = await E(zoe).getIssuers(legacyKit.instance); + const { Bid: _istIssuer, ...auctionIssuers } = allIssuers; + await Promise.all( + Object.keys(auctionIssuers).map(kwd => + E(governedCreatorFacet).addBrand(auctionIssuers[kwd], kwd), + ), + ); + + newAuctioneerKit.resolve( + harden({ + label: 'auctioneer', + creatorFacet: governedCreatorFacet, + adminFacet: governorStartResult.adminFacet, + publicFacet: governedPublicFacet, + instance: governedInstance, + + governor: governorStartResult.instance, + governorCreatorFacet: governorStartResult.creatorFacet, + governorAdminFacet: governorStartResult.adminFacet, + }), + ); + // don't overwrite auctioneerKit or auction instance yet. Wait until + // upgrade-vault.js +}; + +export const ADD_AUCTION_MANIFEST = harden({ + [addAuction.name]: { + consume: { + zoe: true, + board: true, + chainTimerService: true, + priceAuthority: true, + chainStorage: true, + economicCommitteeCreatorFacet: true, + auctioneerKit: true, + }, + produce: { + newAuctioneerKit: true, + }, + instance: { + consume: { reserve: true }, + }, + installation: { + consume: { + auctioneer: true, + contractGovernor: true, + }, + }, + issuer: { + consume: { [Stable.symbol]: true }, + }, + }, +}); + +/* Add a new auction to a chain that already has one. */ +export const getManifestForAddAuction = async () => { + return { manifest: ADD_AUCTION_MANIFEST }; +}; 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..f8b0441e736 --- /dev/null +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -0,0 +1,196 @@ +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); + 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 It has a value until reset just below + auctioneerKitProducer.resolve(auctioneerKit); + auctioneerProducer.reset(); + // @ts-expect-error It has a value until reset just below + 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', + auctioneerKit.instance, + ); + }, + ); + + 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, + newAuctioneerKit: t, + economicCommitteeCreatorFacet: t, + priceAuthority: t, + reserveKit: t, + vaultFactoryKit: t, + board: t, + zoe: t, + }, + produce: { auctioneerKit: t, newAuctioneerKit: t }, + instance: { produce: { auctioneer: t, newAuctioneerKit: t } }, + }, + }, + options: { ...vaultUpgradeOptions }, +});