@agoric/inter-protocol 0.14.1-dev-13c169c.0 → 0.14.1-dev-72b0aa9.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.
@@ -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,12 +51,17 @@ 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
66
  const { details: X, Fail, quote: q } = assert;
66
67
 
@@ -154,10 +155,6 @@ const trace = makeTracer('VM');
154
155
  * }} MutableState
155
156
  */
156
157
 
157
- /** @param {Pick<PriceDescription, 'amountIn' | 'amountOut'>} quoteAmount */
158
- const quoteAsRatio = quoteAmount =>
159
- makeRatioFromAmounts(quoteAmount.amountIn, quoteAmount.amountOut);
160
-
161
158
  /**
162
159
  * @type {(brand: Brand) => {
163
160
  * prioritizedVaults: ReturnType<typeof makePrioritizedVaults>,
@@ -256,8 +253,6 @@ export const prepareVaultManagerKit = (
256
253
  {
257
254
  collateral: M.interface('collateral', {
258
255
  makeVaultInvitation: M.call().returns(M.promise()),
259
- getSubscriber: M.call().returns(SubscriberShape),
260
- getMetrics: M.call().returns(SubscriberShape),
261
256
  getPublicTopics: M.call().returns(TopicsRecordShape),
262
257
  getQuotes: M.call().returns(NotifierShape),
263
258
  getCompoundedInterest: M.call().returns(RatioShape),
@@ -320,14 +315,6 @@ export const prepareVaultManagerKit = (
320
315
  }),
321
316
  );
322
317
  },
323
- /** @deprecated use getPublicTopics */
324
- getSubscriber() {
325
- return this.state.assetTopicKit.subscriber;
326
- },
327
- /** @deprecated use getPublicTopics */
328
- getMetrics() {
329
- return this.state.metricsTopicKit.subscriber;
330
- },
331
318
  getQuotes() {
332
319
  const ephemera = collateralEphemera(this.state.collateralBrand);
333
320
  return ephemera.storedQuotesNotifier;
@@ -506,26 +493,17 @@ export const prepareVaultManagerKit = (
506
493
  // don't wait for response
507
494
  void E.when(invitation, invite => {
508
495
  const proposal = { give: { Collateral: penalty } };
509
- void offerTo(
496
+ return offerTo(
510
497
  zcf,
511
498
  invite,
512
499
  { [seatKeyword]: 'Collateral' },
513
500
  proposal,
514
501
  seat,
515
502
  );
503
+ }).catch(reason => {
504
+ console.error('sendToReserve failed', reason);
516
505
  });
517
506
  },
518
- /** @type {(collatSold: Amount<'nat'>, completed: number, aborted?: number) => void} */
519
- markCollateralLiquidated(collatSold, completed, aborted = 0) {
520
- const { state } = this;
521
-
522
- state.totalCollateral = AmountMath.subtract(
523
- state.totalCollateral,
524
- collatSold,
525
- );
526
- state.numLiquidationsCompleted += completed;
527
- state.numLiquidationsAborted += aborted;
528
- },
529
507
  markLiquidating(debt, collateral) {
530
508
  const { state } = this;
531
509
 
@@ -540,9 +518,10 @@ export const prepareVaultManagerKit = (
540
518
  *
541
519
  * @param {Amount<'nat'>} debt
542
520
  * @param {Amount<'nat'>} collateral
543
- * @param {{ overage: Amount<'nat'>, shortfall: Amount<'nat'> }} accounting
521
+ * @param {Amount<'nat'>} overage
522
+ * @param {Amount<'nat'>} shortfall
544
523
  */
545
- markDoneLiquidating(debt, collateral, accounting) {
524
+ markDoneLiquidating(debt, collateral, overage, shortfall) {
546
525
  const { state } = this;
547
526
 
548
527
  // update liquidation state
@@ -558,7 +537,6 @@ export const prepareVaultManagerKit = (
558
537
 
559
538
  // record shortfall and proceeds
560
539
 
561
- const { overage, shortfall } = accounting;
562
540
  // cumulative values
563
541
  state.totalOverageReceived = AmountMath.add(
564
542
  state.totalOverageReceived,
@@ -582,25 +560,6 @@ export const prepareVaultManagerKit = (
582
560
  console.error('🚨 failed to report liquidation shortfall', err);
583
561
  });
584
562
  },
585
- /**
586
- * If interest was charged between liquidating and liquidated, erase it.
587
- *
588
- * @param {Amount<'nat'>} priorDebtAmount
589
- * @param {Amount<'nat'>} currentDebt
590
- */
591
- subtractPhantomInterest(priorDebtAmount, currentDebt) {
592
- const { state } = this;
593
- const difference = AmountMath.subtract(currentDebt, priorDebtAmount);
594
-
595
- if (!AmountMath.isEmpty(difference)) {
596
- state.totalDebt = AmountMath.subtract(state.totalDebt, difference);
597
- }
598
- },
599
- markRestoreDebt(debt) {
600
- const { state } = this;
601
-
602
- state.totalDebt = AmountMath.add(state.totalDebt, debt);
603
- },
604
563
  writeMetrics() {
605
564
  const { state } = this;
606
565
  const { collateralBrand, retainedCollateralSeat, metricsTopicKit } =
@@ -638,16 +597,25 @@ export const prepareVaultManagerKit = (
638
597
  return E(metricsTopicKit.recorder).write(payload);
639
598
  },
640
599
 
641
- 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(
642
612
  proceeds,
643
613
  totalDebt,
644
614
  oraclePriceAtStart,
645
- liqSeat,
646
615
  vaultData,
647
616
  totalCollateral,
648
617
  ) {
649
618
  const { state, facets } = this;
650
- const { collateralBrand, debtBrand, liquidatingVaults } = this.state;
651
619
 
652
620
  const { Collateral: collateralProceeds } = proceeds;
653
621
  /** @type {Amount<'nat'>} */
@@ -660,312 +628,135 @@ export const prepareVaultManagerKit = (
660
628
  collateralSold,
661
629
  );
662
630
 
663
- const mintedProceeds =
664
- proceeds.Minted || AmountMath.makeEmpty(debtBrand);
665
- const accounting = liquidationResults(totalDebt, mintedProceeds);
666
-
667
- /** @type {Array<Vault>} */
668
- const vaultsToLiquidate = [];
669
- const liquidateAll = () => {
670
- for (const vault of vaultsToLiquidate) {
671
- vault.liquidated();
672
- liquidatingVaults.delete(vault);
673
- }
674
- };
675
-
676
631
  const penaltyRate = facets.self
677
632
  .getGovernedParams()
678
633
  .getLiquidationPenalty();
679
- // charged in collateral
680
- const totalPenalty = ceilMultiplyBy(
681
- totalDebt,
682
- multiplyRatios(
683
- penaltyRate,
684
- quoteAsRatio(oraclePriceAtStart.quoteAmount.value[0]),
685
- ),
686
- );
687
- const debtPortion = makeRatioFromAmounts(totalPenalty, totalDebt);
688
-
689
- // Liquidation.md describes how to process liquidation proceeds
690
634
  const bestToWorst = [...vaultData.entries()].reverse();
691
- if (AmountMath.isEmpty(accounting.shortfall)) {
692
- // Flow #1: no shortfall
693
-
694
- const collateralToDistribute = AmountMath.isGTE(
695
- collateralProceeds,
696
- totalPenalty,
697
- );
698
635
 
699
- const distributableCollateral = collateralToDistribute
700
- ? AmountMath.subtract(collateralProceeds, totalPenalty)
701
- : AmountMath.makeEmptyFromAmount(collateralProceeds);
702
-
703
- facets.helper.burnToCoverDebt(totalDebt, mintedProceeds, liqSeat);
704
- facets.helper.sendToReserve(accounting.overage, liqSeat, 'Minted');
705
-
706
- // return remaining funds to vaults before closing
707
-
708
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
709
- const transfers = [];
710
- let leftToStage = distributableCollateral;
711
-
712
- // iterate from best to worst, returning collateral until it has
713
- // been exhausted. Vaults after that get nothing.
714
- for (const [vault, amounts] of bestToWorst) {
715
- const { collateralAmount: vCollat, debtAmount } = amounts;
716
- facets.helper.subtractPhantomInterest(
717
- debtAmount,
718
- vault.getCurrentDebt(),
719
- );
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);
720
651
 
721
- const price = makeRatioFromAmounts(
722
- mintedProceeds,
723
- collateralSold,
724
- );
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
+ },
725
662
 
726
- // max return is vault value reduced by debt and penalty value
727
- const debtCollat = ceilDivideBy(debtAmount, price);
728
- const penaltyCollat = ceilMultiplyBy(debtAmount, debtPortion);
729
- const lessCollat = AmountMath.add(debtCollat, penaltyCollat);
730
-
731
- const maxCollat = AmountMath.isGTE(vCollat, lessCollat)
732
- ? AmountMath.subtract(vCollat, lessCollat)
733
- : AmountMath.makeEmptyFromAmount(vCollat);
734
- if (!AmountMath.isEmpty(leftToStage)) {
735
- const collatReturn = AmountMath.min(leftToStage, maxCollat);
736
- leftToStage = AmountMath.subtract(leftToStage, collatReturn);
737
- 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} */ ([
738
691
  liqSeat,
739
- vault.getVaultSeat(),
740
- { Collateral: collatReturn },
741
- ]);
742
- }
743
- vaultsToLiquidate.push(vault);
744
- }
745
- if (transfers.length > 0) {
746
- atomicRearrange(zcf, harden(transfers));
747
- }
748
-
749
- const forReserve = collateralToDistribute
750
- ? AmountMath.add(leftToStage, totalPenalty)
751
- : collateralProceeds;
752
- facets.helper.sendToReserve(forReserve, liqSeat);
753
-
754
- facets.helper.markCollateralLiquidated(
755
- totalCollateral,
756
- vaultData.getSize(),
757
- );
758
-
759
- facets.helper.markDoneLiquidating(
760
- totalDebt,
761
- totalCollateral,
762
- accounting,
763
- );
764
- } else if (AmountMath.isEmpty(collateralProceeds)) {
765
- // Flow #2a
766
-
767
- // charge penalty if proceeds are sufficient
768
- const penaltyInMinted = ceilMultiplyBy(totalDebt, penaltyRate);
769
- const recoveredDebt = AmountMath.min(
770
- AmountMath.add(totalDebt, penaltyInMinted),
771
- mintedProceeds,
772
- );
773
-
774
- facets.helper.burnToCoverDebt(
775
- recoveredDebt,
776
- mintedProceeds,
777
- liqSeat,
692
+ vaultsInPlan[vaultIndex].getVaultSeat(),
693
+ amounts,
694
+ ]),
778
695
  );
696
+ atomicRearrange(zcf, harden(transfers));
697
+ }
779
698
 
780
- const coverDebt = AmountMath.isGTE(mintedProceeds, recoveredDebt);
781
- const distributable = coverDebt
782
- ? AmountMath.subtract(mintedProceeds, recoveredDebt)
783
- : AmountMath.makeEmptyFromAmount(mintedProceeds);
784
- let mintedRemaining = distributable;
785
-
786
- const vaultPortion = makeRatioFromAmounts(
787
- distributable,
788
- totalCollateral,
789
- );
790
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
791
- const transfers = [];
792
-
793
- // iterate from best to worst returning remaining funds to vaults
794
- /** @type {Array<[Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}]>} */
795
- for (const [vault, balance] of bestToWorst) {
796
- // from best to worst, return minted above penalty if any remains
797
- const { collateralAmount: vCollat, debtAmount } = balance;
798
- const vaultShare = floorMultiplyBy(vCollat, vaultPortion);
799
- facets.helper.subtractPhantomInterest(
800
- debtAmount,
801
- vault.getCurrentDebt(),
802
- );
803
-
804
- if (!AmountMath.isEmpty(mintedRemaining)) {
805
- const mintedToReturn = AmountMath.isGTE(
806
- mintedRemaining,
807
- vaultShare,
808
- )
809
- ? vaultShare
810
- : mintedRemaining;
811
- mintedRemaining = AmountMath.subtract(
812
- mintedRemaining,
813
- mintedToReturn,
814
- );
815
- const seat = vault.getVaultSeat();
816
- transfers.push([liqSeat, seat, { Minted: mintedToReturn }]);
817
- }
818
- vaultsToLiquidate.push(vault);
819
- }
820
-
821
- if (transfers.length > 0) {
822
- atomicRearrange(zcf, harden(transfers));
823
- }
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
+ }
824
709
 
825
- facets.helper.markCollateralLiquidated(
826
- totalCollateral,
827
- vaultData.getSize(),
710
+ if (!AmountMath.isEmpty(plan.phantomDebt)) {
711
+ state.totalDebt = AmountMath.subtract(
712
+ state.totalDebt,
713
+ plan.phantomDebt,
828
714
  );
715
+ }
829
716
 
830
- facets.helper.markDoneLiquidating(
831
- totalDebt,
832
- totalCollateral,
833
- 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',
834
727
  );
835
- } else {
836
- // Flow #2b: There's unsold collateral; some vaults may be revived.
837
-
838
- facets.helper.burnToCoverDebt(totalDebt, mintedProceeds, liqSeat);
839
- facets.helper.sendToReserve(accounting.overage, liqSeat, 'Minted');
728
+ }
840
729
 
841
- // reconstitute vaults until collateral is insufficient
842
- let reconstituteVaults = AmountMath.isGTE(
843
- collateralProceeds,
844
- 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)}`,
845
741
  );
742
+ }
846
743
 
847
- // charge penalty if proceeds are sufficient
848
- const distributableCollateral = reconstituteVaults
849
- ? AmountMath.subtract(collateralProceeds, totalPenalty)
850
- : AmountMath.makeEmptyFromAmount(collateralProceeds);
851
-
852
- let collatRemaining = distributableCollateral;
853
- /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
854
- const transfers = [];
855
- let liquidated = 0;
856
- /** @type {Array<Vault>} */
857
- const vaultsToReinstate = [];
858
- const reinstateAll = () => {
859
- const { prioritizedVaults } = collateralEphemera(collateralBrand);
860
- for (const vault of vaultsToReinstate) {
861
- const vaultId = vault.abortLiquidation();
862
- prioritizedVaults.addVault(vaultId, vault);
863
- liquidatingVaults.delete(vault);
864
- }
865
- };
866
-
867
- let collateralReduction = AmountMath.makeEmpty(collateralBrand);
868
- let shortfallToReserve = accounting.shortfall;
869
- const reduceCollateral = amount =>
870
- (collateralReduction = AmountMath.add(
871
- collateralReduction,
872
- amount,
873
- ));
874
-
875
- // iterate from best to worst attempting to reconstitute, by
876
- // returning remaining funds to vaults
877
- /** @type {Array<[Vault, { collateralAmount: Amount<'nat'>, debtAmount: Amount<'nat'>}]>} */
878
- for (const [vault, balance] of bestToWorst) {
879
- const { collateralAmount: vCollat, debtAmount } = balance;
880
-
881
- // according to #7123, Collateral for penalty =
882
- // vault debt / total debt * total liquidation penalty
883
- const vaultPenalty = ceilMultiplyBy(
884
- debtAmount,
885
- makeRatioFromAmounts(totalPenalty, totalDebt),
886
- );
887
- const collatPostPenalty = AmountMath.subtract(
888
- vCollat,
889
- vaultPenalty,
890
- );
891
- const vaultDebt = floorMultiplyBy(debtAmount, debtPortion);
892
- if (
893
- reconstituteVaults &&
894
- !AmountMath.isEmpty(collatPostPenalty) &&
895
- AmountMath.isGTE(collatRemaining, collatPostPenalty) &&
896
- AmountMath.isGTE(totalDebt, debtAmount)
897
- ) {
898
- collatRemaining = AmountMath.subtract(
899
- collatRemaining,
900
- collatPostPenalty,
901
- );
902
- shortfallToReserve = AmountMath.isGTE(
903
- shortfallToReserve,
904
- debtAmount,
905
- )
906
- ? AmountMath.subtract(shortfallToReserve, debtAmount)
907
- : AmountMath.makeEmptyFromAmount(shortfallToReserve);
908
- const seat = vault.getVaultSeat();
909
- // must reinstate after atomicRearrange(), so we record them.
910
- vaultsToReinstate.push(vault);
911
- reduceCollateral(vaultDebt);
912
- transfers.push([
913
- liqSeat,
914
- seat,
915
- { Collateral: collatPostPenalty },
916
- ]);
917
- } else {
918
- reconstituteVaults = false;
919
- liquidated += 1;
920
- facets.helper.subtractPhantomInterest(
921
- debtAmount,
922
- vault.getCurrentDebt(),
923
- );
924
-
925
- reduceCollateral(vCollat);
926
- vaultsToLiquidate.push(vault);
927
- }
928
- }
929
-
930
- // Putting all the rearrangements after the loop ensures that errors
931
- // in the calculations don't result in paying back some vaults and
932
- // leaving others hanging.
933
- if (transfers.length > 0) {
934
- atomicRearrange(zcf, harden(transfers));
935
- }
936
- reinstateAll();
937
-
938
- facets.helper.markCollateralLiquidated(
939
- collateralReduction,
940
- liquidated,
941
- transfers.length,
942
- );
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);
943
750
 
944
- const collateralInLiqSeat =
945
- liqSeat.getCurrentAllocation().Collateral;
946
- facets.helper.sendToReserve(collateralInLiqSeat, liqSeat);
751
+ facets.helper.markDoneLiquidating(
752
+ totalDebt,
753
+ totalCollateral,
754
+ plan.overage,
755
+ plan.shortfallToReserve,
756
+ );
947
757
 
948
- if (
949
- !AmountMath.isEqual(
950
- collateralInLiqSeat,
951
- AmountMath.add(collatRemaining, totalPenalty),
952
- )
953
- ) {
954
- console.error(
955
- `🚨 Excess collateral remaining sent to reserve. Expected ${q(
956
- collatRemaining,
957
- )}, sent ${q(collateralInLiqSeat)}`,
958
- );
959
- }
960
- facets.helper.markDoneLiquidating(totalDebt, totalCollateral, {
961
- ...accounting,
962
- shortfall: shortfallToReserve,
963
- });
964
- }
965
758
  // liqSeat should be empty at this point, except that funds are sent
966
759
  // asynchronously to the reserve.
967
- liquidateAll();
968
- return facets.helper.writeMetrics();
969
760
  },
970
761
  },
971
762
 
@@ -1278,6 +1069,7 @@ export const prepareVaultManagerKit = (
1278
1069
 
1279
1070
  const liqMargin = self.getGovernedParams().getLiquidationMargin();
1280
1071
 
1072
+ // totals *among* vaults being liquidated
1281
1073
  const { totalDebt, totalCollateral, vaultData, liqSeat } =
1282
1074
  getLiquidatableVaults(
1283
1075
  zcf,
@@ -1330,14 +1122,35 @@ export const prepareVaultManagerKit = (
1330
1122
  );
1331
1123
 
1332
1124
  trace(`LiqV after long wait`, proceeds);
1333
- return helper.distributeProceeds(
1334
- proceeds,
1335
- totalDebt,
1336
- storedCollateralQuote,
1337
- liqSeat,
1338
- vaultData,
1339
- totalCollateral,
1340
- );
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();
1341
1154
  },
1342
1155
  },
1343
1156
  },