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