@agoric/inter-protocol 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/bundles/bundle-auctioneer-js-meta.json +314 -314
  3. package/bundles/bundle-auctioneer.js +1 -1
  4. package/bundles/bundle-econCommitteeCharter-js-meta.json +208 -208
  5. package/bundles/bundle-econCommitteeCharter.js +1 -1
  6. package/bundles/bundle-feeDistributor-js-meta.json +189 -189
  7. package/bundles/bundle-feeDistributor.js +1 -1
  8. package/bundles/bundle-fluxAggregatorKit-js-meta.json +284 -284
  9. package/bundles/bundle-fluxAggregatorKit.js +1 -1
  10. package/bundles/bundle-psm-js-meta.json +302 -302
  11. package/bundles/bundle-psm.js +1 -1
  12. package/bundles/bundle-reserve-js-meta.json +293 -293
  13. package/bundles/bundle-reserve.js +1 -1
  14. package/bundles/bundle-scaledPriceAuthority-js-meta.json +220 -220
  15. package/bundles/bundle-scaledPriceAuthority.js +1 -1
  16. package/bundles/bundle-vaultFactory-js-meta.json +330 -326
  17. package/bundles/bundle-vaultFactory.js +1 -1
  18. package/package.json +16 -16
  19. package/scripts/init-core.js +2 -2
  20. package/scripts/start-local-chain.sh +1 -1
  21. package/src/auction/auctionBook.js +10 -2
  22. package/src/clientSupport.js +79 -7
  23. package/src/contractSupport.js +16 -45
  24. package/src/price/fluxAggregatorContract.js +1 -1
  25. package/src/price/fluxAggregatorKit.js +2 -15
  26. package/src/price/roundsManager.js +8 -0
  27. package/src/proposals/core-proposal.js +4 -4
  28. package/src/proposals/econ-behaviors.js +13 -12
  29. package/src/reserve/assetReserveKit.js +1 -7
  30. package/src/reserve/doc.md +4 -4
  31. package/src/vaultFactory/Liquidation.md +1 -1
  32. package/src/vaultFactory/liquidation.js +14 -24
  33. package/src/vaultFactory/params.js +8 -8
  34. package/src/vaultFactory/proceeds.js +274 -0
  35. package/src/vaultFactory/types.js +1 -10
  36. package/src/vaultFactory/vaultDirector.js +3 -42
  37. package/src/vaultFactory/vaultFactory.js +3 -3
  38. package/src/vaultFactory/vaultHolder.js +1 -9
  39. package/src/vaultFactory/vaultManager.js +177 -322
@@ -37,17 +37,13 @@ import {
37
37
  import { TransferPartShape } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
38
38
  import {
39
39
  atomicRearrange,
40
- ceilDivideBy,
41
40
  ceilMultiplyBy,
42
41
  floorDivideBy,
43
- floorMultiplyBy,
44
42
  getAmountIn,
45
43
  getAmountOut,
46
44
  makeEphemeraProvider,
47
45
  makeRatio,
48
- makeRatioFromAmounts,
49
46
  makeRecorderTopic,
50
- multiplyRatios,
51
47
  offerTo,
52
48
  SubscriberShape,
53
49
  TopicsRecordShape,
@@ -55,14 +51,19 @@ import {
55
51
  import { PriceQuoteShape, SeatShape } from '@agoric/zoe/src/typeGuards.js';
56
52
  import { E } from '@endo/eventual-send';
57
53
  import { AuctionPFShape } from '../auction/auctioneer.js';
58
- import { checkDebtLimit, makeNatAmountShape } from '../contractSupport.js';
54
+ import {
55
+ checkDebtLimit,
56
+ makeNatAmountShape,
57
+ quoteAsRatio,
58
+ } from '../contractSupport.js';
59
59
  import { chargeInterest } from '../interest.js';
60
- import { getLiquidatableVaults, liquidationResults } from './liquidation.js';
60
+ import { getLiquidatableVaults } from './liquidation.js';
61
61
  import { calculateMinimumCollateralization, minimumPrice } from './math.js';
62
62
  import { makePrioritizedVaults } from './prioritizedVaults.js';
63
63
  import { Phase, prepareVault } from './vault.js';
64
+ import { calculateDistributionPlan } from './proceeds.js';
64
65
 
65
- const { details: X, Fail } = assert;
66
+ const { details: X, Fail, quote: q } = assert;
66
67
 
67
68
  const trace = makeTracer('VM');
68
69
 
@@ -73,6 +74,8 @@ const trace = makeTracer('VM');
73
74
  /**
74
75
  * @typedef {object} MetricsNotification
75
76
  *
77
+ * @property {Ratio | null} lockedQuote priceQuote that will be used for liquidation.
78
+ * Non-null from priceLock time until liquidation has taken place.
76
79
  * @property {number} numActiveVaults present count of vaults
77
80
  * @property {number} numLiquidatingVaults present count of liquidating vaults
78
81
  * @property {Amount<'nat'>} totalCollateral present sum of collateral across all vaults
@@ -152,10 +155,6 @@ const trace = makeTracer('VM');
152
155
  * }} MutableState
153
156
  */
154
157
 
155
- /** @param {Pick<PriceDescription, 'amountIn' | 'amountOut'>} quoteAmount */
156
- const quoteAsRatio = quoteAmount =>
157
- makeRatioFromAmounts(quoteAmount.amountIn, quoteAmount.amountOut);
158
-
159
158
  /**
160
159
  * @type {(brand: Brand) => {
161
160
  * prioritizedVaults: ReturnType<typeof makePrioritizedVaults>,
@@ -254,8 +253,6 @@ export const prepareVaultManagerKit = (
254
253
  {
255
254
  collateral: M.interface('collateral', {
256
255
  makeVaultInvitation: M.call().returns(M.promise()),
257
- getSubscriber: M.call().returns(SubscriberShape),
258
- getMetrics: M.call().returns(SubscriberShape),
259
256
  getPublicTopics: M.call().returns(TopicsRecordShape),
260
257
  getQuotes: M.call().returns(NotifierShape),
261
258
  getCompoundedInterest: M.call().returns(RatioShape),
@@ -318,14 +315,6 @@ export const prepareVaultManagerKit = (
318
315
  }),
319
316
  );
320
317
  },
321
- /** @deprecated use getPublicTopics */
322
- getSubscriber() {
323
- return this.state.assetTopicKit.subscriber;
324
- },
325
- /** @deprecated use getPublicTopics */
326
- getMetrics() {
327
- return this.state.metricsTopicKit.subscriber;
328
- },
329
318
  getQuotes() {
330
319
  const ephemera = collateralEphemera(this.state.collateralBrand);
331
320
  return ephemera.storedQuotesNotifier;
@@ -499,30 +488,22 @@ export const prepareVaultManagerKit = (
499
488
  sendToReserve(penalty, seat, seatKeyword = 'Collateral') {
500
489
  const invitation =
501
490
  E(reservePublicFacet).makeAddCollateralInvitation();
491
+ trace('Sending to reserve: ', penalty);
502
492
 
503
493
  // don't wait for response
504
494
  void E.when(invitation, invite => {
505
495
  const proposal = { give: { Collateral: penalty } };
506
- void offerTo(
496
+ return offerTo(
507
497
  zcf,
508
498
  invite,
509
499
  { [seatKeyword]: 'Collateral' },
510
500
  proposal,
511
501
  seat,
512
502
  );
503
+ }).catch(reason => {
504
+ console.error('sendToReserve failed', reason);
513
505
  });
514
506
  },
515
- /** @type {(collatSold: Amount<'nat'>, completed: number, aborted?: number) => void} */
516
- markCollateralLiquidated(collatSold, completed, aborted = 0) {
517
- const { state } = this;
518
-
519
- state.totalCollateral = AmountMath.subtract(
520
- state.totalCollateral,
521
- collatSold,
522
- );
523
- state.numLiquidationsCompleted += completed;
524
- state.numLiquidationsAborted += aborted;
525
- },
526
507
  markLiquidating(debt, collateral) {
527
508
  const { state } = this;
528
509
 
@@ -537,9 +518,10 @@ export const prepareVaultManagerKit = (
537
518
  *
538
519
  * @param {Amount<'nat'>} debt
539
520
  * @param {Amount<'nat'>} collateral
540
- * @param {{ overage: Amount<'nat'>, shortfall: Amount<'nat'> }} accounting
521
+ * @param {Amount<'nat'>} overage
522
+ * @param {Amount<'nat'>} shortfall
541
523
  */
542
- markDoneLiquidating(debt, collateral, accounting) {
524
+ markDoneLiquidating(debt, collateral, overage, shortfall) {
543
525
  const { state } = this;
544
526
 
545
527
  // update liquidation state
@@ -555,7 +537,6 @@ export const prepareVaultManagerKit = (
555
537
 
556
538
  // record shortfall and proceeds
557
539
 
558
- const { overage, shortfall } = accounting;
559
540
  // cumulative values
560
541
  state.totalOverageReceived = AmountMath.add(
561
542
  state.totalOverageReceived,
@@ -579,25 +560,6 @@ export const prepareVaultManagerKit = (
579
560
  console.error('🚨 failed to report liquidation shortfall', err);
580
561
  });
581
562
  },
582
- /**
583
- * If interest was charged between liquidating and liquidated, erase it.
584
- *
585
- * @param {Amount<'nat'>} priorDebtAmount
586
- * @param {Amount<'nat'>} currentDebt
587
- */
588
- subtractPhantomInterest(priorDebtAmount, currentDebt) {
589
- const { state } = this;
590
- const difference = AmountMath.subtract(currentDebt, priorDebtAmount);
591
-
592
- if (!AmountMath.isEmpty(difference)) {
593
- state.totalDebt = AmountMath.subtract(state.totalDebt, difference);
594
- }
595
- },
596
- markRestoreDebt(debt) {
597
- const { state } = this;
598
-
599
- state.totalDebt = AmountMath.add(state.totalDebt, debt);
600
- },
601
563
  writeMetrics() {
602
564
  const { state } = this;
603
565
  const { collateralBrand, retainedCollateralSeat, metricsTopicKit } =
@@ -608,6 +570,11 @@ export const prepareVaultManagerKit = (
608
570
  retainedCollateralSeat.getCurrentAllocation()?.Collateral ??
609
571
  AmountMath.makeEmpty(collateralBrand, 'nat');
610
572
 
573
+ const quote = state.lockedQuote;
574
+ const lockedQuoteRatio = quote
575
+ ? quoteAsRatio(quote.quoteAmount.value[0])
576
+ : null;
577
+
611
578
  /** @type {MetricsNotification} */
612
579
  const payload = harden({
613
580
  numActiveVaults: prioritizedVaults.getCount(),
@@ -624,21 +591,31 @@ export const prepareVaultManagerKit = (
624
591
  totalOverageReceived: state.totalOverageReceived,
625
592
  totalProceedsReceived: state.totalProceedsReceived,
626
593
  totalShortfallReceived: state.totalShortfallReceived,
594
+ lockedQuote: lockedQuoteRatio,
627
595
  });
628
596
 
629
597
  return E(metricsTopicKit.recorder).write(payload);
630
598
  },
631
599
 
632
- distributeProceeds(
600
+ /**
601
+ * This is designed to tolerate an incomplete plan, in case calculateDistributionPlan encounters
602
+ * an error during its calculation. We don't have a way to induce such errors in CI so we've
603
+ * done so manually in dev and verified this function recovers as expected.
604
+ *
605
+ * @param {AmountKeywordRecord} proceeds
606
+ * @param {Amount<'nat'>} totalDebt
607
+ * @param {Pick<PriceQuote, 'quoteAmount'>} oraclePriceAtStart
608
+ * @param {MapStore<Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}>} vaultData
609
+ * @param {Amount<'nat'>} totalCollateral
610
+ */
611
+ planProceedsDistribution(
633
612
  proceeds,
634
613
  totalDebt,
635
614
  oraclePriceAtStart,
636
- liqSeat,
637
615
  vaultData,
638
616
  totalCollateral,
639
617
  ) {
640
618
  const { state, facets } = this;
641
- const { collateralBrand, debtBrand, liquidatingVaults } = this.state;
642
619
 
643
620
  const { Collateral: collateralProceeds } = proceeds;
644
621
  /** @type {Amount<'nat'>} */
@@ -651,280 +628,135 @@ export const prepareVaultManagerKit = (
651
628
  collateralSold,
652
629
  );
653
630
 
654
- const mintedProceeds =
655
- proceeds.Minted || AmountMath.makeEmpty(debtBrand);
656
- const accounting = liquidationResults(totalDebt, mintedProceeds);
657
-
658
- /** @type {Array<Vault>} */
659
- const vaultsToLiquidate = [];
660
- const liquidateAll = () => {
661
- for (const vault of vaultsToLiquidate) {
662
- vault.liquidated();
663
- liquidatingVaults.delete(vault);
664
- }
665
- };
666
-
667
631
  const penaltyRate = facets.self
668
632
  .getGovernedParams()
669
633
  .getLiquidationPenalty();
670
- // charged in collateral
671
- const totalPenalty = ceilMultiplyBy(
672
- totalDebt,
673
- multiplyRatios(
674
- penaltyRate,
675
- quoteAsRatio(oraclePriceAtStart.quoteAmount.value[0]),
676
- ),
677
- );
678
-
679
- const price = makeRatioFromAmounts(mintedProceeds, collateralSold);
680
- // Liquidation.md describes how to process liquidation proceeds
681
634
  const bestToWorst = [...vaultData.entries()].reverse();
682
- if (AmountMath.isEmpty(accounting.shortfall)) {
683
- // Flow #1: no shortfall
684
-
685
- const collateralToDistribute = AmountMath.isGTE(
686
- collateralProceeds,
687
- totalPenalty,
688
- );
689
-
690
- const distributableCollateral = collateralToDistribute
691
- ? AmountMath.subtract(collateralProceeds, totalPenalty)
692
- : AmountMath.makeEmptyFromAmount(collateralProceeds);
693
635
 
694
- facets.helper.burnToCoverDebt(totalDebt, mintedProceeds, liqSeat);
695
- facets.helper.sendToReserve(accounting.overage, liqSeat, 'Minted');
696
-
697
- // return remaining funds to vaults before closing
636
+ // unzip the entry tuples
637
+ const vaultsInPlan = /** @type {Vault[]} */ ([]);
638
+ const vaultsBalances =
639
+ /** @type {import('./proceeds.js').VaultBalances[]} */ ([]);
640
+ for (const [vault, balances] of bestToWorst) {
641
+ vaultsInPlan.push(vault);
642
+ vaultsBalances.push({
643
+ collateral: balances.collateralAmount,
644
+ // if interest accrued during sale, the current debt will be higher
645
+ presaleDebt: balances.debtAmount,
646
+ currentDebt: vault.getCurrentDebt(),
647
+ });
648
+ }
649
+ harden(vaultsInPlan);
650
+ harden(vaultsBalances);
698
651
 
699
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
700
- const transfers = [];
701
- let leftToStage = distributableCollateral;
652
+ const plan = calculateDistributionPlan({
653
+ proceeds,
654
+ totalDebt,
655
+ totalCollateral,
656
+ oraclePriceAtStart: oraclePriceAtStart.quoteAmount.value[0],
657
+ vaultsBalances,
658
+ penaltyRate,
659
+ });
660
+ return { plan, vaultsInPlan };
661
+ },
702
662
 
703
- // iterate from best to worst, returning collateral until it has
704
- // been exhausted. Vaults after that get nothing.
705
- for (const [vault, amounts] of bestToWorst) {
706
- const { collateralAmount: vCollat, debtAmount } = amounts;
707
- facets.helper.subtractPhantomInterest(
708
- debtAmount,
709
- vault.getCurrentDebt(),
710
- );
711
- const debtInCollateral = ceilDivideBy(debtAmount, price);
712
- const collatPostDebt = AmountMath.isGTE(vCollat, debtInCollateral)
713
- ? AmountMath.subtract(vCollat, debtInCollateral)
714
- : AmountMath.makeEmptyFromAmount(vCollat);
715
- if (!AmountMath.isEmpty(leftToStage)) {
716
- const collat = AmountMath.min(leftToStage, collatPostDebt);
717
- leftToStage = AmountMath.subtract(leftToStage, collat);
718
- transfers.push([
663
+ /**
664
+ * This is designed to tolerate an incomplete plan, in case calculateDistributionPlan encounters
665
+ * an error during its calculation. We don't have a way to induce such errors in CI so we've
666
+ * done so manually in dev and verified this function recovers as expected.
667
+ *
668
+ * @param {object} obj
669
+ * @param {import('./proceeds.js').DistributionPlan} obj.plan
670
+ * @param {Array<Vault>} obj.vaultsInPlan
671
+ * @param {ZCFSeat} obj.liqSeat
672
+ * @param {Amount<'nat'>} obj.totalCollateral
673
+ * @param {Amount<'nat'>} obj.totalDebt
674
+ * @returns {void}
675
+ */
676
+ distributeProceeds({
677
+ plan,
678
+ vaultsInPlan,
679
+ liqSeat,
680
+ totalCollateral,
681
+ totalDebt,
682
+ }) {
683
+ const { state, facets } = this;
684
+ // Putting all the rearrangements after the loop ensures that errors
685
+ // in the calculations don't result in paying back some vaults and
686
+ // leaving others hanging.
687
+ if (plan.transfersToVault.length > 0) {
688
+ const transfers = plan.transfersToVault.map(
689
+ ([vaultIndex, amounts]) =>
690
+ /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart} */ ([
719
691
  liqSeat,
720
- vault.getVaultSeat(),
721
- { Collateral: collat },
722
- ]);
723
- }
724
- vaultsToLiquidate.push(vault);
725
- }
726
- if (transfers.length > 0) {
727
- atomicRearrange(zcf, harden(transfers));
728
- }
729
-
730
- const forReserve = collateralToDistribute
731
- ? AmountMath.add(leftToStage, totalPenalty)
732
- : collateralProceeds;
733
- facets.helper.sendToReserve(forReserve, liqSeat);
734
-
735
- facets.helper.markCollateralLiquidated(
736
- totalCollateral,
737
- vaultData.getSize(),
738
- );
739
-
740
- facets.helper.markDoneLiquidating(
741
- totalDebt,
742
- totalCollateral,
743
- accounting,
744
- );
745
- } else if (AmountMath.isEmpty(collateralProceeds)) {
746
- // Flow #2a
747
-
748
- // charge penalty if proceeds are sufficient
749
- const penaltyInMinted = ceilMultiplyBy(totalDebt, penaltyRate);
750
- const recoveredDebt = AmountMath.min(
751
- AmountMath.add(totalDebt, penaltyInMinted),
752
- mintedProceeds,
753
- );
754
-
755
- facets.helper.burnToCoverDebt(
756
- recoveredDebt,
757
- mintedProceeds,
758
- liqSeat,
692
+ vaultsInPlan[vaultIndex].getVaultSeat(),
693
+ amounts,
694
+ ]),
759
695
  );
696
+ atomicRearrange(zcf, harden(transfers));
697
+ }
760
698
 
761
- const coverDebt = AmountMath.isGTE(mintedProceeds, recoveredDebt);
762
- const distributable = coverDebt
763
- ? AmountMath.subtract(mintedProceeds, recoveredDebt)
764
- : AmountMath.makeEmptyFromAmount(mintedProceeds);
765
- let mintedRemaining = distributable;
699
+ const { prioritizedVaults } = collateralEphemera(
700
+ totalCollateral.brand,
701
+ );
702
+ state.numLiquidationsAborted += plan.vaultsToReinstate.length;
703
+ for (const vaultIndex of plan.vaultsToReinstate) {
704
+ const vault = vaultsInPlan[vaultIndex];
705
+ const vaultId = vault.abortLiquidation();
706
+ prioritizedVaults.addVault(vaultId, vault);
707
+ state.liquidatingVaults.delete(vault);
708
+ }
766
709
 
767
- const vaultPortion = makeRatioFromAmounts(
768
- distributable,
769
- totalCollateral,
770
- );
771
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
772
- const transfers = [];
773
-
774
- // iterate from best to worst returning remaining funds to vaults
775
- /** @type {Array<[Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}]>} */
776
- for (const [vault, balance] of bestToWorst) {
777
- // from best to worst, return minted above penalty if any remains
778
- const { collateralAmount: vCollat, debtAmount } = balance;
779
- const vaultShare = floorMultiplyBy(vCollat, vaultPortion);
780
- facets.helper.subtractPhantomInterest(
781
- debtAmount,
782
- vault.getCurrentDebt(),
783
- );
784
-
785
- if (!AmountMath.isEmpty(mintedRemaining)) {
786
- const mintedToReturn = AmountMath.isGTE(
787
- mintedRemaining,
788
- vaultShare,
789
- )
790
- ? vaultShare
791
- : mintedRemaining;
792
- mintedRemaining = AmountMath.subtract(
793
- mintedRemaining,
794
- mintedToReturn,
795
- );
796
- const seat = vault.getVaultSeat();
797
- transfers.push([liqSeat, seat, { Minted: mintedToReturn }]);
798
- }
799
- vaultsToLiquidate.push(vault);
800
- }
801
-
802
- if (transfers.length > 0) {
803
- atomicRearrange(zcf, harden(transfers));
804
- }
805
-
806
- facets.helper.markCollateralLiquidated(
807
- totalCollateral,
808
- vaultData.getSize(),
710
+ if (!AmountMath.isEmpty(plan.phantomDebt)) {
711
+ state.totalDebt = AmountMath.subtract(
712
+ state.totalDebt,
713
+ plan.phantomDebt,
809
714
  );
715
+ }
810
716
 
811
- facets.helper.markDoneLiquidating(
812
- totalDebt,
813
- totalCollateral,
814
- accounting,
717
+ facets.helper.burnToCoverDebt(
718
+ plan.debtToBurn,
719
+ plan.mintedProceeds,
720
+ liqSeat,
721
+ );
722
+ if (!AmountMath.isEmpty(plan.mintedForReserve)) {
723
+ facets.helper.sendToReserve(
724
+ plan.mintedForReserve,
725
+ liqSeat,
726
+ 'Minted',
815
727
  );
816
- } else {
817
- // Flow #2b: There's unsold collateral; some vaults may be revived.
818
-
819
- facets.helper.burnToCoverDebt(totalDebt, mintedProceeds, liqSeat);
820
- facets.helper.sendToReserve(accounting.overage, liqSeat, 'Minted');
728
+ }
821
729
 
822
- // reconstitute vaults until collateral is insufficient
823
- let reconstituteVaults = AmountMath.isGTE(
824
- collateralProceeds,
825
- totalPenalty,
730
+ // send all that's left in the seat
731
+ const collateralInLiqSeat = liqSeat.getCurrentAllocation().Collateral;
732
+ if (!AmountMath.isEmpty(collateralInLiqSeat)) {
733
+ facets.helper.sendToReserve(collateralInLiqSeat, liqSeat);
734
+ }
735
+ // if it didn't match what was expected, report
736
+ if (!AmountMath.isEqual(collateralInLiqSeat, plan.collatRemaining)) {
737
+ console.error(
738
+ `⚠️ Excess collateral remaining sent to reserve. Expected ${q(
739
+ plan.collatRemaining,
740
+ )}, sent ${q(collateralInLiqSeat)}`,
826
741
  );
742
+ }
827
743
 
828
- // charge penalty if proceeds are sufficient
829
- const distributableCollateral = reconstituteVaults
830
- ? AmountMath.subtract(collateralProceeds, totalPenalty)
831
- : AmountMath.makeEmptyFromAmount(collateralProceeds);
832
-
833
- let collatRemaining = distributableCollateral;
834
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
835
- const transfers = [];
836
- let liquidated = 0;
837
- /** @type {Array<Vault>} */
838
- const vaultsToReinstate = [];
839
- const reinstateAll = () => {
840
- const { prioritizedVaults } = collateralEphemera(collateralBrand);
841
- for (const vault of vaultsToReinstate) {
842
- const vaultId = vault.abortLiquidation();
843
- prioritizedVaults.addVault(vaultId, vault);
844
- liquidatingVaults.delete(vault);
845
- }
846
- };
847
-
848
- let collateralReduction = AmountMath.makeEmpty(collateralBrand);
849
- let shortfallToReserve = accounting.shortfall;
850
- const debtPortion = makeRatioFromAmounts(totalPenalty, totalDebt);
851
- const reduceCollateral = amount =>
852
- (collateralReduction = AmountMath.add(
853
- collateralReduction,
854
- amount,
855
- ));
856
-
857
- // iterate from best to worst attempting to reconstitute, by
858
- // returning remaining funds to vaults
859
- /** @type {Array<[Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}]>} */
860
- for (const [vault, balance] of bestToWorst) {
861
- const { collateralAmount: vCollat, debtAmount } = balance;
862
- const vaultPenalty = ceilMultiplyBy(debtAmount, penaltyRate);
863
- const collatPostPenalty = AmountMath.subtract(
864
- vCollat,
865
- ceilMultiplyBy(vaultPenalty, debtPortion),
866
- );
867
- const vaultDebt = floorMultiplyBy(debtAmount, debtPortion);
868
- if (
869
- reconstituteVaults &&
870
- !AmountMath.isEmpty(collatPostPenalty) &&
871
- AmountMath.isGTE(collatRemaining, collatPostPenalty) &&
872
- AmountMath.isGTE(totalDebt, debtAmount)
873
- ) {
874
- collatRemaining = AmountMath.subtract(
875
- collatRemaining,
876
- collatPostPenalty,
877
- );
878
- shortfallToReserve = AmountMath.isGTE(
879
- shortfallToReserve,
880
- debtAmount,
881
- )
882
- ? AmountMath.subtract(shortfallToReserve, debtAmount)
883
- : AmountMath.makeEmptyFromAmount(shortfallToReserve);
884
- const seat = vault.getVaultSeat();
885
- // must reinstate after atomicRearrange(), so we record them.
886
- vaultsToReinstate.push(vault);
887
- reduceCollateral(vaultDebt);
888
- transfers.push([
889
- liqSeat,
890
- seat,
891
- { Collateral: collatPostPenalty },
892
- ]);
893
- } else {
894
- reconstituteVaults = false;
895
- liquidated += 1;
896
- facets.helper.subtractPhantomInterest(
897
- debtAmount,
898
- vault.getCurrentDebt(),
899
- );
900
-
901
- reduceCollateral(vCollat);
902
- vaultsToLiquidate.push(vault);
903
- }
904
- }
905
-
906
- // Putting all the rearrangements after the loop ensures that errors
907
- // in the calculations don't result in paying pack some vaults and
908
- // leaving others hanging.
909
- if (transfers.length > 0) {
910
- atomicRearrange(zcf, harden(transfers));
911
- }
912
- reinstateAll();
744
+ // 'totalCollateralSold' is only for this liquidation event
745
+ // 'state.totalCollateralSold' represents all active vaults
746
+ const actualCollateralSold = plan.actualCollateralSold;
747
+ state.totalCollateral = AmountMath.isEmpty(actualCollateralSold)
748
+ ? AmountMath.subtract(state.totalCollateral, totalCollateral)
749
+ : AmountMath.subtract(state.totalCollateral, actualCollateralSold);
913
750
 
914
- facets.helper.markCollateralLiquidated(
915
- collateralReduction,
916
- liquidated,
917
- transfers.length,
918
- );
751
+ facets.helper.markDoneLiquidating(
752
+ totalDebt,
753
+ totalCollateral,
754
+ plan.overage,
755
+ plan.shortfallToReserve,
756
+ );
919
757
 
920
- facets.helper.sendToReserve(collatRemaining, liqSeat);
921
- facets.helper.markDoneLiquidating(totalDebt, totalCollateral, {
922
- ...accounting,
923
- shortfall: shortfallToReserve,
924
- });
925
- }
926
- liquidateAll();
927
- return facets.helper.writeMetrics();
758
+ // liqSeat should be empty at this point, except that funds are sent
759
+ // asynchronously to the reserve.
928
760
  },
929
761
  },
930
762
 
@@ -1198,7 +1030,7 @@ export const prepareVaultManagerKit = (
1198
1030
  },
1199
1031
 
1200
1032
  lockOraclePrices() {
1201
- const { state } = this;
1033
+ const { state, facets } = this;
1202
1034
  const { storedCollateralQuote } = collateralEphemera(
1203
1035
  state.collateralBrand,
1204
1036
  );
@@ -1211,6 +1043,7 @@ export const prepareVaultManagerKit = (
1211
1043
  );
1212
1044
 
1213
1045
  state.lockedQuote = storedCollateralQuote;
1046
+ facets.helper.writeMetrics();
1214
1047
  return storedCollateralQuote;
1215
1048
  },
1216
1049
  /**
@@ -1236,6 +1069,7 @@ export const prepareVaultManagerKit = (
1236
1069
 
1237
1070
  const liqMargin = self.getGovernedParams().getLiquidationMargin();
1238
1071
 
1072
+ // totals *among* vaults being liquidated
1239
1073
  const { totalDebt, totalCollateral, vaultData, liqSeat } =
1240
1074
  getLiquidatableVaults(
1241
1075
  zcf,
@@ -1288,14 +1122,35 @@ export const prepareVaultManagerKit = (
1288
1122
  );
1289
1123
 
1290
1124
  trace(`LiqV after long wait`, proceeds);
1291
- return helper.distributeProceeds(
1292
- proceeds,
1293
- totalDebt,
1294
- storedCollateralQuote,
1295
- liqSeat,
1296
- vaultData,
1297
- totalCollateral,
1298
- );
1125
+ try {
1126
+ const { plan, vaultsInPlan } = helper.planProceedsDistribution(
1127
+ proceeds,
1128
+ totalDebt,
1129
+ storedCollateralQuote,
1130
+ vaultData,
1131
+ totalCollateral,
1132
+ );
1133
+ trace('PLAN', plan);
1134
+ // distributeProceeds may reconstitute vaults, removing them from liquidatingVaults
1135
+ helper.distributeProceeds({
1136
+ liqSeat,
1137
+ plan,
1138
+ totalCollateral,
1139
+ totalDebt,
1140
+ vaultsInPlan,
1141
+ });
1142
+ } catch (err) {
1143
+ console.error('🚨 Error distributing proceeds:', err);
1144
+ }
1145
+
1146
+ // for all non-reconstituted vaults, transition to 'liquidated' state
1147
+ state.numLiquidationsCompleted += liquidatingVaults.getSize();
1148
+ for (const vault of liquidatingVaults.values()) {
1149
+ vault.liquidated();
1150
+ liquidatingVaults.delete(vault);
1151
+ }
1152
+
1153
+ await facets.helper.writeMetrics();
1299
1154
  },
1300
1155
  },
1301
1156
  },