From 47d48236ee1702d8b0a903e39143132b56cfd096 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 25 May 2022 16:39:18 -0700 Subject: [PATCH] feat(vaultManager): expose liquidation metrics (#5393) * refactor(vaultManager): updateMetrics in finish() * feat(vaultManager): expose metrics on liquidation * fixup! feat(vaultManager): expose metrics on liquidation * test: helper for metrics state changes * deepDifference has bugs; import open source * fixup test logging * chore: more detailed error msg * fixup! test: helper for metrics state changes * fixup! deepDifference has bugs; import open source * hack: amount deltas (allow negative) * fixup! deepDifference has bugs; import open source * test: metrics subscription testing support * test: metrics subscription testing support * document metric properties * totalDebtTracker * fixup * feat(vaultManager): liquidation overage and shortfall metrics * fixup * improve metric names * fixup superfluous updateMetrics() * cleanup * updateVaultPriority -> updateVaultAccounting * validate sums from liquidation vs total debt ever * comments * cr Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../src/vaultFactory/liquidation.js | 31 +- .../src/vaultFactory/prioritizedVaults.js | 1 + .../run-protocol/src/vaultFactory/types.js | 4 +- .../run-protocol/src/vaultFactory/vault.js | 8 +- .../src/vaultFactory/vaultManager.js | 119 +++++--- packages/run-protocol/test/metrics.js | 49 +++- .../test/vaultFactory/test-vaultFactory.js | 272 +++++++++++++++--- .../vaultFactory/vault-contract-wrapper.js | 2 +- packages/xsnap/src/avaAssertXS.js | 1 + 9 files changed, 408 insertions(+), 79 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 98ed7bb8723..cd92db128ff 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -6,7 +6,26 @@ import { AmountMath } from '@agoric/ertp'; import { makeRatio, offerTo } from '@agoric/zoe/src/contractSupport/index.js'; import { makeTracer } from '../makeTracer.js'; -const trace = makeTracer('LIQ'); +const trace = makeTracer('LIQ', false); + +/** + * + * @param {Amount<'nat'>} debt + * @param {Amount<'nat'>} proceeds + */ +const discrepancy = (debt, proceeds) => { + if (AmountMath.isGTE(debt, proceeds)) { + return { + overage: AmountMath.makeEmptyFromAmount(debt), + shortfall: AmountMath.subtract(debt, proceeds), + }; + } else { + return { + overage: AmountMath.subtract(proceeds, debt), + shortfall: AmountMath.makeEmptyFromAmount(debt), + }; + } +}; /** * Liquidates a Vault, using the strategy to parameterize the particular @@ -24,7 +43,6 @@ const trace = makeTracer('LIQ'); * @param {Liquidator} liquidator * @param {Brand} collateralBrand * @param {Ratio} penaltyRate - * @returns {Promise} */ const liquidate = async ( zcf, @@ -68,15 +86,18 @@ const liquidate = async ( ]); // NB: all the proceeds from AMM sale are on the vault seat instead of a staging seat + const { shortfall, overage } = discrepancy(debt, proceeds.RUN); + const runToBurn = AmountMath.min(proceeds.RUN, debt); - trace('before burn', { debt, proceeds, runToBurn }); + trace('before burn', { debt, proceeds, overage, shortfall, runToBurn }); burnLosses(runToBurn, vaultZcfSeat); // Accounting complete. Update the vault state. vault.liquidated(AmountMath.subtract(debt, runToBurn)); - // remaining funds are left on the vault for the user to close and claim - return vault; + + // for accounting + return { proceeds: proceeds.RUN, overage, shortfall }; }; const liquidationDetailTerms = debtBrand => diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 4baad985d2c..7fb472c6b31 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -168,6 +168,7 @@ export const makePrioritizedVaults = (reschedulePriceCheck = () => {}) => { const refreshVaultPriority = (oldDebt, oldCollateral, vaultId) => { const vault = removeVaultByAttributes(oldDebt, oldCollateral, vaultId); addVault(vaultId, vault); + return vault; }; return Far('PrioritizedVaults', { diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 5540751c92a..f40987aef43 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -97,8 +97,8 @@ /** * @typedef {object} LoanTiming - * @property {RelativeTime} chargingPeriod - * @property {RelativeTime} recordingPeriod + * @property {RelativeTime} chargingPeriod in seconds + * @property {RelativeTime} recordingPeriod in seconds */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 65ca8db0ee1..6d3c1af9311 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -84,7 +84,7 @@ const validTransitions = { * @property {MintAndReallocate} mintAndReallocate * @property {(amount: Amount, seat: ZCFSeat) => void} burnAndRecord * @property {() => Ratio} getCompoundedInterest - * @property {(oldDebt: Amount, oldCollateral: Amount, vaultId: VaultId) => void} updateVaultPriority + * @property {(oldDebt: Amount, oldCollateral: Amount, vaultId: VaultId) => void} updateVaultAccounting * @property {() => import('./vaultManager.js').GovernedParamGetters} getGovernedParams */ @@ -253,7 +253,7 @@ const helperBehavior = { const { helper } = facets; helper.updateDebtSnapshot(newDebt); // update position of this vault in liquidation priority queue - state.manager.updateVaultPriority( + state.manager.updateVaultAccounting( oldDebt, oldCollateral, state.idInManager, @@ -292,7 +292,9 @@ const helperBehavior = { const maxRun = await state.manager.maxDebtFor(collateralAmount); assert( AmountMath.isGTE(maxRun, proposedRunDebt, facets.helper.debtBrand()), - X`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)}`, + X`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)} for ${q( + collateralAmount, + )} collateral`, ); }, diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 262b7634f6c..c20d54969f9 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -21,7 +21,7 @@ import { } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; -import { defineKindMulti, pickFacet } from '@agoric/vat-data'; +import { defineKindMulti, partialAssign, pickFacet } from '@agoric/vat-data'; import { makeVault } from './vault.js'; import { makePrioritizedVaults } from './prioritizedVaults.js'; import { liquidate } from './liquidation.js'; @@ -33,6 +33,21 @@ const { details: X } = assert; const trace = makeTracer('VM', false); +// Metrics naming scheme: nouns are present values; past-participles are accumulative. +/** + * @typedef {object} MetricsNotification + * + * @property {number} numVaults present count of vaults + * @property {Amount<'nat'>} totalCollateral present sum of collateral across all vaults + * @property {Amount<'nat'>} totalDebt present sum of debt across all vaults + * + * @property {Amount<'nat'>} totalCollateralSold running sum of collateral sold in liquidation // totalCollateralSold + * @property {Amount<'nat'>} totalOverageReceived running sum of overages, central received greater than debt + * @property {Amount<'nat'>} totalProceedsReceived running sum of central received from liquidation + * @property {Amount<'nat'>} totalShortfallReceived running sum of shortfalls, central received less than debt + * @property {number} numLiquidationsCompleted running count of liquidations + */ + /** * @typedef {{ * compoundedInterest: Ratio, @@ -42,12 +57,6 @@ const trace = makeTracer('VM', false); * }} AssetState * * @typedef {{ - * numVaults: number, - * totalCollateral: Amount<'nat'>, - * totalDebt: Amount<'nat'>, - * }} MetricsNotification - * - * @typedef {{ * getChargingPeriod: () => bigint, * getRecordingPeriod: () => bigint, * getDebtLimit: () => Amount<'nat'>, @@ -82,8 +91,13 @@ const trace = makeTracer('VM', false); * latestInterestUpdate: bigint, * liquidator?: Liquidator * liquidatorInstance?: Instance + * numLiquidationsCompleted: number, * totalCollateral: Amount<'nat'>, + * totalCollateralSold: Amount<'nat'>, * totalDebt: Amount<'nat'>, + * totalOverageReceived: Amount<'nat'>, + * totalProceedsReceived: Amount<'nat'>, + * totalShortfallReceived: Amount<'nat'>, * vaultCounter: number, * }} MutableState */ @@ -130,18 +144,11 @@ const initState = ( ); const debtBrand = debtMint.getIssuerRecord().brand; - const totalCollateral = AmountMath.makeEmpty(collateralBrand, 'nat'); - const totalDebt = AmountMath.makeEmpty(debtBrand, 'nat'); + const zeroCollateral = AmountMath.makeEmpty(collateralBrand, 'nat'); + const zeroDebt = AmountMath.makeEmpty(debtBrand, 'nat'); const { publication: metricsPublication, subscription: metricsSubscription } = makeSubscriptionKit(); - metricsPublication.updateState( - harden({ - numVaults: 0, - totalCollateral, - totalDebt, - }), - ); /** @type {ImmutableState} */ const fixed = { @@ -178,14 +185,19 @@ const initState = ( ...fixed, assetNotifier, assetUpdater, + compoundedInterest, debtBrand: fixed.debtBrand, - vaultCounter: 0, + latestInterestUpdate, liquidator: undefined, liquidatorInstance: undefined, - totalCollateral, - totalDebt, - compoundedInterest, - latestInterestUpdate, + numLiquidationsCompleted: 0, + totalCollateral: zeroCollateral, + totalDebt: zeroDebt, + totalOverageReceived: zeroDebt, + totalProceedsReceived: zeroDebt, + totalCollateralSold: zeroCollateral, + totalShortfallReceived: zeroDebt, + vaultCounter: 0, }; return state; @@ -243,7 +255,7 @@ const helperBehavior = { }, updateTime, ); - Object.assign(state, stateUpdates); + partialAssign(state, stateUpdates); facets.helper.assetNotify(); trace('chargeAllVaults complete'); facets.helper.reschedulePriceCheck(); @@ -272,6 +284,12 @@ const helperBehavior = { numVaults: state.prioritizedVaults.getCount(), totalCollateral: state.totalCollateral, totalDebt: state.totalDebt, + + numLiquidationsCompleted: state.numLiquidationsCompleted, + totalCollateralSold: state.totalCollateralSold, + totalOverageReceived: state.totalOverageReceived, + totalProceedsReceived: state.totalProceedsReceived, + totalShortfallReceived: state.totalShortfallReceived, }); state.metricsPublication.updateState(payload); }, @@ -393,6 +411,8 @@ const helperBehavior = { const { factoryPowers, prioritizedVaults, zcf } = state; trace('liquidating', vault.getVaultSeat().getProposal()); + const collateralPre = vault.getCollateralAmount(); + // Start liquidation (vaultState: LIQUIDATING) const liquidator = state.liquidator; assert(liquidator); @@ -405,9 +425,27 @@ const helperBehavior = { state.collateralBrand, factoryPowers.getGovernedParams().getLiquidationPenalty(), ) - .then(() => { + .then(accounting => { + console.log('liquidateAndRemove accounting', accounting); + state.totalProceedsReceived = AmountMath.add( + state.totalProceedsReceived, + accounting.proceeds, + ); + state.totalOverageReceived = AmountMath.add( + state.totalOverageReceived, + accounting.overage, + ); + state.totalShortfallReceived = AmountMath.add( + state.totalShortfallReceived, + accounting.shortfall, + ); + state.totalCollateral = AmountMath.subtract( + state.totalCollateral, + collateralPre, + ); prioritizedVaults.removeVault(key); trace('liquidated'); + state.numLiquidationsCompleted += 1; facets.helper.updateMetrics(); }) .catch(e => { @@ -484,15 +522,32 @@ const managerBehavior = { */ getCompoundedInterest: ({ state }) => state.compoundedInterest, /** + * Called by a vault when its balances change. + * * @param {MethodContext} context * @param {Amount<'nat'>} oldDebt * @param {Amount<'nat'>} oldCollateral * @param {VaultId} vaultId */ - updateVaultPriority: ({ state }, oldDebt, oldCollateral, vaultId) => { - const { prioritizedVaults, totalDebt } = state; - prioritizedVaults.refreshVaultPriority(oldDebt, oldCollateral, vaultId); - trace('updateVaultPriority complete', { totalDebt }); + updateVaultAccounting: ( + { state, facets }, + oldDebt, + oldCollateral, + vaultId, + ) => { + const { prioritizedVaults } = state; + const vault = prioritizedVaults.refreshVaultPriority( + oldDebt, + oldCollateral, + vaultId, + ); + // totalCollateral += vault's collateral delta (post — pre) + state.totalCollateral = AmountMath.subtract( + AmountMath.add(state.totalCollateral, vault.getCollateralAmount()), + oldCollateral, + ); + // debt accounting managed through minting and burning + facets.helper.updateMetrics(); }, }; @@ -530,7 +585,7 @@ const selfBehavior = { * @param {MethodContext} context * @param {ZCFSeat} seat */ - makeVaultKit: async ({ state, facets: { helper, manager } }, seat) => { + makeVaultKit: async ({ state, facets: { manager } }, seat) => { const { prioritizedVaults, zcf } = state; assertProposalShape(seat, { give: { Collateral: null }, @@ -549,12 +604,7 @@ const selfBehavior = { // TODO `await` is allowed until the above ordering is fixed // eslint-disable-next-line @jessie.js/no-nested-await const vaultKit = await vault.initVaultKit(seat); - state.totalCollateral = AmountMath.add( - state.totalCollateral, - vaultKit.vault.getCollateralAmount(), - ); seat.exit(); - helper.updateMetrics(); return vaultKit; } catch (err) { // remove it from prioritizedVaults @@ -629,6 +679,9 @@ const selfBehavior = { const finish = ({ state, facets: { helper } }) => { state.prioritizedVaults.setRescheduler(helper.reschedulePriceCheck); + // push initial state of metrics + helper.updateMetrics(); + observeNotifier(state.periodNotifier, { updateState: updateTime => helper diff --git a/packages/run-protocol/test/metrics.js b/packages/run-protocol/test/metrics.js index 440f3d94d47..bc0dbf4fd36 100644 --- a/packages/run-protocol/test/metrics.js +++ b/packages/run-protocol/test/metrics.js @@ -10,6 +10,8 @@ import { diff } from 'deep-object-diff'; export const subscriptionTracker = async (t, subscription) => { const metrics = makeNotifierFromAsyncIterable(subscription); let notif; + const getLastNotif = () => notif; + const assertInitial = async expectedValue => { notif = await metrics.getUpdateSince(); t.deepEqual(notif.value, expectedValue); @@ -25,7 +27,7 @@ export const subscriptionTracker = async (t, subscription) => { notif = await metrics.getUpdateSince(notif.updateCount); t.deepEqual(notif.value, expectedState, 'Unexpected state'); }; - return { assertChange, assertInitial, assertState }; + return { assertChange, assertInitial, assertState, getLastNotif }; }; /** @@ -38,3 +40,48 @@ export const metricsTracker = async (t, publicFacet) => { const metricsSub = await E(publicFacet).getMetrics(); return subscriptionTracker(t, metricsSub); }; + +/** + * @param {import('ava').ExecutionContext} t + * @param {import('../src/vaultFactory/vaultManager').CollateralManager} publicFacet + */ +export const vaultManagerMetricsTracker = async (t, publicFacet) => { + let totalDebtEver = 0n; + const m = await metricsTracker(t, publicFacet); + + /** @returns {bigint} Proceeds - overage + shortfall */ + const liquidatedYet = () => { + // XXX re-use the state until subscriptions are lossy https://github.com/Agoric/agoric-sdk/issues/5413 + const { value: v } = m.getLastNotif(); + const [p, o, s] = [ + v.totalProceedsReceived, + v.totalOverageReceived, + v.totalShortfallReceived, + ].map(a => a.value); + console.log('liquidatedYet', { p, o, s }); + return p - o + s; + }; + + /** @param {bigint} delta */ + const addDebt = delta => { + totalDebtEver += delta; + const liquidated = liquidatedYet(); + t.true( + liquidated < totalDebtEver, + `Liquidated ${liquidated} must be less than total debt ever ${totalDebtEver}`, + ); + }; + + const assertFullyLiquidated = () => { + const liquidated = liquidatedYet(); + t.true( + totalDebtEver - liquidated <= 1, + `Liquidated ${liquidated} must approx equal total debt ever ${totalDebtEver}`, + ); + }; + return harden({ + ...m, + addDebt, + assertFullyLiquidated, + }); +}; diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index 20a32f3788b..1e132d24afb 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -41,10 +41,13 @@ import { installGovernance, } from '../supports.js'; import { unsafeMakeBundleCache } from '../bundleTool.js'; +import { metricsTracker, vaultManagerMetricsTracker } from '../metrics.js'; -import { metricsTracker } from '../metrics.js'; - -/** @type {import('ava').TestInterface} */ +/** @type {import('ava').TestInterface & { + * rates: VaultManagerParamValues, + * loanTiming: LoanTiming, + * }>} */ +// @ts-expect-error cast const test = unknownTest; // #region Support @@ -129,7 +132,12 @@ test.before(async t => { aethInitialLiquidity: AmountMath.make(aethKit.brand, 300n), }; const frozenCtx = await deeplyFulfilled(harden(contextPs)); - t.context = { ...frozenCtx, bundleCache }; + t.context = { + ...frozenCtx, + bundleCache, + debtAmount: num => AmountMath.make(frozenCtx.runKit.brand, BigInt(num)), + collAmount: num => AmountMath.make(aethKit.brand, BigInt(num)), + }; trace(t, 'CONTEXT'); }); @@ -230,9 +238,9 @@ const getRunFromFaucet = async (t, runInitialLiquidity) => { * * @param {import('ava').ExecutionContext} t * @param {Array | Ratio} priceOrList - * @param {Amount} unitAmountIn + * @param {Amount | undefined} unitAmountIn * @param {TimerService} timer - * @param {unknown} quoteInterval + * @param {RelativeTime} quoteInterval * @param {bigint} runInitialLiquidity */ const setupServices = async ( @@ -240,7 +248,7 @@ const setupServices = async ( priceOrList, unitAmountIn, timer = buildManualTimer(t.log), - quoteInterval, + quoteInterval = 1n, runInitialLiquidity, ) => { const { @@ -1774,18 +1782,12 @@ test('mutable liquidity triggers and interest', async t => { }); test('bad chargingPeriod', async t => { - const loanTiming = { - chargingPeriod: 2, - recordingPeriod: 10n, - }; - - t.context.loanTiming = loanTiming; t.throws( () => makeParamManagerBuilder() - // @ts-expect-error It's not a bigint. - .addNat(CHARGING_PERIOD_KEY, loanTiming.chargingPeriod) - .addNat(RECORDING_PERIOD_KEY, loanTiming.recordingPeriod) + // @ts-expect-error bad value for test + .addNat(CHARGING_PERIOD_KEY, 2) + .addNat(RECORDING_PERIOD_KEY, 10n) .build(), { message: '2 must be a bigint' }, ); @@ -2401,57 +2403,259 @@ test('director notifiers', async t => { }); test('manager notifiers', async t => { - const LOAN = 450n; - const DEBT = 473n; // with penalty + const LOAN1 = 450n; + const DEBT1 = 473n; // with penalty + const LOAN2 = 50n; + const DEBT2 = 53n; // with penalty const AMPLE = 100_000n; + const ENOUGH = 10_000n; + + const { aethKit, runKit, debtAmount, collAmount } = t.context; + const manualTimer = buildManualTimer(t.log, 0n, SECONDS_PER_WEEK); + t.context.loanTiming = { + chargingPeriod: SECONDS_PER_WEEK, + recordingPeriod: SECONDS_PER_WEEK, + }; + t.context.rates = { + ...t.context.rates, + interestRate: makeRatio(20n, runKit.brand), + }; - const { aethKit, runKit } = t.context; const services = await setupServices( t, - [10n], - AmountMath.make(aethKit.brand, 900n), + makeRatio(1n, runKit.brand, 100n, aethKit.brand), undefined, + manualTimer, undefined, - AMPLE, + // tuned so first liquidations have overage and the second have shortfall + 3n * (DEBT1 + DEBT2), ); const { aethVaultManager, lender } = services.vaultFactory; const cm = await E(aethVaultManager).getPublicFacet(); - const m = await metricsTracker(t, cm); + const m = await vaultManagerMetricsTracker(t, cm); + trace('0. Creation'); await m.assertInitial({ + // present numVaults: 0, - totalCollateral: AmountMath.makeEmpty(aethKit.brand), - totalDebt: AmountMath.makeEmpty(t.context.runKit.brand), + totalCollateral: collAmount(0), + totalDebt: debtAmount(0), + + // running + numLiquidationsCompleted: 0, + totalOverageReceived: debtAmount(0), + totalProceedsReceived: debtAmount(0), + totalCollateralSold: collAmount(0), + totalShortfallReceived: debtAmount(0), }); - // Create a loan with ample collateral - const collateralAmount = AmountMath.make(aethKit.brand, AMPLE); - const loanAmount = AmountMath.make(runKit.brand, LOAN); + trace('1. Create a loan with ample collateral'); /** @type {UserSeat} */ - const vaultSeat = await E(services.zoe).offer( + let vaultSeat = await E(services.zoe).offer( await E(lender).makeVaultInvitation(), harden({ - give: { Collateral: collateralAmount }, - want: { RUN: loanAmount }, + give: { Collateral: collAmount(AMPLE) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN1) }, + }), + harden({ + Collateral: t.context.aethKit.mint.mintPayment(collAmount(AMPLE)), + }), + ); + const { vault: vault1 } = await E(vaultSeat).getOfferResult(); + m.addDebt(DEBT1); + await m.assertChange({ + numVaults: 1, + totalCollateral: { value: AMPLE }, + totalDebt: { value: DEBT1 }, + }); + + trace('2. Remove collateral'); + const adjustBalances1 = await E(vault1).makeAdjustBalancesInvitation(); + const taken = collAmount(50_000); + const takeCollateralSeat = await E(services.zoe).offer( + adjustBalances1, + harden({ + give: {}, + want: { Collateral: taken }, + }), + ); + await E(takeCollateralSeat).getOfferResult(); + await m.assertChange({ + totalCollateral: { value: AMPLE - taken.value }, + }); + + trace('3. Liquidate all (1 loan)'); + await E(aethVaultManager).liquidateAll(); + let totalProceedsReceived = 474n; + let totalOverageReceived = totalProceedsReceived - DEBT1; + await m.assertChange({ + numVaults: 0, + totalCollateral: { value: 0n }, + totalDebt: { value: 0n }, + numLiquidationsCompleted: 1, + totalOverageReceived: { value: totalOverageReceived }, + totalProceedsReceived: { value: totalProceedsReceived }, + }); + await m.assertFullyLiquidated(); + + trace('4. Make another LOAN1 loan'); + vaultSeat = await E(services.zoe).offer( + await E(lender).makeVaultInvitation(), + harden({ + give: { Collateral: collAmount(AMPLE) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN1) }, }), harden({ - Collateral: t.context.aethKit.mint.mintPayment(collateralAmount), + Collateral: t.context.aethKit.mint.mintPayment(collAmount(AMPLE)), }), ); + await E(vaultSeat).getOfferResult(); + await m.assertChange({ + numVaults: 1, + totalCollateral: { value: AMPLE }, + totalDebt: { value: DEBT1 }, + }); + m.addDebt(DEBT1); + trace('5. Make a LOAN2 loan'); + vaultSeat = await E(services.zoe).offer( + await E(lender).makeVaultInvitation(), + harden({ + give: { Collateral: collAmount(ENOUGH) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN2) }, + }), + harden({ + Collateral: t.context.aethKit.mint.mintPayment(collAmount(ENOUGH)), + }), + ); await E(vaultSeat).getOfferResult(); + await m.assertChange({ + numVaults: 2, + totalCollateral: { value: AMPLE + ENOUGH }, + totalDebt: { value: DEBT1 + DEBT2 }, + }); + m.addDebt(DEBT2); + + trace('6. Liquidate all (2 loans)'); + await E(aethVaultManager).liquidateAll(); + totalProceedsReceived += 54n; + totalOverageReceived += 54n - DEBT2; + await m.assertChange({ + numLiquidationsCompleted: 2, + numVaults: 1, + totalCollateral: { value: AMPLE }, + totalDebt: { value: 0n }, + totalOverageReceived: { value: totalOverageReceived }, + totalProceedsReceived: { value: totalProceedsReceived }, + }); + totalProceedsReceived += 473n; + await m.assertChange({ + numLiquidationsCompleted: 3, + numVaults: 0, + totalCollateral: { value: 0n }, + totalProceedsReceived: { value: totalProceedsReceived }, + }); + await m.assertFullyLiquidated(); + trace('7. Make another LOAN2 loan'); + vaultSeat = await E(services.zoe).offer( + await E(lender).makeVaultInvitation(), + harden({ + give: { Collateral: collAmount(ENOUGH) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN2) }, + }), + harden({ + Collateral: t.context.aethKit.mint.mintPayment(collAmount(ENOUGH)), + }), + ); + await E(vaultSeat).getOfferResult(); await m.assertChange({ numVaults: 1, - totalCollateral: { value: collateralAmount.value }, - totalDebt: { value: DEBT }, + totalCollateral: { value: collAmount(ENOUGH).value }, + totalDebt: { value: DEBT2 }, }); + m.addDebt(DEBT2); + trace('8. Liquidate all'); await E(aethVaultManager).liquidateAll(); + totalProceedsReceived += 53n; await m.assertChange({ + numLiquidationsCompleted: 4, numVaults: 0, + totalCollateral: { value: 0n }, totalDebt: { value: 0n }, + totalProceedsReceived: { value: totalProceedsReceived }, + }); + + trace('9. Loan interest'); + vaultSeat = await E(services.zoe).offer( + await E(lender).makeVaultInvitation(), + harden({ + give: { Collateral: collAmount(AMPLE) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN1) }, + }), + harden({ + Collateral: t.context.aethKit.mint.mintPayment(collAmount(AMPLE)), + }), + ); + await E(vaultSeat).getOfferResult(); + await m.assertChange({ + numVaults: 1, + totalCollateral: { value: AMPLE }, + totalDebt: { value: DEBT1 }, + }); + m.addDebt(DEBT1); + const periods = 5n; + for (let i = 0; i < periods; i += 1) { + // eslint-disable-next-line no-await-in-loop + await manualTimer.tick(); + } + const interestAccrued = periods * 2n; + m.addDebt(interestAccrued); + // make another loan to trigger a publish + vaultSeat = await E(services.zoe).offer( + await E(lender).makeVaultInvitation(), + harden({ + give: { Collateral: collAmount(ENOUGH) }, + want: { RUN: AmountMath.make(runKit.brand, LOAN2) }, + }), + harden({ + Collateral: t.context.aethKit.mint.mintPayment(collAmount(ENOUGH)), + }), + ); + await E(vaultSeat).getOfferResult(); + await m.assertChange({ + numVaults: 2, + totalCollateral: { value: AMPLE + ENOUGH }, + totalDebt: { value: DEBT1 + interestAccrued + DEBT2 }, + }); + m.addDebt(DEBT2); + + trace('10. Liquidate all including interest'); + // liquidateAll executes in parallel, allowing the two burns to complete before the proceed calculations begin + await E(aethVaultManager).liquidateAll(); + let nextProceeds = 53n; + totalProceedsReceived += nextProceeds; + console.log({ DEBT1, DEBT2, interestAccrued, nextProceeds }); + await m.assertChange({ + numLiquidationsCompleted: 5, + numVaults: 1, + totalCollateral: { value: AMPLE }, + totalDebt: { value: DEBT1 + DEBT2 + interestAccrued - nextProceeds - 296n }, // debt changed already with proceeds from next notification + totalProceedsReceived: { value: totalProceedsReceived }, + }); + nextProceeds = 296n; + totalProceedsReceived += nextProceeds; + await m.assertChange({ + numLiquidationsCompleted: 6, + numVaults: 0, + totalCollateral: { value: 0n }, + totalProceedsReceived: { value: totalProceedsReceived }, + totalShortfallReceived: { + value: DEBT1 + DEBT2 + interestAccrued - nextProceeds - 53n - 1n, // compensate for previous proceeds and rounding + }, }); + await m.assertFullyLiquidated(); }); diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index 66c08ef8e67..6cf3f3026fa 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -140,7 +140,7 @@ export async function start(zcf, privateArgs) { return Promise.reject(Error('Not implemented')); }, getCompoundedInterest: () => compoundedInterest, - updateVaultPriority: () => { + updateVaultAccounting: () => { // noop }, mintforVault: async amount => { diff --git a/packages/xsnap/src/avaAssertXS.js b/packages/xsnap/src/avaAssertXS.js index 3f76d15405f..c917f32ac64 100644 --- a/packages/xsnap/src/avaAssertXS.js +++ b/packages/xsnap/src/avaAssertXS.js @@ -8,6 +8,7 @@ const { assign, freeze, keys } = Object; /** * deep equal value comparison + * XXX broken https://github.com/Agoric/agoric-sdk/pull/5398 * * originally based on code from Paul Roub Aug 2014 * https://stackoverflow.com/a/25456134/7963