@agoric/inter-protocol 0.13.2-dev-8f9e16b.0 → 0.13.2-dev-8ed2bf6.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 (31) hide show
  1. package/bundles/bundle-amm-meta.js +127 -127
  2. package/bundles/bundle-amm.js +1 -1
  3. package/bundles/bundle-econCommitteeCharter-meta.js +70 -70
  4. package/bundles/bundle-econCommitteeCharter.js +1 -1
  5. package/bundles/bundle-feeDistributor-meta.js +53 -53
  6. package/bundles/bundle-feeDistributor.js +1 -1
  7. package/bundles/bundle-interchainPool-meta.js +103 -103
  8. package/bundles/bundle-interchainPool.js +1 -1
  9. package/bundles/bundle-liquidateIncrementally-meta.js +68 -68
  10. package/bundles/bundle-liquidateIncrementally.js +1 -1
  11. package/bundles/bundle-liquidateMinimum-meta.js +73 -73
  12. package/bundles/bundle-liquidateMinimum.js +1 -1
  13. package/bundles/bundle-priceAggregatorChainlink-meta.js +72 -72
  14. package/bundles/bundle-priceAggregatorChainlink.js +1 -1
  15. package/bundles/bundle-psm-meta.js +106 -106
  16. package/bundles/bundle-psm.js +1 -1
  17. package/bundles/bundle-reserve-meta.js +180 -180
  18. package/bundles/bundle-reserve.js +1 -1
  19. package/bundles/bundle-scaledPriceAuthority-meta.js +68 -68
  20. package/bundles/bundle-scaledPriceAuthority.js +1 -1
  21. package/bundles/bundle-stakeFactory-meta.js +114 -114
  22. package/bundles/bundle-stakeFactory.js +1 -1
  23. package/bundles/bundle-vaultFactory-meta.js +123 -123
  24. package/bundles/bundle-vaultFactory.js +1 -1
  25. package/package.json +15 -15
  26. package/src/contractSupport.js +42 -1
  27. package/src/vaultFactory/vault.js +619 -605
  28. package/src/vaultFactory/vaultDirector.js +42 -15
  29. package/src/vaultFactory/vaultHolder.js +103 -93
  30. package/src/vaultFactory/vaultKit.js +31 -32
  31. package/src/vaultFactory/vaultManager.js +853 -829
@@ -16,41 +16,36 @@ import '@agoric/zoe/exported.js';
16
16
 
17
17
  import { AmountMath, AmountShape, BrandShape, RatioShape } from '@agoric/ertp';
18
18
  import {
19
- makeStoredPublishKit,
19
+ makeStoredSubscriber,
20
20
  observeNotifier,
21
21
  SubscriberShape,
22
+ vivifyDurablePublishKit,
22
23
  } from '@agoric/notifier';
23
24
  import {
24
- defineDurableFarClassKit,
25
25
  M,
26
- makeKindHandle,
27
26
  makeScalarBigMapStore,
28
27
  makeScalarBigSetStore,
29
- pickFacet,
28
+ vivifyFarClassKit,
30
29
  } from '@agoric/vat-data';
31
30
  import {
32
31
  assertProposalShape,
32
+ atomicTransfer,
33
33
  ceilDivideBy,
34
34
  floorDivideBy,
35
35
  getAmountIn,
36
36
  getAmountOut,
37
37
  makeRatio,
38
38
  makeRatioFromAmounts,
39
- atomicTransfer,
40
39
  } from '@agoric/zoe/src/contractSupport/index.js';
41
- import { E } from '@endo/eventual-send';
42
40
  import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js';
43
41
  import { InstallationShape, SeatShape } from '@agoric/zoe/src/typeGuards.js';
44
- import {
45
- checkDebtLimit,
46
- makeEphemeraProvider,
47
- makeMetricsPublisherKit,
48
- } from '../contractSupport.js';
42
+ import { E } from '@endo/eventual-send';
43
+ import { checkDebtLimit, makeEphemeraProvider } from '../contractSupport.js';
49
44
  import { chargeInterest } from '../interest.js';
50
45
  import { makeTracer } from '../makeTracer.js';
51
46
  import { liquidate, makeQuote, updateQuote } from './liquidation.js';
52
47
  import { makePrioritizedVaults } from './prioritizedVaults.js';
53
- import { makeVault, Phase } from './vault.js';
48
+ import { Phase, vivifyVault } from './vault.js';
54
49
 
55
50
  const { details: X } = assert;
56
51
 
@@ -97,9 +92,13 @@ const trace = makeTracer('VM', false);
97
92
 
98
93
  /**
99
94
  * @typedef {Readonly<{
95
+ * assetSubscriber: Subscriber<AssetState>,
96
+ * assetPublisher: Publisher<AssetState>,
100
97
  * collateralBrand: Brand<'nat'>,
101
98
  * debtBrand: Brand<'nat'>,
102
99
  * debtMint: ZCFMint<'nat'>,
100
+ * metricsPublisher: Publisher<MetricsNotification>,
101
+ * metricsSubscriber: Subscriber<MetricsNotification>,
103
102
  * poolIncrementSeat: ZCFSeat,
104
103
  * retainedCollateralSeat: ZCFSeat,
105
104
  * unsettledVaults: MapStore<string, Vault>,
@@ -135,8 +134,6 @@ const trace = makeTracer('VM', false);
135
134
  * using a proxy for the manager object, collateralBrand.
136
135
  *
137
136
  * @type {(collateralBrand: Brand) => Partial<{
138
- * assetSubscriber: Subscriber<AssetState>,
139
- * assetPublisher: Publisher<AssetState>,
140
137
  * factoryPowers: import('./vaultDirector.js').FactoryPowersFacet?,
141
138
  * liquidationQueueing: boolean,
142
139
  * outstandingQuote: Promise<MutableQuote>?,
@@ -144,8 +141,8 @@ const trace = makeTracer('VM', false);
144
141
  * periodNotifier: ERef<Notifier<Timestamp>>,
145
142
  * priceAuthority: ERef<PriceAuthority>,
146
143
  * prioritizedVaults: ReturnType<typeof makePrioritizedVaults>,
147
- * metricsPublication: IterationObserver<MetricsNotification>,
148
- * metricsSubscription: StoredSubscription<MetricsNotification>,
144
+ * storedAssetSubscriber: StoredSubscriber<AssetState>,
145
+ * storedMetricsSubscriber: StoredSubscriber<MetricsNotification>,
149
146
  * storageNode: ERef<StorageNode>,
150
147
  * zcf: import('./vaultFactory.js').VaultFactoryZCF,
151
148
  * }>} */
@@ -153,127 +150,6 @@ const provideEphemera = makeEphemeraProvider(() => ({
153
150
  liquidationQueueing: false,
154
151
  }));
155
152
 
156
- /**
157
- * Create state for the Vault Manager kind
158
- *
159
- * @param {import('./vaultFactory.js').VaultFactoryZCF} zcf
160
- * @param {ZCFMint<'nat'>} debtMint
161
- * @param {Brand<'nat'>} collateralBrand
162
- * @param {ERef<PriceAuthority>} priceAuthority
163
- * @param {import('./vaultDirector.js').FactoryPowersFacet} factoryPowers
164
- * @param {ERef<TimerService>} timerService
165
- * @param {Timestamp} startTimeStamp
166
- * @param {ERef<StorageNode>} storageNode
167
- * @param {ERef<Marshaller>} marshaller
168
- */
169
- const initState = (
170
- zcf,
171
- debtMint,
172
- collateralBrand,
173
- priceAuthority,
174
- factoryPowers,
175
- timerService,
176
- startTimeStamp,
177
- storageNode,
178
- marshaller,
179
- ) => {
180
- assert(
181
- storageNode && marshaller,
182
- 'VaultManager missing storageNode or marshaller',
183
- );
184
-
185
- const periodNotifier = E(timerService).makeNotifier(
186
- 0n,
187
- factoryPowers.getGovernedParams().getChargingPeriod(),
188
- );
189
-
190
- const debtBrand = debtMint.getIssuerRecord().brand;
191
- const zeroCollateral = AmountMath.makeEmpty(collateralBrand, 'nat');
192
- const zeroDebt = AmountMath.makeEmpty(debtBrand, 'nat');
193
-
194
- const { metricsPublication, metricsSubscription } = makeMetricsPublisherKit(
195
- storageNode,
196
- marshaller,
197
- );
198
-
199
- /** @type {MapStore<string, Vault>} */
200
- const unsettledVaults = makeScalarBigMapStore('orderedVaultStore', {
201
- durable: true,
202
- });
203
-
204
- /**
205
- * If things are going well, the set will contain at most one Vault. Otherwise
206
- * failures remain and are available to be repaired via contract upgrade.
207
- *
208
- * @type {SetStore<Vault>}
209
- */
210
- const liquidatingVaults = makeScalarBigSetStore('liquidatingVaults', {
211
- durable: true,
212
- });
213
-
214
- /** @type {ImmutableState} */
215
- const fixed = {
216
- collateralBrand,
217
- debtBrand,
218
- debtMint,
219
- poolIncrementSeat: zcf.makeEmptySeatKit().zcfSeat,
220
- retainedCollateralSeat: zcf.makeEmptySeatKit().zcfSeat,
221
- unsettledVaults,
222
- liquidatingVaults,
223
- };
224
-
225
- const compoundedInterest = makeRatio(100n, fixed.debtBrand); // starts at 1.0, no interest
226
- // timestamp of most recent update to interest
227
- const latestInterestUpdate = startTimeStamp;
228
-
229
- /** @type {PublishKit<AssetState>} */
230
- const { publisher: assetPublisher, subscriber: assetSubscriber } =
231
- makeStoredPublishKit(storageNode, marshaller);
232
-
233
- assetPublisher.publish(
234
- harden({
235
- compoundedInterest,
236
- interestRate: factoryPowers.getGovernedParams().getInterestRate(),
237
- latestInterestUpdate,
238
- }),
239
- );
240
-
241
- const ephemera = provideEphemera(collateralBrand);
242
- Object.assign(ephemera, {
243
- assetSubscriber,
244
- assetPublisher,
245
- factoryPowers,
246
- marshaller,
247
- metricsPublication,
248
- metricsSubscription,
249
- periodNotifier,
250
- priceAuthority,
251
- prioritizedVaults: makePrioritizedVaults(unsettledVaults),
252
- storageNode,
253
- zcf,
254
- });
255
-
256
- /** @type {MutableState & ImmutableState} */
257
- const state = {
258
- ...fixed,
259
- compoundedInterest,
260
- debtBrand: fixed.debtBrand,
261
- latestInterestUpdate,
262
- liquidator: undefined,
263
- liquidatorInstance: undefined,
264
- numLiquidationsCompleted: 0,
265
- totalCollateral: zeroCollateral,
266
- totalDebt: zeroDebt,
267
- totalOverageReceived: zeroDebt,
268
- totalProceedsReceived: zeroDebt,
269
- totalCollateralSold: zeroCollateral,
270
- totalShortfallReceived: zeroDebt,
271
- vaultCounter: 0,
272
- };
273
-
274
- return state;
275
- };
276
-
277
153
  // XXX type error when destructuring params
278
154
  const finish = context => {
279
155
  const {
@@ -310,738 +186,886 @@ const finish = context => {
310
186
  });
311
187
  };
312
188
 
313
- const makeVaultManagerKit = defineDurableFarClassKit(
314
- makeKindHandle('VaultManagerKit'),
315
- {
316
- collateral: M.interface('collateral', {
317
- makeVaultInvitation: M.call().returns(M.promise()),
318
- getSubscriber: M.call().returns(SubscriberShape),
319
- getMetrics: M.call().returns(SubscriberShape),
320
- getCompoundedInterest: M.call().returns(RatioShape),
321
- }),
322
- helper: M.interface(
323
- 'helper',
324
- // not exposed so sloppy okay
325
- {},
326
- { sloppy: true },
327
- ),
328
- manager: M.interface('manager', {
329
- getGovernedParams: M.call().returns(M.remotable()),
330
- maxDebtFor: M.call(AmountShape).returns(M.promise()),
331
- mintAndReallocate: M.call(AmountShape, AmountShape, SeatShape)
332
- .rest()
333
- .returns(),
334
- burnAndRecord: M.call(AmountShape, SeatShape).returns(),
335
- getAssetSubscriber: M.call().returns(SubscriberShape),
336
- getCollateralBrand: M.call().returns(BrandShape),
337
- getDebtBrand: M.call().returns(BrandShape),
338
- getCompoundedInterest: M.call().returns(RatioShape),
339
- handleBalanceChange: M.call(
340
- AmountShape,
341
- AmountShape,
342
- M.string(),
343
- M.string(),
344
- M.remotable(),
345
- ).returns(),
346
- }),
347
- self: M.interface('self', {
348
- getGovernedParams: M.call().returns(M.remotable()),
349
- liquidateAll: M.call().returns(M.promise()),
350
- makeVaultKit: M.call(SeatShape).returns(M.promise()),
351
- setupLiquidator: M.call(InstallationShape, M.record()).returns(
352
- M.promise(),
189
+ // TODO move params of initState here because it's a singleton
190
+ // and remove from State what doesn't need to be stored between upgrades
191
+ export const vivifyVaultManagerKit = baggage => {
192
+ const makeVault = vivifyVault(baggage);
193
+ const makeVaultManagerMetricsPublishKit = vivifyDurablePublishKit(
194
+ baggage,
195
+ 'Vault Manager metrics',
196
+ );
197
+ const makeVaultManagerAssetsPublishKit = vivifyDurablePublishKit(
198
+ baggage,
199
+ 'Vault Manager assets',
200
+ );
201
+
202
+ /**
203
+ * Create state for the Vault Manager kind
204
+ *
205
+ * @param {import('./vaultFactory.js').VaultFactoryZCF} zcf
206
+ * @param {ZCFMint<'nat'>} debtMint
207
+ * @param {Brand<'nat'>} collateralBrand
208
+ * @param {ERef<PriceAuthority>} priceAuthority
209
+ * @param {import('./vaultDirector.js').FactoryPowersFacet} factoryPowers
210
+ * @param {ERef<TimerService>} timerService
211
+ * @param {Timestamp} startTimeStamp
212
+ * @param {ERef<StorageNode>} storageNode
213
+ * @param {ERef<Marshaller>} marshaller
214
+ */
215
+ const initState = (
216
+ zcf,
217
+ debtMint,
218
+ collateralBrand,
219
+ priceAuthority,
220
+ factoryPowers,
221
+ timerService,
222
+ startTimeStamp,
223
+ storageNode,
224
+ marshaller,
225
+ ) => {
226
+ assert(
227
+ storageNode && marshaller,
228
+ 'VaultManager missing storageNode or marshaller',
229
+ );
230
+
231
+ const periodNotifier = E(timerService).makeNotifier(
232
+ 0n,
233
+ factoryPowers.getGovernedParams().getChargingPeriod(),
234
+ );
235
+
236
+ const debtBrand = debtMint.getIssuerRecord().brand;
237
+ const zeroCollateral = AmountMath.makeEmpty(collateralBrand, 'nat');
238
+ const zeroDebt = AmountMath.makeEmpty(debtBrand, 'nat');
239
+
240
+ const { publisher: metricsPublisher, subscriber: metricsSubscriber } =
241
+ makeVaultManagerMetricsPublishKit();
242
+
243
+ /** @type {PublishKit<AssetState>} */
244
+ const { publisher: assetPublisher, subscriber: assetSubscriber } =
245
+ makeVaultManagerAssetsPublishKit();
246
+
247
+ /** @type {MapStore<string, Vault>} */
248
+ const unsettledVaults = makeScalarBigMapStore('orderedVaultStore', {
249
+ durable: true,
250
+ });
251
+
252
+ /**
253
+ * If things are going well, the set will contain at most one Vault. Otherwise
254
+ * failures remain and are available to be repaired via contract upgrade.
255
+ *
256
+ * @type {SetStore<Vault>}
257
+ */
258
+ const liquidatingVaults = makeScalarBigSetStore('liquidatingVaults', {
259
+ durable: true,
260
+ });
261
+
262
+ /** @type {ImmutableState} */
263
+ const fixed = {
264
+ collateralBrand,
265
+ debtBrand,
266
+ debtMint,
267
+ poolIncrementSeat: zcf.makeEmptySeatKit().zcfSeat,
268
+ retainedCollateralSeat: zcf.makeEmptySeatKit().zcfSeat,
269
+ unsettledVaults,
270
+ liquidatingVaults,
271
+ assetSubscriber,
272
+ assetPublisher,
273
+ metricsPublisher,
274
+ metricsSubscriber,
275
+ };
276
+
277
+ const compoundedInterest = makeRatio(100n, fixed.debtBrand); // starts at 1.0, no interest
278
+ // timestamp of most recent update to interest
279
+ const latestInterestUpdate = startTimeStamp;
280
+
281
+ assetPublisher.publish(
282
+ harden({
283
+ compoundedInterest,
284
+ interestRate: factoryPowers.getGovernedParams().getInterestRate(),
285
+ latestInterestUpdate,
286
+ }),
287
+ );
288
+
289
+ const storedMetricsSubscriber = makeStoredSubscriber(
290
+ metricsSubscriber,
291
+ E(storageNode).makeChildNode('metrics'),
292
+ marshaller,
293
+ );
294
+
295
+ const storedAssetSubscriber = makeStoredSubscriber(
296
+ assetSubscriber,
297
+ storageNode,
298
+ marshaller,
299
+ );
300
+
301
+ const ephemera = provideEphemera(collateralBrand);
302
+ Object.assign(ephemera, {
303
+ factoryPowers,
304
+ marshaller,
305
+ periodNotifier,
306
+ priceAuthority,
307
+ prioritizedVaults: makePrioritizedVaults(unsettledVaults),
308
+ storedAssetSubscriber,
309
+ storedMetricsSubscriber,
310
+ storageNode,
311
+ zcf,
312
+ });
313
+
314
+ /** @type {MutableState & ImmutableState} */
315
+ const state = {
316
+ ...fixed,
317
+ compoundedInterest,
318
+ debtBrand: fixed.debtBrand,
319
+ latestInterestUpdate,
320
+ liquidator: undefined,
321
+ liquidatorInstance: undefined,
322
+ numLiquidationsCompleted: 0,
323
+ totalCollateral: zeroCollateral,
324
+ totalDebt: zeroDebt,
325
+ totalOverageReceived: zeroDebt,
326
+ totalProceedsReceived: zeroDebt,
327
+ totalCollateralSold: zeroCollateral,
328
+ totalShortfallReceived: zeroDebt,
329
+ vaultCounter: 0,
330
+ };
331
+
332
+ return state;
333
+ };
334
+
335
+ // TODO find a way to not have to indent a level deeper than defineDurableFarClassKit does
336
+ return vivifyFarClassKit(
337
+ baggage,
338
+ 'VaultManagerKit',
339
+ {
340
+ collateral: M.interface('collateral', {
341
+ makeVaultInvitation: M.call().returns(M.promise()),
342
+ getSubscriber: M.call().returns(SubscriberShape),
343
+ getMetrics: M.call().returns(SubscriberShape),
344
+ getCompoundedInterest: M.call().returns(RatioShape),
345
+ }),
346
+ helper: M.interface(
347
+ 'helper',
348
+ // not exposed so sloppy okay
349
+ {},
350
+ { sloppy: true },
353
351
  ),
354
- getCollateralQuote: M.call().returns(M.promise()),
355
- getPublicFacet: M.call().returns(M.remotable()),
356
- }),
357
- },
358
- initState,
359
- {
360
- collateral: {
361
- makeVaultInvitation() {
362
- const { zcf } = provideEphemera(this.state.collateralBrand);
363
- assert(zcf);
364
- return zcf.makeInvitation(
365
- seat => this.facets.self.makeVaultKit(seat),
366
- 'MakeVault',
367
- );
368
- },
369
- getSubscriber() {
370
- const { state } = this;
371
- const { assetSubscriber } = provideEphemera(state.collateralBrand);
372
- assert(assetSubscriber);
373
- return assetSubscriber;
374
- },
375
- getMetrics() {
376
- const { state } = this;
377
- const { metricsSubscription } = provideEphemera(state.collateralBrand);
378
- assert(metricsSubscription);
379
- return metricsSubscription;
380
- },
381
- getCompoundedInterest() {
382
- return this.state.compoundedInterest;
383
- },
352
+ manager: M.interface('manager', {
353
+ getGovernedParams: M.call().returns(M.remotable()),
354
+ maxDebtFor: M.call(AmountShape).returns(M.promise()),
355
+ mintAndReallocate: M.call(AmountShape, AmountShape, SeatShape)
356
+ .rest()
357
+ .returns(),
358
+ burnAndRecord: M.call(AmountShape, SeatShape).returns(),
359
+ getAssetSubscriber: M.call().returns(SubscriberShape),
360
+ getCollateralBrand: M.call().returns(BrandShape),
361
+ getDebtBrand: M.call().returns(BrandShape),
362
+ getCompoundedInterest: M.call().returns(RatioShape),
363
+ handleBalanceChange: M.call(
364
+ AmountShape,
365
+ AmountShape,
366
+ M.string(),
367
+ M.string(),
368
+ M.remotable(),
369
+ ).returns(),
370
+ }),
371
+ self: M.interface('self', {
372
+ getGovernedParams: M.call().returns(M.remotable()),
373
+ liquidateAll: M.call().returns(M.promise()),
374
+ makeVaultKit: M.call(SeatShape).returns(M.promise()),
375
+ setupLiquidator: M.call(InstallationShape, M.record()).returns(
376
+ M.promise(),
377
+ ),
378
+ getCollateralQuote: M.call().returns(M.promise()),
379
+ getPublicFacet: M.call().returns(M.remotable()),
380
+ }),
384
381
  },
382
+ initState,
383
+ {
384
+ collateral: {
385
+ makeVaultInvitation() {
386
+ const { zcf } = provideEphemera(this.state.collateralBrand);
387
+ assert(zcf);
388
+ return zcf.makeInvitation(
389
+ seat => this.facets.self.makeVaultKit(seat),
390
+ 'MakeVault',
391
+ );
392
+ },
393
+ getSubscriber() {
394
+ const { storedAssetSubscriber } = provideEphemera(
395
+ this.state.collateralBrand,
396
+ );
397
+ assert(storedAssetSubscriber);
398
+ return storedAssetSubscriber;
399
+ },
400
+ getMetrics() {
401
+ const { storedMetricsSubscriber } = provideEphemera(
402
+ this.state.collateralBrand,
403
+ );
404
+ assert(storedMetricsSubscriber);
405
+ return storedMetricsSubscriber;
406
+ },
407
+ getCompoundedInterest() {
408
+ return this.state.compoundedInterest;
409
+ },
410
+ },
385
411
 
386
- // Some of these could go in closures but are kept on a facet anticipating future durability options.
387
- helper: {
388
- /**
389
- * @param {Timestamp} updateTime
390
- * @param {ZCFSeat} poolIncrementSeat
391
- */
392
- async chargeAllVaults(updateTime, poolIncrementSeat) {
393
- const { state, facets } = this;
394
- trace('chargeAllVaults', state.collateralBrand, {
395
- updateTime,
396
- });
397
- const { factoryPowers } = provideEphemera(state.collateralBrand);
398
- assert(factoryPowers);
399
-
400
- const interestRate = factoryPowers
401
- .getGovernedParams()
402
- .getInterestRate();
403
-
404
- // Update state with the results of charging interest
405
-
406
- const changes = chargeInterest(
407
- {
408
- mint: state.debtMint,
409
- mintAndReallocateWithFee: factoryPowers.mintAndReallocate,
410
- poolIncrementSeat,
411
- seatAllocationKeyword: 'Minted',
412
- },
413
- {
412
+ // Some of these could go in closures but are kept on a facet anticipating future durability options.
413
+ helper: {
414
+ /**
415
+ * @param {Timestamp} updateTime
416
+ * @param {ZCFSeat} poolIncrementSeat
417
+ */
418
+ async chargeAllVaults(updateTime, poolIncrementSeat) {
419
+ const { state, facets } = this;
420
+ trace('chargeAllVaults', state.collateralBrand, {
421
+ updateTime,
422
+ });
423
+ const { factoryPowers } = provideEphemera(state.collateralBrand);
424
+ assert(factoryPowers);
425
+
426
+ const interestRate = factoryPowers
427
+ .getGovernedParams()
428
+ .getInterestRate();
429
+
430
+ // Update state with the results of charging interest
431
+
432
+ const changes = chargeInterest(
433
+ {
434
+ mint: state.debtMint,
435
+ mintAndReallocateWithFee: factoryPowers.mintAndReallocate,
436
+ poolIncrementSeat,
437
+ seatAllocationKeyword: 'Minted',
438
+ },
439
+ {
440
+ interestRate,
441
+ chargingPeriod: factoryPowers
442
+ .getGovernedParams()
443
+ .getChargingPeriod(),
444
+ recordingPeriod: factoryPowers
445
+ .getGovernedParams()
446
+ .getRecordingPeriod(),
447
+ },
448
+ {
449
+ latestInterestUpdate: state.latestInterestUpdate,
450
+ compoundedInterest: state.compoundedInterest,
451
+ totalDebt: state.totalDebt,
452
+ },
453
+ updateTime,
454
+ );
455
+
456
+ state.compoundedInterest = changes.compoundedInterest;
457
+ state.latestInterestUpdate = changes.latestInterestUpdate;
458
+ state.totalDebt = changes.totalDebt;
459
+
460
+ facets.helper.assetNotify();
461
+ trace('chargeAllVaults complete', state.collateralBrand);
462
+ // price to check against has changed
463
+ return facets.helper.reschedulePriceCheck();
464
+ },
465
+
466
+ assetNotify() {
467
+ const { state } = this;
468
+ const ephemera = provideEphemera(state.collateralBrand);
469
+ assert(ephemera.factoryPowers);
470
+ const interestRate = ephemera.factoryPowers
471
+ .getGovernedParams()
472
+ .getInterestRate();
473
+ /** @type {AssetState} */
474
+ const payload = harden({
475
+ compoundedInterest: state.compoundedInterest,
414
476
  interestRate,
415
- chargingPeriod: factoryPowers
416
- .getGovernedParams()
417
- .getChargingPeriod(),
418
- recordingPeriod: factoryPowers
419
- .getGovernedParams()
420
- .getRecordingPeriod(),
421
- },
422
- {
423
477
  latestInterestUpdate: state.latestInterestUpdate,
424
- compoundedInterest: state.compoundedInterest,
425
- totalDebt: state.totalDebt,
426
- },
427
- updateTime,
428
- );
429
-
430
- state.compoundedInterest = changes.compoundedInterest;
431
- state.latestInterestUpdate = changes.latestInterestUpdate;
432
- state.totalDebt = changes.totalDebt;
433
-
434
- facets.helper.assetNotify();
435
- trace('chargeAllVaults complete', state.collateralBrand);
436
- // price to check against has changed
437
- return facets.helper.reschedulePriceCheck();
438
- },
439
-
440
- assetNotify() {
441
- const { state } = this;
442
- const ephemera = provideEphemera(state.collateralBrand);
443
- assert(ephemera.factoryPowers && ephemera.assetPublisher);
444
- const interestRate = ephemera.factoryPowers
445
- .getGovernedParams()
446
- .getInterestRate();
447
- /** @type {AssetState} */
448
- const payload = harden({
449
- compoundedInterest: state.compoundedInterest,
450
- interestRate,
451
- latestInterestUpdate: state.latestInterestUpdate,
452
- // NB: the liquidator is determined by governance but the resulting
453
- // instance is a concern of the manager. The param manager knows only
454
- // about the installation and terms of the liqudation contract. We could
455
- // have another notifier for state downstream of governance changes, but
456
- // that doesn't seem to be cost-effective.
457
- liquidatorInstance: state.liquidatorInstance,
458
- });
459
- ephemera.assetPublisher.publish(payload);
460
- },
478
+ // NB: the liquidator is determined by governance but the resulting
479
+ // instance is a concern of the manager. The param manager knows only
480
+ // about the installation and terms of the liqudation contract. We could
481
+ // have another notifier for state downstream of governance changes, but
482
+ // that doesn't seem to be cost-effective.
483
+ liquidatorInstance: state.liquidatorInstance,
484
+ });
485
+ state.assetPublisher.publish(payload);
486
+ },
461
487
 
462
- updateMetrics() {
463
- const { state } = this;
464
- const { metricsPublication, prioritizedVaults } = provideEphemera(
465
- state.collateralBrand,
466
- );
467
- assert(metricsPublication && prioritizedVaults);
468
-
469
- const retainedCollateral =
470
- state.retainedCollateralSeat.getCurrentAllocation()?.Collateral ??
471
- AmountMath.makeEmpty(state.collateralBrand, 'nat');
472
- /** @type {MetricsNotification} */
473
- const payload = harden({
474
- numActiveVaults: prioritizedVaults.getCount(),
475
- numLiquidatingVaults: state.liquidatingVaults.getSize(),
476
- totalCollateral: state.totalCollateral,
477
- totalDebt: state.totalDebt,
478
- retainedCollateral,
479
-
480
- numLiquidationsCompleted: state.numLiquidationsCompleted,
481
- totalCollateralSold: state.totalCollateralSold,
482
- totalOverageReceived: state.totalOverageReceived,
483
- totalProceedsReceived: state.totalProceedsReceived,
484
- totalShortfallReceived: state.totalShortfallReceived,
485
- });
486
- metricsPublication.updateState(payload);
487
- },
488
+ updateMetrics() {
489
+ const { state } = this;
490
+ const { prioritizedVaults } = provideEphemera(state.collateralBrand);
491
+ assert(prioritizedVaults);
488
492
 
489
- /**
490
- * When any Vault's debt ratio is higher than the current high-water level,
491
- * call `reschedulePriceCheck()` to request a fresh notification from the
492
- * priceAuthority. There will be extra outstanding requests since we can't
493
- * cancel them. (https://github.com/Agoric/agoric-sdk/issues/2713).
494
- *
495
- * When the vault with the current highest debt ratio is removed or reduces
496
- * its ratio, we won't reschedule the priceAuthority requests to reduce churn.
497
- * Instead, when a priceQuote is received, we'll only reschedule if the
498
- * high-water level when the request was made matches the current high-water
499
- * level.
500
- *
501
- * @param {Ratio} [highestRatio]
502
- * @returns {Promise<void>}
503
- */
504
- async reschedulePriceCheck(highestRatio) {
505
- const { state, facets } = this;
506
- const { prioritizedVaults, ...ephemera } = provideEphemera(
507
- state.collateralBrand,
508
- );
509
- assert(ephemera.factoryPowers && prioritizedVaults);
510
- trace('reschedulePriceCheck', state.collateralBrand, ephemera);
511
- // INTERLOCK: the first time through, start the activity to wait for
512
- // and process liquidations over time.
513
- if (!ephemera.liquidationQueueing) {
514
- ephemera.liquidationQueueing = true;
515
- // eslint-disable-next-line consistent-return
516
- return facets.helper
517
- .processLiquidations()
518
- .catch(e => console.error('Liquidator failed', e))
519
- .finally(() => {
520
- ephemera.liquidationQueueing = false;
521
- });
522
- }
523
-
524
- if (!ephemera.outstandingQuote) {
525
- // the new threshold will be picked up by the next quote request
526
- return;
527
- }
528
-
529
- const highestDebtRatio =
530
- highestRatio || prioritizedVaults.highestRatio();
531
- if (!highestDebtRatio) {
532
- // if there aren't any open vaults, we don't need an outstanding RFQ.
533
- trace('no open vaults');
534
- return;
535
- }
536
-
537
- // There is already an activity processing liquidations. It may be
538
- // waiting for the oracle price to cross a threshold.
539
- // Update the current in-progress quote.
540
- const govParams = ephemera.factoryPowers.getGovernedParams();
541
- const liquidationMargin = govParams.getLiquidationMargin();
542
- // Safe to call extraneously (lightweight and idempotent)
543
- updateQuote(
544
- ephemera.outstandingQuote,
545
- highestDebtRatio,
546
- liquidationMargin,
547
- );
548
- trace('update quote', state.collateralBrand, highestDebtRatio);
549
- },
493
+ const retainedCollateral =
494
+ state.retainedCollateralSeat.getCurrentAllocation()?.Collateral ??
495
+ AmountMath.makeEmpty(state.collateralBrand, 'nat');
496
+ /** @type {MetricsNotification} */
497
+ const payload = harden({
498
+ numActiveVaults: prioritizedVaults.getCount(),
499
+ numLiquidatingVaults: state.liquidatingVaults.getSize(),
500
+ totalCollateral: state.totalCollateral,
501
+ totalDebt: state.totalDebt,
502
+ retainedCollateral,
550
503
 
551
- async processLiquidations() {
552
- const { state, facets } = this;
553
- const { prioritizedVaults, ...ephemera } = provideEphemera(
554
- state.collateralBrand,
555
- );
556
- assert(ephemera.factoryPowers && ephemera.priceAuthority);
557
- const { priceAuthority } = ephemera;
558
- const govParams = ephemera.factoryPowers.getGovernedParams();
504
+ numLiquidationsCompleted: state.numLiquidationsCompleted,
505
+ totalCollateralSold: state.totalCollateralSold,
506
+ totalOverageReceived: state.totalOverageReceived,
507
+ totalProceedsReceived: state.totalProceedsReceived,
508
+ totalShortfallReceived: state.totalShortfallReceived,
509
+ });
510
+ state.metricsPublisher.publish(payload);
511
+ },
512
+
513
+ /**
514
+ * When any Vault's debt ratio is higher than the current high-water level,
515
+ * call `reschedulePriceCheck()` to request a fresh notification from the
516
+ * priceAuthority. There will be extra outstanding requests since we can't
517
+ * cancel them. (https://github.com/Agoric/agoric-sdk/issues/2713).
518
+ *
519
+ * When the vault with the current highest debt ratio is removed or reduces
520
+ * its ratio, we won't reschedule the priceAuthority requests to reduce churn.
521
+ * Instead, when a priceQuote is received, we'll only reschedule if the
522
+ * high-water level when the request was made matches the current high-water
523
+ * level.
524
+ *
525
+ * @param {Ratio} [highestRatio]
526
+ * @returns {Promise<void>}
527
+ */
528
+ async reschedulePriceCheck(highestRatio) {
529
+ const { state, facets } = this;
530
+ const { prioritizedVaults, ...ephemera } = provideEphemera(
531
+ state.collateralBrand,
532
+ );
533
+ assert(ephemera.factoryPowers && prioritizedVaults);
534
+ trace('reschedulePriceCheck', state.collateralBrand, ephemera);
535
+ // INTERLOCK: the first time through, start the activity to wait for
536
+ // and process liquidations over time.
537
+ if (!ephemera.liquidationQueueing) {
538
+ ephemera.liquidationQueueing = true;
539
+ // eslint-disable-next-line consistent-return
540
+ return facets.helper
541
+ .processLiquidations()
542
+ .catch(e => console.error('Liquidator failed', e))
543
+ .finally(() => {
544
+ ephemera.liquidationQueueing = false;
545
+ });
546
+ }
559
547
 
560
- async function* eventualLiquidations() {
561
- assert(prioritizedVaults);
562
- while (true) {
563
- const highestDebtRatio = prioritizedVaults.highestRatio();
564
- if (!highestDebtRatio) {
565
- return;
566
- }
567
- const liquidationMargin = govParams.getLiquidationMargin();
548
+ if (!ephemera.outstandingQuote) {
549
+ // the new threshold will be picked up by the next quote request
550
+ return;
551
+ }
568
552
 
569
- // ask to be alerted when the price level falls enough that the vault
570
- // with the highest debt to collateral ratio will no longer be valued at the
571
- // liquidationMargin above its debt.
572
- ephemera.outstandingQuote = makeQuote(
573
- priceAuthority,
574
- highestDebtRatio,
575
- liquidationMargin,
576
- );
577
- trace(
578
- 'posted quote request',
579
- state.collateralBrand,
580
- highestDebtRatio,
581
- );
553
+ const highestDebtRatio =
554
+ highestRatio || prioritizedVaults.highestRatio();
555
+ if (!highestDebtRatio) {
556
+ // if there aren't any open vaults, we don't need an outstanding RFQ.
557
+ trace('no open vaults');
558
+ return;
559
+ }
582
560
 
583
- // The rest of this method will not happen until after a quote is received.
584
- // This may not happen until much later, when the market changes.
585
- // eslint-disable-next-line no-await-in-loop
586
- const quote = await E(ephemera.outstandingQuote).getPromise();
587
- ephemera.outstandingQuote = null;
588
- // When we receive a quote, we check whether the vault with the highest
589
- // ratio of debt to collateral is below the liquidationMargin, and if so,
590
- // we liquidate it. We use ceilDivide to round up because ratios above
591
- // this will be liquidated.
592
- const quoteRatioPlusMargin = makeRatioFromAmounts(
593
- ceilDivideBy(getAmountOut(quote), liquidationMargin),
594
- getAmountIn(quote),
595
- );
596
- trace('quote', state.collateralBrand, quote, quoteRatioPlusMargin);
561
+ // There is already an activity processing liquidations. It may be
562
+ // waiting for the oracle price to cross a threshold.
563
+ // Update the current in-progress quote.
564
+ const govParams = ephemera.factoryPowers.getGovernedParams();
565
+ const liquidationMargin = govParams.getLiquidationMargin();
566
+ // Safe to call extraneously (lightweight and idempotent)
567
+ updateQuote(
568
+ ephemera.outstandingQuote,
569
+ highestDebtRatio,
570
+ liquidationMargin,
571
+ );
572
+ trace('update quote', state.collateralBrand, highestDebtRatio);
573
+ },
597
574
 
598
- // Liquidate the head of the queue
599
- const [next] =
600
- prioritizedVaults.entriesPrioritizedGTE(quoteRatioPlusMargin);
601
- if (next) {
602
- yield next;
575
+ async processLiquidations() {
576
+ const { state, facets } = this;
577
+ const { prioritizedVaults, ...ephemera } = provideEphemera(
578
+ state.collateralBrand,
579
+ );
580
+ assert(ephemera.factoryPowers && ephemera.priceAuthority);
581
+ const { priceAuthority } = ephemera;
582
+ const govParams = ephemera.factoryPowers.getGovernedParams();
583
+
584
+ async function* eventualLiquidations() {
585
+ assert(prioritizedVaults);
586
+ while (true) {
587
+ const highestDebtRatio = prioritizedVaults.highestRatio();
588
+ if (!highestDebtRatio) {
589
+ return;
590
+ }
591
+ const liquidationMargin = govParams.getLiquidationMargin();
592
+
593
+ // ask to be alerted when the price level falls enough that the vault
594
+ // with the highest debt to collateral ratio will no longer be valued at the
595
+ // liquidationMargin above its debt.
596
+ ephemera.outstandingQuote = makeQuote(
597
+ priceAuthority,
598
+ highestDebtRatio,
599
+ liquidationMargin,
600
+ );
601
+ trace(
602
+ 'posted quote request',
603
+ state.collateralBrand,
604
+ highestDebtRatio,
605
+ );
606
+
607
+ // The rest of this method will not happen until after a quote is received.
608
+ // This may not happen until much later, when the market changes.
609
+ // eslint-disable-next-line no-await-in-loop, @jessie.js/no-nested-await -- loop/nesting to yield each unconditionally
610
+ const quote = await E(ephemera.outstandingQuote).getPromise();
611
+ ephemera.outstandingQuote = null;
612
+ // When we receive a quote, we check whether the vault with the highest
613
+ // ratio of debt to collateral is below the liquidationMargin, and if so,
614
+ // we liquidate it. We use ceilDivide to round up because ratios above
615
+ // this will be liquidated.
616
+ const quoteRatioPlusMargin = makeRatioFromAmounts(
617
+ ceilDivideBy(getAmountOut(quote), liquidationMargin),
618
+ getAmountIn(quote),
619
+ );
620
+ trace(
621
+ 'quote',
622
+ state.collateralBrand,
623
+ quote,
624
+ quoteRatioPlusMargin,
625
+ );
626
+
627
+ // Liquidate the head of the queue
628
+ const [next] =
629
+ prioritizedVaults.entriesPrioritizedGTE(quoteRatioPlusMargin);
630
+ if (next) {
631
+ yield next;
632
+ }
603
633
  }
604
634
  }
605
- }
606
- for await (const next of eventualLiquidations()) {
607
- await facets.helper.liquidateAndRemove(next);
608
- trace('price check liq', state.collateralBrand, next && next[0]);
609
- }
635
+ for await (const next of eventualLiquidations()) {
636
+ await facets.helper.liquidateAndRemove(next);
637
+ trace('price check liq', state.collateralBrand, next && next[0]);
638
+ }
639
+ },
640
+
641
+ /**
642
+ * @param {[key: string, vaultKit: Vault]} record
643
+ */
644
+ liquidateAndRemove([key, vault]) {
645
+ const { state, facets } = this;
646
+ const { factoryPowers, prioritizedVaults, zcf } = provideEphemera(
647
+ state.collateralBrand,
648
+ );
649
+ assert(factoryPowers && prioritizedVaults && zcf);
650
+ const vaultSeat = vault.getVaultSeat();
651
+ trace('liquidating', state.collateralBrand, vaultSeat.getProposal());
652
+
653
+ const collateralPre = vault.getCollateralAmount();
654
+
655
+ // Start liquidation (vaultState: LIQUIDATING)
656
+ const liquidator = state.liquidator;
657
+ assert(liquidator);
658
+ state.liquidatingVaults.add(vault);
659
+ prioritizedVaults.removeVault(key);
660
+
661
+ return liquidate(
662
+ zcf,
663
+ vault,
664
+ liquidator,
665
+ state.collateralBrand,
666
+ factoryPowers.getGovernedParams().getLiquidationPenalty(),
667
+ )
668
+ .then(accounting => {
669
+ facets.manager.burnAndRecord(accounting.toBurn, vaultSeat);
670
+
671
+ // current values
672
+
673
+ // Sometimes, the AMM will sell less than all the collateral. If there
674
+ // was a shortfall, the investor doesn't keep the change, so we get it.
675
+ // If there was no shortfall, the collateral is returned.
676
+ const collateralPost = vault.getCollateralAmount();
677
+ if (
678
+ !AmountMath.isEmpty(collateralPost) &&
679
+ !AmountMath.isEmpty(accounting.shortfall)
680
+ ) {
681
+ // The borrower doesn't get the excess collateral remaining when
682
+ // liquidation results in a shortfall. We currently do nothing with
683
+ // it. We could hold it until it crosses some threshold, then sell it
684
+ // to the AMM, or we could transfer it to the reserve. At least it's
685
+ // visible in the accounting.
686
+ atomicTransfer(zcf, vaultSeat, state.retainedCollateralSeat, {
687
+ Collateral: collateralPost,
688
+ });
689
+ }
690
+
691
+ // Reduce totalCollateral by collateralPre, since all the collateral was
692
+ // sold, returned to the vault owner, or held by the VaultManager.
693
+ state.totalCollateral = AmountMath.subtract(
694
+ state.totalCollateral,
695
+ collateralPre,
696
+ );
697
+ state.totalDebt = AmountMath.subtract(
698
+ state.totalDebt,
699
+ accounting.shortfall,
700
+ );
701
+
702
+ // cumulative values
703
+ state.totalProceedsReceived = AmountMath.add(
704
+ state.totalProceedsReceived,
705
+ accounting.proceeds,
706
+ );
707
+ state.totalOverageReceived = AmountMath.add(
708
+ state.totalOverageReceived,
709
+ accounting.overage,
710
+ );
711
+ state.totalShortfallReceived = AmountMath.add(
712
+ state.totalShortfallReceived,
713
+ accounting.shortfall,
714
+ );
715
+ state.liquidatingVaults.delete(vault);
716
+ trace('liquidated', state.collateralBrand);
717
+ state.numLiquidationsCompleted += 1;
718
+ facets.helper.updateMetrics();
719
+
720
+ if (!AmountMath.isEmpty(accounting.shortfall)) {
721
+ E(factoryPowers.getShortfallReporter())
722
+ .increaseLiquidationShortfall(accounting.shortfall)
723
+ .catch(reason =>
724
+ console.error(
725
+ 'liquidateAndRemove failed to increaseLiquidationShortfall',
726
+ reason,
727
+ ),
728
+ );
729
+ }
730
+ })
731
+ .catch(e => {
732
+ // XXX should notify interested parties
733
+ console.error('liquidateAndRemove failed with', e);
734
+ throw e;
735
+ });
736
+ },
610
737
  },
611
-
612
- /**
613
- * @param {[key: string, vaultKit: Vault]} record
614
- */
615
- liquidateAndRemove([key, vault]) {
616
- const { state, facets } = this;
617
- const { factoryPowers, prioritizedVaults, zcf } = provideEphemera(
618
- state.collateralBrand,
619
- );
620
- assert(factoryPowers && prioritizedVaults && zcf);
621
- const vaultSeat = vault.getVaultSeat();
622
- trace('liquidating', state.collateralBrand, vaultSeat.getProposal());
623
-
624
- const collateralPre = vault.getCollateralAmount();
625
-
626
- // Start liquidation (vaultState: LIQUIDATING)
627
- const liquidator = state.liquidator;
628
- assert(liquidator);
629
- state.liquidatingVaults.add(vault);
630
- prioritizedVaults.removeVault(key);
631
-
632
- return liquidate(
633
- zcf,
738
+ manager: {
739
+ getGovernedParams() {
740
+ const { state } = this;
741
+ const ephemera = provideEphemera(state.collateralBrand);
742
+ assert(ephemera.factoryPowers);
743
+ return ephemera.factoryPowers.getGovernedParams();
744
+ },
745
+
746
+ /**
747
+ * @param {Amount<'nat'>} collateralAmount
748
+ */
749
+ async maxDebtFor(collateralAmount) {
750
+ const { state } = this;
751
+ const { debtBrand } = state;
752
+ const { priceAuthority, ...ephemera } = provideEphemera(
753
+ state.collateralBrand,
754
+ );
755
+ assert(ephemera.factoryPowers && priceAuthority);
756
+ const quoteAmount = await E(priceAuthority).quoteGiven(
757
+ collateralAmount,
758
+ debtBrand,
759
+ );
760
+ // floorDivide because we want the debt ceiling lower
761
+ return floorDivideBy(
762
+ getAmountOut(quoteAmount),
763
+ ephemera.factoryPowers.getGovernedParams().getLiquidationMargin(),
764
+ );
765
+ },
766
+ /**
767
+ * TODO utility method to turn a callback into non-actual one
768
+ * was type {MintAndReallocate}
769
+ *
770
+ * @param {Amount<'nat'>} toMint
771
+ * @param {Amount<'nat'>} fee
772
+ * @param {ZCFSeat} seat
773
+ * @param {...ZCFSeat} otherSeats
774
+ * @returns {void}
775
+ */
776
+ mintAndReallocate(toMint, fee, seat, ...otherSeats) {
777
+ const { state } = this;
778
+ const { totalDebt } = state;
779
+ const { factoryPowers } = provideEphemera(state.collateralBrand);
780
+ assert(factoryPowers);
781
+
782
+ checkDebtLimit(
783
+ factoryPowers.getGovernedParams().getDebtLimit(),
784
+ totalDebt,
785
+ toMint,
786
+ );
787
+ factoryPowers.mintAndReallocate(toMint, fee, seat, ...otherSeats);
788
+ state.totalDebt = AmountMath.add(state.totalDebt, toMint);
789
+ },
790
+ /**
791
+ * @param {Amount<'nat'>} toBurn
792
+ * @param {ZCFSeat} seat
793
+ */
794
+ burnAndRecord(toBurn, seat) {
795
+ const { state } = this;
796
+ const { factoryPowers } = provideEphemera(state.collateralBrand);
797
+ assert(factoryPowers);
798
+ trace('burnAndRecord', state.collateralBrand, {
799
+ toBurn,
800
+ totalDebt: state.totalDebt,
801
+ });
802
+ const { burnDebt } = factoryPowers;
803
+ burnDebt(toBurn, seat);
804
+ state.totalDebt = AmountMath.subtract(state.totalDebt, toBurn);
805
+ },
806
+ getAssetSubscriber() {
807
+ const { storedAssetSubscriber } = provideEphemera(
808
+ this.state.collateralBrand,
809
+ );
810
+ assert(storedAssetSubscriber);
811
+ return storedAssetSubscriber;
812
+ },
813
+ getCollateralBrand() {
814
+ return this.state.collateralBrand;
815
+ },
816
+ getDebtBrand() {
817
+ return this.state.debtBrand;
818
+ },
819
+ /**
820
+ * coefficient on existing debt to calculate new debt
821
+ */
822
+ getCompoundedInterest() {
823
+ return this.state.compoundedInterest;
824
+ },
825
+ /**
826
+ * Called by a vault when its balances change.
827
+ *
828
+ * @param {NormalizedDebt} oldDebtNormalized
829
+ * @param {Amount<'nat'>} oldCollateral
830
+ * @param {VaultId} vaultId
831
+ * @param {import('./vault.js').VaultPhase} vaultPhase at the end of whatever change updated balances
832
+ * @param {Vault} vault
833
+ */
834
+ handleBalanceChange(
835
+ oldDebtNormalized,
836
+ oldCollateral,
837
+ vaultId,
838
+ vaultPhase,
634
839
  vault,
635
- liquidator,
636
- state.collateralBrand,
637
- factoryPowers.getGovernedParams().getLiquidationPenalty(),
638
- )
639
- .then(accounting => {
640
- facets.manager.burnAndRecord(accounting.toBurn, vaultSeat);
641
-
642
- // current values
643
-
644
- // Sometimes, the AMM will sell less than all the collateral. If there
645
- // was a shortfall, the investor doesn't keep the change, so we get it.
646
- // If there was no shortfall, the collateral is returned.
647
- const collateralPost = vault.getCollateralAmount();
648
- if (
649
- !AmountMath.isEmpty(collateralPost) &&
650
- !AmountMath.isEmpty(accounting.shortfall)
651
- ) {
652
- // The borrower doesn't get the excess collateral remaining when
653
- // liquidation results in a shortfall. We currently do nothing with
654
- // it. We could hold it until it crosses some threshold, then sell it
655
- // to the AMM, or we could transfer it to the reserve. At least it's
656
- // visible in the accounting.
657
- atomicTransfer(zcf, vaultSeat, state.retainedCollateralSeat, {
658
- Collateral: collateralPost,
659
- });
660
- }
840
+ ) {
841
+ const { state, facets } = this;
842
+ const { prioritizedVaults } = provideEphemera(state.collateralBrand);
843
+ assert(prioritizedVaults);
661
844
 
662
- // Reduce totalCollateral by collateralPre, since all the collateral was
663
- // sold, returned to the vault owner, or held by the VaultManager.
664
- state.totalCollateral = AmountMath.subtract(
665
- state.totalCollateral,
666
- collateralPre,
667
- );
668
- state.totalDebt = AmountMath.subtract(
669
- state.totalDebt,
670
- accounting.shortfall,
671
- );
845
+ // the manager holds only vaults that can accrue interest or be liquidated;
846
+ // i.e. vaults that have debt. The one exception is at the outset when
847
+ // a vault has been added to the manager but not yet accounted for.
848
+ const settled =
849
+ AmountMath.isEmpty(oldDebtNormalized) &&
850
+ vaultPhase !== Phase.ACTIVE;
672
851
 
673
- // cumulative values
674
- state.totalProceedsReceived = AmountMath.add(
675
- state.totalProceedsReceived,
676
- accounting.proceeds,
677
- );
678
- state.totalOverageReceived = AmountMath.add(
679
- state.totalOverageReceived,
680
- accounting.overage,
681
- );
682
- state.totalShortfallReceived = AmountMath.add(
683
- state.totalShortfallReceived,
684
- accounting.shortfall,
852
+ if (settled) {
853
+ assert(
854
+ !prioritizedVaults.hasVaultByAttributes(
855
+ oldDebtNormalized,
856
+ oldCollateral,
857
+ vaultId,
858
+ ),
859
+ 'Settled vaults must not be retained in storage',
685
860
  );
686
- state.liquidatingVaults.delete(vault);
687
- trace('liquidated', state.collateralBrand);
688
- state.numLiquidationsCompleted += 1;
689
- facets.helper.updateMetrics();
690
-
691
- if (!AmountMath.isEmpty(accounting.shortfall)) {
692
- E(factoryPowers.getShortfallReporter())
693
- .increaseLiquidationShortfall(accounting.shortfall)
694
- .catch(reason =>
695
- console.error(
696
- 'liquidateAndRemove failed to increaseLiquidationShortfall',
697
- reason,
698
- ),
699
- );
861
+ } else {
862
+ const isNew = AmountMath.isEmpty(oldDebtNormalized);
863
+ if (!isNew) {
864
+ // its position in the queue is no longer valid
865
+
866
+ const vaultInStore = prioritizedVaults.removeVaultByAttributes(
867
+ oldDebtNormalized,
868
+ oldCollateral,
869
+ vaultId,
870
+ );
871
+ assert(
872
+ vault === vaultInStore,
873
+ 'handleBalanceChange for two different vaults',
874
+ );
700
875
  }
701
- })
702
- .catch(e => {
703
- // XXX should notify interested parties
704
- console.error('liquidateAndRemove failed with', e);
705
- throw e;
706
- });
707
- },
708
- },
709
- manager: {
710
- getGovernedParams() {
711
- const { state } = this;
712
- const ephemera = provideEphemera(state.collateralBrand);
713
- assert(ephemera.factoryPowers);
714
- return ephemera.factoryPowers.getGovernedParams();
715
- },
716
876
 
717
- /**
718
- * @param {Amount<'nat'>} collateralAmount
719
- */
720
- async maxDebtFor(collateralAmount) {
721
- const { state } = this;
722
- const { debtBrand } = state;
723
- const { priceAuthority, ...ephemera } = provideEphemera(
724
- state.collateralBrand,
725
- );
726
- assert(ephemera.factoryPowers && priceAuthority);
727
- const quoteAmount = await E(priceAuthority).quoteGiven(
728
- collateralAmount,
729
- debtBrand,
730
- );
731
- // floorDivide because we want the debt ceiling lower
732
- return floorDivideBy(
733
- getAmountOut(quoteAmount),
734
- ephemera.factoryPowers.getGovernedParams().getLiquidationMargin(),
735
- );
736
- },
737
- /**
738
- * TODO utility method to turn a callback into non-actual one
739
- * was type {MintAndReallocate}
740
- *
741
- * @param {Amount<'nat'>} toMint
742
- * @param {Amount<'nat'>} fee
743
- * @param {ZCFSeat} seat
744
- * @param {...ZCFSeat} otherSeats
745
- * @returns {void}
746
- */
747
- mintAndReallocate(toMint, fee, seat, ...otherSeats) {
748
- const { state } = this;
749
- const { totalDebt } = state;
750
- const { factoryPowers } = provideEphemera(state.collateralBrand);
751
- assert(factoryPowers);
752
-
753
- checkDebtLimit(
754
- factoryPowers.getGovernedParams().getDebtLimit(),
755
- totalDebt,
756
- toMint,
757
- );
758
- factoryPowers.mintAndReallocate(toMint, fee, seat, ...otherSeats);
759
- state.totalDebt = AmountMath.add(state.totalDebt, toMint);
760
- },
761
- /**
762
- * @param {Amount<'nat'>} toBurn
763
- * @param {ZCFSeat} seat
764
- */
765
- burnAndRecord(toBurn, seat) {
766
- const { state } = this;
767
- const { factoryPowers } = provideEphemera(state.collateralBrand);
768
- assert(factoryPowers);
769
- trace('burnAndRecord', state.collateralBrand, {
770
- toBurn,
771
- totalDebt: state.totalDebt,
772
- });
773
- const { burnDebt } = factoryPowers;
774
- burnDebt(toBurn, seat);
775
- state.totalDebt = AmountMath.subtract(state.totalDebt, toBurn);
776
- },
777
- getAssetSubscriber() {
778
- const { state } = this;
779
- const { assetSubscriber } = provideEphemera(state.collateralBrand);
780
- assert(assetSubscriber);
781
- return assetSubscriber;
782
- },
783
- getCollateralBrand() {
784
- return this.state.collateralBrand;
785
- },
786
- getDebtBrand() {
787
- return this.state.debtBrand;
788
- },
789
- /**
790
- * coefficient on existing debt to calculate new debt
791
- */
792
- getCompoundedInterest() {
793
- return this.state.compoundedInterest;
794
- },
795
- /**
796
- * Called by a vault when its balances change.
797
- *
798
- * @param {NormalizedDebt} oldDebtNormalized
799
- * @param {Amount<'nat'>} oldCollateral
800
- * @param {VaultId} vaultId
801
- * @param {import('./vault.js').VaultPhase} vaultPhase at the end of whatever change updated balances
802
- * @param {Vault} vault
803
- */
804
- handleBalanceChange(
805
- oldDebtNormalized,
806
- oldCollateral,
807
- vaultId,
808
- vaultPhase,
809
- vault,
810
- ) {
811
- const { state, facets } = this;
812
- const { prioritizedVaults } = provideEphemera(state.collateralBrand);
813
- assert(prioritizedVaults);
814
-
815
- // the manager holds only vaults that can accrue interest or be liquidated;
816
- // i.e. vaults that have debt. The one exception is at the outset when
817
- // a vault has been added to the manager but not yet accounted for.
818
- const settled =
819
- AmountMath.isEmpty(oldDebtNormalized) && vaultPhase !== Phase.ACTIVE;
820
-
821
- if (settled) {
822
- assert(
823
- !prioritizedVaults.hasVaultByAttributes(
824
- oldDebtNormalized,
825
- oldCollateral,
826
- vaultId,
827
- ),
828
- 'Settled vaults must not be retained in storage',
829
- );
830
- } else {
831
- const isNew = AmountMath.isEmpty(oldDebtNormalized);
832
- if (!isNew) {
833
- // its position in the queue is no longer valid
877
+ // replace in queue, but only if it can accrue interest or be liquidated (i.e. has debt).
878
+ // getCurrentDebt() would also work (0x = 0) but require more computation.
879
+ if (!AmountMath.isEmpty(vault.getNormalizedDebt())) {
880
+ prioritizedVaults.addVault(vaultId, vault);
881
+ }
834
882
 
835
- const vaultInStore = prioritizedVaults.removeVaultByAttributes(
836
- oldDebtNormalized,
883
+ // totalCollateral += vault's collateral delta (post — pre)
884
+ state.totalCollateral = AmountMath.subtract(
885
+ AmountMath.add(
886
+ state.totalCollateral,
887
+ vault.getCollateralAmount(),
888
+ ),
837
889
  oldCollateral,
838
- vaultId,
839
- );
840
- assert(
841
- vault === vaultInStore,
842
- 'handleBalanceChange for two different vaults',
843
890
  );
891
+ // debt accounting managed through minting and burning
892
+ facets.helper.updateMetrics();
844
893
  }
894
+ },
895
+ },
896
+ self: {
897
+ getGovernedParams() {
898
+ const { state } = this;
899
+ const { factoryPowers } = provideEphemera(state.collateralBrand);
900
+ assert(factoryPowers);
901
+ return factoryPowers.getGovernedParams();
902
+ },
903
+
904
+ /**
905
+ * In extreme situations, system health may require liquidating all vaults.
906
+ * This starts the liquidations all in parallel.
907
+ */
908
+ async liquidateAll() {
909
+ const {
910
+ state,
911
+ facets: { helper },
912
+ } = this;
913
+ const { prioritizedVaults } = provideEphemera(state.collateralBrand);
914
+ assert(prioritizedVaults);
915
+ const toLiquidate = Array.from(prioritizedVaults.entries()).map(
916
+ entry => helper.liquidateAndRemove(entry),
917
+ );
918
+ await Promise.all(toLiquidate);
919
+ },
920
+
921
+ /**
922
+ * @param {ZCFSeat} seat
923
+ */
924
+ async makeVaultKit(seat) {
925
+ const {
926
+ state,
927
+ facets: { manager },
928
+ } = this;
929
+ const { marshaller, prioritizedVaults, storageNode, zcf } =
930
+ provideEphemera(state.collateralBrand);
931
+ assert(marshaller, 'makeVaultKit missing marshaller');
932
+ assert(prioritizedVaults, 'makeVaultKit missing prioritizedVaults');
933
+ assert(storageNode, 'makeVaultKit missing storageNode');
934
+ assert(zcf, 'makeVaultKit missing zcf');
935
+ assertProposalShape(seat, {
936
+ give: { Collateral: null },
937
+ want: { Minted: null },
938
+ });
845
939
 
846
- // replace in queue, but only if it can accrue interest or be liquidated (i.e. has debt).
847
- // getCurrentDebt() would also work (0x = 0) but require more computation.
848
- if (!AmountMath.isEmpty(vault.getNormalizedDebt())) {
849
- prioritizedVaults.addVault(vaultId, vault);
850
- }
940
+ state.vaultCounter += 1;
941
+ const vaultId = String(state.vaultCounter);
851
942
 
852
- // totalCollateral += vault's collateral delta (post — pre)
853
- state.totalCollateral = AmountMath.subtract(
854
- AmountMath.add(state.totalCollateral, vault.getCollateralAmount()),
855
- oldCollateral,
856
- );
857
- // debt accounting managed through minting and burning
858
- facets.helper.updateMetrics();
859
- }
860
- },
861
- },
862
- self: {
863
- getGovernedParams() {
864
- const { state } = this;
865
- const { factoryPowers } = provideEphemera(state.collateralBrand);
866
- assert(factoryPowers);
867
- return factoryPowers.getGovernedParams();
868
- },
943
+ const vaultStorageNode = E(
944
+ E(storageNode).makeChildNode(`vaults`),
945
+ ).makeChildNode(`vault${vaultId}`);
869
946
 
870
- /**
871
- * In extreme situations, system health may require liquidating all vaults.
872
- * This starts the liquidations all in parallel.
873
- */
874
- async liquidateAll() {
875
- const {
876
- state,
877
- facets: { helper },
878
- } = this;
879
- const { prioritizedVaults } = provideEphemera(state.collateralBrand);
880
- assert(prioritizedVaults);
881
- const toLiquidate = Array.from(prioritizedVaults.entries()).map(entry =>
882
- helper.liquidateAndRemove(entry),
883
- );
884
- await Promise.all(toLiquidate);
885
- },
947
+ const { self: vault } = makeVault(
948
+ zcf,
949
+ manager,
950
+ vaultId,
951
+ vaultStorageNode,
952
+ marshaller,
953
+ );
886
954
 
887
- /**
888
- * @param {ZCFSeat} seat
889
- */
890
- async makeVaultKit(seat) {
891
- const {
892
- state,
893
- facets: { manager },
894
- } = this;
895
- const { marshaller, prioritizedVaults, storageNode, zcf } =
896
- provideEphemera(state.collateralBrand);
897
- assert(marshaller, 'makeVaultKit missing marshaller');
898
- assert(prioritizedVaults, 'makeVaultKit missing prioritizedVaults');
899
- assert(storageNode, 'makeVaultKit missing storageNode');
900
- assert(zcf, 'makeVaultKit missing zcf');
901
- assertProposalShape(seat, {
902
- give: { Collateral: null },
903
- want: { Minted: null },
904
- });
905
-
906
- state.vaultCounter += 1;
907
- const vaultId = String(state.vaultCounter);
908
-
909
- const vaultStorageNode = E(
910
- E(storageNode).makeChildNode(`vaults`),
911
- ).makeChildNode(`vault${vaultId}`);
912
-
913
- const vault = makeVault(
914
- zcf,
915
- manager,
916
- vaultId,
917
- vaultStorageNode,
918
- marshaller,
919
- );
920
-
921
- try {
922
- // TODO `await` is allowed until the above ordering is fixed
923
- // eslint-disable-next-line @jessie.js/no-nested-await
924
- const vaultKit = await vault.initVaultKit(seat, vaultStorageNode);
925
- // initVaultKit calls back to handleBalanceChange() which will add the
926
- // vault to prioritizedVaults
927
- seat.exit();
928
- return vaultKit;
929
- } catch (err) {
930
- // ??? do we still need this cleanup? it won't get into the store unless it has collateral,
931
- // which should qualify it to be in the store. If we drop this catch then the nested await
932
- // for `vault.initVaultKit()` goes away.
933
-
934
- // remove it from the store if it got in
935
- /** @type {NormalizedDebt} */
936
- // @ts-expect-error cast
937
- const normalizedDebt = AmountMath.makeEmpty(state.debtBrand);
938
- const collateralPre = seat.getCurrentAllocation().Collateral;
939
955
  try {
940
- prioritizedVaults.removeVaultByAttributes(
941
- normalizedDebt,
942
- collateralPre,
943
- vaultId,
944
- );
945
- console.error(
946
- 'removed vault',
947
- vaultId,
948
- 'after initVaultKit failure',
949
- );
950
- } catch {
951
- console.error(
952
- 'vault',
953
- vaultId,
954
- 'never stored during initVaultKit failure',
955
- );
956
+ // TODO `await` is allowed until the above ordering is fixed
957
+ // eslint-disable-next-line @jessie.js/no-nested-await
958
+ const vaultKit = await vault.initVaultKit(seat, vaultStorageNode);
959
+ // initVaultKit calls back to handleBalanceChange() which will add the
960
+ // vault to prioritizedVaults
961
+ seat.exit();
962
+ return vaultKit;
963
+ } catch (err) {
964
+ // ??? do we still need this cleanup? it won't get into the store unless it has collateral,
965
+ // which should qualify it to be in the store. If we drop this catch then the nested await
966
+ // for `vault.initVaultKit()` goes away.
967
+
968
+ // remove it from the store if it got in
969
+ /** @type {NormalizedDebt} */
970
+ // @ts-expect-error cast
971
+ const normalizedDebt = AmountMath.makeEmpty(state.debtBrand);
972
+ const collateralPre = seat.getCurrentAllocation().Collateral;
973
+ try {
974
+ prioritizedVaults.removeVaultByAttributes(
975
+ normalizedDebt,
976
+ collateralPre,
977
+ vaultId,
978
+ );
979
+ console.error(
980
+ 'removed vault',
981
+ vaultId,
982
+ 'after initVaultKit failure',
983
+ );
984
+ } catch {
985
+ console.error(
986
+ 'vault',
987
+ vaultId,
988
+ 'never stored during initVaultKit failure',
989
+ );
990
+ }
991
+ throw err;
956
992
  }
957
- throw err;
958
- }
959
- },
960
-
961
- /**
962
- *
963
- * @param {Installation} liquidationInstall
964
- * @param {object} liquidationTerms
965
- */
966
- async setupLiquidator(liquidationInstall, liquidationTerms) {
967
- const { state, facets } = this;
968
- const { zcf } = provideEphemera(state.collateralBrand);
969
- assert(zcf);
970
- const { debtBrand, collateralBrand } = state;
971
- const {
972
- ammPublicFacet,
973
- priceAuthority,
974
- reservePublicFacet,
975
- timerService,
976
- } = zcf.getTerms();
977
- const zoe = zcf.getZoeService();
978
- const collateralIssuer = zcf.getIssuerForBrand(collateralBrand);
979
- const debtIssuer = zcf.getIssuerForBrand(debtBrand);
980
- trace('setup liquidator', state.collateralBrand, {
981
- debtBrand,
982
- debtIssuer,
983
- collateralBrand,
984
- liquidationTerms,
985
- });
986
- const { creatorFacet, instance } = await E(zoe).startInstance(
987
- liquidationInstall,
988
- harden({ Minted: debtIssuer, Collateral: collateralIssuer }),
989
- harden({
990
- ...liquidationTerms,
991
- amm: ammPublicFacet,
992
- debtBrand,
993
- reservePublicFacet,
993
+ },
994
+
995
+ /**
996
+ *
997
+ * @param {Installation} liquidationInstall
998
+ * @param {object} liquidationTerms
999
+ */
1000
+ async setupLiquidator(liquidationInstall, liquidationTerms) {
1001
+ const { state, facets } = this;
1002
+ const { zcf } = provideEphemera(state.collateralBrand);
1003
+ assert(zcf);
1004
+ const { debtBrand, collateralBrand } = state;
1005
+ const {
1006
+ ammPublicFacet,
994
1007
  priceAuthority,
1008
+ reservePublicFacet,
995
1009
  timerService,
996
- }),
997
- );
998
- trace('setup liquidator complete', state.collateralBrand, {
999
- instance,
1000
- old: state.liquidatorInstance,
1001
- equal: state.liquidatorInstance === instance,
1002
- });
1003
- state.liquidatorInstance = instance;
1004
- state.liquidator = creatorFacet;
1005
- facets.helper.assetNotify();
1006
- },
1007
-
1008
- async getCollateralQuote() {
1009
- const { state } = this;
1010
- const { priceAuthority } = provideEphemera(state.collateralBrand);
1011
- assert(priceAuthority);
1012
-
1013
- const { debtBrand } = state;
1014
- // get a quote for one unit of the collateral
1015
- const collateralUnit = await unitAmount(state.collateralBrand);
1016
- return E(priceAuthority).quoteGiven(collateralUnit, debtBrand);
1017
- },
1018
-
1019
- getPublicFacet() {
1020
- return this.facets.collateral;
1010
+ } = zcf.getTerms();
1011
+ const zoe = zcf.getZoeService();
1012
+ const collateralIssuer = zcf.getIssuerForBrand(collateralBrand);
1013
+ const debtIssuer = zcf.getIssuerForBrand(debtBrand);
1014
+ trace('setup liquidator', state.collateralBrand, {
1015
+ debtBrand,
1016
+ debtIssuer,
1017
+ collateralBrand,
1018
+ liquidationTerms,
1019
+ });
1020
+ const { creatorFacet, instance } = await E(zoe).startInstance(
1021
+ liquidationInstall,
1022
+ harden({ Minted: debtIssuer, Collateral: collateralIssuer }),
1023
+ harden({
1024
+ ...liquidationTerms,
1025
+ amm: ammPublicFacet,
1026
+ debtBrand,
1027
+ reservePublicFacet,
1028
+ priceAuthority,
1029
+ timerService,
1030
+ }),
1031
+ );
1032
+ trace('setup liquidator complete', state.collateralBrand, {
1033
+ instance,
1034
+ old: state.liquidatorInstance,
1035
+ equal: state.liquidatorInstance === instance,
1036
+ });
1037
+ state.liquidatorInstance = instance;
1038
+ state.liquidator = creatorFacet;
1039
+ facets.helper.assetNotify();
1040
+ },
1041
+
1042
+ async getCollateralQuote() {
1043
+ const { state } = this;
1044
+ const { priceAuthority } = provideEphemera(state.collateralBrand);
1045
+ assert(priceAuthority);
1046
+
1047
+ const { debtBrand } = state;
1048
+ // get a quote for one unit of the collateral
1049
+ const collateralUnit = await unitAmount(state.collateralBrand);
1050
+ return E(priceAuthority).quoteGiven(collateralUnit, debtBrand);
1051
+ },
1052
+
1053
+ getPublicFacet() {
1054
+ return this.facets.collateral;
1055
+ },
1021
1056
  },
1022
1057
  },
1023
- },
1024
- {
1025
- finish,
1026
- },
1027
- );
1058
+ {
1059
+ finish,
1060
+ },
1061
+ );
1062
+ };
1028
1063
 
1029
1064
  /**
1065
+ * @typedef {ReturnType<ReturnType<typeof vivifyVaultManagerKit>>['self']} VaultManager
1030
1066
  * Each VaultManager manages a single collateral type.
1031
1067
  *
1032
1068
  * It manages some number of outstanding loans, each called a Vault, for which
1033
1069
  * the collateral is provided in exchange for borrowed Minted.
1034
- *
1035
- * @param {ZCF} zcf
1036
- * @param {ZCFMint<'nat'>} debtMint
1037
- * @param {Brand} collateralBrand
1038
- * @param {ERef<PriceAuthority>} priceAuthority
1039
- * @param {import('./vaultDirector.js').FactoryPowersFacet} factoryPowers
1040
- * @param {ERef<TimerService>} timerService
1041
- * @param {Timestamp} startTimeStamp
1042
1070
  */
1043
- export const makeVaultManager = pickFacet(makeVaultManagerKit, 'self');
1044
-
1045
- /** @typedef {ReturnType<typeof makeVaultManagerKit>['manager']} VaultKitManager */
1046
- /** @typedef {ReturnType<typeof makeVaultManager>} VaultManager */
1047
1071
  /** @typedef {ReturnType<VaultManager['getPublicFacet']>} CollateralManager */