Skip to content

Commit

Permalink
fix(vaults): reconstitue vaults correctly
Browse files Browse the repository at this point in the history
fixes: Agoric#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.
  • Loading branch information
Chris-Hibbert committed May 18, 2023
1 parent fa4a08d commit 7e286a5
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 24 deletions.
26 changes: 16 additions & 10 deletions packages/inter-protocol/src/vaultFactory/vaultManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down
231 changes: 220 additions & 11 deletions packages/inter-protocol/test/vaultFactory/test-vaultLiquidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 } =
Expand All @@ -155,7 +158,11 @@ const setupServices = async (
priceOrList,
quoteInterval,
unitAmountIn,
{ startFrequency, discountStep },
{
StartFreuency: startFrequency,
LowestRate: lowestRate,
DiscountStep: discountStep,
},
);

const { consume } = space;
Expand Down Expand Up @@ -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 },
Expand All @@ -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);
Expand All @@ -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(),
},
});
Expand Down Expand Up @@ -2378,28 +2385,28 @@ 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());
await E(bidderSeat3).tryExit();
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;
Expand Down Expand Up @@ -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<VaultKit>} */
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<VaultKit>} */
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<VaultKit>} */
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(),
},
});
});
10 changes: 7 additions & 3 deletions packages/inter-protocol/test/vaultFactory/vaultFactoryUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const defaultParamValues = debtBrand =>
* @param {Array<NatValue> | Ratio} priceOrList
* @param {RelativeTime} quoteInterval
* @param {Amount | undefined} unitAmountIn
* @param {Pick<import('../../src/auction/params.js').AuctionParams, 'StartFrequency' | 'DiscountStep'>} actionParams
* @param {Pick<import('../../src/auction/params.js').AuctionParams, 'StartFrequency' | 'DiscountStep' | 'LowestRate'>} [actionParamArgs]
*/
export const setupElectorateReserveAndAuction = async (
t,
Expand All @@ -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,
Expand Down Expand Up @@ -108,7 +112,7 @@ export const setupElectorateReserveAndAuction = async (
StartFrequency,
ClockStep: 2n,
StartingRate: 10500n,
LowestRate: 5500n,
LowestRate,
DiscountStep,
AuctionStartDelay: 10n,
PriceLockPeriod: 3n,
Expand Down

0 comments on commit 7e286a5

Please sign in to comment.