diff --git a/a3p-integration/proposals/e:upgrade-next/priceFeed-follower-auction.test.js b/a3p-integration/proposals/e:upgrade-next/priceFeed-follower-auction.test.js deleted file mode 100644 index 30ae903b075..00000000000 --- a/a3p-integration/proposals/e:upgrade-next/priceFeed-follower-auction.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import test from 'ava'; -import { getDetailsMatchingVats } from './vatDetails.js'; - -test('new auction vat', async t => { - const details = await getDetailsMatchingVats('auctioneer'); - // This query matches both the auction and its governor, so 2*2 - t.is(Object.keys(details).length, 4); -}); diff --git a/a3p-integration/proposals/f:replace-price-feeds/README.md b/a3p-integration/proposals/f:replace-price-feeds/README.md new file mode 100644 index 00000000000..414bb07fe81 --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/README.md @@ -0,0 +1,10 @@ +# CoreEvalProposal to replace existing price_feed and scaledPriceAuthority vats +# with new contracts. Auctions will need to be replaced, and Vaults will need to +# get at least a null upgrade in order to make use of the new prices. Oracle +# operators will need to accept new invitations, and sync to the roundId (0) of +# the new contracts in order to feed the new pipelines. + +The `submission` for this proposal is automatically generated during `yarn build` +in [a3p-integration](../..) using the code in agoric-sdk through +[build-all-submissions.sh](../../scripts/build-all-submissions.sh) and +[build-submission.sh](../../scripts/build-submission.sh). diff --git a/a3p-integration/proposals/e:upgrade-next/agd-tools.js b/a3p-integration/proposals/f:replace-price-feeds/agd-tools.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/agd-tools.js rename to a3p-integration/proposals/f:replace-price-feeds/agd-tools.js diff --git a/a3p-integration/proposals/f:replace-price-feeds/package.json b/a3p-integration/proposals/f:replace-price-feeds/package.json new file mode 100644 index 00000000000..fe1c38a203a --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/package.json @@ -0,0 +1,34 @@ +{ + "agoricProposal": { + "releaseNotes": false, + "sdkImageTag": "unreleased", + "planName": "UNRELEASED_A3P_INTEGRATION", + "upgradeInfo": { + "coreProposals": [] + }, + "sdk-generate": [ + "vats/replacePriceFeeds.js", + "vats/replace-scaledPriceAuthorities.js", + "vats/add-auction.js", + "vats/upgradeVaults.js" + ], + "type": "Software Upgrade Proposal" + }, + "type": "module", + "license": "Apache-2.0", + "dependencies": { + "@agoric/synthetic-chain": "^0.1.0", + "ava": "^5.3.1" + }, + "ava": { + "concurrency": 1, + "timeout": "2m", + "files": [ + "!submission" + ] + }, + "scripts": { + "agops": "yarn --cwd /usr/src/agoric-sdk/ --silent agops" + }, + "packageManager": "yarn@4.2.2" +} diff --git a/a3p-integration/proposals/f:replace-price-feeds/test.sh b/a3p-integration/proposals/f:replace-price-feeds/test.sh new file mode 100755 index 00000000000..23a194f7f79 --- /dev/null +++ b/a3p-integration/proposals/f:replace-price-feeds/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Place here any test that should be executed using the proposal. +# The effects of this step are not persisted in further layers. + +yarn ava ./*.test.js diff --git a/a3p-integration/proposals/e:upgrade-next/.gitignore b/a3p-integration/proposals/n:upgrade-next/.gitignore similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/.gitignore rename to a3p-integration/proposals/n:upgrade-next/.gitignore diff --git a/a3p-integration/proposals/e:upgrade-next/.yarnrc.yml b/a3p-integration/proposals/n:upgrade-next/.yarnrc.yml similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/.yarnrc.yml rename to a3p-integration/proposals/n:upgrade-next/.yarnrc.yml diff --git a/a3p-integration/proposals/e:upgrade-next/README.md b/a3p-integration/proposals/n:upgrade-next/README.md similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/README.md rename to a3p-integration/proposals/n:upgrade-next/README.md diff --git a/a3p-integration/proposals/n:upgrade-next/agd-tools.js b/a3p-integration/proposals/n:upgrade-next/agd-tools.js new file mode 100644 index 00000000000..550668e110b --- /dev/null +++ b/a3p-integration/proposals/n:upgrade-next/agd-tools.js @@ -0,0 +1,208 @@ +import { + agd, + agops, + agopsLocation, + CHAINID, + executeCommand, + executeOffer, + GOV1ADDR, + GOV2ADDR, + GOV3ADDR, + newOfferId, + VALIDATORADDR, +} 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 registerOraclesForBrand = async (brandIn, oraclesByBrand) => { + await null; + const promiseArray = []; + + const oraclesWithID = oraclesByBrand.get(brandIn); + for (const oracle of oraclesWithID) { + const { address, offerId } = oracle; + promiseArray.push( + executeOffer( + address, + agops.oracle('accept', '--offerId', offerId, `--pair ${brandIn}.USD`), + ), + ); + } + + return Promise.all(promiseArray); +}; + +/** + * Generate a consistent map of oracleIDs for a brand that can be used to + * register oracles or to push prices. The baseID changes each time new + * invitations are sent/accepted, and need to be maintained as constants in + * scripts that use the oracles. Each oracleAddress and brand needs a unique + * offerId, so we create recoverable IDs using the brandName and oracle id, + * mixed with the upgrade at which the invitations were accepted. + * + * @param {string} baseId + * @param {string} brandName + */ +const addOraclesForBrand = (baseId, brandName) => { + const oraclesWithID = []; + for (let i = 0; i < ORACLE_ADDRESSES.length; i += 1) { + const oracleAddress = ORACLE_ADDRESSES[i]; + const offerId = `${brandName}.${baseId}.${i}`; + oraclesWithID.push({ address: oracleAddress, offerId }); + } + return oraclesWithID; +}; + +export const addPreexistingOracles = async (brandIn, oraclesByBrand) => { + await null; + + const oraclesWithID = []; + for (let i = 0; i < ORACLE_ADDRESSES.length; i += 1) { + const oracleAddress = ORACLE_ADDRESSES[i]; + + const path = `published.wallet.${oracleAddress}.current`; + const wallet = await getQuoteBody(path); + const idToInvitation = wallet.offerToUsedInvitation.find(([k]) => { + return !isNaN(k[0]); + }); + if (idToInvitation) { + oraclesWithID.push({ + address: oracleAddress, + offerId: idToInvitation[0], + }); + } else { + console.log('AGD addO skip', oraclesWithID); + } + } + + oraclesByBrand.set(brandIn, oraclesWithID); +}; + +/** + * Generate a consistent map of oracleIDs and brands that can be used to + * register oracles or to push prices. The baseID changes each time new + * invitations are sent/accepted, and need to be maintained as constants in + * scripts that use these records to push prices. + * + * @param {string} baseId + * @param {string[]} brandNames + */ +export const generateOracleMap = (baseId, brandNames) => { + const oraclesByBrand = new Map(); + for (const brandName of brandNames) { + const oraclesWithID = addOraclesForBrand(baseId, brandName); + oraclesByBrand.set(brandName, oraclesWithID); + } + return oraclesByBrand; +}; + +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 getVaultPrices = async index => { + const path = `published.vaultFactory.managers.manager${index}.quotes`; + const body = await getQuoteBody(path); + return body.quoteAmount; +}; + +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); +}; + +export const getProvisionPoolMetrics = async () => { + const path = `published.provisionPool.metrics`; + return getQuoteBody(path); +}; diff --git a/a3p-integration/proposals/e:upgrade-next/initial.test.js b/a3p-integration/proposals/n:upgrade-next/initial.test.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/initial.test.js rename to a3p-integration/proposals/n:upgrade-next/initial.test.js diff --git a/a3p-integration/proposals/e:upgrade-next/localchain.test.js b/a3p-integration/proposals/n:upgrade-next/localchain.test.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/localchain.test.js rename to a3p-integration/proposals/n:upgrade-next/localchain.test.js diff --git a/a3p-integration/proposals/e:upgrade-next/package.json b/a3p-integration/proposals/n:upgrade-next/package.json similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/package.json rename to a3p-integration/proposals/n:upgrade-next/package.json diff --git a/a3p-integration/proposals/e:upgrade-next/prepare.sh b/a3p-integration/proposals/n:upgrade-next/prepare.sh similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/prepare.sh rename to a3p-integration/proposals/n:upgrade-next/prepare.sh diff --git a/a3p-integration/proposals/e:upgrade-next/provisionPool.test.js b/a3p-integration/proposals/n:upgrade-next/provisionPool.test.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/provisionPool.test.js rename to a3p-integration/proposals/n:upgrade-next/provisionPool.test.js diff --git a/a3p-integration/proposals/e:upgrade-next/synthetic-chain-excerpt.js b/a3p-integration/proposals/n:upgrade-next/synthetic-chain-excerpt.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/synthetic-chain-excerpt.js rename to a3p-integration/proposals/n:upgrade-next/synthetic-chain-excerpt.js diff --git a/a3p-integration/proposals/e:upgrade-next/test.sh b/a3p-integration/proposals/n:upgrade-next/test.sh similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/test.sh rename to a3p-integration/proposals/n:upgrade-next/test.sh diff --git a/a3p-integration/proposals/e:upgrade-next/tsconfig.json b/a3p-integration/proposals/n:upgrade-next/tsconfig.json similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/tsconfig.json rename to a3p-integration/proposals/n:upgrade-next/tsconfig.json diff --git a/a3p-integration/proposals/e:upgrade-next/vatDetails.js b/a3p-integration/proposals/n:upgrade-next/vatDetails.js similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/vatDetails.js rename to a3p-integration/proposals/n:upgrade-next/vatDetails.js diff --git a/a3p-integration/proposals/e:upgrade-next/yarn.lock b/a3p-integration/proposals/n:upgrade-next/yarn.lock similarity index 100% rename from a3p-integration/proposals/e:upgrade-next/yarn.lock rename to a3p-integration/proposals/n:upgrade-next/yarn.lock diff --git a/packages/boot/test/bootstrapTests/price-feed-replace.test.ts b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts new file mode 100644 index 00000000000..e3c60c5016c --- /dev/null +++ b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts @@ -0,0 +1,186 @@ +/** + * @file The goal of this test is to see that the + * upgrade scripts re-wire all the contracts so new auctions and + * price feeds are connected to vaults correctly. + * + * 1. enter a bid + * 2. force prices to drop so a vault liquidates + * 3. verify that the bidder gets the liquidated assets. + */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import type { TestFn } from 'ava'; +import { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js'; +import { NonNullish } from '@agoric/internal'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { + LiquidationTestContext, + likePayouts, + makeLiquidationTestContext, + scale6, + LiquidationSetup, + atomConfig, +} from '../../tools/liquidation.js'; + +const test = anyTest as TestFn; +test.before( + async t => + (t.context = await makeLiquidationTestContext(t, { env: process.env })), +); +test.after.always(t => t.context.shutdown()); + +const collateralBrandKey = 'ATOM'; +const managerIndex = 0; + +const setup: LiquidationSetup = { + vaults: [{ atom: 15, ist: 100, debt: 100.5 }], + bids: [{ give: '20IST', discount: 0.1 }], + price: { + starting: 12.34, + trigger: 9.99, + }, + auction: { + start: { collateral: 15, debt: 100.5 }, + end: { collateral: 9.659301, debt: 0 }, + }, +}; + +const outcome = { + bids: [{ payouts: { Bid: 0, Collateral: 1.800828 } }], +}; + +test.serial('setupVaults; run replace-price-feeds proposals', async t => { + const { + agoricNamesRemotes, + buildProposal, + evalProposal, + priceFeedDrivers, + refreshAgoricNamesRemotes, + setupVaults, + } = t.context; + + await setupVaults(collateralBrandKey, managerIndex, setup); + + const instancePre = agoricNamesRemotes.instance['ATOM-USD price feed']; + + const priceFeedBuilder = + '@agoric/builders/scripts/inter-protocol/updatePriceFeeds.js'; + t.log('building', priceFeedBuilder); + const brandName = collateralBrandKey; + + t.log('building all relevant CoreEvals'); + const coreEvals = await Promise.all([ + buildProposal(priceFeedBuilder, ['UNRELEASED_main']), + buildProposal('@agoric/builders/scripts/vats/upgradeVaults.js'), + buildProposal('@agoric/builders/scripts/vats/add-auction.js'), + ]); + const combined = { + evals: coreEvals.flatMap(e => e.evals), + bundles: coreEvals.flatMap(e => e.bundles), + }; + t.log('evaluating', coreEvals.length, 'scripts'); + await evalProposal(combined); + + refreshAgoricNamesRemotes(); + const instancePost = agoricNamesRemotes.instance['ATOM-USD price feed']; + t.not(instancePre, instancePost); + + await priceFeedDrivers[collateralBrandKey].refreshInvitations(); +}); + +test.serial('1. place bid', async t => { + const { placeBids, readLatest } = t.context; + await placeBids(collateralBrandKey, 'agoric1buyer', setup, 0); + + t.like(readLatest('published.wallet.agoric1buyer.current'), { + liveOffers: [['ATOM-bid1', { id: 'ATOM-bid1' }]], + }); +}); + +test.serial('2. trigger liquidation by changing price', async t => { + const { priceFeedDrivers, readLatest } = t.context; + + await priceFeedDrivers[collateralBrandKey].setPrice(9.99); + + t.log(readLatest('published.priceFeed.ATOM-USD_price_feed'), { + // aka 9.99 + amountIn: { value: 1000000n }, + amountOut: { value: 9990000n }, + }); + + // check nothing liquidating yet + const liveSchedule: ScheduleNotification = readLatest( + 'published.auction.schedule', + ); + t.is(liveSchedule.activeStartTime, null); + const metricsPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`; + + t.like(readLatest(metricsPath), { + numActiveVaults: setup.vaults.length, + numLiquidatingVaults: 0, + }); +}); + +test.serial('3. verify liquidation', async t => { + const { advanceTimeBy, advanceTimeTo, readLatest } = t.context; + + const liveSchedule: ScheduleNotification = readLatest( + 'published.auction.schedule', + ); + const metricsPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`; + + // advance time to start an auction + console.log(collateralBrandKey, 'step 1 of 10'); + await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime)); + await eventLoopIteration(); // let promises to update vstorage settle + + // vaultFactory sent collateral for liquidation + t.like(readLatest(metricsPath), { + numActiveVaults: 0, + numLiquidatingVaults: setup.vaults.length, + liquidatingCollateral: { + value: scale6(setup.auction.start.collateral), + }, + liquidatingDebt: { value: scale6(setup.auction.start.debt) }, + lockedQuote: null, + }); + + console.log(collateralBrandKey, 'step 2 of 10'); + await advanceTimeBy(3, 'minutes'); + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { value: scale6(setup.auction.start.collateral) }, + startCollateral: { value: scale6(setup.auction.start.collateral) }, + startProceedsGoal: { value: scale6(setup.auction.start.debt) }, + }); + + console.log(collateralBrandKey, 'step 3 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log(collateralBrandKey, 'step 4 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log(collateralBrandKey, 'step 5 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log(collateralBrandKey, 'step 6 of 10'); + await advanceTimeBy(3, 'minutes'); + t.like(readLatest(`published.auction.book${managerIndex}`), { + collateralAvailable: { value: 13199172n }, + }); + + console.log(collateralBrandKey, 'step 7 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log(collateralBrandKey, 'step 8 of 10'); + await advanceTimeBy(3, 'minutes'); + + console.log(collateralBrandKey, 'step 9 of 10'); + await advanceTimeBy(3, 'minutes'); + + t.like(readLatest('published.wallet.agoric1buyer'), { + status: { + id: `${collateralBrandKey}-bid1`, + payouts: likePayouts(outcome.bids[0].payouts), + }, + }); +}); diff --git a/packages/boot/tools/drivers.ts b/packages/boot/tools/drivers.ts index 6f36e56aeb7..33453f5d884 100644 --- a/packages/boot/tools/drivers.ts +++ b/packages/boot/tools/drivers.ts @@ -151,24 +151,28 @@ export const makePriceFeedDriver = async ( oracleAddresses.map(addr => walletFactoryDriver.provideSmartWallet(addr)), ); - const priceFeedInstance = agoricNamesRemotes.instance[priceFeedName]; - priceFeedInstance || Fail`no price feed ${priceFeedName}`; - const adminOfferId = `accept-${collateralBrandKey}-oracleInvitation`; - - // accept invitations - await Promise.all( - oracleWallets.map(w => - w.executeOffer({ - id: adminOfferId, - invitationSpec: { - source: 'purse', - instance: priceFeedInstance, - description: 'oracle invitation', - }, - proposal: {}, - }), - ), - ); + let nonce = 0; + let adminOfferId; + const acceptInvitations = async () => { + const priceFeedInstance = agoricNamesRemotes.instance[priceFeedName]; + priceFeedInstance || Fail`no price feed ${priceFeedName}`; + nonce += 1; + adminOfferId = `accept-${collateralBrandKey}-oracleInvitation${nonce}`; + return Promise.all( + oracleWallets.map(w => + w.executeOffer({ + id: adminOfferId, + invitationSpec: { + source: 'purse', + instance: priceFeedInstance, + description: 'oracle invitation', + }, + proposal: {}, + }), + ), + ); + }; + await acceptInvitations(); // zero is the initial lastReportedRoundId so causes an error: cannot report on previous rounds let roundId = 1n; @@ -192,6 +196,10 @@ export const makePriceFeedDriver = async ( roundId += 1n; // TODO confirm the new price is written to storage }, + async refreshInvitations() { + roundId = 1n; + await acceptInvitations(); + }, }; }; harden(makePriceFeedDriver); diff --git a/packages/boot/tools/liquidation.ts b/packages/boot/tools/liquidation.ts index 221f98e7260..1945abdc9fe 100644 --- a/packages/boot/tools/liquidation.ts +++ b/packages/boot/tools/liquidation.ts @@ -53,6 +53,17 @@ export type LiquidationSetup = { }; }; +// TODO read from the config file +export const atomConfig = { + oracleAddresses: [ + 'agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr', + 'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8', + 'agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78', + 'agoric19d6gnr9fyp6hev4tlrg87zjrzsd5gzr5qlfq2p', + 'agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj', + ], +}; + export const scale6 = x => BigInt(Math.round(x * 1_000_000)); const DebtLimitValue = scale6(100_000); @@ -103,14 +114,7 @@ export const makeLiquidationTestKit = async ({ collateralBrandKey, agoricNamesRemotes, walletFactoryDriver, - // TODO read from the config file - [ - 'agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr', - 'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8', - 'agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78', - 'agoric19d6gnr9fyp6hev4tlrg87zjrzsd5gzr5qlfq2p', - 'agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj', - ], + atomConfig.oracleAddresses, ); } @@ -304,8 +308,14 @@ export const makeLiquidationTestKit = async ({ }; }; -export const makeLiquidationTestContext = async t => { - const swingsetTestKit = await makeSwingsetTestKit(t.log); +export const makeLiquidationTestContext = async ( + t, + io: { env?: Record } = {}, +) => { + const { env = {} } = io; + const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { + slogFile: env.SLOGFILE, + }); console.time('DefaultTestContext'); const { runUtils, storage } = swingsetTestKit; diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 36af57d01cc..6b62a9b287e 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -154,6 +154,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'], { @@ -162,7 +163,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { }); return childProcess.execFileSync( out.toString().trim(), - ['run', scriptPath], + ['run', scriptPath, ...cliArgs], { cwd: outputDir, env, @@ -193,7 +194,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-'), ); @@ -203,6 +204,7 @@ export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { tmpDir, await importSpec(builderPath), process.env, + args, ).toString(), ); diff --git a/packages/builders/scripts/inter-protocol/updatePriceFeeds.js b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js new file mode 100644 index 00000000000..746128fe909 --- /dev/null +++ b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js @@ -0,0 +1,81 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; +import { getManifestForPriceFeeds } from '@agoric/inter-protocol/src/proposals/deploy-price-feeds.js'; + +/** @import {PriceFeedConfig} from '@agoric/inter-protocol/src/proposals/deploy-price-feeds.js'; */ + +/** @type {Record} */ +const configurations = { + UNRELEASED_A3P_INTEGRATION: { + oracleAddresses: [ + 'agoric1lu9hh5vgx05hmlpfu47hukershgdxctk6l5s05', // GOV1 + 'agoric15lpnq2mjsdhtztf6khp7mrsq66hyrssspy92pd', // GOV2 + 'agoric1mwm224epc4l3pjcz7qsxnudcuktpynwkmnfqfp', // GOV3 + ], + inBrandNames: ['ATOM', 'stATOM'], + }, + UNRELEASED_main: { + oracleAddresses: [ + 'agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78', // DSRV + 'agoric19d6gnr9fyp6hev4tlrg87zjrzsd5gzr5qlfq2p', // Stakin + 'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8', // 01node + 'agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr', // Simply Staking + 'agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj', // P2P + ], + inBrandNames: ['ATOM', 'stATOM', 'stOSMO', 'stTIA', 'stkATOM'], + contractTerms: { minSubmissionCount: 3 }, + }, + UNRELEASED_devnet: { + oracleAddresses: [ + 'agoric1lw4e4aas9q84tq0q92j85rwjjjapf8dmnllnft', // DSRV + 'agoric1zj6vrrrjq4gsyr9lw7dplv4vyejg3p8j2urm82', // Stakin + 'agoric1ra0g6crtsy6r3qnpu7ruvm7qd4wjnznyzg5nu4', // 01node + 'agoric1qj07c7vfk3knqdral0sej7fa6eavkdn8vd8etf', // Simply Staking + 'agoric10vjkvkmpp9e356xeh6qqlhrny2htyzp8hf88fk', // P2P + ], + inBrandNames: ['ATOM', 'stTIA', 'stkATOM'], + }, +}; + +/** @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/deploy-price-feeds.js', + getManifestCall: [ + getManifestForPriceFeeds.name, + { + ...opts, + priceAggregatorRef: publishRef( + install( + '@agoric/inter-protocol/src/price/fluxAggregatorContract.js', + '../bundles/bundle-fluxAggregatorKit.js', + ), + ), + scaledPARef: publishRef( + install( + '@agoric/zoe/src/contracts/scaledPriceAuthority.js', + '../bundles/bundle-scaledPriceAuthority.js', + ), + ), + }, + ], + }); +}; + +const { keys } = Object; +const Usage = `agoric run updatePriceFeed.js ${keys(configurations).join(' | ')}`; + +export default async (homeP, endowments) => { + const { scriptArgs } = endowments; + const config = configurations[scriptArgs?.[0]]; + if (!config) { + console.error(Usage); + process.exit(1); + } + console.log('UPPrices', scriptArgs, config); + + const { writeCoreEval } = await makeHelpers(homeP, endowments); + + await writeCoreEval('gov-price-feeds', (utils, opts) => + defaultProposalBuilder(utils, { ...opts, ...config }), + ); +}; diff --git a/packages/builders/scripts/vats/priceFeedSupport.js b/packages/builders/scripts/vats/priceFeedSupport.js deleted file mode 100644 index 5f916d3c793..00000000000 --- a/packages/builders/scripts/vats/priceFeedSupport.js +++ /dev/null @@ -1,90 +0,0 @@ -/* global process */ - -import { DEFAULT_CONTRACT_TERMS } from '../inter-protocol/price-feed-core.js'; - -const { Fail } = assert; - -/** - * modified copy of ../inter-protocol/price-feed-core.js - * - * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} - */ -export const strictPriceFeedProposalBuilder = async ( - { publishRef, install }, - options, -) => { - const { - AGORIC_INSTANCE_NAME, - IN_BRAND_LOOKUP, - IN_BRAND_NAME = IN_BRAND_LOOKUP[IN_BRAND_LOOKUP.length - 1], - ORACLE_ADDRESSES, - } = options; - - const oracleAddresses = ORACLE_ADDRESSES; - Array.isArray(oracleAddresses) || - Fail`ORACLE_ADDRESSES array is required; got ${oracleAddresses}`; - - AGORIC_INSTANCE_NAME || - Fail`AGORIC_INSTANCE_NAME is required; got ${AGORIC_INSTANCE_NAME}`; - - Array.isArray(IN_BRAND_LOOKUP) || - Fail`IN_BRAND_NAME array is required; got ${IN_BRAND_LOOKUP}`; - - return harden({ - sourceSpec: '@agoric/inter-protocol/src/proposals/price-feed-proposal.js', - getManifestCall: [ - 'getManifestForPriceFeed', - { - AGORIC_INSTANCE_NAME, - contractTerms: DEFAULT_CONTRACT_TERMS, - oracleAddresses, - IN_BRAND_NAME, - IN_BRAND_DECIMALS: 6, - OUT_BRAND_DECIMALS: 4, - OUT_BRAND_NAME: 'USD', - priceAggregatorRef: publishRef( - install( - '@agoric/inter-protocol/src/price/fluxAggregatorContract.js', - '../bundles/bundle-fluxAggregatorKit.js', - ), - ), - }, - ], - }); -}; - -/** - * @deprecated use `strictPriceFeedProposalBuilder` and specify arguments instead - * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} - */ -export const deprecatedPriceFeedProposalBuilder = async (powers, options) => { - console.warn( - 'deprecated ambient `priceFeedProposalBuilder`; use `strictPriceFeedProposalBuilder` instead', - ); - - const DEFAULT_ORACLE_ADDRESSES = [ - // XXX These are the oracle addresses. They must be provided before the chain - // is running, which means they must be known ahead of time. - // see https://github.com/Agoric/agoric-3-proposals/issues/5 - 'agoric1lu9hh5vgx05hmlpfu47hukershgdxctk6l5s05', - 'agoric15lpnq2mjsdhtztf6khp7mrsq66hyrssspy92pd', - 'agoric1mwm224epc4l3pjcz7qsxnudcuktpynwkmnfqfp', - ]; - - const { GOV1ADDR, GOV2ADDR, GOV3ADDR } = process.env; - const governanceAddressEnv = [GOV1ADDR, GOV2ADDR, GOV3ADDR].filter(x => x); - const ORACLE_ADDRESSES = governanceAddressEnv.length - ? governanceAddressEnv - : DEFAULT_ORACLE_ADDRESSES; - - return strictPriceFeedProposalBuilder(powers, { - ...options, - ORACLE_ADDRESSES, - }); -}; - -/** - * @deprecated use `strictPriceFeedProposalBuilder` and specify arguments instead - * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} - */ -export const priceFeedProposalBuilder = deprecatedPriceFeedProposalBuilder; diff --git a/packages/inter-protocol/src/proposals/add-auction.js b/packages/inter-protocol/src/proposals/add-auction.js index ca5bb004759..6bfad088ee6 100644 --- a/packages/inter-protocol/src/proposals/add-auction.js +++ b/packages/inter-protocol/src/proposals/add-auction.js @@ -27,7 +27,7 @@ export const addAuction = async ( chainTimerService, economicCommitteeCreatorFacet: electorateCreatorFacet, econCharterKit, - priceAuthority, + priceAuthority8400, zoe, }, produce: { auctioneerKit: produceAuctioneerKit, auctionUpgradeNewInstance }, @@ -95,7 +95,7 @@ export const addAuction = async ( const auctionTerms = makeGovernedATerms( { storageNode, marshaller }, chainTimerService, - priceAuthority, + priceAuthority8400, reservePublicFacet, { ...params, @@ -197,7 +197,7 @@ export const ADD_AUCTION_MANIFEST = harden({ chainTimerService: true, econCharterKit: true, economicCommitteeCreatorFacet: true, - priceAuthority: true, + priceAuthority8400: true, zoe: true, }, produce: { diff --git a/packages/inter-protocol/src/proposals/deploy-price-feeds.js b/packages/inter-protocol/src/proposals/deploy-price-feeds.js new file mode 100644 index 00000000000..f0f9ab2e15a --- /dev/null +++ b/packages/inter-protocol/src/proposals/deploy-price-feeds.js @@ -0,0 +1,312 @@ +import { makeTracer } from '@agoric/internal'; +import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { E } from '@endo/far'; + +import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js'; +import { + oracleBrandFeedName, + reserveThenDeposit, + sanitizePathSegment, +} from './utils.js'; +import { upgradeScaledPriceAuthorities } from './upgrade-scaledPriceAuthorities.js'; + +const STORAGE_PATH = 'priceFeed'; + +/** @type {ChainlinkConfig} */ +export const DEFAULT_CONTRACT_TERMS = { + maxSubmissionCount: 1000, + minSubmissionCount: 2, + restartDelay: 1n, // the number of rounds an Oracle has to wait before they can initiate another round + timeout: 10, // in seconds according to chainTimerService + minSubmissionValue: 1, + maxSubmissionValue: 2 ** 256, +}; + +/** @import {EconomyBootstrapPowers} from './econ-behaviors.js'; */ +/** @import {ChainlinkConfig} from '@agoric/inter-protocol/src/price/fluxAggregatorKit.js'; */ +/** @typedef {typeof import('@agoric/inter-protocol/src/price/fluxAggregatorContract.js').start} FluxStartFn */ + +const trace = makeTracer('RunPriceFeed', true); + +/** + * @typedef {{ + * oracleAddresses: string[]; + * inBrandNames: string[]; + * contractTerms?: Partial; + * }} PriceFeedConfig + */ + +/** + * @param {EconomyBootstrapPowers} powers + * @param {string} bundleID + */ +const installPriceAggregator = async ( + { + consume: { zoe }, + installation: { + produce: { priceAggregator }, + }, + }, + bundleID, +) => { + /** @type {Installation} */ + const installation = await E(zoe).installBundleID(bundleID); + priceAggregator.reset(); + priceAggregator.resolve(installation); + trace('installed priceAggregator', bundleID.slice(0, 'b1-1234567'.length)); + return installation; +}; + +/** + * Create inert brands (no mint or issuer) referred to by price oracles. + * + * @param {EconomyBootstrapPowers & NamedVatPowers} space + * @param {{ name: string; decimalPlaces: number }} opt + * @returns {Promise>} + */ +export const ensureOracleBrand = async ( + { + namedVat: { + consume: { agoricNames }, + }, + oracleBrand: { produce: oracleBrandProduce }, + }, + { name, decimalPlaces }, +) => { + const brand = E(agoricNames).provideInertBrand(name, { + assetKind: 'nat', + decimalPlaces, + }); + + oracleBrandProduce[name].reset(); + oracleBrandProduce[name].resolve(brand); + return brand; +}; + +/** + * @param {EconomyBootstrapPowers} powers + * @param {{ + * AGORIC_INSTANCE_NAME: string; + * contractTerms: import('@agoric/inter-protocol/src/price/fluxAggregatorKit.js').ChainlinkConfig; + * brandIn: Brand<'nat'>; + * brandOut: Brand<'nat'>; + * }} config + * @param {Installation} installation + */ +const startPriceAggegatorInstance = async ( + { + consume: { + board, + chainStorage, + chainTimerService, + econCharterKit, + highPrioritySendersManager, + namesByAddressAdmin, + startGovernedUpgradable, + }, + instance: { produce: produceInstance }, + }, + { AGORIC_INSTANCE_NAME, contractTerms, brandIn, brandOut }, + installation, +) => { + trace('startPriceAggegatorInstance', AGORIC_INSTANCE_NAME); + const label = sanitizePathSegment(AGORIC_INSTANCE_NAME); + + const feedsStorage = await makeStorageNodeChild(chainStorage, STORAGE_PATH); + const storageNode = await E(feedsStorage).makeChildNode(label); + const marshaller = await E(board).getReadonlyMarshaller(); + + const terms = harden({ + ...contractTerms, + description: AGORIC_INSTANCE_NAME, + brandIn, + brandOut, + timer: await chainTimerService, + unitAmountIn: await unitAmount(brandIn), + }); + const privateArgs = { + highPrioritySendersManager: await highPrioritySendersManager, + marshaller, + namesByAddressAdmin, + storageNode, + }; + const governedKit = await E(startGovernedUpgradable)({ + governedParams: {}, + privateArgs, + terms, + label, + // @ts-expect-error GovernableStartFn vs. fluxAggregatorContract.js start + installation, + }); + produceInstance[AGORIC_INSTANCE_NAME].reset(); + produceInstance[AGORIC_INSTANCE_NAME].resolve(governedKit.instance); + trace( + 'new instance', + label, + { terms, privateArgs, installation }, + governedKit, + ); + + await E(E.get(econCharterKit).creatorFacet).addInstance( + governedKit.instance, + governedKit.governorCreatorFacet, + AGORIC_INSTANCE_NAME, + ); + trace('added', label, 'instance to econCharter'); + + /** @type {import('@agoric/zoe/src/zoeService/utils.js').StartedInstanceKit} */ + // @ts-expect-error + const { instance, publicFacet, creatorFacet } = governedKit; + + return harden({ instance, publicFacet, creatorFacet }); +}; + +/** + * Send invitations to oracle operators for a price feed. + * + * @param {EconomyBootstrapPowers} powers + * @param {{ oracleAddresses: string[]; AGORIC_INSTANCE_NAME: string }} config + * @param {any} creatorFacet + */ +const distributeInvitations = async ( + { consume: { namesByAddressAdmin } }, + { oracleAddresses, AGORIC_INSTANCE_NAME }, + creatorFacet, +) => { + /** @param {string} addr */ + const addOracle = async addr => { + const invitation = await E(creatorFacet).makeOracleInvitation(addr); + const debugName = `${AGORIC_INSTANCE_NAME} member ${addr}`; + await reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + invitation, + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); + }; + + trace('distributing invitations', oracleAddresses); + // This doesn't resolve until oracle operators create their smart wallets. + // Don't block bootstrap on it. + void Promise.all(oracleAddresses.map(addOracle)); + trace('createPriceFeed complete'); +}; + +/** + * @param {EconomyBootstrapPowers & NamedVatPowers} powers + * @param {{ + * options: PriceFeedConfig & { + * priceAggregatorRef: { bundleID: string }; + * scaledPARef: { bundleID: string }; + * inBrandsDecimals?: number; + * contractTerms?: ChainlinkConfig; + * outBrandName?: string; + * outBrandDecimals?: number; + * }; + * }} config + */ +export const deployPriceFeeds = async (powers, config) => { + const { + inBrandNames, + oracleAddresses, + contractTerms, + priceAggregatorRef, + scaledPARef, + inBrandsDecimals = 6, + outBrandName = 'USD', + outBrandDecimals = 6, + } = config.options; + await null; + + const installation = await installPriceAggregator( + powers, + priceAggregatorRef.bundleID, + ); + + const { priceAuthorityAdmin, priceAuthority } = powers.consume; + for (const inBrandName of inBrandNames) { + const AGORIC_INSTANCE_NAME = oracleBrandFeedName(inBrandName, outBrandName); + const brandIn = await ensureOracleBrand(powers, { + name: inBrandName, + decimalPlaces: inBrandsDecimals, + }); + const brandOut = await ensureOracleBrand(powers, { + name: outBrandName, + decimalPlaces: outBrandDecimals, + }); + const kit = await startPriceAggegatorInstance( + powers, + { + AGORIC_INSTANCE_NAME, + brandIn, + brandOut, + contractTerms: { ...DEFAULT_CONTRACT_TERMS, ...contractTerms }, + }, + installation, + ); + + const forceReplace = true; + await E(priceAuthorityAdmin).registerPriceAuthority( + E(kit.publicFacet).getPriceAuthority(), + brandIn, + brandOut, + forceReplace, + ); + + await distributeInvitations( + powers, + { oracleAddresses, AGORIC_INSTANCE_NAME }, + kit.creatorFacet, + ); + } + + await upgradeScaledPriceAuthorities(powers, { + options: { scaledPARef }, + }); + + // cf. #8400 QuotePayments storage leak + powers.produce.priceAuthority8400.resolve(priceAuthority); + powers.produce.priceAuthority.resolve(priceAuthority); +}; + +const t = 'priceFeed'; + +/** + * Thread price feed upgrade options through from builder to core-eval. + * + * @param {object} utils + * @param {any} utils.restoreRef + * @param {PriceFeedConfig & { priceAggregatorRef: any }} priceFeedOptions + */ +export const getManifestForPriceFeeds = async ( + { restoreRef }, + priceFeedOptions, +) => ({ + manifest: { + [deployPriceFeeds.name]: { + namedVat: t, + consume: { + agoricNamesAdmin: t, + board: t, + chainStorage: t, + chainTimerService: t, + contractKits: t, + econCharterKit: t, + highPrioritySendersManager: t, + instancePrivateArgs: t, + namesByAddressAdmin: t, + priceAuthority: t, + priceAuthorityAdmin: t, + startGovernedUpgradable: t, + zoe: t, + }, + installation: { produce: { priceAggregator: t } }, + instance: { + produce: t, + }, + oracleBrand: { produce: t }, + produce: { + priceAuthority: t, + priceAuthority8400: t, + }, + }, + }, + options: { ...priceFeedOptions }, +}); diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index ef0808cea6e..ffe124dbd4d 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -227,7 +227,8 @@ export const createPriceFeed = async ( // being after the above awaits means that when this resolves, the consumer // gets notified that the authority is in the registry and its instance is in - // agoricNames. + // agoricNames. reset() in case we're replacing an existing feed. + produceInstance[AGORIC_INSTANCE_NAME].reset(); produceInstance[AGORIC_INSTANCE_NAME].resolve(faKit.instance); E(E.get(econCharterKit).creatorFacet).addInstance( diff --git a/packages/inter-protocol/src/proposals/upgrade-vaults.js b/packages/inter-protocol/src/proposals/upgrade-vaults.js index 2088aac9e67..f98e7293ce2 100644 --- a/packages/inter-protocol/src/proposals/upgrade-vaults.js +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -27,6 +27,7 @@ export const upgradeVaults = async ( reserveKit, vaultFactoryKit, zoe, + priceAuthority8400, }, produce: { auctionUpgradeNewInstance: auctionUpgradeNewInstanceProducer }, installation: { @@ -44,6 +45,9 @@ export const upgradeVaults = async ( const allBrands = await E(zoe).getBrands(directorInstance); const { Minted: _istBrand, ...vaultBrands } = allBrands; + console.log('upgradeVaults awaiting priceAuthority8400'); + await priceAuthority8400; + const bundleID = vaultsRef.bundleID; console.log(`upgradeVaults: bundleId`, bundleID); /** @@ -200,6 +204,7 @@ export const getManifestForUpgradeVaults = async ( manifest: { [upgradeVaults.name]: { consume: { + priceAuthority8400: uV, auctionUpgradeNewInstance: uV, chainTimerService: uV, economicCommitteeCreatorFacet: uV, diff --git a/packages/inter-protocol/src/proposals/utils.js b/packages/inter-protocol/src/proposals/utils.js index f0c00e9e18e..c191824468f 100644 --- a/packages/inter-protocol/src/proposals/utils.js +++ b/packages/inter-protocol/src/proposals/utils.js @@ -2,6 +2,7 @@ import { Fail } from '@endo/errors'; import { E } from '@endo/far'; import { WalletName } from '@agoric/internal'; import { getCopyMapEntries, makeCopyMap } from '@agoric/store'; +import { assertPathSegment } from '@agoric/internal/src/lib-chainStorage.js'; /** @import {CopyMap} from '@endo/patterns'; */ @@ -163,3 +164,10 @@ export const oracleBrandFeedName = (inBrandName, outBrandName) => export const scaledPriceFeedName = issuerName => `scaledPriceAuthority-${issuerName}`; + +/** @type {(name: string) => string} */ +export const sanitizePathSegment = name => { + const candidate = name.replace(/ /g, '_'); + assertPathSegment(candidate); + return candidate; +}; diff --git a/packages/vats/src/core/types-ambient.d.ts b/packages/vats/src/core/types-ambient.d.ts index f87f529db5a..a4a88296125 100644 --- a/packages/vats/src/core/types-ambient.d.ts +++ b/packages/vats/src/core/types-ambient.d.ts @@ -230,6 +230,7 @@ type WellKnownContracts = { mintHolder: typeof import('@agoric/vats/src/mintHolder.js').start; psm: typeof import('@agoric/inter-protocol/src/psm/psm.js').start; provisionPool: typeof import('@agoric/inter-protocol/src/provisionPool.js').start; + priceAggregator: typeof import('@agoric/inter-protocol/src/price/fluxAggregatorContract.js').start; reserve: typeof import('@agoric/inter-protocol/src/reserve/assetReserve.js').start; VaultFactory: typeof import('@agoric/inter-protocol/src/vaultFactory/vaultFactory.js').start; // no typeof because walletFactory is exporting `start` as a type @@ -377,6 +378,8 @@ type ChainBootstrapSpaceT = { powerStore: MapStore; priceAuthorityVat: Awaited; priceAuthority: import('@agoric/zoe/tools/types.js').PriceAuthority; + // signal that price feeds have #8400 QuotePayments storage leak fixed + priceAuthority8400: import('@agoric/zoe/tools/types.js').PriceAuthority; priceAuthorityAdmin: import('@agoric/vats/src/priceAuthorityRegistry').PriceAuthorityRegistryAdmin; provisioning: Awaited | undefined; provisionBridgeManager: