From 7e286a5ed76de7bfaee4307a147190b919043109 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 17 May 2023 23:03:56 -0700 Subject: [PATCH] fix(vaults): reconstitue vaults correctly fixes: #7784 I over-corrected with my last fix, generalizing a fix that applied to flow #1 to apply also to flow 2b, which it did not. The output here now matches the scenarios the PMs have been testing. --- .../src/vaultFactory/vaultManager.js | 26 +- .../vaultFactory/test-vaultLiquidation.js | 231 +++++++++++++++++- .../test/vaultFactory/vaultFactoryUtils.js | 10 +- 3 files changed, 243 insertions(+), 24 deletions(-) diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index 3e95b100687..128b8f02c01 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -847,7 +847,7 @@ export const prepareVaultManagerKit = ( let collateralReduction = AmountMath.makeEmpty(collateralBrand); let shortfallToReserve = accounting.shortfall; - + const debtPortion = makeRatioFromAmounts(totalPenalty, totalDebt); const reduceCollateral = amount => (collateralReduction = AmountMath.add( collateralReduction, @@ -859,19 +859,21 @@ export const prepareVaultManagerKit = ( /** @type {Array<[Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}]>} */ for (const [vault, balance] of bestToWorst) { const { collateralAmount: vCollat, debtAmount } = balance; - const debtInCollateral = ceilDivideBy(debtAmount, price); - const collatPostDebt = AmountMath.isGTE(vCollat, debtInCollateral) - ? AmountMath.subtract(vCollat, debtInCollateral) - : AmountMath.makeEmptyFromAmount(vCollat); + const vaultPenalty = ceilMultiplyBy(debtAmount, penaltyRate); + const collatPostPenalty = AmountMath.subtract( + vCollat, + ceilMultiplyBy(vaultPenalty, debtPortion), + ); + const vaultDebt = floorMultiplyBy(debtAmount, debtPortion); if ( reconstituteVaults && - !AmountMath.isEmpty(collatPostDebt) && - AmountMath.isGTE(collatRemaining, collatPostDebt) && + !AmountMath.isEmpty(collatPostPenalty) && + AmountMath.isGTE(collatRemaining, collatPostPenalty) && AmountMath.isGTE(totalDebt, debtAmount) ) { collatRemaining = AmountMath.subtract( collatRemaining, - collatPostDebt, + collatPostPenalty, ); shortfallToReserve = AmountMath.isGTE( shortfallToReserve, @@ -882,8 +884,12 @@ export const prepareVaultManagerKit = ( const seat = vault.getVaultSeat(); // must reinstate after atomicRearrange(), so we record them. vaultsToReinstate.push(vault); - reduceCollateral(debtInCollateral); - transfers.push([liqSeat, seat, { Collateral: collatPostDebt }]); + reduceCollateral(vaultDebt); + transfers.push([ + liqSeat, + seat, + { Collateral: collatPostPenalty }, + ]); } else { reconstituteVaults = false; liquidated += 1; diff --git a/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js b/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js index 971e79ae945..5da5d0fedaa 100644 --- a/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js +++ b/packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js @@ -130,6 +130,7 @@ test.before(async t => { * @param {object} actionParams * @param {bigint} [actionParams.startFrequency] * @param {bigint} [actionParams.discountStep] + * @param {bigint} [actionParams.lowestRate] */ const setupServices = async ( t, @@ -138,9 +139,11 @@ const setupServices = async ( timer = buildManualTimer(), quoteInterval = 1n, runInitialLiquidity, - { startFrequency, discountStep } = { + // FIXME: should be an AuctionParams + { startFrequency, discountStep, lowestRate } = { startFrequency: undefined, discountStep: undefined, + lowestRate: undefined, }, ) => { const { zoe, run, aeth, interestTiming, minInitialDebt, endorsedUi, rates } = @@ -155,7 +158,11 @@ const setupServices = async ( priceOrList, quoteInterval, unitAmountIn, - { startFrequency, discountStep }, + { + StartFreuency: startFrequency, + LowestRate: lowestRate, + DiscountStep: discountStep, + }, ); const { consume } = space; @@ -1876,7 +1883,7 @@ test('reinstate vault', async t => { liquidatingDebt: { value: 0n }, liquidatingCollateral: { value: 0n }, totalDebt: { value: 158n }, - totalCollateral: { value: 10n }, + totalCollateral: { value: 44n }, totalProceedsReceived: { value: 34n }, totalShortfallReceived: { value: 66n }, totalCollateralSold: { value: 8n }, @@ -1892,7 +1899,7 @@ test('reinstate vault', async t => { // Reduce Bob's collateral by liquidation penalty const recoveredBobCollateral = AmountMath.subtract( bobCollateralAmount, - aeth.make(38n), + aeth.make(1n), ); bobUpdate = await E(bobNotifier).getUpdateSince(); t.is(bobUpdate.value.vaultState, Phase.ACTIVE); @@ -1906,7 +1913,7 @@ test('reinstate vault', async t => { const m = await subscriptionTracker(t, metricsTopic); await m.assertLike({ allocations: { - Aeth: aeth.make(38n), + Aeth: aeth.make(1n), Fee: run.makeEmpty(), }, }); @@ -2378,17 +2385,17 @@ test('Bug 7346 excess collateral to holder', async t => { totalDebt: { value: 0n }, liquidatingCollateral: { value: 0n }, totalCollateral: { value: 0n }, - totalCollateralSold: { value: 36_451n }, + totalCollateralSold: { value: 35_339n }, totalProceedsReceived: { value: 309_540n }, numLiquidatingVaults: 0, numLiquidationsCompleted: 3, }); - t.deepEqual(await E(aliceVault).getCollateralAmount(), aeth.make(3_165n)); + t.deepEqual(await E(aliceVault).getCollateralAmount(), aeth.make(3_526n)); t.deepEqual(await E(aliceVault).getCurrentDebt(), run.makeEmpty()); - t.deepEqual(await E(bobVault).getCollateralAmount(), aeth.make(2810n)); + t.deepEqual(await E(bobVault).getCollateralAmount(), aeth.make(3182n)); t.deepEqual(await E(bobVault).getCurrentDebt(), run.makeEmpty()); - t.deepEqual(await E(carolVault).getCollateralAmount(), aeth.make(2_264n)); + t.deepEqual(await E(carolVault).getCollateralAmount(), aeth.make(2_643n)); t.deepEqual(await E(carolVault).getCurrentDebt(), run.makeEmpty()); t.false(await E(bidderSeat3).hasExited()); @@ -2396,10 +2403,10 @@ test('Bug 7346 excess collateral to holder', async t => { t.true(await E(bidderSeat3).hasExited()); await assertBidderPayout(t, bidderSeat3, run, 10_460n, aeth, 0n); t.true(await E(bidderSeat1).hasExited()); - await assertBidderPayout(t, bidderSeat1, run, 0n, aeth, 9421n); + await assertBidderPayout(t, bidderSeat1, run, 0n, aeth, 8897n); t.true(await E(bidderSeat2).hasExited()); - await assertBidderPayout(t, bidderSeat2, run, 0n, aeth, 10598n); + await assertBidderPayout(t, bidderSeat2, run, 0n, aeth, 10_010n); const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) .metrics; @@ -2616,3 +2623,205 @@ test('refund to one of two loans', async t => { }, }); }); + +test('Bug 7784 reconstitute both', async t => { + const { zoe, aeth, run, rates: defaultRates } = t.context; + + const rates = harden({ + ...defaultRates, + liquidationPenalty: makeRatio(1n, run.brand), + liquidationMargin: run.makeRatio(150n), + mintFee: run.makeRatio(50n, 10_000n), + }); + t.context.rates = rates; + + const manualTimer = buildManualTimer(); + const services = await setupServices( + t, + makeRatio(1234n, run.brand, 100n, aeth.brand), + aeth.make(1000n), + manualTimer, + SECONDS_PER_WEEK, + 500n, + { discountStep: 500n, lowestRate: 6500n }, + ); + + const { + vaultFactory: { aethVaultManager, aethCollateralManager }, + auctioneerKit: auctKit, + priceAuthority, + reserveKit: { reserveCreatorFacet, reservePublicFacet }, + } = services; + await E(reserveCreatorFacet).addIssuer(aeth.issuer, 'Aeth'); + + const cm = await E(aethVaultManager).getPublicFacet(); + const aethVaultMetrics = await vaultManagerMetricsTracker(t, cm); + await aethVaultMetrics.assertInitial({ + // present + numActiveVaults: 0, + numLiquidatingVaults: 0, + totalCollateral: aeth.make(0n), + totalDebt: run.make(0n), + retainedCollateral: aeth.make(0n), + + // running + numLiquidationsCompleted: 0, + numLiquidationsAborted: 0, + totalOverageReceived: run.make(0n), + totalProceedsReceived: run.make(0n), + totalCollateralSold: aeth.make(0n), + liquidatingCollateral: aeth.make(0n), + liquidatingDebt: run.make(0n), + totalShortfallReceived: run.make(0n), + }); + + const openVault = (collateral, want) => + E(zoe).offer( + E(aethCollateralManager).makeVaultInvitation(), + harden({ + give: { Collateral: collateral }, + want: { Minted: want }, + }), + harden({ + Collateral: aeth.mint.mintPayment(collateral), + }), + ); + + const aliceWantMinted = run.make(100_000n); + const collateral = aeth.make(15_000n); + /** @type {UserSeat} */ + const aliceVaultSeat = await openVault(collateral, aliceWantMinted); + const { + vault: aliceVault, + publicNotifiers: { vault: aliceNotifier }, + } = await legacyOfferResult(aliceVaultSeat); + let aliceUpdate = await E(aliceNotifier).getUpdateSince(); + t.is(aliceUpdate.value.vaultState, Phase.ACTIVE); + await aethVaultMetrics.assertChange({ + numActiveVaults: 1, + totalCollateral: { value: 15_000n }, + totalDebt: { value: 100_500n }, + }); + + const bobWantMinted = run.make(103_000n); + /** @type {UserSeat} */ + const bobVaultSeat = await openVault(collateral, bobWantMinted); + const { + vault: bobVault, + publicNotifiers: { vault: bobNotifier }, + } = await legacyOfferResult(bobVaultSeat); + let bobUpdate = await E(bobNotifier).getUpdateSince(); + t.is(bobUpdate.value.vaultState, Phase.ACTIVE); + + await aethVaultMetrics.assertChange({ + numActiveVaults: 2, + totalCollateral: { value: 30_000n }, + totalDebt: { value: 204_015n }, + }); + + const carolWantMinted = run.make(105_000n); + /** @type {UserSeat} */ + const carolVaultSeat = await openVault(collateral, carolWantMinted); + const { + vault: carolVault, + publicNotifiers: { vault: carolNotifier }, + } = await legacyOfferResult(carolVaultSeat); + let carolUpdate = await E(carolNotifier).getUpdateSince(); + t.is(carolUpdate.value.vaultState, Phase.ACTIVE); + await aethVaultMetrics.assertChange({ + numActiveVaults: 3, + totalCollateral: { value: 45_000n }, + totalDebt: { value: 309_540n }, + }); + + const { Minted: aliceLentAmount } = await E( + aliceVaultSeat, + ).getFinalAllocation(); + const aliceProceeds = await E(aliceVaultSeat).getPayouts(); + t.deepEqual(aliceLentAmount, aliceWantMinted, 'received 95 Minted'); + + const aliceRunLent = await aliceProceeds.Minted; + t.deepEqual(await E(run.issuer).getAmountOf(aliceRunLent), aliceWantMinted); + + // BIDDERs place BIDs ////////////////////////// + const bidderSeat1 = await bidDiscount( + t, + zoe, + auctKit, + aeth, + run.make(25_000n), + aeth.make(1000_000n), + makeRatio(70n, run.brand), + ); + const bidderSeat3 = await bidDiscount( + t, + zoe, + auctKit, + aeth, + run.make(75_000n), + aeth.make(1000_000n), + makeRatio(78n, run.brand), + ); + + // price falls + // @ts-expect-error setupServices() should return the right type + await priceAuthority.setPrice(makeRatio(9990n, run.brand, 1000n, aeth.brand)); + await eventLoopIteration(); + + const { startTime } = await startAuctionClock(auctKit, manualTimer); + + await setClockAndAdvanceNTimes(manualTimer, 8n, startTime, 2n); + + await aethVaultMetrics.assertChange({ + liquidatingDebt: { value: 309_540n }, + liquidatingCollateral: { value: 45_000n }, + numActiveVaults: 0, + numLiquidatingVaults: 3, + }); + + aliceUpdate = await E(aliceNotifier).getUpdateSince(); + t.is(aliceUpdate.value.vaultState, Phase.ACTIVE); + bobUpdate = await E(bobNotifier).getUpdateSince(); + t.is(bobUpdate.value.vaultState, Phase.ACTIVE); + carolUpdate = await E(carolNotifier).getUpdateSince(); + t.is(carolUpdate.value.vaultState, Phase.LIQUIDATED); + + await aethVaultMetrics.assertChange({ + liquidatingDebt: { value: 0n }, + totalDebt: { value: 204_015n }, + liquidatingCollateral: { value: 0n }, + totalCollateral: { value: 29797n }, + totalCollateralSold: { value: 13_585n }, + totalProceedsReceived: { value: 100_000n }, + totalShortfallReceived: { value: 5_525n }, + numActiveVaults: 2, + numLiquidatingVaults: 0, + numLiquidationsCompleted: 1, + numLiquidationsAborted: 2, + }); + + t.deepEqual(await E(aliceVault).getCollateralAmount(), aeth.make(14_998n)); + t.deepEqual(await E(aliceVault).getCurrentDebt(), run.make(100_500n)); + t.deepEqual(await E(bobVault).getCollateralAmount(), aeth.make(14_998n)); + t.deepEqual(await E(bobVault).getCurrentDebt(), run.make(103_515n)); + t.deepEqual(await E(carolVault).getCollateralAmount(), aeth.makeEmpty()); + t.deepEqual(await E(carolVault).getCurrentDebt(), run.makeEmpty()); + + t.true(await E(bidderSeat3).hasExited()); + await assertBidderPayout(t, bidderSeat3, run, 0n, aeth, 0n); + t.true(await E(bidderSeat1).hasExited()); + await assertBidderPayout(t, bidderSeat1, run, 0n, aeth, 3575n); + + const metricsTopic = await E.get(E(reservePublicFacet).getPublicTopics()) + .metrics; + const m = await subscriptionTracker(t, metricsTopic); + + await m.assertState({ + ...reserveInitialState(run.makeEmpty()), + shortfallBalance: run.make(5_525n), + allocations: { + Aeth: aeth.make(1_109n), + Fee: run.makeEmpty(), + }, + }); +}); diff --git a/packages/inter-protocol/test/vaultFactory/vaultFactoryUtils.js b/packages/inter-protocol/test/vaultFactory/vaultFactoryUtils.js index 7a4587e8e56..83e25d925aa 100644 --- a/packages/inter-protocol/test/vaultFactory/vaultFactoryUtils.js +++ b/packages/inter-protocol/test/vaultFactory/vaultFactoryUtils.js @@ -59,7 +59,7 @@ export const defaultParamValues = debtBrand => * @param {Array | Ratio} priceOrList * @param {RelativeTime} quoteInterval * @param {Amount | undefined} unitAmountIn - * @param {Pick} actionParams + * @param {Pick} [actionParamArgs] */ export const setupElectorateReserveAndAuction = async ( t, @@ -68,7 +68,11 @@ export const setupElectorateReserveAndAuction = async ( priceOrList, quoteInterval, unitAmountIn, - { StartFrequency = SECONDS_PER_WEEK, DiscountStep = 2000n }, + { + StartFrequency = SECONDS_PER_WEEK, + DiscountStep = 2000n, + LowestRate = 5500n, + }, ) => { const { zoe, @@ -108,7 +112,7 @@ export const setupElectorateReserveAndAuction = async ( StartFrequency, ClockStep: 2n, StartingRate: 10500n, - LowestRate: 5500n, + LowestRate, DiscountStep, AuctionStartDelay: 10n, PriceLockPeriod: 3n,