From cf2f84e76a12372a3e260bc17760dc123415591c Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 20 Apr 2023 19:36:06 -0500 Subject: [PATCH] feat: restore PSM state from former VM vstorage contents fixes: #6645 - compute total fees - style: keep to jessie-check in startPSM - use vatPowers.chainStorageEntries in restorePSM --- packages/inter-protocol/package.json | 3 +- .../src/proposals/econ-behaviors.js | 2 + .../inter-protocol/src/proposals/startPSM.js | 240 +++++++++++++- packages/inter-protocol/test/psm/setupPsm.js | 2 +- packages/inter-protocol/test/psm/test-psm.js | 302 +++++++++++++++++- 5 files changed, 525 insertions(+), 24 deletions(-) diff --git a/packages/inter-protocol/package.json b/packages/inter-protocol/package.json index b075639faaab..242631eda594 100644 --- a/packages/inter-protocol/package.json +++ b/packages/inter-protocol/package.json @@ -44,7 +44,8 @@ "@endo/far": "^0.2.18", "@endo/marshal": "^0.8.5", "@endo/nat": "^4.1.27", - "agoric": "^0.18.2" + "agoric": "^0.18.2", + "jessie.js": "^0.3.2" }, "devDependencies": { "@agoric/deploy-script-support": "^0.9.4", diff --git a/packages/inter-protocol/src/proposals/econ-behaviors.js b/packages/inter-protocol/src/proposals/econ-behaviors.js index e381422ffc6d..45eb22db29e4 100644 --- a/packages/inter-protocol/src/proposals/econ-behaviors.js +++ b/packages/inter-protocol/src/proposals/econ-behaviors.js @@ -60,6 +60,8 @@ const BASIS_POINTS = 10_000n; * periodicFeeCollectors: import('../feeDistributor.js').PeriodicFeeCollector[], * bankMints: Mint[], * psmKit: MapStore, + * psmFeePurse: ERef>, + * anchorBalancePayments: MapStore>, * econCharterKit: EconCharterStartResult, * reserveKit: GovernanceFacetKit, * stakeFactoryKit: GovernanceFacetKit, diff --git a/packages/inter-protocol/src/proposals/startPSM.js b/packages/inter-protocol/src/proposals/startPSM.js index acf255ddd452..ecbb2d28842a 100644 --- a/packages/inter-protocol/src/proposals/startPSM.js +++ b/packages/inter-protocol/src/proposals/startPSM.js @@ -1,11 +1,13 @@ // @jessie-check +import { makeMap, makeSet } from 'jessie.js'; import { AmountMath, AssetKind } from '@agoric/ertp'; import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; import { Stable } from '@agoric/vats/src/tokens.js'; +import { boardSlottingMarshaller } from '@agoric/vats/tools/board-utils.js'; import { deeplyFulfilledObject } from '@agoric/internal'; import { makeScalarMapStore } from '@agoric/vat-data'; @@ -18,24 +20,157 @@ import { } from './committee-proposal.js'; /** @typedef {import('@agoric/vats/src/core/lib-boot.js').BootstrapManifest} BootstrapManifest */ +/** @typedef {import('../psm/psm.js').MetricsNotification} MetricsNotification */ +/** @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */ const BASIS_POINTS = 10000n; -const { details: X } = assert; +const { details: X, Fail } = assert; export { inviteCommitteeMembers, startEconCharter, inviteToEconCharter }; /** - * @param {EconomyBootstrapPowers & WellKnownSpaces} powers + * Decode vstorage value to CapData + * XXX already written somewhere? + * + * @param {unknown} value + */ +const decodeToCapData = value => { + assert.typeof(value, 'string'); + + // { blockHeight: 123, values: [ ... ] } + const item = JSON.parse(value); // or throw + assert.typeof(item, 'object'); + assert(item); + const { values } = item; + assert(Array.isArray(values)); + + assert.equal(values.length, 1); + // { body: "...", slots: [ ... ] } + const data = JSON.parse(values[0]); + assert.typeof(data, 'object'); + assert(data); + assert.typeof(data.body, 'string'); + assert(Array.isArray(data.slots)); + + /** @type {import('@endo/marshal').CapData} */ + // @ts-expect-error cast + const capData = data; + return capData; +}; + +/** + * Provide access to object graphs serialized in vstorage. + * + * @param {Array<[string, string]>} entries + * @param {(slot: string, iface?: string) => any} [slotToVal] + */ +export const makeHistoryReviver = (entries, slotToVal = undefined) => { + const board = boardSlottingMarshaller(slotToVal); + const vsMap = makeMap(entries); + + const getItem = key => { + const raw = vsMap.get(key) || Fail`no ${key}`; + const capData = decodeToCapData(raw); + return harden(board.fromCapData(capData)); + }; + const children = prefix => [ + ...makeSet( + entries + .map(([k, _]) => k) + .filter(k => k.length > prefix.length && k.startsWith(prefix)) + .map(k => k.slice(prefix.length).split('.')[0]), + ), + ]; + return harden({ getItem, children, has: k => vsMap.has(k) }); +}; + +/** + * @param {Array<[key: string, value: string]>} chainStorageEntries + * @param {string} keyword + * @param {{ minted: Brand<'nat'>, anchor: Brand<'nat'> }} brands + * @returns {{ metrics?: MetricsNotification, governance?: Record }} + */ +const findOldPSMState = (chainStorageEntries, keyword, brands) => { + // In this reviver, object references are revived as boardIDs + // from the pre-bulldozer board. + const toSlotReviver = makeHistoryReviver(chainStorageEntries); + if (!toSlotReviver.has(`published.psm.${Stable.symbol}.${keyword}.metrics`)) { + return {}; + } + const metricsWithOldBoardIDs = toSlotReviver.getItem( + `published.psm.${Stable.symbol}.${keyword}.metrics`, + ); + const oldIDtoNewBrand = makeMap([ + [metricsWithOldBoardIDs.feePoolBalance.brand, brands.minted], + [metricsWithOldBoardIDs.anchorPoolBalance.brand, brands.anchor], + ]); + // revive brands; other object references map to undefined + const brandReviver = makeHistoryReviver(chainStorageEntries, s => + oldIDtoNewBrand.get(s), + ); + return { + metrics: brandReviver.getItem(`published.psm.IST.${keyword}.metrics`), + governance: brandReviver.getItem(`published.psm.IST.${keyword}.governance`) + .current, + }; +}; + +/** + * Mint IST needed to restore feePoolBalance for each PSM. + * + * @param { EconomyBootstrapPowers & ChainStorageVatParams } powers + */ +export const mintPSMFees = async ({ + vatParameters: { chainStorageEntries }, + consume: { feeMintAccess: feeMintAccessP, zoe }, + produce: { psmFeePurse }, + installation: { + consume: { centralSupply }, + }, +}) => { + const old = makeHistoryReviver(chainStorageEntries || []); + const psmRootKey = `published.psm.${Stable.symbol}.`; + const psmNames = old.children(psmRootKey); + const purse = E(E(zoe).getFeeIssuer()).makeEmptyPurse(); + + const depositPayment = async () => { + const values = psmNames.map( + a => old.getItem(`${psmRootKey}${a}.metrics`).feePoolBalance.value, + ); + const total = values.reduce((tot, v) => tot + v); + console.log('minting', total, ' fees for ', psmNames); + + const feeMintAccess = await feeMintAccessP; + /** @type {Awaited>} */ + const { creatorFacet } = await E(zoe).startInstance( + centralSupply, + {}, + { bootstrapPaymentValue: total }, + { feeMintAccess }, + 'centralSupply', + ); + const payment = await E(creatorFacet).getBootstrapPayment(); + await E(purse).deposit(payment); + }; + await (psmNames.length > 0 && depositPayment()); + psmFeePurse.resolve(purse); +}; +harden(mintPSMFees); + +/** + * @typedef {{ + * vatParameters: { chainStorageEntries?: Array<[k: string, v: string]>, + * }}} ChainStorageVatParams + * @param {EconomyBootstrapPowers & WellKnownSpaces & ChainStorageVatParams} powers * @param {object} [config] * @param {bigint} [config.WantMintedFeeBP] * @param {bigint} [config.GiveMintedFeeBP] * @param {bigint} [config.MINT_LIMIT] * @param {{ anchorOptions?: AnchorOptions } } [config.options] - * - * @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */ export const startPSM = async ( { + vatParameters: { chainStorageEntries }, consume: { agoricNamesAdmin, board, @@ -47,6 +182,8 @@ export const startPSM = async ( chainStorage, chainTimerService, psmKit, + anchorBalancePayments: anchorBalancePaymentsP, + psmFeePurse, }, produce: { psmKit: producepsmKit }, installation: { @@ -97,6 +234,12 @@ export const startPSM = async ( const mintLimit = AmountMath.make(minted, MINT_LIMIT); const anchorDecimalPlaces = anchorInfo.decimalPlaces || 1n; const mintedDecimalPlaces = mintedInfo.decimalPlaces || 1n; + + const oldState = findOldPSMState(chainStorageEntries || [], keyword, { + minted, + anchor: anchorBrand, + }); + const terms = await deeplyFulfilledObject( harden({ anchorBrand, @@ -107,10 +250,6 @@ export const startPSM = async ( minted, ), governedParams: { - [CONTRACT_ELECTORATE]: { - type: ParamTypes.INVITATION, - value: electorateInvitationAmount, - }, WantMintedFee: { type: ParamTypes.RATIO, value: makeRatio(WantMintedFeeBP, minted, BASIS_POINTS), @@ -120,6 +259,11 @@ export const startPSM = async ( value: makeRatio(GiveMintedFeeBP, minted, BASIS_POINTS), }, MintLimit: { type: ParamTypes.AMOUNT, value: mintLimit }, + ...oldState.governance, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: electorateInvitationAmount, + }, }, [CONTRACT_ELECTORATE]: { type: ParamTypes.INVITATION, @@ -170,6 +314,33 @@ export const startPSM = async ( E(governorFacets.creatorFacet).getAdminFacet(), ]); + /** @param {MetricsNotification} metrics */ + const restoreMetrics = async metrics => { + const anchorBalancePayments = await anchorBalancePaymentsP; + const anchorPmt = anchorBalancePayments.get(anchorBrand); + const feePoolPmt = await E(psmFeePurse).withdraw(metrics.feePoolBalance); + + const { + feePoolBalance: _f, + anchorPoolBalance: _a, + ...nonPaymentMetrics + } = metrics; + + const seat = E(zoe).offer( + E(psmCreatorFacet).makeRestoreMetricsInvitation(), + harden({ + give: { + Anchor: metrics.anchorPoolBalance, + Minted: metrics.feePoolBalance, + }, + }), + harden({ Anchor: anchorPmt, Minted: feePoolPmt }), + harden(nonPaymentMetrics), + ); + await E(seat).getPayouts(); + }; + await (oldState.metrics && restoreMetrics(oldState.metrics)); + /** @typedef {import('./econ-behaviors.js').PSMKit} psmKit */ /** @type {psmKit} */ const newpsmKit = { @@ -217,18 +388,26 @@ harden(startPSM); * Make anchor issuer out of a Cosmos asset; presumably * USDC over IBC. Add it to BankManager. * + * Also, if vatParameters shows an anchorPoolBalance for this asset, + * mint a payment for that balance. + * * TODO: address redundancy with publishInterchainAssetFromBank * - * @param {EconomyBootstrapPowers & WellKnownSpaces} powers + * @param {EconomyBootstrapPowers & WellKnownSpaces & ChainStorageVatParams} powers * @param {{options?: { anchorOptions?: AnchorOptions } }} [config] */ export const makeAnchorAsset = async ( { - consume: { agoricNamesAdmin, bankManager, zoe }, + vatParameters: { chainStorageEntries }, + consume: { agoricNamesAdmin, bankManager, zoe, anchorBalancePayments }, installation: { consume: { mintHolder }, }, - produce: { testFirstAnchorKit }, + // XXX: prune testFirstAnchorKit in favor of anchorMints + produce: { + testFirstAnchorKit, + anchorBalancePayments: produceAnchorBalancePayments, + }, }, { options: { anchorOptions = {} } = {} }, ) => { @@ -268,7 +447,23 @@ export const makeAnchorAsset = async ( testFirstAnchorKit.resolve(kit); - return Promise.all([ + const toSlotReviver = makeHistoryReviver(chainStorageEntries || []); + const metricsKey = `published.psm.${Stable.symbol}.${keyword}.metrics`; + if (toSlotReviver.has(metricsKey)) { + const metrics = toSlotReviver.getItem(metricsKey); + produceAnchorBalancePayments.resolve(makeScalarMapStore()); + // XXX this rule should only apply to the 1st await + // eslint-disable-next-line @jessie.js/no-nested-await + const anchorPaymentMap = await anchorBalancePayments; + + // eslint-disable-next-line @jessie.js/no-nested-await + const pmt = await E(mint).mintPayment( + AmountMath.make(brand, metrics.anchorPoolBalance.value), + ); + anchorPaymentMap.init(brand, pmt); + } + + await Promise.all([ E(E(agoricNamesAdmin).lookupAdmin('issuer')).update(keyword, kit.issuer), E(E(agoricNamesAdmin).lookupAdmin('brand')).update(keyword, kit.brand), E(bankManager).addAsset( @@ -374,12 +569,27 @@ export const INVITE_PSM_COMMITTEE_MANIFEST = harden( /** @type {BootstrapManifest} */ export const PSM_MANIFEST = { + [mintPSMFees.name]: { + vatParameters: { chainStorageEntries: true }, + consume: { feeMintAccess: 'zoe', zoe: 'zoe' }, + produce: { psmFeePurse: true }, + installation: { + consume: { centralSupply: 'zoe' }, + }, + }, [makeAnchorAsset.name]: { - consume: { agoricNamesAdmin: true, bankManager: 'bank', zoe: 'zoe' }, + vatParameters: { chainStorageEntries: true }, + consume: { + agoricNamesAdmin: true, + bankManager: 'bank', + zoe: 'zoe', + anchorBalancePayments: true, + }, installation: { consume: { mintHolder: 'zoe' } }, - produce: { testFirstAnchorKit: true }, + produce: { testFirstAnchorKit: true, anchorBalancePayments: true }, }, [startPSM.name]: { + vatParameters: { chainStorageEntries: true }, consume: { agoricNamesAdmin: true, board: true, @@ -391,6 +601,8 @@ export const PSM_MANIFEST = { econCharterKit: 'econCommitteeCharter', chainTimerService: 'timer', psmKit: true, + anchorBalancePayments: true, + psmFeePurse: true, }, produce: { psmKit: 'true' }, installation: { diff --git a/packages/inter-protocol/test/psm/setupPsm.js b/packages/inter-protocol/test/psm/setupPsm.js index 8942b4e2b1f3..78a1fa1ccfe4 100644 --- a/packages/inter-protocol/test/psm/setupPsm.js +++ b/packages/inter-protocol/test/psm/setupPsm.js @@ -74,7 +74,7 @@ export const setupPsmBootstrap = async ( produce.chainStorage.resolve(mockChainStorage); produce.board.resolve(makeBoard()); - return { produce, consume, ...spaces, mockChainStorage }; + return { vatParameters: {}, produce, consume, ...spaces, mockChainStorage }; }; /** diff --git a/packages/inter-protocol/test/psm/test-psm.js b/packages/inter-protocol/test/psm/test-psm.js index 2baabaed0e35..052b9ba6c837 100644 --- a/packages/inter-protocol/test/psm/test-psm.js +++ b/packages/inter-protocol/test/psm/test-psm.js @@ -6,6 +6,7 @@ import '../../src/vaultFactory/types.js'; import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { split } from '@agoric/ertp/src/legacy-payment-helpers.js'; import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; +import contractGovernorBundle from '@agoric/governance/bundles/bundle-contractGovernor.js'; import committeeBundle from '@agoric/governance/bundles/bundle-committee.js'; import { unsafeMakeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js'; import { makeBoard } from '@agoric/vats/src/lib-board.js'; @@ -16,12 +17,16 @@ import { natSafeMath as NatMath, } from '@agoric/zoe/src/contractSupport/index.js'; import centralSupplyBundle from '@agoric/vats/bundles/bundle-centralSupply.js'; -import { E } from '@endo/eventual-send'; +import mintHolderBundle from '@agoric/vats/bundles/bundle-mintHolder.js'; + +import { E, Far } from '@endo/far'; import { NonNullish } from '@agoric/assert'; import path from 'path'; import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js'; import { makeTracer } from '@agoric/internal'; import { documentStorageSchema } from '@agoric/governance/tools/storageDoc.js'; +import { makeAgoricNamesAccess, makePromiseSpace } from '@agoric/vats'; +import { Stable } from '@agoric/vats/src/tokens.js'; import { makeMockChainStorageRoot, mintRunPayment, @@ -29,6 +34,12 @@ import { subscriptionKey, withAmountUtils, } from '../supports.js'; +import { + makeAnchorAsset, + makeHistoryReviver, + mintPSMFees, + startPSM, +} from '../../src/proposals/startPSM.js'; /** @type {import('ava').TestFn>>} */ const test = anyTest; @@ -98,21 +109,27 @@ const makeTestContext = async () => { const minted = withAmountUtils(mintedKit); const anchor = withAmountUtils(makeIssuerKit('aUSD')); - const committeeInstall = await E(zoe).install(committeeBundle); - const psmInstall = await E(zoe).install(psmBundle); - const centralSupply = await E(zoe).install(centralSupplyBundle); + const installs = { + contractGovernor: await E(zoe).install(contractGovernorBundle), + committeeInstall: await E(zoe).install(committeeBundle), + psmInstall: await E(zoe).install(psmBundle), + centralSupply: await E(zoe).install(centralSupplyBundle), + mintHolder: await E(zoe).install(mintHolderBundle), + }; - const marshaller = makeBoard().getReadonlyMarshaller(); + const board = makeBoard(); + const marshaller = board.getReadonlyMarshaller(); + const chainStorage = makeMockChainStorageRoot(); const { creatorFacet: committeeCreator } = await E(zoe).startInstance( - committeeInstall, + installs.committeeInstall, harden({}), { committeeName: 'Demos', committeeSize: 1, }, { - storageNode: makeMockChainStorageRoot().makeChildNode('thisCommittee'), + storageNode: chainStorage.makeChildNode('thisCommittee'), marshaller, }, ); @@ -126,10 +143,13 @@ const makeTestContext = async () => { bundles: { psmBundle }, zoe: await zoe, feeMintAccess, + economicCommitteeCreatorFacet: committeeCreator, initialPoserInvitation, + chainStorage, minted, anchor, - installs: { committeeInstall, psmInstall, centralSupply }, + installs, + board, marshaller, terms: { anchorBrand: anchor.brand, @@ -700,3 +720,269 @@ test('extra give wantMintedInvitation', async t => { }, ); }); + +/** + * Test data for restoring PSM state. + * + * Taken from mainnet; for example, the 1st value comes from... + * + * `agd --node https://main.rpc.agoric.net:443 query vstorage data published.agoricNames.brand -o json | jq .value` + * + * @type {Array<[key: string, value: string]>} + */ +const chainStorageEntries = [ + [ + 'published.psm.IST.USDC_axl.governance', + '{"blockHeight":"9004077","values":["{\\"body\\":\\"{\\\\\\"current\\\\\\":{\\\\\\"Electorate\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"invitation\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: Zoe Invitation brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":[{\\\\\\"description\\\\\\":\\\\\\"questionPoser\\\\\\",\\\\\\"handle\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InvitationHandle\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"installation\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: BundleInstallation\\\\\\",\\\\\\"index\\\\\\":2},\\\\\\"instance\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InstanceHandle\\\\\\",\\\\\\"index\\\\\\":3}}]}},\\\\\\"GiveMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}},\\\\\\"MintLimit\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"amount\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"1000000000000\\\\\\"}}},\\\\\\"WantMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}}}}\\",\\"slots\\":[\\"board04016\\",\\"board00917\\",\\"board00218\\",\\"board0074\\",\\"board02314\\"]}"]}', + ], + [ + 'published.psm.IST.USDC_axl.metrics', + '{"blockHeight":"9555449","values":["{\\"body\\":\\"{\\\\\\"anchorPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: USDC_axl brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"487464281410\\\\\\"}},\\\\\\"feePoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}},\\\\\\"mintedPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"487464281410\\\\\\"}},\\\\\\"totalAnchorProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"4327825824427\\\\\\"}},\\\\\\"totalMintedProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"4815290105837\\\\\\"}}}\\",\\"slots\\":[\\"board0223\\",\\"board02314\\"]}"]}', + ], + + [ + 'published.psm.IST.USDT_axl.governance', + '{"blockHeight":"9174468","values":["{\\"body\\":\\"{\\\\\\"current\\\\\\":{\\\\\\"Electorate\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"invitation\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: Zoe Invitation brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":[{\\\\\\"description\\\\\\":\\\\\\"questionPoser\\\\\\",\\\\\\"handle\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InvitationHandle\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"installation\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: BundleInstallation\\\\\\",\\\\\\"index\\\\\\":2},\\\\\\"instance\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InstanceHandle\\\\\\",\\\\\\"index\\\\\\":3}}]}},\\\\\\"GiveMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}},\\\\\\"MintLimit\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"amount\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"650000000000\\\\\\"}}},\\\\\\"WantMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}}}}\\",\\"slots\\":[\\"board04016\\",\\"board06120\\",\\"board00218\\",\\"board0074\\",\\"board02314\\"]}"]}', + ], + [ + 'published.psm.IST.USDT_axl.metrics', + '{"blockHeight":"9554534","values":["{\\"body\\":\\"{\\\\\\"anchorPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: USDT_axl brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"12155727701\\\\\\"}},\\\\\\"feePoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"58732736\\\\\\"}},\\\\\\"mintedPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"12155727701\\\\\\"}},\\\\\\"totalAnchorProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"301812236296\\\\\\"}},\\\\\\"totalMintedProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"313967963997\\\\\\"}}}\\",\\"slots\\":[\\"board0188\\",\\"board02314\\"]}"]}', + ], + + [ + 'published.psm.IST.DAI_axl.governance', + '{"blockHeight":"7739600","values":["{\\"body\\":\\"{\\\\\\"current\\\\\\":{\\\\\\"Electorate\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"invitation\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: Zoe Invitation brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":[{\\\\\\"description\\\\\\":\\\\\\"questionPoser\\\\\\",\\\\\\"handle\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InvitationHandle\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"installation\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: BundleInstallation\\\\\\",\\\\\\"index\\\\\\":2},\\\\\\"instance\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: InstanceHandle\\\\\\",\\\\\\"index\\\\\\":3}}]}},\\\\\\"GiveMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}},\\\\\\"MintLimit\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"amount\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"1100000000000\\\\\\"}}},\\\\\\"WantMintedFee\\\\\\":{\\\\\\"type\\\\\\":\\\\\\"ratio\\\\\\",\\\\\\"value\\\\\\":{\\\\\\"denominator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"10000\\\\\\"}},\\\\\\"numerator\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":4},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}}}}}}\\",\\"slots\\":[\\"board04016\\",\\"board01759\\",\\"board00218\\",\\"board0074\\",\\"board02314\\"]}"]}', + ], + [ + 'published.psm.IST.DAI_axl.metrics', + '{"blockHeight":"9555443","values":["{\\"body\\":\\"{\\\\\\"anchorPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: DAI_axl brand\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"100064908627531159731648\\\\\\"}},\\\\\\"feePoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"iface\\\\\\":\\\\\\"Alleged: IST brand\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"0\\\\\\"}},\\\\\\"mintedPoolBalance\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"100064908610\\\\\\"}},\\\\\\"totalAnchorProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":0},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"329326452472000000000000\\\\\\"}},\\\\\\"totalMintedProvided\\\\\\":{\\\\\\"brand\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"slot\\\\\\",\\\\\\"index\\\\\\":1},\\\\\\"value\\\\\\":{\\\\\\"@qclass\\\\\\":\\\\\\"bigint\\\\\\",\\\\\\"digits\\\\\\":\\\\\\"429391361082\\\\\\"}}}\\",\\"slots\\":[\\"board02656\\",\\"board02314\\"]}"]}', + ], +]; + +/** + * Sample of what appears in chain config file + * + * @type {AnchorOptions[]} + */ +const anchorAssets = [ + { + keyword: 'USDC_axl', + proposedName: 'USD Coin', + decimalPlaces: 6, + denom: 'ibc/toyusdc', + }, + { + keyword: 'USDT_axl', + proposedName: 'Tether USD', + decimalPlaces: 6, + denom: + 'ibc/F2331645B9683116188EF36FC04A809C28BD36B54555E8705A37146D0182F045', + }, + { + keyword: 'DAI_axl', + proposedName: 'DAI', + decimalPlaces: 18, + denom: + 'ibc/3914BDEF46F429A26917E4D8D434620EC4817DC6B6E68FB327E190902F1E9242', + }, +]; + +// XXX copied (with minor tweak) from packages/inter-protocol/test/test-gov-collateral.js +const makeMockBankManager = t => { + /** @type {BankManager} */ + const bankManager = Far('mock BankManager', { + getAssetSubscription: () => assert.fail('not impl'), + getModuleAccountAddress: () => assert.fail('not impl'), + getRewardDistributorDepositFacet: () => + Far('depositFacet', { + receive: () => /** @type {any} */ (null), + }), + addAsset: async (denom, keyword, proposedName, kit) => { + t.log('addAsset', { denom, keyword, issuer: `${kit.issuer}` }); + t.truthy(kit.mint); + }, + getBankForAddress: () => assert.fail('not impl'), + }); + return bankManager; +}; + +test('restore PSM: decode metrics, governance with old board IDs', async t => { + const toSlotReviver = makeHistoryReviver(chainStorageEntries); + + const psmNames = toSlotReviver.children('published.psm.IST.'); + t.true(psmNames.includes('USDC_axl')); + + const a0 = { + metrics: toSlotReviver.getItem(`published.psm.IST.USDC_axl.metrics`), + governance: toSlotReviver.getItem(`published.psm.IST.USDC_axl.governance`), + }; + + t.deepEqual( + a0.metrics.anchorPoolBalance, + { brand: 'board0223', value: 487_464_281_410n }, + 'metrics.anchorPoolBalance', + ); + t.deepEqual( + a0.governance.current.MintLimit.value, + { brand: 'board02314', value: 1_000_000_000_000n }, + 'governance.MintLimit', + ); +}); + +test('restore PSM: startPSM with previous metrics, params', async t => { + /** @type { import('../../src/proposals/econ-behaviors').EconomyBootstrapPowers } */ + // @ts-expect-error mock + const { produce, consume } = makePromiseSpace(); + const { agoricNames, agoricNamesAdmin, spaces } = makeAgoricNamesAccess(); + const { zoe } = t.context; + + // Prep bootstrap space + { + const { + installs, + board, + minted, + feeMintAccess, + economicCommitteeCreatorFacet, + chainStorage, + } = t.context; + + const provisionPoolStartResult = harden({ + creatorFacet: { + initPSM: (brand, psm) => t.log('initPSM', { brand, psm }), + }, + }); + + const econCharterKit = harden({ + creatorFacet: { + addInstance: (psm, creatorFacet, instance) => + t.log('addInstance', { psm, creatorFacet, instance }), + }, + }); + + for (const [name, value] of Object.entries({ + agoricNamesAdmin, + board, + chainStorage, + zoe, + feeMintAccess, + economicCommitteeCreatorFacet, + econCharterKit, + provisionPoolStartResult, + bankManager: makeMockBankManager(t), + chainTimerService: null, // not used in this test + })) { + produce[name].resolve(value); + } + + for (const [name, installation] of Object.entries({ + ...installs, + psm: installs.psmInstall, + })) { + spaces.installation.produce[name].resolve(installation); + } + + spaces.brand.produce[Stable.symbol].resolve(minted.brand); + } + + const powers = { + vatParameters: { chainStorageEntries, anchorAssets }, + produce, + consume, + ...spaces, + }; + + // Run code under test + await Promise.all([ + mintPSMFees(powers), + ...anchorAssets.map(anchorOptions => + makeAnchorAsset(powers, { + options: { anchorOptions }, + }), + ), + ...anchorAssets.map(anchorOptions => + startPSM(powers, { + options: { anchorOptions }, + }), + ), + ]); + + // Check results: USDC_axl metrics, params + const stableIssuer = E(zoe).getFeeIssuer(); + const stableBrand = await E(stableIssuer).getBrand(); + { + const anchorBrand = await agoricNames.lookup('brand', 'USDC_axl'); + const expected = { + metrics: { + anchorPoolBalance: { brand: anchorBrand, value: 487_464_281_410n }, + feePoolBalance: { brand: stableBrand, value: 0n }, + mintedPoolBalance: { brand: stableBrand, value: 487_464_281_410n }, + totalAnchorProvided: { brand: anchorBrand, value: 4_327_825_824_427n }, + totalMintedProvided: { brand: stableBrand, value: 4_815_290_105_837n }, + }, + params: { + GiveMintedFee: { + type: 'ratio', + value: { + numerator: { brand: stableBrand, value: 0n }, + denominator: { brand: stableBrand, value: 10_000n }, + }, + }, + MintLimit: { + type: 'amount', + value: { brand: stableBrand, value: 1_000_000_000_000n }, + }, + WantMintedFee: { + type: 'ratio', + value: { + numerator: { brand: stableBrand, value: 0n }, + denominator: { brand: stableBrand, value: 10_000n }, + }, + }, + }, + }; + + const instance0 = await E(E(agoricNamesAdmin).readonly()).lookup( + 'instance', + 'psm-IST-USDC_axl', + ); + /** @type {ERef} */ + const pf0 = E(zoe).getPublicFacet(instance0); + + const { value: contractMetrics } = await E( + E(pf0).getMetrics(), + ).getUpdateSince(); + t.deepEqual(contractMetrics, expected.metrics); + + const contractParams = await E(pf0).getGovernedParams(); + + /** @param {ParamStateRecord} p */ + const omitElectorate = ({ Electorate: _, ...params }) => params; + t.deepEqual(omitElectorate(contractParams), expected.params); + } + + // Check USDT fees + { + const expected = { brand: stableBrand, value: 58_732_736n }; + const anchorBrand = await agoricNames.lookup('brand', 'USDT_axl'); + const instance1 = await E(E(agoricNamesAdmin).readonly()).lookup( + 'instance', + 'psm-IST-USDT_axl', + ); + /** @type {ERef} */ + const pf1 = E(zoe).getPublicFacet(instance1); + + const { value: contractMetrics } = await E( + E(pf1).getMetrics(), + ).getUpdateSince(); + t.deepEqual(contractMetrics.feePoolBalance, expected); + + // actually collect the fees and see how much we got + const psmKit = await consume.psmKit; + const { psmCreatorFacet } = psmKit.get(anchorBrand); + const seat = E(zoe).offer(E(psmCreatorFacet).makeCollectFeesInvitation()); + const pmt = await E.get(E(seat).getPayouts()).Fee; + const amt = await E(stableIssuer).getAmountOf(pmt); + t.deepEqual(amt, { brand: stableBrand, value: expected.value }); + } +});