@agoric/inter-protocol 0.13.3 → 0.13.4-dev-d32a66a.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 (140) hide show
  1. package/README.md +26 -20
  2. package/bundles/bundle-auctioneer-js-meta.json +722 -0
  3. package/bundles/bundle-auctioneer.js +1 -0
  4. package/bundles/bundle-econCommitteeCharter-js-meta.json +514 -0
  5. package/bundles/bundle-econCommitteeCharter.js +1 -0
  6. package/bundles/bundle-feeDistributor-js-meta.json +458 -0
  7. package/bundles/bundle-feeDistributor.js +1 -1
  8. package/bundles/bundle-fluxAggregatorKit-js-meta.json +674 -0
  9. package/bundles/bundle-fluxAggregatorKit.js +1 -0
  10. package/bundles/bundle-psm-js-meta.json +666 -0
  11. package/bundles/bundle-psm.js +1 -1
  12. package/bundles/bundle-reserve-js-meta.json +662 -0
  13. package/bundles/bundle-reserve.js +1 -1
  14. package/bundles/bundle-scaledPriceAuthority-js-meta.json +542 -0
  15. package/bundles/bundle-scaledPriceAuthority.js +1 -1
  16. package/bundles/bundle-vaultFactory-js-meta.json +786 -0
  17. package/bundles/bundle-vaultFactory.js +1 -1
  18. package/exported.js +0 -2
  19. package/package.json +34 -31
  20. package/scripts/add-collateral-core.js +14 -4
  21. package/scripts/build-bundles.js +9 -2
  22. package/scripts/deploy-contracts.js +1 -4
  23. package/scripts/init-core.js +20 -38
  24. package/scripts/invite-committee-core.js +1 -1
  25. package/scripts/manual-price-feed.js +2 -2
  26. package/scripts/price-feed-core.js +7 -6
  27. package/scripts/start-local-chain.sh +84 -0
  28. package/src/auction/auctionBook.js +755 -0
  29. package/src/auction/auctioneer.js +720 -0
  30. package/src/auction/offerBook.js +221 -0
  31. package/src/auction/params.js +174 -0
  32. package/src/auction/scheduleMath.js +167 -0
  33. package/src/auction/scheduler.js +370 -0
  34. package/src/auction/sortedOffers.js +136 -0
  35. package/src/auction/util.js +55 -0
  36. package/src/clientSupport.js +324 -0
  37. package/src/collectFees.js +8 -14
  38. package/src/contractSupport.js +24 -68
  39. package/src/econCommitteeCharter.js +161 -77
  40. package/src/feeDistributor.js +60 -20
  41. package/src/index.js +2 -0
  42. package/src/interest-math.js +1 -4
  43. package/src/interest.js +20 -13
  44. package/src/price/README.md +57 -0
  45. package/src/price/fluxAggregatorContract.js +190 -0
  46. package/src/price/fluxAggregatorKit.js +375 -0
  47. package/src/price/priceOracleKit.js +127 -0
  48. package/src/price/roundsManager.js +726 -0
  49. package/src/proposals/addAssetToVault.js +89 -94
  50. package/src/proposals/committee-proposal.js +141 -40
  51. package/src/proposals/core-proposal.js +100 -167
  52. package/src/proposals/econ-behaviors.js +320 -564
  53. package/src/proposals/price-feed-proposal.js +175 -78
  54. package/src/proposals/startEconCommittee.js +25 -12
  55. package/src/proposals/startPSM.js +231 -211
  56. package/src/proposals/utils.js +31 -17
  57. package/src/psm/README.md +71 -0
  58. package/src/psm/psm.js +272 -160
  59. package/src/psm/types.js +2 -0
  60. package/src/reserve/assetReserve.js +56 -413
  61. package/src/reserve/assetReserveKit.js +303 -0
  62. package/src/reserve/doc.md +1 -3
  63. package/src/reserve/params.js +2 -5
  64. package/src/typeGuards.js +13 -0
  65. package/src/vaultFactory/Liquidation.md +100 -0
  66. package/src/vaultFactory/burn.js +2 -1
  67. package/src/vaultFactory/liquidation.js +270 -108
  68. package/src/vaultFactory/math.js +95 -0
  69. package/src/vaultFactory/orderedVaultStore.js +0 -1
  70. package/src/vaultFactory/params.js +133 -112
  71. package/src/vaultFactory/prioritizedVaults.js +36 -79
  72. package/src/vaultFactory/storeUtils.js +67 -9
  73. package/src/vaultFactory/type-imports.js +2 -0
  74. package/src/vaultFactory/types.js +22 -26
  75. package/src/vaultFactory/vault.js +772 -709
  76. package/src/vaultFactory/vaultDirector.js +480 -483
  77. package/src/vaultFactory/vaultFactory.js +99 -60
  78. package/src/vaultFactory/vaultHolder.js +121 -95
  79. package/src/vaultFactory/vaultKit.js +39 -31
  80. package/src/vaultFactory/vaultManager.js +1249 -868
  81. package/bundles/bundle-amm-meta.js +0 -618
  82. package/bundles/bundle-amm.js +0 -1
  83. package/bundles/bundle-feeDistributor-meta.js +0 -346
  84. package/bundles/bundle-interchainPool-meta.js +0 -534
  85. package/bundles/bundle-interchainPool.js +0 -1
  86. package/bundles/bundle-liquidateIncrementally-meta.js +0 -442
  87. package/bundles/bundle-liquidateIncrementally.js +0 -1
  88. package/bundles/bundle-liquidateMinimum-meta.js +0 -442
  89. package/bundles/bundle-liquidateMinimum.js +0 -1
  90. package/bundles/bundle-psm-meta.js +0 -538
  91. package/bundles/bundle-psmCharter-meta.js +0 -418
  92. package/bundles/bundle-psmCharter.js +0 -1
  93. package/bundles/bundle-reserve-meta.js +0 -542
  94. package/bundles/bundle-scaledPriceAuthority-meta.js +0 -442
  95. package/bundles/bundle-stakeFactory-meta.js +0 -574
  96. package/bundles/bundle-stakeFactory.js +0 -1
  97. package/bundles/bundle-vaultFactory-meta.js +0 -598
  98. package/scripts/add-initial-liquidity.js +0 -180
  99. package/src/collect.js +0 -13
  100. package/src/interchainPool.js +0 -135
  101. package/src/makeTracer.js +0 -36
  102. package/src/my-lien.js +0 -85
  103. package/src/proposals/demoIssuers.js +0 -685
  104. package/src/proposals/sim-behaviors.js +0 -40
  105. package/src/psm/psmCharter.js +0 -139
  106. package/src/stakeFactory/README.md +0 -62
  107. package/src/stakeFactory/attestation.js +0 -267
  108. package/src/stakeFactory/attestationTool.js +0 -66
  109. package/src/stakeFactory/constants.js +0 -16
  110. package/src/stakeFactory/params.js +0 -31
  111. package/src/stakeFactory/stakeFactory.js +0 -236
  112. package/src/stakeFactory/stakeFactoryKit.js +0 -455
  113. package/src/stakeFactory/stakeFactoryManager.js +0 -296
  114. package/src/stakeFactory/types.js +0 -42
  115. package/src/vaultFactory/liquidateIncrementally.js +0 -350
  116. package/src/vaultFactory/liquidateMinimum.js +0 -100
  117. package/src/vpool-xyk-amm/AttackersGuide.md +0 -89
  118. package/src/vpool-xyk-amm/addLiquidity.js +0 -222
  119. package/src/vpool-xyk-amm/addPool.js +0 -197
  120. package/src/vpool-xyk-amm/constantProduct/AMM-Trade.jpeg +0 -0
  121. package/src/vpool-xyk-amm/constantProduct/README.md +0 -204
  122. package/src/vpool-xyk-amm/constantProduct/calcFees.js +0 -99
  123. package/src/vpool-xyk-amm/constantProduct/calcSwapPrices.js +0 -85
  124. package/src/vpool-xyk-amm/constantProduct/core.js +0 -153
  125. package/src/vpool-xyk-amm/constantProduct/defaults.js +0 -5
  126. package/src/vpool-xyk-amm/constantProduct/getXY.js +0 -52
  127. package/src/vpool-xyk-amm/constantProduct/internal-types.js +0 -116
  128. package/src/vpool-xyk-amm/constantProduct/invariants.js +0 -43
  129. package/src/vpool-xyk-amm/constantProduct/swap.js +0 -272
  130. package/src/vpool-xyk-amm/constantProduct/types.js +0 -32
  131. package/src/vpool-xyk-amm/doublePool.js +0 -194
  132. package/src/vpool-xyk-amm/estimate.js +0 -155
  133. package/src/vpool-xyk-amm/multipoolMarketMaker.js +0 -520
  134. package/src/vpool-xyk-amm/params.js +0 -61
  135. package/src/vpool-xyk-amm/pool.js +0 -396
  136. package/src/vpool-xyk-amm/priceAuthority.js +0 -94
  137. package/src/vpool-xyk-amm/removeLiquidity.js +0 -31
  138. package/src/vpool-xyk-amm/singlePool.js +0 -73
  139. package/src/vpool-xyk-amm/swap.js +0 -69
  140. package/src/vpool-xyk-amm/types.js +0 -154
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable consistent-return */
2
- // @ts-check
3
2
  /**
4
3
  * @file Vault Manager object manages vault-based debts for a collateral type.
5
4
  *
@@ -12,45 +11,63 @@
12
11
  * Once a vault is settled (liquidated or closed) it can still be used, traded,
13
12
  * etc. but is no longer the concern of the manager. It can't be liquidated,
14
13
  * have interest charged, or be counted in the metrics.
14
+ *
15
+ * Undercollateralized vaults can have their assets sent to the auctioneer to be
16
+ * liquidated. If the auction is unsuccessful, the liquidation may be reverted.
15
17
  */
16
18
  import '@agoric/zoe/exported.js';
17
19
 
18
- import { AmountMath } from '@agoric/ertp';
19
- import { makeStoredPublishKit, observeNotifier } from '@agoric/notifier';
20
20
  import {
21
- defineDurableKindMulti,
22
- makeKindHandle,
21
+ AmountMath,
22
+ AmountShape,
23
+ BrandShape,
24
+ NotifierShape,
25
+ RatioShape,
26
+ } from '@agoric/ertp';
27
+ import { makeTracer } from '@agoric/internal';
28
+ import { makeStoredNotifier, observeNotifier } from '@agoric/notifier';
29
+ import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
30
+ import {
31
+ M,
23
32
  makeScalarBigMapStore,
24
33
  makeScalarBigSetStore,
25
- pickFacet,
34
+ prepareExoClassKit,
35
+ provide,
26
36
  } from '@agoric/vat-data';
37
+ import { TransferPartShape } from '@agoric/zoe/src/contractSupport/atomicTransfer.js';
27
38
  import {
28
- assertProposalShape,
39
+ atomicRearrange,
29
40
  ceilDivideBy,
41
+ ceilMultiplyBy,
30
42
  floorDivideBy,
43
+ floorMultiplyBy,
31
44
  getAmountIn,
32
45
  getAmountOut,
46
+ makeEphemeraProvider,
33
47
  makeRatio,
34
48
  makeRatioFromAmounts,
49
+ makeRecorderTopic,
50
+ multiplyRatios,
51
+ offerTo,
52
+ SubscriberShape,
53
+ TopicsRecordShape,
35
54
  } from '@agoric/zoe/src/contractSupport/index.js';
55
+ import { PriceQuoteShape, SeatShape } from '@agoric/zoe/src/typeGuards.js';
36
56
  import { E } from '@endo/eventual-send';
37
- import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js';
38
- import {
39
- checkDebtLimit,
40
- makeEphemeraProvider,
41
- makeMetricsPublisherKit,
42
- } from '../contractSupport.js';
57
+ import { AuctionPFShape } from '../auction/auctioneer.js';
58
+ import { checkDebtLimit, makeNatAmountShape } from '../contractSupport.js';
43
59
  import { chargeInterest } from '../interest.js';
44
- import { makeTracer } from '../makeTracer.js';
45
- import { liquidate, makeQuote, updateQuote } from './liquidation.js';
60
+ import { getLiquidatableVaults, liquidationResults } from './liquidation.js';
61
+ import { calculateMinimumCollateralization, minimumPrice } from './math.js';
46
62
  import { makePrioritizedVaults } from './prioritizedVaults.js';
47
- import { makeVault, Phase } from './vault.js';
63
+ import { Phase, prepareVault } from './vault.js';
48
64
 
49
- const { details: X } = assert;
65
+ const { details: X, Fail } = assert;
50
66
 
51
67
  const trace = makeTracer('VM');
52
68
 
53
69
  /** @typedef {import('./storeUtils.js').NormalizedDebt} NormalizedDebt */
70
+ /** @typedef {import('@agoric/time/src/types').RelativeTime} RelativeTime */
54
71
 
55
72
  // Metrics naming scheme: nouns are present values; past-participles are accumulative.
56
73
  /**
@@ -61,13 +78,16 @@ const trace = makeTracer('VM');
61
78
  * @property {Amount<'nat'>} totalCollateral present sum of collateral across all vaults
62
79
  * @property {Amount<'nat'>} totalDebt present sum of debt across all vaults
63
80
  * @property {Amount<'nat'>} retainedCollateral collateral held as a result of not returning excess refunds
64
- * from AMM to owners of vaults liquidated with shortfalls
81
+ * to owners of vaults liquidated with shortfalls
82
+ * @property {Amount<'nat'>} liquidatingCollateral present sum of collateral in vaults sent for liquidation
83
+ * @property {Amount<'nat'>} liquidatingDebt present sum of debt in vaults sent for liquidation
65
84
  *
66
85
  * @property {Amount<'nat'>} totalCollateralSold running sum of collateral sold in liquidation
67
86
  * @property {Amount<'nat'>} totalOverageReceived running sum of overages, central received greater than debt
68
- * @property {Amount<'nat'>} totalProceedsReceived running sum of central received from liquidation
69
- * @property {Amount<'nat'>} totalShortfallReceived running sum of shortfalls, central received less than debt
70
- * @property {number} numLiquidationsCompleted running count of liquidations
87
+ * @property {Amount<'nat'>} totalProceedsReceived running sum of minted received from liquidation
88
+ * @property {Amount<'nat'>} totalShortfallReceived running sum of shortfalls, minted received less than debt
89
+ * @property {number} numLiquidationsCompleted running count of liquidated vaults
90
+ * @property {number} numLiquidationsAborted running count of vault liquidations that were reverted.
71
91
  */
72
92
 
73
93
  /**
@@ -75,7 +95,6 @@ const trace = makeTracer('VM');
75
95
  * compoundedInterest: Ratio,
76
96
  * interestRate: Ratio,
77
97
  * latestInterestUpdate: Timestamp,
78
- * liquidatorInstance?: Instance,
79
98
  * }} AssetState
80
99
  *
81
100
  * @typedef {{
@@ -83,914 +102,1276 @@ const trace = makeTracer('VM');
83
102
  * getRecordingPeriod: () => RelativeTime,
84
103
  * getDebtLimit: () => Amount<'nat'>,
85
104
  * getInterestRate: () => Ratio,
105
+ * getLiquidationPadding: () => Ratio,
86
106
  * getLiquidationMargin: () => Ratio,
87
107
  * getLiquidationPenalty: () => Ratio,
88
- * getLoanFee: () => Ratio,
108
+ * getMintFee: () => Ratio,
109
+ * getMinInitialDebt: () => Amount<'nat'>,
89
110
  * }} GovernedParamGetters
90
111
  */
91
112
 
92
113
  /**
93
114
  * @typedef {Readonly<{
94
- * collateralBrand: Brand<'nat'>,
95
- * debtBrand: Brand<'nat'>,
96
- * debtMint: ZCFMint<'nat'>,
97
- * poolIncrementSeat: ZCFSeat,
98
- * retainedCollateralSeat: ZCFSeat,
99
- * unsettledVaults: MapStore<string, Vault>,
100
- * liquidatingVaults: SetStore<Vault>,
101
- * }>} ImmutableState
115
+ * debtMint: ZCFMint<'nat'>,
116
+ * collateralBrand: Brand<'nat'>,
117
+ * collateralUnit: Amount<'nat'>,
118
+ * descriptionScope: string,
119
+ * startTimeStamp: Timestamp,
120
+ * storageNode: StorageNode,
121
+ * }>} HeldParams
102
122
  */
103
123
 
104
124
  /**
105
125
  * @typedef {{
106
- * compoundedInterest: Ratio,
107
- * latestInterestUpdate: Timestamp,
108
- * liquidator?: Liquidator
109
- * liquidatorInstance?: Instance
110
- * numLiquidationsCompleted: number,
111
- * totalCollateral: Amount<'nat'>,
112
- * totalCollateralSold: Amount<'nat'>,
113
- * totalDebt: Amount<'nat'>,
114
- * totalOverageReceived: Amount<'nat'>,
115
- * totalProceedsReceived: Amount<'nat'>,
116
- * totalShortfallReceived: Amount<'nat'>,
117
- * vaultCounter: number,
118
- * }} MutableState
126
+ * assetTopicKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<AssetState>,
127
+ * debtBrand: Brand<'nat'>,
128
+ * liquidatingVaults: SetStore<Vault>,
129
+ * metricsTopicKit: import('@agoric/zoe/src/contractSupport/recorder.js').RecorderKit<MetricsNotification>,
130
+ * poolIncrementSeat: ZCFSeat,
131
+ * retainedCollateralSeat: ZCFSeat,
132
+ * unsettledVaults: MapStore<string, Vault>,
133
+ * }} ImmutableState
119
134
  */
120
135
 
121
136
  /**
122
- * @typedef {Readonly<{
123
- * state: ImmutableState & MutableState,
124
- * facets: {
125
- * collateral: import('@agoric/vat-data/src/types').KindFacet<typeof collateralBehavior>,
126
- * helper: import('@agoric/vat-data/src/types').KindFacet<typeof helperBehavior>,
127
- * manager: import('@agoric/vat-data/src/types').KindFacet<typeof managerBehavior>,
128
- * self: import('@agoric/vat-data/src/types').KindFacet<typeof selfBehavior>,
129
- * }
130
- * }>} MethodContext
137
+ * @typedef {{
138
+ * compoundedInterest: Ratio,
139
+ * latestInterestUpdate: Timestamp,
140
+ * numLiquidationsCompleted: number,
141
+ * numLiquidationsAborted: number,
142
+ * totalCollateral: Amount<'nat'>,
143
+ * totalCollateralSold: Amount<'nat'>,
144
+ * totalDebt: Amount<'nat'>,
145
+ * liquidatingCollateral: Amount<'nat'>,
146
+ * liquidatingDebt: Amount<'nat'>,
147
+ * totalOverageReceived: Amount<'nat'>,
148
+ * totalProceedsReceived: Amount<'nat'>,
149
+ * totalShortfallReceived: Amount<'nat'>,
150
+ * vaultCounter: number,
151
+ * lockedQuote: PriceQuote | undefined,
152
+ * }} MutableState
131
153
  */
132
154
 
155
+ /** @param {Pick<PriceDescription, 'amountIn' | 'amountOut'>} quoteAmount */
156
+ const quoteAsRatio = quoteAmount =>
157
+ makeRatioFromAmounts(quoteAmount.amountIn, quoteAmount.amountOut);
158
+
133
159
  /**
134
- * Ephemera are the elements of state that cannot (or need not) be durable.
135
- * When there's a single instance it can be held in a closure, but there are
136
- * many vault manaager objects. So we hold their ephemera keyed by the durable
137
- * vault manager.
138
- *
139
- * XXX However since we don't have the vault manager object during
140
- * initState when we hold values that need to go into ephemera, we key for now
141
- * using a proxy for the manager object, collateralBrand.
142
- *
143
- * @type {(collateralBrand: Brand) => Partial<{
144
- * assetSubscriber: Subscriber<AssetState>,
145
- * assetPublisher: Publisher<AssetState>,
146
- * factoryPowers: import('./vaultDirector.js').FactoryPowersFacet?,
147
- * liquidationQueueing: boolean,
148
- * outstandingQuote: Promise<MutableQuote>?,
149
- * marshaller: ERef<Marshaller>,
150
- * periodNotifier: ERef<Notifier<Timestamp>>,
151
- * priceAuthority: ERef<PriceAuthority>,
160
+ * @type {(brand: Brand) => {
152
161
  * prioritizedVaults: ReturnType<typeof makePrioritizedVaults>,
153
- * metricsPublication: IterationObserver<MetricsNotification>,
154
- * metricsSubscription: StoredSubscription<MetricsNotification>,
155
- * storageNode: ERef<StorageNode>,
156
- * zcf: import('./vaultFactory.js').VaultFactoryZCF,
157
- * }>} */
158
- const provideEphemera = makeEphemeraProvider(() => ({
159
- liquidationQueueing: false,
160
- }));
162
+ * storedQuotesNotifier: import('@agoric/notifier').StoredNotifier<PriceQuote>,
163
+ * storedCollateralQuote: PriceQuote,
164
+ * }}
165
+ */
166
+ // any b/c will be filled after start()
167
+ const collateralEphemera = makeEphemeraProvider(() => /** @type {any} */ ({}));
161
168
 
162
169
  /**
163
- * Create state for the Vault Manager kind
164
- *
165
- * @param {import('./vaultFactory.js').VaultFactoryZCF} zcf
166
- * @param {ZCFMint<'nat'>} debtMint
167
- * @param {Brand} collateralBrand
168
- * @param {ERef<PriceAuthority>} priceAuthority
169
- * @param {import('./vaultDirector.js').FactoryPowersFacet} factoryPowers
170
- * @param {ERef<TimerService>} timerService
171
- * @param {Timestamp} startTimeStamp
172
- * @param {ERef<StorageNode>} storageNode
173
- * @param {ERef<Marshaller>} marshaller
170
+ * @param {import('@agoric/ertp').Baggage} baggage
171
+ * @param {{
172
+ * zcf: import('./vaultFactory.js').VaultFactoryZCF,
173
+ * marshaller: ERef<Marshaller>,
174
+ * makeRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit,
175
+ * makeERecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeERecorderKit,
176
+ * factoryPowers: import('./vaultDirector.js').FactoryPowersFacet,
177
+ * }} powers
174
178
  */
175
- const initState = (
176
- zcf,
177
- debtMint,
178
- collateralBrand,
179
- priceAuthority,
180
- factoryPowers,
181
- timerService,
182
- startTimeStamp,
183
- storageNode,
184
- marshaller,
179
+ export const prepareVaultManagerKit = (
180
+ baggage,
181
+ { zcf, marshaller, makeRecorderKit, factoryPowers },
185
182
  ) => {
186
- assert(
187
- storageNode && marshaller,
188
- 'VaultManager missing storageNode or marshaller',
189
- );
183
+ const { priceAuthority, timerService, reservePublicFacet } = zcf.getTerms();
190
184
 
191
- const periodNotifier = E(timerService).makeNotifier(
192
- 0n,
193
- factoryPowers.getGovernedParams().getChargingPeriod(),
194
- );
195
-
196
- const debtBrand = debtMint.getIssuerRecord().brand;
197
- const zeroCollateral = AmountMath.makeEmpty(collateralBrand, 'nat');
198
- const zeroDebt = AmountMath.makeEmpty(debtBrand, 'nat');
199
-
200
- const { metricsPublication, metricsSubscription } = makeMetricsPublisherKit(
201
- storageNode,
202
- marshaller,
203
- );
204
-
205
- /** @type {MapStore<string, Vault>} */
206
- const unsettledVaults = makeScalarBigMapStore('orderedVaultStore', {
207
- durable: true,
208
- });
185
+ const makeVault = prepareVault(baggage, makeRecorderKit, zcf);
209
186
 
210
187
  /**
211
- * If things are going well, the set will contain at most one Vault. Otherwise
212
- * failures remain and are available to be repaired via contract upgrade.
213
- *
214
- * @type {SetStore<Vault>}
188
+ * @param {HeldParams & { metricsStorageNode: StorageNode }} params
189
+ * @returns {HeldParams & ImmutableState & MutableState}
215
190
  */
216
- const liquidatingVaults = makeScalarBigSetStore('liquidatingVaults', {
217
- durable: true,
218
- });
219
-
220
- /** @type {ImmutableState} */
221
- const fixed = {
222
- collateralBrand,
223
- debtBrand,
224
- debtMint,
225
- poolIncrementSeat: zcf.makeEmptySeatKit().zcfSeat,
226
- retainedCollateralSeat: zcf.makeEmptySeatKit().zcfSeat,
227
- unsettledVaults,
228
- liquidatingVaults,
229
- };
191
+ const initState = params => {
192
+ const {
193
+ debtMint,
194
+ collateralBrand,
195
+ metricsStorageNode,
196
+ startTimeStamp,
197
+ storageNode,
198
+ } = params;
199
+ const debtBrand = debtMint.getIssuerRecord().brand;
200
+
201
+ /** @type {ImmutableState} */
202
+ const immutable = {
203
+ debtBrand,
204
+ poolIncrementSeat: zcf.makeEmptySeatKit().zcfSeat,
205
+
206
+ /**
207
+ * Vaults that have been sent for liquidation. When we get proceeds (or lack
208
+ * thereof) back from the liquidator, we will allocate them among the vaults.
209
+ *
210
+ * @type {SetStore<Vault>}
211
+ */
212
+ liquidatingVaults: makeScalarBigSetStore('liquidatingVaults', {
213
+ durable: true,
214
+ }),
230
215
 
231
- const compoundedInterest = makeRatio(100n, fixed.debtBrand); // starts at 1.0, no interest
232
- // timestamp of most recent update to interest
233
- const latestInterestUpdate = startTimeStamp;
216
+ assetTopicKit: makeRecorderKit(storageNode),
234
217
 
235
- /** @type {PublishKit<AssetState>} */
236
- const { publisher: assetPublisher, subscriber: assetSubscriber } =
237
- makeStoredPublishKit(storageNode, marshaller);
218
+ metricsTopicKit: makeRecorderKit(metricsStorageNode),
238
219
 
239
- assetPublisher.publish(
240
- harden({
241
- compoundedInterest,
242
- interestRate: factoryPowers.getGovernedParams().getInterestRate(),
243
- latestInterestUpdate,
244
- }),
245
- );
220
+ // TODO(#7074) not used while liquidation is disabled. Reinstate with #7074
221
+ retainedCollateralSeat: zcf.makeEmptySeatKit().zcfSeat,
246
222
 
247
- const ephemera = provideEphemera(collateralBrand);
248
- Object.assign(ephemera, {
249
- assetSubscriber,
250
- assetPublisher,
251
- factoryPowers,
252
- marshaller,
253
- metricsPublication,
254
- metricsSubscription,
255
- periodNotifier,
256
- priceAuthority,
257
- prioritizedVaults: makePrioritizedVaults(unsettledVaults),
258
- storageNode,
259
- zcf,
260
- });
261
-
262
- /** @type {MutableState & ImmutableState} */
263
- const state = {
264
- ...fixed,
265
- compoundedInterest,
266
- debtBrand: fixed.debtBrand,
267
- latestInterestUpdate,
268
- liquidator: undefined,
269
- liquidatorInstance: undefined,
270
- numLiquidationsCompleted: 0,
271
- totalCollateral: zeroCollateral,
272
- totalDebt: zeroDebt,
273
- totalOverageReceived: zeroDebt,
274
- totalProceedsReceived: zeroDebt,
275
- totalCollateralSold: zeroCollateral,
276
- totalShortfallReceived: zeroDebt,
277
- vaultCounter: 0,
223
+ unsettledVaults: makeScalarBigMapStore('orderedVaultStore', {
224
+ durable: true,
225
+ }),
226
+ };
227
+
228
+ const zeroCollateral = AmountMath.makeEmpty(collateralBrand, 'nat');
229
+ const zeroDebt = AmountMath.makeEmpty(debtBrand, 'nat');
230
+
231
+ return harden({
232
+ ...params,
233
+ ...immutable,
234
+ compoundedInterest: makeRatio(100n, debtBrand), // starts at 1.0, no interest
235
+ latestInterestUpdate: startTimeStamp,
236
+ numLiquidationsCompleted: 0,
237
+ numLiquidationsAborted: 0,
238
+ totalCollateral: zeroCollateral,
239
+ totalCollateralSold: zeroCollateral,
240
+ totalDebt: zeroDebt,
241
+ liquidatingCollateral: zeroCollateral,
242
+ liquidatingDebt: zeroDebt,
243
+ totalOverageReceived: zeroDebt,
244
+ totalProceedsReceived: zeroDebt,
245
+ totalShortfallReceived: zeroDebt,
246
+ vaultCounter: 0,
247
+ lockedQuote: undefined,
248
+ });
278
249
  };
279
250
 
280
- return state;
281
- };
282
-
283
- // Some of these could go in closures but are kept on a facet anticipating future durability options.
284
- const helperBehavior = {
285
- /**
286
- * @param {MethodContext} context
287
- * @param {Timestamp} updateTime
288
- * @param {ZCFSeat} poolIncrementSeat
289
- */
290
- chargeAllVaults: async ({ state, facets }, updateTime, poolIncrementSeat) => {
291
- trace('chargeAllVaults', state.collateralBrand, {
292
- updateTime,
293
- });
294
- const { factoryPowers } = provideEphemera(state.collateralBrand);
295
- assert(factoryPowers);
251
+ const makeVaultManagerKitInternal = prepareExoClassKit(
252
+ baggage,
253
+ 'VaultManagerKit',
254
+ {
255
+ collateral: M.interface('collateral', {
256
+ makeVaultInvitation: M.call().returns(M.promise()),
257
+ getSubscriber: M.call().returns(SubscriberShape),
258
+ getMetrics: M.call().returns(SubscriberShape),
259
+ getPublicTopics: M.call().returns(TopicsRecordShape),
260
+ getQuotes: M.call().returns(NotifierShape),
261
+ getCompoundedInterest: M.call().returns(RatioShape),
262
+ }),
263
+ helper: M.interface(
264
+ 'helper',
265
+ // not exposed so sloppy okay
266
+ {},
267
+ { sloppy: true },
268
+ ),
269
+ manager: M.interface('manager', {
270
+ getGovernedParams: M.call().returns(M.remotable('governedParams')),
271
+ maxDebtFor: M.call(AmountShape).returns(AmountShape),
272
+ mintAndTransfer: M.call(
273
+ SeatShape,
274
+ AmountShape,
275
+ AmountShape,
276
+ M.arrayOf(TransferPartShape),
277
+ ).returns(),
278
+ burn: M.call(AmountShape, SeatShape).returns(),
279
+ getAssetSubscriber: M.call().returns(SubscriberShape),
280
+ getCollateralBrand: M.call().returns(BrandShape),
281
+ getDebtBrand: M.call().returns(BrandShape),
282
+ getCompoundedInterest: M.call().returns(RatioShape),
283
+ scopeDescription: M.call(M.string()).returns(M.string()),
284
+ handleBalanceChange: M.call(
285
+ AmountShape,
286
+ AmountShape,
287
+ M.string(),
288
+ M.string(),
289
+ M.remotable('vault'),
290
+ ).returns(),
291
+ }),
292
+ self: M.interface('self', {
293
+ getGovernedParams: M.call().returns(M.remotable('governedParams')),
294
+ makeVaultKit: M.call(SeatShape).returns(M.promise()),
295
+ getCollateralQuote: M.call().returns(PriceQuoteShape),
296
+ getPublicFacet: M.call().returns(M.remotable('publicFacet')),
297
+ lockOraclePrices: M.call().returns(PriceQuoteShape),
298
+ liquidateVaults: M.call(AuctionPFShape).returns(M.promise()),
299
+ }),
300
+ },
301
+ initState,
302
+ {
303
+ collateral: {
304
+ makeVaultInvitation() {
305
+ const { facets } = this;
306
+ const { collateralBrand, debtBrand } = this.state;
307
+ return zcf.makeInvitation(
308
+ seat => this.facets.self.makeVaultKit(seat),
309
+ facets.manager.scopeDescription('MakeVault'),
310
+ undefined,
311
+ M.splitRecord({
312
+ give: {
313
+ Collateral: makeNatAmountShape(collateralBrand),
314
+ },
315
+ want: {
316
+ Minted: makeNatAmountShape(debtBrand),
317
+ },
318
+ }),
319
+ );
320
+ },
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
+ getQuotes() {
330
+ const ephemera = collateralEphemera(this.state.collateralBrand);
331
+ return ephemera.storedQuotesNotifier;
332
+ },
333
+ getCompoundedInterest() {
334
+ return this.state.compoundedInterest;
335
+ },
336
+ getPublicTopics() {
337
+ const { assetTopicKit, metricsTopicKit } = this.state;
338
+ return harden({
339
+ asset: makeRecorderTopic(
340
+ 'State of the assets managed',
341
+ assetTopicKit,
342
+ ),
343
+ metrics: makeRecorderTopic(
344
+ 'Vault Factory metrics',
345
+ metricsTopicKit,
346
+ ),
347
+ });
348
+ },
349
+ },
296
350
 
297
- const interestRate = factoryPowers.getGovernedParams().getInterestRate();
351
+ // Some of these could go in closures but are kept on a facet anticipating future durability options.
352
+ helper: {
353
+ /**
354
+ * Start non-durable processes (or restart if needed after vat restart)
355
+ */
356
+ start() {
357
+ const { state, facets } = this;
358
+ trace(state.collateralBrand, 'helper.start()', state.vaultCounter);
359
+ const {
360
+ collateralBrand,
361
+ collateralUnit,
362
+ debtBrand,
363
+ storageNode,
364
+ unsettledVaults,
365
+ } = state;
366
+
367
+ const ephemera = collateralEphemera(collateralBrand);
368
+ ephemera.prioritizedVaults = makePrioritizedVaults(unsettledVaults);
369
+
370
+ trace('helper.start() making periodNotifier');
371
+ const periodNotifier = E(timerService).makeNotifier(
372
+ 0n,
373
+ factoryPowers
374
+ .getGovernedParams(collateralBrand)
375
+ .getChargingPeriod(),
376
+ );
298
377
 
299
- // Update state with the results of charging interest
378
+ trace('helper.start() starting observe periodNotifier');
379
+ void observeNotifier(periodNotifier, {
380
+ updateState: updateTime =>
381
+ facets.helper
382
+ .chargeAllVaults(updateTime)
383
+ .catch(e =>
384
+ console.error('🚨 vaultManager failed to charge interest', e),
385
+ ),
386
+ fail: reason => {
387
+ zcf.shutdownWithFailure(
388
+ assert.error(X`Unable to continue without a timer: ${reason}`),
389
+ );
390
+ },
391
+ finish: done => {
392
+ zcf.shutdownWithFailure(
393
+ assert.error(X`Unable to continue without a timer: ${done}`),
394
+ );
395
+ },
396
+ });
397
+
398
+ trace('helper.start() making quoteNotifier from', priceAuthority);
399
+ const quoteNotifier = E(priceAuthority).makeQuoteNotifier(
400
+ collateralUnit,
401
+ debtBrand,
402
+ );
403
+ ephemera.storedQuotesNotifier = makeStoredNotifier(
404
+ quoteNotifier,
405
+ E(storageNode).makeChildNode('quotes'),
406
+ marshaller,
407
+ );
408
+ trace('helper.start() awaiting observe storedQuotesNotifier');
409
+ // NB: upon restart, there may not be a price for a while. If manager
410
+ // operations are permitted, ones the depend on price information will
411
+ // throw. See https://github.com/Agoric/agoric-sdk/issues/4317
412
+ void observeNotifier(quoteNotifier, {
413
+ updateState(value) {
414
+ trace('vaultManager got new collateral quote', value);
415
+ ephemera.storedCollateralQuote = value;
416
+ },
417
+ fail(reason) {
418
+ console.error('quoteNotifier failed to iterate', reason);
419
+ },
420
+ });
421
+ trace('helper.start() done');
422
+ },
423
+ /**
424
+ * @param {Timestamp} updateTime
425
+ */
426
+ async chargeAllVaults(updateTime) {
427
+ const { state, facets } = this;
428
+ const { collateralBrand, debtMint, poolIncrementSeat } = state;
429
+ trace(collateralBrand, 'chargeAllVaults', {
430
+ updateTime,
431
+ });
432
+
433
+ const interestRate = factoryPowers
434
+ .getGovernedParams(collateralBrand)
435
+ .getInterestRate();
436
+
437
+ // Update state with the results of charging interest
438
+
439
+ const changes = chargeInterest(
440
+ {
441
+ mint: debtMint,
442
+ mintAndTransferWithFee: factoryPowers.mintAndTransfer,
443
+ poolIncrementSeat,
444
+ seatAllocationKeyword: 'Minted',
445
+ },
446
+ {
447
+ interestRate,
448
+ chargingPeriod: factoryPowers
449
+ .getGovernedParams(collateralBrand)
450
+ .getChargingPeriod(),
451
+ recordingPeriod: factoryPowers
452
+ .getGovernedParams(collateralBrand)
453
+ .getRecordingPeriod(),
454
+ },
455
+ {
456
+ latestInterestUpdate: state.latestInterestUpdate,
457
+ compoundedInterest: state.compoundedInterest,
458
+ totalDebt: state.totalDebt,
459
+ },
460
+ updateTime,
461
+ );
300
462
 
301
- const changes = chargeInterest(
302
- {
303
- mint: state.debtMint,
304
- mintAndReallocateWithFee: factoryPowers.mintAndReallocate,
305
- poolIncrementSeat,
306
- seatAllocationKeyword: 'Minted',
307
- },
308
- {
309
- interestRate,
310
- chargingPeriod: factoryPowers.getGovernedParams().getChargingPeriod(),
311
- recordingPeriod: factoryPowers.getGovernedParams().getRecordingPeriod(),
312
- },
313
- {
314
- latestInterestUpdate: state.latestInterestUpdate,
315
- compoundedInterest: state.compoundedInterest,
316
- totalDebt: state.totalDebt,
317
- },
318
- updateTime,
319
- );
320
-
321
- state.compoundedInterest = changes.compoundedInterest;
322
- state.latestInterestUpdate = changes.latestInterestUpdate;
323
- state.totalDebt = changes.totalDebt;
324
-
325
- facets.helper.assetNotify();
326
- trace('chargeAllVaults complete', state.collateralBrand);
327
- // price to check against has changed
328
- return facets.helper.reschedulePriceCheck();
329
- },
330
-
331
- /** @param {MethodContext} context */
332
- assetNotify: ({ state }) => {
333
- const ephemera = provideEphemera(state.collateralBrand);
334
- assert(ephemera.factoryPowers && ephemera.assetPublisher);
335
- const interestRate = ephemera.factoryPowers
336
- .getGovernedParams()
337
- .getInterestRate();
338
- /** @type {AssetState} */
339
- const payload = harden({
340
- compoundedInterest: state.compoundedInterest,
341
- interestRate,
342
- latestInterestUpdate: state.latestInterestUpdate,
343
- // NB: the liquidator is determined by governance but the resulting
344
- // instance is a concern of the manager. The param manager knows only
345
- // about the installation and terms of the liqudation contract. We could
346
- // have another notifier for state downstream of governance changes, but
347
- // that doesn't seem to be cost-effective.
348
- liquidatorInstance: state.liquidatorInstance,
349
- });
350
- ephemera.assetPublisher.publish(payload);
351
- },
352
-
353
- /** @param {MethodContext} context */
354
- updateMetrics: ({ state }) => {
355
- const { metricsPublication, prioritizedVaults } = provideEphemera(
356
- state.collateralBrand,
357
- );
358
- assert(metricsPublication && prioritizedVaults);
359
-
360
- const retainedCollateral =
361
- state.retainedCollateralSeat.getCurrentAllocation()?.Collateral ??
362
- AmountMath.makeEmpty(state.collateralBrand, 'nat');
363
- /** @type {MetricsNotification} */
364
- const payload = harden({
365
- numActiveVaults: prioritizedVaults.getCount(),
366
- numLiquidatingVaults: state.liquidatingVaults.getSize(),
367
- totalCollateral: state.totalCollateral,
368
- totalDebt: state.totalDebt,
369
- retainedCollateral,
370
-
371
- numLiquidationsCompleted: state.numLiquidationsCompleted,
372
- totalCollateralSold: state.totalCollateralSold,
373
- totalOverageReceived: state.totalOverageReceived,
374
- totalProceedsReceived: state.totalProceedsReceived,
375
- totalShortfallReceived: state.totalShortfallReceived,
376
- });
377
- metricsPublication.updateState(payload);
378
- },
463
+ state.compoundedInterest = changes.compoundedInterest;
464
+ state.latestInterestUpdate = changes.latestInterestUpdate;
465
+ state.totalDebt = changes.totalDebt;
466
+
467
+ return facets.helper.assetNotify();
468
+ },
469
+ assetNotify() {
470
+ const { state } = this;
471
+ const { collateralBrand, assetTopicKit } = state;
472
+ const interestRate = factoryPowers
473
+ .getGovernedParams(collateralBrand)
474
+ .getInterestRate();
475
+ /** @type {AssetState} */
476
+ const payload = harden({
477
+ compoundedInterest: state.compoundedInterest,
478
+ interestRate,
479
+ latestInterestUpdate: state.latestInterestUpdate,
480
+ });
481
+ return assetTopicKit.recorder.write(payload);
482
+ },
483
+ burnToCoverDebt(debt, proceeds, seat) {
484
+ const { state } = this;
485
+
486
+ if (AmountMath.isGTE(proceeds, debt)) {
487
+ factoryPowers.burnDebt(debt, seat);
488
+ state.totalDebt = AmountMath.subtract(state.totalDebt, debt);
489
+ } else {
490
+ factoryPowers.burnDebt(proceeds, seat);
491
+ state.totalDebt = AmountMath.subtract(state.totalDebt, proceeds);
492
+ }
493
+
494
+ state.totalProceedsReceived = AmountMath.add(
495
+ state.totalProceedsReceived,
496
+ proceeds,
497
+ );
498
+ },
499
+ sendToReserve(penalty, seat, seatKeyword = 'Collateral') {
500
+ const invitation =
501
+ E(reservePublicFacet).makeAddCollateralInvitation();
502
+
503
+ // don't wait for response
504
+ void E.when(invitation, invite => {
505
+ const proposal = { give: { Collateral: penalty } };
506
+ void offerTo(
507
+ zcf,
508
+ invite,
509
+ { [seatKeyword]: 'Collateral' },
510
+ proposal,
511
+ seat,
512
+ );
513
+ });
514
+ },
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
+ markLiquidating(debt, collateral) {
527
+ const { state } = this;
528
+
529
+ state.liquidatingCollateral = AmountMath.add(
530
+ state.liquidatingCollateral,
531
+ collateral,
532
+ );
379
533
 
380
- /**
381
- * When any Vault's debt ratio is higher than the current high-water level,
382
- * call `reschedulePriceCheck()` to request a fresh notification from the
383
- * priceAuthority. There will be extra outstanding requests since we can't
384
- * cancel them. (https://github.com/Agoric/agoric-sdk/issues/2713).
385
- *
386
- * When the vault with the current highest debt ratio is removed or reduces
387
- * its ratio, we won't reschedule the priceAuthority requests to reduce churn.
388
- * Instead, when a priceQuote is received, we'll only reschedule if the
389
- * high-water level when the request was made matches the current high-water
390
- * level.
391
- *
392
- * @param {MethodContext} context
393
- * @param {Ratio} [highestRatio]
394
- * @returns {Promise<void>}
395
- */
396
- reschedulePriceCheck: async ({ state, facets }, highestRatio) => {
397
- const { prioritizedVaults, ...ephemera } = provideEphemera(
398
- state.collateralBrand,
399
- );
400
- assert(ephemera.factoryPowers && prioritizedVaults);
401
- trace('reschedulePriceCheck', state.collateralBrand, ephemera);
402
- // INTERLOCK: the first time through, start the activity to wait for
403
- // and process liquidations over time.
404
- if (!ephemera.liquidationQueueing) {
405
- ephemera.liquidationQueueing = true;
406
- // eslint-disable-next-line consistent-return
407
- return facets.helper
408
- .processLiquidations()
409
- .catch(e => console.error('Liquidator failed', e))
410
- .finally(() => {
411
- ephemera.liquidationQueueing = false;
412
- });
413
- }
414
-
415
- if (!ephemera.outstandingQuote) {
416
- // the new threshold will be picked up by the next quote request
417
- return;
418
- }
419
-
420
- const highestDebtRatio = highestRatio || prioritizedVaults.highestRatio();
421
- if (!highestDebtRatio) {
422
- // if there aren't any open vaults, we don't need an outstanding RFQ.
423
- trace('no open vaults');
424
- return;
425
- }
426
-
427
- // There is already an activity processing liquidations. It may be
428
- // waiting for the oracle price to cross a threshold.
429
- // Update the current in-progress quote.
430
- const govParams = ephemera.factoryPowers.getGovernedParams();
431
- const liquidationMargin = govParams.getLiquidationMargin();
432
- // Safe to call extraneously (lightweight and idempotent)
433
- updateQuote(ephemera.outstandingQuote, highestDebtRatio, liquidationMargin);
434
- trace('update quote', state.collateralBrand, highestDebtRatio);
435
- },
534
+ state.liquidatingDebt = AmountMath.add(state.liquidatingDebt, debt);
535
+ },
536
+ /**
537
+ *
538
+ * @param {Amount<'nat'>} debt
539
+ * @param {Amount<'nat'>} collateral
540
+ * @param {{ overage: Amount<'nat'>, shortfall: Amount<'nat'> }} accounting
541
+ */
542
+ markDoneLiquidating(debt, collateral, accounting) {
543
+ const { state } = this;
544
+
545
+ // update liquidation state
546
+
547
+ state.liquidatingCollateral = AmountMath.subtract(
548
+ state.liquidatingCollateral,
549
+ collateral,
550
+ );
551
+ state.liquidatingDebt = AmountMath.subtract(
552
+ state.liquidatingDebt,
553
+ debt,
554
+ );
436
555
 
437
- /**
438
- * @param {MethodContext} context
439
- */
440
- processLiquidations: async ({ state, facets }) => {
441
- const { prioritizedVaults, ...ephemera } = provideEphemera(
442
- state.collateralBrand,
443
- );
444
- assert(ephemera.factoryPowers && ephemera.priceAuthority);
445
- const { priceAuthority } = ephemera;
446
- const govParams = ephemera.factoryPowers.getGovernedParams();
447
-
448
- async function* eventualLiquidations() {
449
- assert(prioritizedVaults);
450
- while (true) {
451
- const highestDebtRatio = prioritizedVaults.highestRatio();
452
- if (!highestDebtRatio) {
453
- return;
454
- }
455
- const liquidationMargin = govParams.getLiquidationMargin();
456
-
457
- // ask to be alerted when the price level falls enough that the vault
458
- // with the highest debt to collateral ratio will no longer be valued at the
459
- // liquidationMargin above its debt.
460
- ephemera.outstandingQuote = makeQuote(
461
- priceAuthority,
462
- highestDebtRatio,
463
- liquidationMargin,
464
- );
465
- trace('posted quote request', state.collateralBrand, highestDebtRatio);
466
-
467
- // The rest of this method will not happen until after a quote is received.
468
- // This may not happen until much later, when the market changes.
469
- // eslint-disable-next-line no-await-in-loop
470
- const quote = await E(ephemera.outstandingQuote).getPromise();
471
- ephemera.outstandingQuote = null;
472
- // When we receive a quote, we check whether the vault with the highest
473
- // ratio of debt to collateral is below the liquidationMargin, and if so,
474
- // we liquidate it. We use ceilDivide to round up because ratios above
475
- // this will be liquidated.
476
- const quoteRatioPlusMargin = makeRatioFromAmounts(
477
- ceilDivideBy(getAmountOut(quote), liquidationMargin),
478
- getAmountIn(quote),
479
- );
480
- trace('quote', state.collateralBrand, quote, quoteRatioPlusMargin);
481
-
482
- // Liquidate the head of the queue
483
- const [next] =
484
- prioritizedVaults.entriesPrioritizedGTE(quoteRatioPlusMargin);
485
- if (next) {
486
- yield next;
487
- }
488
- }
489
- }
490
- for await (const next of eventualLiquidations()) {
491
- await facets.helper.liquidateAndRemove(next);
492
- trace('price check liq', state.collateralBrand, next && next[0]);
493
- }
494
- },
556
+ // record shortfall and proceeds
495
557
 
496
- /**
497
- * @param {MethodContext} context
498
- * @param {[key: string, vaultKit: Vault]} record
499
- */
500
- liquidateAndRemove: ({ state, facets }, [key, vault]) => {
501
- const { factoryPowers, prioritizedVaults, zcf } = provideEphemera(
502
- state.collateralBrand,
503
- );
504
- assert(factoryPowers && prioritizedVaults && zcf);
505
- const vaultSeat = vault.getVaultSeat();
506
- trace('liquidating', state.collateralBrand, vaultSeat.getProposal());
507
-
508
- const collateralPre = vault.getCollateralAmount();
509
-
510
- // Start liquidation (vaultState: LIQUIDATING)
511
- const liquidator = state.liquidator;
512
- assert(liquidator);
513
- state.liquidatingVaults.add(vault);
514
- prioritizedVaults.removeVault(key);
515
-
516
- return liquidate(
517
- zcf,
518
- vault,
519
- liquidator,
520
- state.collateralBrand,
521
- factoryPowers.getGovernedParams().getLiquidationPenalty(),
522
- )
523
- .then(accounting => {
524
- facets.manager.burnAndRecord(accounting.toBurn, vaultSeat);
525
-
526
- // current values
527
-
528
- // Sometimes, the AMM will sell less than all the collateral. If there
529
- // was a shortfall, the investor doesn't keep the change, so we get it.
530
- // If there was no shortfall, the collateral is returned.
531
- const collateralPost = vault.getCollateralAmount();
532
- if (
533
- !AmountMath.isEmpty(collateralPost) &&
534
- !AmountMath.isEmpty(accounting.shortfall)
535
- ) {
536
- // The borrower doesn't get the excess collateral remaining when
537
- // liquidation results in a shortfall. We currently do nothing with
538
- // it. We could hold it until it crosses some threshold, then sell it
539
- // to the AMM, or we could transfer it to the reserve. At least it's
540
- // visible in the accounting.
541
- vaultSeat.decrementBy(
542
- state.retainedCollateralSeat.incrementBy({
543
- Collateral: collateralPost,
544
- }),
558
+ const { overage, shortfall } = accounting;
559
+ // cumulative values
560
+ state.totalOverageReceived = AmountMath.add(
561
+ state.totalOverageReceived,
562
+ overage,
545
563
  );
546
- zcf.reallocate(vaultSeat, state.retainedCollateralSeat);
547
- }
548
-
549
- // Reduce totalCollateral by collateralPre, since all the collateral was
550
- // sold, returned to the vault owner, or held by the VaultManager.
551
- state.totalCollateral = AmountMath.subtract(
552
- state.totalCollateral,
553
- collateralPre,
554
- );
555
- state.totalDebt = AmountMath.subtract(
556
- state.totalDebt,
557
- accounting.shortfall,
558
- );
564
+ state.totalShortfallReceived = AmountMath.add(
565
+ state.totalShortfallReceived,
566
+ shortfall,
567
+ );
568
+ state.totalDebt = AmountMath.subtract(state.totalDebt, shortfall);
559
569
 
560
- // cumulative values
561
- state.totalProceedsReceived = AmountMath.add(
562
- state.totalProceedsReceived,
563
- accounting.proceeds,
564
- );
565
- state.totalOverageReceived = AmountMath.add(
566
- state.totalOverageReceived,
567
- accounting.overage,
568
- );
569
- state.totalShortfallReceived = AmountMath.add(
570
- state.totalShortfallReceived,
571
- accounting.shortfall,
572
- );
573
- state.liquidatingVaults.delete(vault);
574
- trace('liquidated', state.collateralBrand);
575
- state.numLiquidationsCompleted += 1;
576
- facets.helper.updateMetrics();
577
-
578
- if (!AmountMath.isEmpty(accounting.shortfall)) {
579
- E(factoryPowers.getShortfallReporter())
580
- .increaseLiquidationShortfall(accounting.shortfall)
581
- .catch(reason =>
570
+ E.when(
571
+ factoryPowers.getShortfallReporter(),
572
+ reporter => E(reporter).increaseLiquidationShortfall(shortfall),
573
+ err =>
582
574
  console.error(
583
- 'liquidateAndRemove failed to increaseLiquidationShortfall',
584
- reason,
575
+ '🛠️ getShortfallReporter() failed during liquidation; repair by updating governance',
576
+ err,
585
577
  ),
578
+ ).catch(err => {
579
+ console.error('🚨 failed to report liquidation shortfall', err);
580
+ });
581
+ },
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
+ writeMetrics() {
602
+ const { state } = this;
603
+ const { collateralBrand, retainedCollateralSeat, metricsTopicKit } =
604
+ state;
605
+ const { prioritizedVaults } = collateralEphemera(collateralBrand);
606
+
607
+ const retainedCollateral =
608
+ retainedCollateralSeat.getCurrentAllocation()?.Collateral ??
609
+ AmountMath.makeEmpty(collateralBrand, 'nat');
610
+
611
+ /** @type {MetricsNotification} */
612
+ const payload = harden({
613
+ numActiveVaults: prioritizedVaults.getCount(),
614
+ numLiquidatingVaults: state.liquidatingVaults.getSize(),
615
+ totalCollateral: state.totalCollateral,
616
+ totalDebt: state.totalDebt,
617
+ retainedCollateral,
618
+
619
+ numLiquidationsCompleted: state.numLiquidationsCompleted,
620
+ numLiquidationsAborted: state.numLiquidationsAborted,
621
+ totalCollateralSold: state.totalCollateralSold,
622
+ liquidatingCollateral: state.liquidatingCollateral,
623
+ liquidatingDebt: state.liquidatingDebt,
624
+ totalOverageReceived: state.totalOverageReceived,
625
+ totalProceedsReceived: state.totalProceedsReceived,
626
+ totalShortfallReceived: state.totalShortfallReceived,
627
+ });
628
+
629
+ return E(metricsTopicKit.recorder).write(payload);
630
+ },
631
+
632
+ distributeProceeds(
633
+ proceeds,
634
+ totalDebt,
635
+ oraclePriceAtStart,
636
+ liqSeat,
637
+ vaultData,
638
+ totalCollateral,
639
+ ) {
640
+ const { state, facets } = this;
641
+ const { collateralBrand, debtBrand, liquidatingVaults } = this.state;
642
+
643
+ const { Collateral: collateralProceeds } = proceeds;
644
+ /** @type {Amount<'nat'>} */
645
+ const collateralSold = AmountMath.subtract(
646
+ totalCollateral,
647
+ collateralProceeds,
648
+ );
649
+ state.totalCollateralSold = AmountMath.add(
650
+ state.totalCollateralSold,
651
+ collateralSold,
652
+ );
653
+
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
+ const penaltyRate = facets.self
668
+ .getGovernedParams()
669
+ .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
+ 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,
586
688
  );
587
- }
588
- })
589
- .catch(e => {
590
- // XXX should notify interested parties
591
- console.error('liquidateAndRemove failed with', e);
592
- throw e;
593
- });
594
- },
595
- };
596
689
 
597
- const managerBehavior = {
598
- /** @param {MethodContext} context */
599
- getGovernedParams: ({ state }) => {
600
- const ephemera = provideEphemera(state.collateralBrand);
601
- assert(ephemera.factoryPowers);
602
- return ephemera.factoryPowers.getGovernedParams();
603
- },
690
+ const distributableCollateral = collateralToDistribute
691
+ ? AmountMath.subtract(collateralProceeds, totalPenalty)
692
+ : AmountMath.makeEmptyFromAmount(collateralProceeds);
693
+
694
+ facets.helper.burnToCoverDebt(totalDebt, mintedProceeds, liqSeat);
695
+ facets.helper.sendToReserve(accounting.overage, liqSeat, 'Minted');
696
+
697
+ // return remaining funds to vaults before closing
698
+
699
+ /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
700
+ const transfers = [];
701
+ let leftToStage = distributableCollateral;
702
+
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([
719
+ 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
+ );
604
739
 
605
- /**
606
- * @param {MethodContext} context
607
- * @param {Amount<'nat'>} collateralAmount
608
- */
609
- maxDebtFor: async ({ state }, collateralAmount) => {
610
- const { debtBrand } = state;
611
- const { priceAuthority, ...ephemera } = provideEphemera(
612
- state.collateralBrand,
613
- );
614
- assert(ephemera.factoryPowers && priceAuthority);
615
- const quoteAmount = await E(priceAuthority).quoteGiven(
616
- collateralAmount,
617
- debtBrand,
618
- );
619
- // floorDivide because we want the debt ceiling lower
620
- return floorDivideBy(
621
- getAmountOut(quoteAmount),
622
- ephemera.factoryPowers.getGovernedParams().getLiquidationMargin(),
623
- );
624
- },
625
- /**
626
- * TODO utility method to turn a callback into non-actual one
627
- * was type {MintAndReallocate}
628
- *
629
- * @param {MethodContext} context
630
- * @param {Amount} toMint
631
- * @param {Amount} fee
632
- * @param {ZCFSeat} seat
633
- * @param {...ZCFSeat} otherSeats
634
- * @returns {void}
635
- */
636
- mintAndReallocate: ({ state }, toMint, fee, seat, ...otherSeats) => {
637
- const { totalDebt } = state;
638
- const { factoryPowers } = provideEphemera(state.collateralBrand);
639
- assert(factoryPowers);
640
-
641
- checkDebtLimit(
642
- factoryPowers.getGovernedParams().getDebtLimit(),
643
- totalDebt,
644
- toMint,
645
- );
646
- factoryPowers.mintAndReallocate(toMint, fee, seat, ...otherSeats);
647
- state.totalDebt = AmountMath.add(state.totalDebt, toMint);
648
- },
649
- /**
650
- * @param {MethodContext} context
651
- * @param {Amount<'nat'>} toBurn
652
- * @param {ZCFSeat} seat
653
- */
654
- burnAndRecord: ({ state }, toBurn, seat) => {
655
- const { factoryPowers } = provideEphemera(state.collateralBrand);
656
- assert(factoryPowers);
657
- trace('burnAndRecord', state.collateralBrand, {
658
- toBurn,
659
- totalDebt: state.totalDebt,
660
- });
661
- const { burnDebt } = factoryPowers;
662
- burnDebt(toBurn, seat);
663
- state.totalDebt = AmountMath.subtract(state.totalDebt, toBurn);
664
- },
665
- /** @param {MethodContext} context */
666
- getAssetSubscriber: ({ state }) => {
667
- const { assetSubscriber } = provideEphemera(state.collateralBrand);
668
- assert(assetSubscriber);
669
- return assetSubscriber;
670
- },
671
- /** @param {MethodContext} context */
672
- getCollateralBrand: ({ state }) => state.collateralBrand,
673
- /** @param {MethodContext} context */
674
- getDebtBrand: ({ state }) => state.debtBrand,
675
- /**
676
- * coefficient on existing debt to calculate new debt
677
- *
678
- * @param {MethodContext} context
679
- */
680
- getCompoundedInterest: ({ state }) => state.compoundedInterest,
681
- /**
682
- * Called by a vault when its balances change.
683
- *
684
- * @param {MethodContext} context
685
- * @param {NormalizedDebt} oldDebtNormalized
686
- * @param {Amount<'nat'>} oldCollateral
687
- * @param {VaultId} vaultId
688
- * @param {import('./vault.js').VaultPhase} vaultPhase at the end of whatever change updated balances
689
- * @param {Vault} vault
690
- */
691
- handleBalanceChange: (
692
- { state, facets },
693
- oldDebtNormalized,
694
- oldCollateral,
695
- vaultId,
696
- vaultPhase,
697
- vault,
698
- ) => {
699
- const { prioritizedVaults } = provideEphemera(state.collateralBrand);
700
- assert(prioritizedVaults);
701
-
702
- // the manager holds only vaults that can accrue interest or be liquidated;
703
- // i.e. vaults that have debt. The one exception is at the outset when
704
- // a vault has been added to the manager but not yet accounted for.
705
- const settled =
706
- AmountMath.isEmpty(oldDebtNormalized) && vaultPhase !== Phase.ACTIVE;
707
-
708
- if (settled) {
709
- assert(
710
- !prioritizedVaults.hasVaultByAttributes(
711
- oldDebtNormalized,
712
- oldCollateral,
713
- vaultId,
714
- ),
715
- 'Settled vaults must not be retained in storage',
716
- );
717
- } else {
718
- const isNew = AmountMath.isEmpty(oldDebtNormalized);
719
- if (!isNew) {
720
- // its position in the queue is no longer valid
721
-
722
- const vaultInStore = prioritizedVaults.removeVaultByAttributes(
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,
759
+ );
760
+
761
+ const coverDebt = AmountMath.isGTE(mintedProceeds, recoveredDebt);
762
+ const distributable = coverDebt
763
+ ? AmountMath.subtract(mintedProceeds, recoveredDebt)
764
+ : AmountMath.makeEmptyFromAmount(mintedProceeds);
765
+ let mintedRemaining = distributable;
766
+
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(),
809
+ );
810
+
811
+ facets.helper.markDoneLiquidating(
812
+ totalDebt,
813
+ totalCollateral,
814
+ accounting,
815
+ );
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');
821
+
822
+ // reconstitute vaults until collateral is insufficient
823
+ let reconstituteVaults = AmountMath.isGTE(
824
+ collateralProceeds,
825
+ totalPenalty,
826
+ );
827
+
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();
913
+
914
+ facets.helper.markCollateralLiquidated(
915
+ collateralReduction,
916
+ liquidated,
917
+ transfers.length,
918
+ );
919
+
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();
928
+ },
929
+ },
930
+
931
+ manager: {
932
+ getGovernedParams() {
933
+ const { collateralBrand } = this.state;
934
+ return factoryPowers.getGovernedParams(collateralBrand);
935
+ },
936
+
937
+ /**
938
+ * Look up the most recent price authority price to determine the max
939
+ * debt this manager config will allow for the collateral.
940
+ *
941
+ * @param {Amount<'nat'>} collateralAmount
942
+ */
943
+ maxDebtFor(collateralAmount) {
944
+ const { collateralBrand } = this.state;
945
+ const { storedCollateralQuote } = collateralEphemera(collateralBrand);
946
+ if (!storedCollateralQuote)
947
+ throw Fail`maxDebtFor called before a collateral quote was available`;
948
+ // use the lower price to prevent vault adjustments that put them imminently underwater
949
+ const collateralPrice = minimumPrice(
950
+ storedCollateralQuote,
951
+ this.state.lockedQuote,
952
+ );
953
+ const collatlVal = ceilMultiplyBy(collateralAmount, collateralPrice);
954
+ const minimumCollateralization = calculateMinimumCollateralization(
955
+ factoryPowers
956
+ .getGovernedParams(collateralBrand)
957
+ .getLiquidationMargin(),
958
+ factoryPowers
959
+ .getGovernedParams(collateralBrand)
960
+ .getLiquidationPadding(),
961
+ );
962
+ // floorDivide because we want the debt ceiling lower
963
+ return floorDivideBy(collatlVal, minimumCollateralization);
964
+ },
965
+ /** @type {MintAndTransfer} */
966
+ mintAndTransfer(mintReceiver, toMint, fee, transfers) {
967
+ const { state } = this;
968
+ const { collateralBrand, totalDebt } = state;
969
+
970
+ checkDebtLimit(
971
+ factoryPowers.getGovernedParams(collateralBrand).getDebtLimit(),
972
+ totalDebt,
973
+ toMint,
974
+ );
975
+ factoryPowers.mintAndTransfer(mintReceiver, toMint, fee, transfers);
976
+ },
977
+ /**
978
+ * @param {Amount<'nat'>} toBurn
979
+ * @param {ZCFSeat} seat
980
+ */
981
+ burn(toBurn, seat) {
982
+ const { state } = this;
983
+ const { collateralBrand } = this.state;
984
+
985
+ trace(collateralBrand, 'burn', {
986
+ toBurn,
987
+ totalDebt: state.totalDebt,
988
+ });
989
+ factoryPowers.burnDebt(toBurn, seat);
990
+ },
991
+ getAssetSubscriber() {
992
+ return this.state.assetTopicKit.subscriber;
993
+ },
994
+ getCollateralBrand() {
995
+ const { collateralBrand } = this.state;
996
+ return collateralBrand;
997
+ },
998
+ getDebtBrand() {
999
+ const { debtBrand } = this.state;
1000
+ return debtBrand;
1001
+ },
1002
+ /**
1003
+ * Prepend with an identifier of this vault manager
1004
+ *
1005
+ * @param {string} base
1006
+ */
1007
+ scopeDescription(base) {
1008
+ const { descriptionScope } = this.state;
1009
+ return `${descriptionScope}: ${base}`;
1010
+ },
1011
+ /**
1012
+ * coefficient on existing debt to calculate new debt
1013
+ */
1014
+ getCompoundedInterest() {
1015
+ return this.state.compoundedInterest;
1016
+ },
1017
+ /**
1018
+ * Called by a vault when its balances change.
1019
+ *
1020
+ * @param {NormalizedDebt} oldDebtNormalized
1021
+ * @param {Amount<'nat'>} oldCollateral
1022
+ * @param {VaultId} vaultId
1023
+ * @param {import('./vault.js').VaultPhase} vaultPhase at the end of whatever change updated balances
1024
+ * @param {Vault} vault
1025
+ * @returns {void}
1026
+ */
1027
+ handleBalanceChange(
723
1028
  oldDebtNormalized,
724
1029
  oldCollateral,
725
1030
  vaultId,
726
- );
727
- assert(
728
- vault === vaultInStore,
729
- 'handleBalanceChange for two different vaults',
730
- );
731
- }
732
-
733
- // replace in queue, but only if it can accrue interest or be liquidated (i.e. has debt).
734
- // getCurrentDebt() would also work (0x = 0) but require more computation.
735
- if (!AmountMath.isEmpty(vault.getNormalizedDebt())) {
736
- prioritizedVaults.addVault(vaultId, vault);
737
- }
738
-
739
- // totalCollateral += vault's collateral delta (post — pre)
740
- state.totalCollateral = AmountMath.subtract(
741
- AmountMath.add(state.totalCollateral, vault.getCollateralAmount()),
742
- oldCollateral,
743
- );
744
- // debt accounting managed through minting and burning
745
- facets.helper.updateMetrics();
746
- }
747
- },
748
- };
1031
+ vaultPhase,
1032
+ vault,
1033
+ ) {
1034
+ const { state, facets } = this;
1035
+
1036
+ // the manager holds only vaults that can accrue interest or be liquidated;
1037
+ // i.e. vaults that have debt. The one exception is at the outset when
1038
+ // a vault has been added to the manager but not yet accounted for.
1039
+ const settled =
1040
+ AmountMath.isEmpty(oldDebtNormalized) &&
1041
+ vaultPhase !== Phase.ACTIVE;
1042
+
1043
+ trace('handleBalanceChange', {
1044
+ oldDebtNormalized,
1045
+ oldCollateral,
1046
+ vaultId,
1047
+ vaultPhase,
1048
+ vault,
1049
+ settled,
1050
+ });
1051
+
1052
+ const { prioritizedVaults } = collateralEphemera(
1053
+ state.collateralBrand,
1054
+ );
1055
+ if (settled) {
1056
+ assert(
1057
+ !prioritizedVaults.hasVaultByAttributes(
1058
+ oldDebtNormalized,
1059
+ oldCollateral,
1060
+ vaultId,
1061
+ ),
1062
+ 'Settled vaults must not be retained in storage',
1063
+ );
1064
+ return;
1065
+ }
1066
+
1067
+ const isNew = AmountMath.isEmpty(oldDebtNormalized);
1068
+ trace(state.collateralBrand, { isNew });
1069
+ if (!isNew) {
1070
+ // its position in the queue is no longer valid
1071
+
1072
+ const vaultInStore = prioritizedVaults.removeVaultByAttributes(
1073
+ oldDebtNormalized,
1074
+ oldCollateral,
1075
+ vaultId,
1076
+ );
1077
+ assert(
1078
+ vault === vaultInStore,
1079
+ 'handleBalanceChange for two different vaults',
1080
+ );
1081
+ trace('removed', vault, vaultId);
1082
+ }
1083
+
1084
+ // replace in queue, but only if it can accrue interest or be liquidated (i.e. has debt).
1085
+ // getCurrentDebt() would also work (0x = 0) but require more computation.
1086
+ if (!AmountMath.isEmpty(vault.getNormalizedDebt())) {
1087
+ prioritizedVaults.addVault(vaultId, vault);
1088
+ }
1089
+
1090
+ // total += vault's delta (post — pre)
1091
+ state.totalCollateral = AmountMath.subtract(
1092
+ AmountMath.add(state.totalCollateral, vault.getCollateralAmount()),
1093
+ oldCollateral,
1094
+ );
1095
+ state.totalDebt = AmountMath.subtract(
1096
+ AmountMath.add(state.totalDebt, vault.getCurrentDebt()),
1097
+ oldDebtNormalized,
1098
+ );
1099
+ void facets.helper.writeMetrics();
1100
+ },
1101
+ },
1102
+ self: {
1103
+ getGovernedParams() {
1104
+ const { collateralBrand } = this.state;
1105
+ return factoryPowers.getGovernedParams(collateralBrand);
1106
+ },
1107
+
1108
+ /**
1109
+ * @param {ZCFSeat} seat
1110
+ */
1111
+ async makeVaultKit(seat) {
1112
+ const {
1113
+ state,
1114
+ facets: { manager },
1115
+ } = this;
1116
+ trace(state.collateralBrand, 'makeVaultKit');
1117
+ const { storageNode } = this.state;
1118
+ assert(marshaller, 'makeVaultKit missing marshaller');
1119
+ assert(storageNode, 'makeVaultKit missing storageNode');
1120
+ assert(zcf, 'makeVaultKit missing zcf');
1121
+
1122
+ const vaultId = String(state.vaultCounter);
1123
+
1124
+ // must be a presence to be stored in vault state
1125
+ const vaultStorageNode = await E(
1126
+ E(storageNode).makeChildNode(`vaults`),
1127
+ ).makeChildNode(`vault${vaultId}`);
1128
+
1129
+ const { self: vault } = makeVault(manager, vaultId, vaultStorageNode);
1130
+ trace(state.collateralBrand, 'makeVaultKit made vault', vault);
1131
+
1132
+ try {
1133
+ // TODO `await` is allowed until the above ordering is fixed
1134
+ // eslint-disable-next-line @jessie.js/no-nested-await
1135
+ const vaultKit = await vault.initVaultKit(seat, vaultStorageNode);
1136
+ // initVaultKit calls back to handleBalanceChange() which will add the
1137
+ // vault to prioritizedVaults
1138
+
1139
+ // initVaultKit doesn't write to the storage node until it's returning
1140
+ // so if it returned then we know the node key was consumed
1141
+ state.vaultCounter += 1;
1142
+
1143
+ return vaultKit;
1144
+ } catch (err) {
1145
+ console.error(
1146
+ 'attempting recovery after initVaultKit failure',
1147
+ err,
1148
+ );
1149
+ // ??? do we still need this cleanup? it won't get into the store unless it has collateral,
1150
+ // which should qualify it to be in the store. If we drop this catch then the nested await
1151
+ // for `vault.initVaultKit()` goes away.
1152
+
1153
+ // remove it from the store if it got in
1154
+ /** @type {NormalizedDebt} */
1155
+ // @ts-expect-error cast
1156
+ const normalizedDebt = AmountMath.makeEmpty(state.debtBrand);
1157
+ const collateralPre = seat.getCurrentAllocation().Collateral;
1158
+ const { prioritizedVaults } = collateralEphemera(
1159
+ state.collateralBrand,
1160
+ );
1161
+ try {
1162
+ prioritizedVaults.removeVaultByAttributes(
1163
+ normalizedDebt,
1164
+ collateralPre,
1165
+ vaultId,
1166
+ );
1167
+ console.warn(
1168
+ 'removed vault',
1169
+ vaultId,
1170
+ 'after initVaultKit failure',
1171
+ );
1172
+ } catch {
1173
+ console.info(
1174
+ 'vault',
1175
+ vaultId,
1176
+ 'never stored during initVaultKit failure',
1177
+ );
1178
+ }
1179
+ throw err;
1180
+ } finally {
1181
+ if (!seat.hasExited()) {
1182
+ seat.exit();
1183
+ }
1184
+ }
1185
+ },
1186
+
1187
+ getCollateralQuote() {
1188
+ const { storedCollateralQuote } = collateralEphemera(
1189
+ this.state.collateralBrand,
1190
+ );
1191
+ if (!storedCollateralQuote)
1192
+ throw Fail`getCollateralQuote called before a collateral quote was available`;
1193
+ return storedCollateralQuote;
1194
+ },
1195
+
1196
+ getPublicFacet() {
1197
+ return this.facets.collateral;
1198
+ },
1199
+
1200
+ lockOraclePrices() {
1201
+ const { state } = this;
1202
+ const { storedCollateralQuote } = collateralEphemera(
1203
+ state.collateralBrand,
1204
+ );
1205
+ if (!storedCollateralQuote)
1206
+ throw Fail`lockOraclePrices called before a collateral quote was available`;
1207
+ trace(
1208
+ `lockPrice`,
1209
+ getAmountIn(storedCollateralQuote),
1210
+ getAmountOut(storedCollateralQuote),
1211
+ );
749
1212
 
750
- const collateralBehavior = {
751
- /** @param {MethodContext} context */
752
- makeVaultInvitation: ({ state, facets: { self } }) => {
753
- const { zcf } = provideEphemera(state.collateralBrand);
754
- assert(zcf);
755
- return zcf.makeInvitation(seat => self.makeVaultKit(seat), 'MakeVault');
756
- },
757
- /** @param {MethodContext} context */
758
- getSubscriber: ({ state }) => {
759
- const { assetSubscriber } = provideEphemera(state.collateralBrand);
760
- assert(assetSubscriber);
761
- return assetSubscriber;
762
- },
763
- /** @param {MethodContext} context */
764
- getMetrics: ({ state }) => {
765
- const { metricsSubscription } = provideEphemera(state.collateralBrand);
766
- assert(metricsSubscription);
767
- return metricsSubscription;
768
- },
769
- /** @param {MethodContext} context */
770
- getCompoundedInterest: ({ state }) => state.compoundedInterest,
771
- };
1213
+ state.lockedQuote = storedCollateralQuote;
1214
+ return storedCollateralQuote;
1215
+ },
1216
+ /**
1217
+ * @param {AuctioneerPublicFacet} auctionPF
1218
+ */
1219
+ async liquidateVaults(auctionPF) {
1220
+ const { state, facets } = this;
1221
+ const { self, helper } = facets;
1222
+ const {
1223
+ collateralBrand,
1224
+ compoundedInterest,
1225
+ debtBrand,
1226
+ liquidatingVaults,
1227
+ lockedQuote,
1228
+ } = state;
1229
+ trace(collateralBrand, 'considering liquidation');
1230
+
1231
+ const { prioritizedVaults } = collateralEphemera(collateralBrand);
1232
+ assert(factoryPowers && prioritizedVaults && zcf);
1233
+ lockedQuote ||
1234
+ Fail`Must have locked a quote before liquidating vaults.`;
1235
+ assert(lockedQuote); // redundant with previous line
1236
+
1237
+ const liqMargin = self.getGovernedParams().getLiquidationMargin();
1238
+
1239
+ const { totalDebt, totalCollateral, vaultData, liqSeat } =
1240
+ getLiquidatableVaults(
1241
+ zcf,
1242
+ {
1243
+ quote: lockedQuote,
1244
+ interest: compoundedInterest,
1245
+ margin: liqMargin,
1246
+ },
1247
+ prioritizedVaults,
1248
+ liquidatingVaults,
1249
+ debtBrand,
1250
+ collateralBrand,
1251
+ );
1252
+ // reset lockedQuote after we've used it for the liquidation decision
1253
+ state.lockedQuote = undefined;
1254
+
1255
+ if (vaultData.getSize() === 0) {
1256
+ return;
1257
+ }
1258
+ trace(
1259
+ ' Found vaults to liquidate',
1260
+ liquidatingVaults.getSize(),
1261
+ totalCollateral,
1262
+ );
772
1263
 
773
- const selfBehavior = {
774
- /** @param {MethodContext} context */
775
- getGovernedParams: ({ state }) => {
776
- const { factoryPowers } = provideEphemera(state.collateralBrand);
777
- assert(factoryPowers);
778
- return factoryPowers.getGovernedParams();
779
- },
1264
+ helper.markLiquidating(totalDebt, totalCollateral);
1265
+ void helper.writeMetrics();
1266
+
1267
+ const { userSeatPromise, deposited } = await E.when(
1268
+ E(auctionPF).makeDepositInvitation(),
1269
+ depositInvitation =>
1270
+ offerTo(
1271
+ zcf,
1272
+ depositInvitation,
1273
+ harden({ Minted: 'Bid' }),
1274
+ harden({ give: { Collateral: totalCollateral } }),
1275
+ liqSeat,
1276
+ liqSeat,
1277
+ { goal: totalDebt },
1278
+ ),
1279
+ );
780
1280
 
781
- /**
782
- * In extreme situations, system health may require liquidating all vaults.
783
- * This starts the liquidations all in parallel.
784
- *
785
- * @param {MethodContext} context
786
- */
787
- liquidateAll: async ({ state, facets: { helper } }) => {
788
- const { prioritizedVaults } = provideEphemera(state.collateralBrand);
789
- assert(prioritizedVaults);
790
- const toLiquidate = Array.from(prioritizedVaults.entries()).map(entry =>
791
- helper.liquidateAndRemove(entry),
792
- );
793
- await Promise.all(toLiquidate);
794
- },
1281
+ // This is expected to wait for the duration of the auction, which
1282
+ // is controlled by the auction parameters startFrequency, clockStep,
1283
+ // and the difference between startingRate and lowestRate.
1284
+ const [proceeds] = await Promise.all([deposited, userSeatPromise]);
795
1285
 
796
- /**
797
- * @param {MethodContext} context
798
- * @param {ZCFSeat} seat
799
- */
800
- makeVaultKit: async ({ state, facets: { manager } }, seat) => {
801
- const { marshaller, prioritizedVaults, storageNode, zcf } = provideEphemera(
802
- state.collateralBrand,
803
- );
804
- assert(marshaller, 'makeVaultKit missing marshaller');
805
- assert(prioritizedVaults, 'makeVaultKit missing prioritizedVaults');
806
- assert(storageNode, 'makeVaultKit missing storageNode');
807
- assert(zcf, 'makeVaultKit missing zcf');
808
- assertProposalShape(seat, {
809
- give: { Collateral: null },
810
- want: { Minted: null },
811
- });
1286
+ const { storedCollateralQuote } = collateralEphemera(
1287
+ this.state.collateralBrand,
1288
+ );
812
1289
 
813
- state.vaultCounter += 1;
814
- const vaultId = String(state.vaultCounter);
815
-
816
- const vaultStorageNode = E(
817
- E(storageNode).makeChildNode(`vaults`),
818
- ).makeChildNode(`vault${vaultId}`);
819
-
820
- const vault = makeVault(
821
- zcf,
822
- manager,
823
- vaultId,
824
- vaultStorageNode,
825
- marshaller,
826
- );
827
-
828
- try {
829
- // TODO `await` is allowed until the above ordering is fixed
830
- // eslint-disable-next-line @jessie.js/no-nested-await
831
- const vaultKit = await vault.initVaultKit(
832
- seat,
833
- vaultStorageNode,
834
- marshaller,
835
- );
836
- // initVaultKit calls back to handleBalanceChange() which will add the
837
- // vault to prioritizedVaults
838
- seat.exit();
839
- return vaultKit;
840
- } catch (err) {
841
- // ??? do we still need this cleanup? it won't get into the store unless it has collateral,
842
- // which should qualify it to be in the store. If we drop this catch then the nested await
843
- // for `vault.initVaultKit()` goes away.
844
-
845
- // remove it from the store if it got in
846
- /** @type {NormalizedDebt} */
847
- // @ts-expect-error cast
848
- const normalizedDebt = AmountMath.makeEmpty(state.debtBrand);
849
- const collateralPre = seat.getCurrentAllocation().Collateral;
850
- try {
851
- prioritizedVaults.removeVaultByAttributes(
852
- normalizedDebt,
853
- collateralPre,
854
- vaultId,
855
- );
856
- console.error('removed vault', vaultId, 'after initVaultKit failure');
857
- } catch {
858
- console.error(
859
- 'vault',
860
- vaultId,
861
- 'never stored during initVaultKit failure',
862
- );
863
- }
864
- throw err;
865
- }
866
- },
1290
+ trace(`LiqV after long wait`, proceeds);
1291
+ return helper.distributeProceeds(
1292
+ proceeds,
1293
+ totalDebt,
1294
+ storedCollateralQuote,
1295
+ liqSeat,
1296
+ vaultData,
1297
+ totalCollateral,
1298
+ );
1299
+ },
1300
+ },
1301
+ },
867
1302
 
868
- /**
869
- *
870
- * @param {MethodContext} param
871
- * @param {Installation} liquidationInstall
872
- * @param {object} liquidationTerms
873
- */
874
- setupLiquidator: async (
875
- { state, facets },
876
- liquidationInstall,
877
- liquidationTerms,
878
- ) => {
879
- const { zcf } = provideEphemera(state.collateralBrand);
880
- assert(zcf);
881
- const { debtBrand, collateralBrand } = state;
882
- const { ammPublicFacet, priceAuthority, reservePublicFacet, timerService } =
883
- zcf.getTerms();
884
- const zoe = zcf.getZoeService();
885
- const collateralIssuer = zcf.getIssuerForBrand(collateralBrand);
886
- const debtIssuer = zcf.getIssuerForBrand(debtBrand);
887
- trace('setup liquidator', state.collateralBrand, {
888
- debtBrand,
889
- debtIssuer,
890
- collateralBrand,
891
- liquidationTerms,
892
- });
893
- const { creatorFacet, instance } = await E(zoe).startInstance(
894
- liquidationInstall,
895
- harden({ Minted: debtIssuer, Collateral: collateralIssuer }),
896
- harden({
897
- ...liquidationTerms,
898
- amm: ammPublicFacet,
899
- debtBrand,
900
- reservePublicFacet,
901
- priceAuthority,
902
- timerService,
903
- }),
904
- );
905
- trace('setup liquidator complete', state.collateralBrand, {
906
- instance,
907
- old: state.liquidatorInstance,
908
- equal: state.liquidatorInstance === instance,
909
- });
910
- state.liquidatorInstance = instance;
911
- state.liquidator = creatorFacet;
912
- facets.helper.assetNotify();
913
- },
914
-
915
- /** @param {MethodContext} context */
916
- getCollateralQuote: async ({ state }) => {
917
- const { priceAuthority } = provideEphemera(state.collateralBrand);
918
- assert(priceAuthority);
919
-
920
- const { debtBrand } = state;
921
- // get a quote for one unit of the collateral
922
- const collateralUnit = await unitAmount(state.collateralBrand);
923
- return E(priceAuthority).quoteGiven(collateralUnit, debtBrand);
924
- },
925
-
926
- /** @param {MethodContext} context */
927
- getPublicFacet: ({ facets }) => facets.collateral,
928
- };
1303
+ {
1304
+ finish: ({ state, facets: { helper } }) => {
1305
+ helper.start();
1306
+ void state.assetTopicKit.recorder.write(
1307
+ harden({
1308
+ compoundedInterest: state.compoundedInterest,
1309
+ interestRate: factoryPowers
1310
+ .getGovernedParams(state.collateralBrand)
1311
+ .getInterestRate(),
1312
+ latestInterestUpdate: state.latestInterestUpdate,
1313
+ }),
1314
+ );
929
1315
 
930
- /** @param {MethodContext} context */
931
- const finish = ({ state, facets: { helper } }) => {
932
- const { periodNotifier, prioritizedVaults, zcf } = provideEphemera(
933
- state.collateralBrand,
934
- );
935
- assert(periodNotifier && prioritizedVaults && zcf);
936
-
937
- prioritizedVaults.onHigherHighest(() => helper.reschedulePriceCheck());
938
-
939
- // push initial state of metrics
940
- helper.updateMetrics();
941
-
942
- void observeNotifier(periodNotifier, {
943
- updateState: updateTime =>
944
- helper
945
- .chargeAllVaults(updateTime, state.poolIncrementSeat)
946
- .catch(e =>
947
- console.error('🚨 vaultManager failed to charge interest', e),
948
- ),
949
- fail: reason => {
950
- zcf.shutdownWithFailure(
951
- assert.error(X`Unable to continue without a timer: ${reason}`),
952
- );
953
- },
954
- finish: done => {
955
- zcf.shutdownWithFailure(
956
- assert.error(X`Unable to continue without a timer: ${done}`),
957
- );
1316
+ // push initial state of metrics
1317
+ void helper.writeMetrics();
1318
+ },
958
1319
  },
959
- });
960
- };
1320
+ );
961
1321
 
962
- const behavior = {
963
- collateral: collateralBehavior,
964
- helper: helperBehavior,
965
- manager: managerBehavior,
966
- self: selfBehavior,
1322
+ /** @param {Omit<Parameters<typeof makeVaultManagerKitInternal>[0], 'metricsStorageNode'>} externalParams */
1323
+ const makeVaultManagerKit = async externalParams => {
1324
+ const metricsStorageNode = await E(
1325
+ externalParams.storageNode,
1326
+ ).makeChildNode('metrics');
1327
+ return makeVaultManagerKitInternal({
1328
+ ...externalParams,
1329
+ metricsStorageNode,
1330
+ });
1331
+ };
1332
+ return makeVaultManagerKit;
967
1333
  };
968
1334
 
969
- const makeVaultManagerKit = defineDurableKindMulti(
970
- makeKindHandle('VaultManagerKit'),
971
- initState,
972
- behavior,
973
- {
974
- finish,
975
- },
976
- );
977
-
978
1335
  /**
1336
+ * @typedef {Awaited<ReturnType<ReturnType<typeof prepareVaultManagerKit>>>} VaultManagerKit
1337
+ */
1338
+ /**
1339
+ * @typedef {VaultManagerKit['self']} VaultManager
979
1340
  * Each VaultManager manages a single collateral type.
980
1341
  *
981
- * It manages some number of outstanding loans, each called a Vault, for which
982
- * the collateral is provided in exchange for borrowed Minted.
1342
+ * It manages some number of outstanding debt positions, each called a Vault,
1343
+ * for which the collateral is provided in exchange for borrowed Minted.
1344
+ */
1345
+ /** @typedef {VaultManagerKit['collateral']} CollateralManager */
1346
+
1347
+ /**
1348
+ * Support restarting kits from baggage and mutating the array holding them
983
1349
  *
984
- * @param {ZCF} zcf
985
- * @param {ZCFMint<'nat'>} debtMint
986
- * @param {Brand} collateralBrand
987
- * @param {ERef<PriceAuthority>} priceAuthority
988
- * @param {import('./vaultDirector.js').FactoryPowersFacet} factoryPowers
989
- * @param {ERef<TimerService>} timerService
990
- * @param {Timestamp} startTimeStamp
1350
+ * @param {import('@agoric/vat-data').Baggage} baggage
991
1351
  */
992
- export const makeVaultManager = pickFacet(makeVaultManagerKit, 'self');
1352
+ export const provideAndStartVaultManagerKits = baggage => {
1353
+ trace('provideAndStartVaultManagerKits start');
1354
+ const key = 'vaultManagerKits';
1355
+
1356
+ const noKits = /** @type {VaultManagerKit[]} */ (harden([]));
993
1357
 
994
- /** @typedef {ReturnType<typeof makeVaultManagerKit>['manager']} VaultKitManager */
995
- /** @typedef {ReturnType<typeof makeVaultManager>} VaultManager */
996
- /** @typedef {ReturnType<VaultManager['getPublicFacet']>} CollateralManager */
1358
+ for (const kit of provide(baggage, key, () => noKits)) {
1359
+ kit.helper.start();
1360
+ }
1361
+
1362
+ trace('provideAndStartVaultManagerKits returning');
1363
+ return {
1364
+ /** @type {(kit: VaultManagerKit) => void} */
1365
+ add: kit => {
1366
+ appendToStoredArray(baggage, key, kit);
1367
+ },
1368
+ /** @type {(index: number) => VaultManagerKit} */
1369
+ get: index => {
1370
+ const kits = baggage.get(key);
1371
+ index < kits.length || Fail`no VaultManagerKit at index ${index}`;
1372
+ return kits[index];
1373
+ },
1374
+ length: () => baggage.get(key).length,
1375
+ };
1376
+ };
1377
+ harden(provideAndStartVaultManagerKits);