@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
@@ -1,31 +1,26 @@
1
- import '@agoric/zoe/exported.js';
2
-
1
+ import { AmountMath, AmountShape } from '@agoric/ertp';
2
+ import { StorageNodeShape } from '@agoric/notifier/src/typeGuards.js';
3
+ import { M, vivifyFarClassKit } from '@agoric/vat-data';
3
4
  import {
4
5
  assertProposalShape,
5
- makeRatioFromAmounts,
6
+ atomicTransfer,
6
7
  ceilMultiplyBy,
7
8
  floorMultiplyBy,
8
- atomicTransfer,
9
+ makeRatioFromAmounts,
9
10
  } from '@agoric/zoe/src/contractSupport/index.js';
10
- import { AmountMath, AmountShape } from '@agoric/ertp';
11
- import { StorageNodeShape } from '@agoric/notifier/src/typeGuards.js';
12
- import {
13
- defineDurableFarClassKit,
14
- M,
15
- makeKindHandle,
16
- pickFacet,
17
- } from '@agoric/vat-data';
18
11
  import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
19
- import { makeTracer } from '../makeTracer.js';
20
- import { calculateCurrentDebt, reverseInterest } from '../interest-math.js';
21
- import { makeVaultKit } from './vaultKit.js';
22
12
  import {
23
13
  addSubtract,
24
14
  assertOnlyKeys,
25
15
  makeEphemeraProvider,
26
16
  stageDelta,
27
17
  } from '../contractSupport.js';
18
+ import { calculateCurrentDebt, reverseInterest } from '../interest-math.js';
19
+ import { makeTracer } from '../makeTracer.js';
28
20
  import { UnguardedHelperI } from '../typeGuards.js';
21
+ import { vivifyVaultKit } from './vaultKit.js';
22
+
23
+ import '@agoric/zoe/exported.js';
29
24
 
30
25
  const { quote: q, Fail } = assert;
31
26
 
@@ -217,619 +212,638 @@ export const VaultI = M.interface('Vault', {
217
212
  makeTransferInvitation: M.call().returns(M.promise()),
218
213
  });
219
214
 
220
- const vaultKind = makeKindHandle('Vault');
221
- const makeVaultBase = defineDurableFarClassKit(
222
- vaultKind,
223
- {
224
- helper: UnguardedHelperI,
225
- self: VaultI,
226
- },
227
- initState,
228
- {
229
- helper: {
230
- // #region Computed constants
231
- collateralBrand() {
232
- return this.state.manager.getCollateralBrand();
233
- },
234
- debtBrand() {
235
- return this.state.manager.getDebtBrand();
236
- },
237
-
238
- emptyCollateral() {
239
- return AmountMath.makeEmpty(this.facets.helper.collateralBrand());
240
- },
241
- emptyDebt() {
242
- return AmountMath.makeEmpty(this.facets.helper.debtBrand());
243
- },
244
- // #endregion
245
-
246
- // #region Phase logic
247
- /**
248
- * @param {VaultPhase} newPhase
249
- */
250
- assignPhase(newPhase) {
251
- const { state } = this;
252
-
253
- const { phase } = state;
254
- const validNewPhases = validTransitions[phase];
255
- validNewPhases.includes(newPhase) ||
256
- Fail`Vault cannot transition from ${q(phase)} to ${q(newPhase)}`;
257
- state.phase = newPhase;
258
- },
259
-
260
- assertActive() {
261
- const { phase } = this.state;
262
- assert(phase === Phase.ACTIVE);
263
- },
264
-
265
- assertCloseable() {
266
- const { phase } = this.state;
267
- phase === Phase.ACTIVE ||
268
- phase === Phase.LIQUIDATED ||
269
- Fail`to be closed a vault must be active or liquidated, not ${phase}`;
270
- },
271
- // #endregion
272
-
273
- /**
274
- * Called whenever the debt is paid or created through a transaction,
275
- * but not for interest accrual.
276
- *
277
- * @param {Amount<'nat'>} newDebt - principal and all accrued interest
278
- */
279
- updateDebtSnapshot(newDebt) {
280
- const { state } = this;
281
-
282
- // update local state
283
- state.debtSnapshot = newDebt;
284
- state.interestSnapshot = state.manager.getCompoundedInterest();
285
- },
215
+ export const vivifyVault = baggage => {
216
+ const makeVaultKit = vivifyVaultKit(baggage);
286
217
 
287
- /**
288
- * Update the debt balance and propagate upwards to
289
- * maintain aggregate debt and liquidation order.
290
- *
291
- * @param {NormalizedDebt} oldDebtNormalized - prior principal and all accrued interest, normalized to the launch of the vaultManager
292
- * @param {Amount<'nat'>} oldCollateral - actual collateral
293
- * @param {Amount<'nat'>} newDebtActual - actual principal and all accrued interest
294
- */
295
- updateDebtAccounting(oldDebtNormalized, oldCollateral, newDebtActual) {
296
- const { state, facets } = this;
297
- const { helper } = facets;
298
- helper.updateDebtSnapshot(newDebtActual);
299
- // notify manager so it can notify and clean up as appropriate
300
- state.manager.handleBalanceChange(
301
- oldDebtNormalized,
302
- oldCollateral,
303
- state.idInManager,
304
- state.phase,
305
- facets.self,
306
- );
307
- },
308
-
309
- /**
310
- *
311
- * @param {ZCFSeat} seat
312
- */
313
- getCollateralAllocated(seat) {
314
- return seat.getAmountAllocated(
315
- 'Collateral',
316
- this.facets.helper.collateralBrand(),
317
- );
318
- },
319
- getMintedAllocated(seat) {
320
- return seat.getAmountAllocated(
321
- 'Minted',
322
- this.facets.helper.debtBrand(),
323
- );
324
- },
325
-
326
- assertVaultHoldsNoMinted() {
327
- const { state, facets } = this;
328
- const { vaultSeat } = state;
329
- AmountMath.isEmpty(facets.helper.getMintedAllocated(vaultSeat)) ||
330
- Fail`Vault should be empty of Minted`;
331
- },
332
-
333
- /**
334
- *
335
- * @param {Amount<'nat'>} collateralAmount
336
- * @param {Amount<'nat'>} proposedRunDebt
337
- */
338
- async assertSufficientCollateral(collateralAmount, proposedRunDebt) {
339
- const { state, facets } = this;
340
- const maxRun = await state.manager.maxDebtFor(collateralAmount);
341
- AmountMath.isGTE(maxRun, proposedRunDebt, facets.helper.debtBrand()) ||
342
- Fail`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)} for ${q(
343
- collateralAmount,
344
- )} collateral`;
345
- },
346
-
347
- /**
348
- *
349
- * @param {HolderPhase} newPhase
350
- */
351
- getStateSnapshot(newPhase) {
352
- const { state, facets } = this;
353
-
354
- const { debtSnapshot: debt, interestSnapshot: interest } = state;
355
- /** @type {VaultNotification} */
356
- return harden({
357
- debtSnapshot: { debt, interest },
358
- locked: facets.self.getCollateralAmount(),
359
- // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase
360
- // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415
361
- vaultState: newPhase,
362
- });
363
- },
364
-
365
- /**
366
- * call this whenever anything changes!
367
- */
368
- updateUiState() {
369
- const { state, facets } = this;
370
- const ephemera = provideEphemera(state.idInManager);
371
- const { outerUpdater } = ephemera;
372
- if (!outerUpdater) {
373
- // It's not an error to change to liquidating during transfer
374
- return;
375
- }
376
- const { phase } = state;
377
- const uiState = facets.helper.getStateSnapshot(phase);
378
- trace('updateUiState', state.idInManager, uiState);
379
-
380
- switch (phase) {
381
- case Phase.ACTIVE:
382
- case Phase.LIQUIDATING:
383
- case Phase.LIQUIDATED:
384
- outerUpdater.publish(uiState);
385
- break;
386
- case Phase.CLOSED:
387
- outerUpdater.finish(uiState);
388
- ephemera.outerUpdater = null;
389
- break;
390
- default:
391
- throw Error(`unreachable vault phase: ${phase}`);
392
- }
393
- },
218
+ // TODO find a way to not have to indent a level deeper than defineDurableFarClassKit does
219
+ const maker = vivifyFarClassKit(
220
+ baggage,
221
+ 'Vault',
222
+ {
223
+ helper: UnguardedHelperI,
224
+ self: VaultI,
225
+ },
226
+ initState,
227
+ {
228
+ helper: {
229
+ // #region Computed constants
230
+ collateralBrand() {
231
+ return this.state.manager.getCollateralBrand();
232
+ },
233
+ debtBrand() {
234
+ return this.state.manager.getDebtBrand();
235
+ },
236
+
237
+ emptyCollateral() {
238
+ return AmountMath.makeEmpty(this.facets.helper.collateralBrand());
239
+ },
240
+ emptyDebt() {
241
+ return AmountMath.makeEmpty(this.facets.helper.debtBrand());
242
+ },
243
+ // #endregion
244
+
245
+ // #region Phase logic
246
+ /**
247
+ * @param {VaultPhase} newPhase
248
+ */
249
+ assignPhase(newPhase) {
250
+ const { state } = this;
251
+
252
+ const { phase } = state;
253
+ const validNewPhases = validTransitions[phase];
254
+ validNewPhases.includes(newPhase) ||
255
+ Fail`Vault cannot transition from ${q(phase)} to ${q(newPhase)}`;
256
+ state.phase = newPhase;
257
+ },
258
+
259
+ assertActive() {
260
+ const { phase } = this.state;
261
+ assert(phase === Phase.ACTIVE);
262
+ },
263
+
264
+ assertCloseable() {
265
+ const { phase } = this.state;
266
+ phase === Phase.ACTIVE ||
267
+ phase === Phase.LIQUIDATED ||
268
+ Fail`to be closed a vault must be active or liquidated, not ${phase}`;
269
+ },
270
+ // #endregion
271
+
272
+ /**
273
+ * Called whenever the debt is paid or created through a transaction,
274
+ * but not for interest accrual.
275
+ *
276
+ * @param {Amount<'nat'>} newDebt - principal and all accrued interest
277
+ */
278
+ updateDebtSnapshot(newDebt) {
279
+ const { state } = this;
280
+
281
+ // update local state
282
+ state.debtSnapshot = newDebt;
283
+ state.interestSnapshot = state.manager.getCompoundedInterest();
284
+ },
285
+
286
+ /**
287
+ * Update the debt balance and propagate upwards to
288
+ * maintain aggregate debt and liquidation order.
289
+ *
290
+ * @param {NormalizedDebt} oldDebtNormalized - prior principal and all accrued interest, normalized to the launch of the vaultManager
291
+ * @param {Amount<'nat'>} oldCollateral - actual collateral
292
+ * @param {Amount<'nat'>} newDebtActual - actual principal and all accrued interest
293
+ */
294
+ updateDebtAccounting(oldDebtNormalized, oldCollateral, newDebtActual) {
295
+ const { state, facets } = this;
296
+ const { helper } = facets;
297
+ helper.updateDebtSnapshot(newDebtActual);
298
+ // notify manager so it can notify and clean up as appropriate
299
+ state.manager.handleBalanceChange(
300
+ oldDebtNormalized,
301
+ oldCollateral,
302
+ state.idInManager,
303
+ state.phase,
304
+ facets.self,
305
+ );
306
+ },
307
+
308
+ /**
309
+ *
310
+ * @param {ZCFSeat} seat
311
+ */
312
+ getCollateralAllocated(seat) {
313
+ return seat.getAmountAllocated(
314
+ 'Collateral',
315
+ this.facets.helper.collateralBrand(),
316
+ );
317
+ },
318
+ getMintedAllocated(seat) {
319
+ return seat.getAmountAllocated(
320
+ 'Minted',
321
+ this.facets.helper.debtBrand(),
322
+ );
323
+ },
324
+
325
+ assertVaultHoldsNoMinted() {
326
+ const { state, facets } = this;
327
+ const { vaultSeat } = state;
328
+ AmountMath.isEmpty(facets.helper.getMintedAllocated(vaultSeat)) ||
329
+ Fail`Vault should be empty of Minted`;
330
+ },
331
+
332
+ /**
333
+ *
334
+ * @param {Amount<'nat'>} collateralAmount
335
+ * @param {Amount<'nat'>} proposedRunDebt
336
+ */
337
+ async assertSufficientCollateral(collateralAmount, proposedRunDebt) {
338
+ const { state, facets } = this;
339
+ const maxRun = await state.manager.maxDebtFor(collateralAmount);
340
+ AmountMath.isGTE(
341
+ maxRun,
342
+ proposedRunDebt,
343
+ facets.helper.debtBrand(),
344
+ ) ||
345
+ Fail`Requested ${q(proposedRunDebt)} exceeds max ${q(
346
+ maxRun,
347
+ )} for ${q(collateralAmount)} collateral`;
348
+ },
349
+
350
+ /**
351
+ *
352
+ * @param {HolderPhase} newPhase
353
+ */
354
+ getStateSnapshot(newPhase) {
355
+ const { state, facets } = this;
356
+
357
+ const { debtSnapshot: debt, interestSnapshot: interest } = state;
358
+ /** @type {VaultNotification} */
359
+ return harden({
360
+ debtSnapshot: { debt, interest },
361
+ locked: facets.self.getCollateralAmount(),
362
+ // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase
363
+ // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415
364
+ vaultState: newPhase,
365
+ });
366
+ },
367
+
368
+ /**
369
+ * call this whenever anything changes!
370
+ */
371
+ updateUiState() {
372
+ const { state, facets } = this;
373
+ const ephemera = provideEphemera(state.idInManager);
374
+ const { outerUpdater } = ephemera;
375
+ if (!outerUpdater) {
376
+ // It's not an error to change to liquidating during transfer
377
+ return;
378
+ }
379
+ const { phase } = state;
380
+ const uiState = facets.helper.getStateSnapshot(phase);
381
+ trace('updateUiState', state.idInManager, uiState);
382
+
383
+ switch (phase) {
384
+ case Phase.ACTIVE:
385
+ case Phase.LIQUIDATING:
386
+ case Phase.LIQUIDATED:
387
+ outerUpdater.publish(uiState);
388
+ break;
389
+ case Phase.CLOSED:
390
+ outerUpdater.finish(uiState);
391
+ ephemera.outerUpdater = null;
392
+ break;
393
+ default:
394
+ throw Error(`unreachable vault phase: ${phase}`);
395
+ }
396
+ },
397
+
398
+ /**
399
+ * @param {ZCFSeat} seat
400
+ */
401
+ async closeHook(seat) {
402
+ const { state, facets } = this;
403
+
404
+ const { self, helper } = facets;
405
+ helper.assertCloseable();
406
+ const { phase, vaultSeat } = state;
407
+ const { zcf } = provideEphemera(state.idInManager);
408
+
409
+ // Held as keys for cleanup in the manager
410
+ const oldDebtNormalized = self.getNormalizedDebt();
411
+ const oldCollateral = self.getCollateralAmount();
412
+
413
+ if (phase === Phase.ACTIVE) {
414
+ assertProposalShape(seat, {
415
+ give: { Minted: null },
416
+ });
417
+
418
+ // you're paying off the debt, you get everything back.
419
+ const debt = self.getCurrentDebt();
420
+ const {
421
+ give: { Minted: given },
422
+ } = seat.getProposal();
423
+
424
+ // you must pay off the entire remainder but if you offer too much, we won't
425
+ // take more than you owe
426
+ AmountMath.isGTE(given, debt) ||
427
+ Fail`Offer ${given} is not sufficient to pay off debt ${debt}`;
428
+
429
+ // Return any overpayment
430
+ atomicTransfer(
431
+ zcf,
432
+ vaultSeat,
433
+ seat,
434
+ vaultSeat.getCurrentAllocation(),
435
+ );
436
+
437
+ state.manager.burnAndRecord(debt, seat);
438
+ } else if (phase === Phase.LIQUIDATED) {
439
+ // Simply reallocate vault assets to the offer seat.
440
+ // Don't take anything from the offer, even if vault is underwater.
441
+ // TODO verify that returning Minted here doesn't mess up debt limits
442
+
443
+ atomicTransfer(
444
+ zcf,
445
+ vaultSeat,
446
+ seat,
447
+ vaultSeat.getCurrentAllocation(),
448
+ );
449
+ } else {
450
+ throw new Error('only active and liquidated vaults can be closed');
451
+ }
452
+
453
+ seat.exit();
454
+ helper.assignPhase(Phase.CLOSED);
455
+ helper.updateDebtSnapshot(helper.emptyDebt());
456
+ helper.updateUiState();
457
+
458
+ helper.assertVaultHoldsNoMinted();
459
+ vaultSeat.exit();
460
+
461
+ state.manager.handleBalanceChange(
462
+ oldDebtNormalized,
463
+ oldCollateral,
464
+ state.idInManager,
465
+ state.phase,
466
+ facets.self,
467
+ );
394
468
 
395
- /**
396
- * @param {ZCFSeat} seat
397
- */
398
- async closeHook(seat) {
399
- const { state, facets } = this;
469
+ return 'your loan is closed, thank you for your business';
470
+ },
471
+
472
+ /**
473
+ * Calculate the fee, the amount to mint and the resulting debt.
474
+ * The give and the want together reflect a delta, where typically
475
+ * one is zero because they come from the gave/want of an offer
476
+ * proposal. If the `want` is zero, the `fee` will also be zero,
477
+ * so the simple math works.
478
+ *
479
+ * @param {Amount<'nat'>} currentDebt
480
+ * @param {Amount<'nat'>} giveAmount
481
+ * @param {Amount<'nat'>} wantAmount
482
+ */
483
+ loanFee(currentDebt, giveAmount, wantAmount) {
484
+ const { state } = this;
485
+
486
+ const fee = ceilMultiplyBy(
487
+ wantAmount,
488
+ state.manager.getGovernedParams().getLoanFee(),
489
+ );
490
+ const toMint = AmountMath.add(wantAmount, fee);
491
+ const newDebt = addSubtract(currentDebt, toMint, giveAmount);
492
+ return { newDebt, toMint, fee };
493
+ },
494
+
495
+ /**
496
+ * Adjust principal and collateral (atomically for offer safety)
497
+ *
498
+ * @param {ZCFSeat} clientSeat
499
+ * @returns {Promise<string>} success message
500
+ */
501
+ async adjustBalancesHook(clientSeat) {
502
+ const { state, facets } = this;
503
+
504
+ const { self, helper } = facets;
505
+ const ephemera = provideEphemera(state.idInManager);
506
+ const { outerUpdater: updaterPre } = ephemera;
507
+ const { vaultSeat } = state;
508
+ const proposal = clientSeat.getProposal();
509
+ assertOnlyKeys(proposal, ['Collateral', 'Minted']);
510
+
511
+ const allEmpty = amounts => {
512
+ return amounts.every(a => AmountMath.isEmpty(a));
513
+ };
514
+
515
+ const normalizedDebtPre = self.getNormalizedDebt();
516
+ const collateralPre = helper.getCollateralAllocated(vaultSeat);
517
+
518
+ const giveColl = proposal.give.Collateral || helper.emptyCollateral();
519
+ const wantColl = proposal.want.Collateral || helper.emptyCollateral();
520
+
521
+ const newCollateralPre = addSubtract(
522
+ collateralPre,
523
+ giveColl,
524
+ wantColl,
525
+ );
526
+ // max debt supported by current Collateral as modified by proposal
527
+ const maxDebtPre = await state.manager.maxDebtFor(newCollateralPre);
528
+ updaterPre === ephemera.outerUpdater ||
529
+ Fail`Transfer during vault adjustment`;
530
+ helper.assertActive();
400
531
 
401
- const { self, helper } = facets;
402
- helper.assertCloseable();
403
- const { phase, vaultSeat } = state;
404
- const { zcf } = provideEphemera(state.idInManager);
532
+ // After the `await`, we retrieve the vault's allocations again,
533
+ // so we can compare to the debt limit based on the new values.
534
+ const collateral = helper.getCollateralAllocated(vaultSeat);
535
+ const newCollateral = addSubtract(collateral, giveColl, wantColl);
405
536
 
406
- // Held as keys for cleanup in the manager
407
- const oldDebtNormalized = self.getNormalizedDebt();
408
- const oldCollateral = self.getCollateralAmount();
537
+ const debt = self.getCurrentDebt();
538
+ const giveMinted = AmountMath.min(
539
+ proposal.give.Minted || helper.emptyDebt(),
540
+ debt,
541
+ );
542
+ const wantMinted = proposal.want.Minted || helper.emptyDebt();
543
+ if (allEmpty([giveColl, giveMinted, wantColl, wantMinted])) {
544
+ clientSeat.exit();
545
+ return 'no transaction, as requested';
546
+ }
547
+
548
+ // Calculate the fee, the amount to mint and the resulting debt. We'll
549
+ // verify that the target debt doesn't violate the collateralization ratio,
550
+ // then mint, reallocate, and burn.
551
+ const { newDebt, fee, toMint } = helper.loanFee(
552
+ debt,
553
+ giveMinted,
554
+ wantMinted,
555
+ );
409
556
 
410
- if (phase === Phase.ACTIVE) {
411
- assertProposalShape(seat, {
412
- give: { Minted: null },
557
+ trace('adjustBalancesHook', state.idInManager, {
558
+ newCollateralPre,
559
+ newCollateral,
560
+ fee,
561
+ toMint,
562
+ newDebt,
413
563
  });
414
564
 
415
- // you're paying off the debt, you get everything back.
416
- const debt = self.getCurrentDebt();
417
- const {
418
- give: { Minted: given },
419
- } = seat.getProposal();
420
-
421
- // you must pay off the entire remainder but if you offer too much, we won't
422
- // take more than you owe
423
- AmountMath.isGTE(given, debt) ||
424
- Fail`Offer ${given} is not sufficient to pay off debt ${debt}`;
565
+ if (
566
+ checkRestart(newCollateralPre, maxDebtPre, newCollateral, newDebt)
567
+ ) {
568
+ return helper.adjustBalancesHook(clientSeat);
569
+ }
425
570
 
426
- // Return any overpayment
427
- atomicTransfer(
428
- zcf,
571
+ stageDelta(clientSeat, vaultSeat, giveColl, wantColl, 'Collateral');
572
+ // `wantMinted` is allocated in the reallocate and mint operation, and so not here
573
+ stageDelta(
574
+ clientSeat,
429
575
  vaultSeat,
430
- seat,
431
- vaultSeat.getCurrentAllocation(),
576
+ giveMinted,
577
+ helper.emptyDebt(),
578
+ 'Minted',
432
579
  );
433
580
 
434
- state.manager.burnAndRecord(debt, seat);
435
- } else if (phase === Phase.LIQUIDATED) {
436
- // Simply reallocate vault assets to the offer seat.
437
- // Don't take anything from the offer, even if vault is underwater.
438
- // TODO verify that returning Minted here doesn't mess up debt limits
439
-
440
- atomicTransfer(
441
- zcf,
442
- vaultSeat,
443
- seat,
444
- vaultSeat.getCurrentAllocation(),
581
+ /** @type {Array<ZCFSeat>} */
582
+ const vaultSeatOpt = allEmpty([giveColl, giveMinted, wantColl])
583
+ ? []
584
+ : [vaultSeat];
585
+ state.manager.mintAndReallocate(
586
+ toMint,
587
+ fee,
588
+ clientSeat,
589
+ ...vaultSeatOpt,
445
590
  );
446
- } else {
447
- throw new Error('only active and liquidated vaults can be closed');
448
- }
449
-
450
- seat.exit();
451
- helper.assignPhase(Phase.CLOSED);
452
- helper.updateDebtSnapshot(helper.emptyDebt());
453
- helper.updateUiState();
454
-
455
- helper.assertVaultHoldsNoMinted();
456
- vaultSeat.exit();
457
-
458
- state.manager.handleBalanceChange(
459
- oldDebtNormalized,
460
- oldCollateral,
461
- state.idInManager,
462
- state.phase,
463
- facets.self,
464
- );
465
-
466
- return 'your loan is closed, thank you for your business';
467
- },
468
591
 
469
- /**
470
- * Calculate the fee, the amount to mint and the resulting debt.
471
- * The give and the want together reflect a delta, where typically
472
- * one is zero because they come from the gave/want of an offer
473
- * proposal. If the `want` is zero, the `fee` will also be zero,
474
- * so the simple math works.
475
- *
476
- * @param {Amount<'nat'>} currentDebt
477
- * @param {Amount<'nat'>} giveAmount
478
- * @param {Amount<'nat'>} wantAmount
479
- */
480
- loanFee(currentDebt, giveAmount, wantAmount) {
481
- const { state } = this;
482
-
483
- const fee = ceilMultiplyBy(
484
- wantAmount,
485
- state.manager.getGovernedParams().getLoanFee(),
486
- );
487
- const toMint = AmountMath.add(wantAmount, fee);
488
- const newDebt = addSubtract(currentDebt, toMint, giveAmount);
489
- return { newDebt, toMint, fee };
490
- },
592
+ // parent needs to know about the change in debt
593
+ helper.updateDebtAccounting(
594
+ normalizedDebtPre,
595
+ collateralPre,
596
+ newDebt,
597
+ );
598
+ state.manager.burnAndRecord(giveMinted, vaultSeat);
599
+ helper.assertVaultHoldsNoMinted();
491
600
 
492
- /**
493
- * Adjust principal and collateral (atomically for offer safety)
494
- *
495
- * @param {ZCFSeat} clientSeat
496
- * @returns {Promise<string>} success message
497
- */
498
- async adjustBalancesHook(clientSeat) {
499
- const { state, facets } = this;
500
-
501
- const { self, helper } = facets;
502
- const ephemera = provideEphemera(state.idInManager);
503
- const { outerUpdater: updaterPre } = ephemera;
504
- const { vaultSeat } = state;
505
- const proposal = clientSeat.getProposal();
506
- assertOnlyKeys(proposal, ['Collateral', 'Minted']);
507
-
508
- const allEmpty = amounts => {
509
- return amounts.every(a => AmountMath.isEmpty(a));
510
- };
511
-
512
- const normalizedDebtPre = self.getNormalizedDebt();
513
- const collateralPre = helper.getCollateralAllocated(vaultSeat);
514
-
515
- const giveColl = proposal.give.Collateral || helper.emptyCollateral();
516
- const wantColl = proposal.want.Collateral || helper.emptyCollateral();
517
-
518
- const newCollateralPre = addSubtract(collateralPre, giveColl, wantColl);
519
- // max debt supported by current Collateral as modified by proposal
520
- const maxDebtPre = await state.manager.maxDebtFor(newCollateralPre);
521
- updaterPre === ephemera.outerUpdater ||
522
- Fail`Transfer during vault adjustment`;
523
- helper.assertActive();
524
-
525
- // After the `await`, we retrieve the vault's allocations again,
526
- // so we can compare to the debt limit based on the new values.
527
- const collateral = helper.getCollateralAllocated(vaultSeat);
528
- const newCollateral = addSubtract(collateral, giveColl, wantColl);
529
-
530
- const debt = self.getCurrentDebt();
531
- const giveMinted = AmountMath.min(
532
- proposal.give.Minted || helper.emptyDebt(),
533
- debt,
534
- );
535
- const wantMinted = proposal.want.Minted || helper.emptyDebt();
536
- if (allEmpty([giveColl, giveMinted, wantColl, wantMinted])) {
601
+ helper.updateUiState();
537
602
  clientSeat.exit();
538
- return 'no transaction, as requested';
539
- }
540
-
541
- // Calculate the fee, the amount to mint and the resulting debt. We'll
542
- // verify that the target debt doesn't violate the collateralization ratio,
543
- // then mint, reallocate, and burn.
544
- const { newDebt, fee, toMint } = helper.loanFee(
545
- debt,
546
- giveMinted,
547
- wantMinted,
548
- );
549
-
550
- trace('adjustBalancesHook', state.idInManager, {
551
- newCollateralPre,
552
- newCollateral,
553
- fee,
554
- toMint,
555
- newDebt,
556
- });
557
-
558
- if (
559
- checkRestart(newCollateralPre, maxDebtPre, newCollateral, newDebt)
560
- ) {
561
- return helper.adjustBalancesHook(clientSeat);
562
- }
563
-
564
- stageDelta(clientSeat, vaultSeat, giveColl, wantColl, 'Collateral');
565
- // `wantMinted` is allocated in the reallocate and mint operation, and so not here
566
- stageDelta(
567
- clientSeat,
568
- vaultSeat,
569
- giveMinted,
570
- helper.emptyDebt(),
571
- 'Minted',
572
- );
573
-
574
- /** @type {Array<ZCFSeat>} */
575
- const vaultSeatOpt = allEmpty([giveColl, giveMinted, wantColl])
576
- ? []
577
- : [vaultSeat];
578
- state.manager.mintAndReallocate(
579
- toMint,
580
- fee,
581
- clientSeat,
582
- ...vaultSeatOpt,
583
- );
584
-
585
- // parent needs to know about the change in debt
586
- helper.updateDebtAccounting(normalizedDebtPre, collateralPre, newDebt);
587
- state.manager.burnAndRecord(giveMinted, vaultSeat);
588
- helper.assertVaultHoldsNoMinted();
589
-
590
- helper.updateUiState();
591
- clientSeat.exit();
592
- return 'We have adjusted your balances, thank you for your business';
593
- },
594
-
595
- /**
596
- *
597
- * @param {ZCFSeat} seat
598
- * @returns {VaultKit}
599
- */
600
- makeTransferInvitationHook(seat) {
601
- const { state, facets } = this;
602
-
603
- const { self, helper } = facets;
604
- helper.assertCloseable();
605
- seat.exit();
606
-
607
- const ephemera = provideEphemera(state.idInManager);
608
-
609
- // eslint-disable-next-line no-use-before-define
610
- const vaultKit = makeVaultKit(
611
- self,
612
- ephemera.storageNode,
613
- ephemera.marshaller,
614
- state.manager.getAssetSubscriber(),
615
- );
616
- ephemera.outerUpdater = vaultKit.vaultUpdater;
617
- helper.updateUiState();
618
-
619
- return vaultKit;
620
- },
621
- },
622
- self: {
623
- getVaultSeat() {
624
- return this.state.vaultSeat;
625
- },
626
-
627
- /**
628
- * @param {ZCFSeat} seat
629
- * @param {ERef<StorageNode>} storageNode
630
- */
631
- async initVaultKit(seat, storageNode) {
632
- const { state, facets } = this;
633
-
634
- const ephemera = provideEphemera(state.idInManager);
635
-
636
- const { self, helper } = facets;
637
-
638
- const normalizedDebtPre = self.getNormalizedDebt();
639
- const actualDebtPre = self.getCurrentDebt();
640
- (AmountMath.isEmpty(normalizedDebtPre) &&
641
- AmountMath.isEmpty(actualDebtPre)) ||
642
- Fail`vault must be empty initially`;
643
-
644
- const collateralPre = self.getCollateralAmount();
645
- trace('initVaultKit start: collateral', state.idInManager, {
646
- actualDebtPre,
647
- collateralPre,
648
- });
649
-
650
- // get the payout to provide access to the collateral if the
651
- // contract abandons
652
- const {
653
- give: { Collateral: giveCollateral },
654
- want: { Minted: wantMinted },
655
- } = seat.getProposal();
656
-
657
- const {
658
- newDebt: newDebtPre,
659
- fee,
660
- toMint,
661
- } = helper.loanFee(actualDebtPre, helper.emptyDebt(), wantMinted);
662
- !AmountMath.isEmpty(fee) ||
663
- Fail`loan requested (${wantMinted}) is too small; cannot accrue interest`;
664
- AmountMath.isEqual(newDebtPre, toMint) || Fail`fee mismatch for vault`;
665
- trace(
666
- 'initVault',
667
- state.idInManager,
668
- { wantedRun: wantMinted, fee },
669
- self.getCollateralAmount(),
670
- );
671
-
672
- await helper.assertSufficientCollateral(giveCollateral, newDebtPre);
673
-
674
- const { vaultSeat } = state;
675
- vaultSeat.incrementBy(
676
- seat.decrementBy(harden({ Collateral: giveCollateral })),
677
- );
678
- state.manager.mintAndReallocate(toMint, fee, seat, vaultSeat);
679
- helper.updateDebtAccounting(
680
- normalizedDebtPre,
681
- collateralPre,
682
- newDebtPre,
683
- );
684
-
685
- const vaultKit = makeVaultKit(
686
- self,
687
- storageNode,
688
- ephemera.marshaller,
689
- state.manager.getAssetSubscriber(),
690
- );
691
- ephemera.outerUpdater = vaultKit.vaultUpdater;
692
- helper.updateUiState();
693
- return vaultKit;
694
- },
695
-
696
- /**
697
- * Called by manager at start of liquidation.
698
- */
699
- liquidating() {
700
- const { facets } = this;
701
-
702
- const { helper } = facets;
703
- helper.assignPhase(Phase.LIQUIDATING);
704
- helper.updateUiState();
705
- },
603
+ return 'We have adjusted your balances, thank you for your business';
604
+ },
605
+
606
+ /**
607
+ *
608
+ * @param {ZCFSeat} seat
609
+ * @returns {VaultKit}
610
+ */
611
+ makeTransferInvitationHook(seat) {
612
+ const { state, facets } = this;
613
+
614
+ const { self, helper } = facets;
615
+ helper.assertCloseable();
616
+ seat.exit();
617
+
618
+ const ephemera = provideEphemera(state.idInManager);
619
+
620
+ // eslint-disable-next-line no-use-before-define
621
+ const vaultKit = makeVaultKit(
622
+ self,
623
+ ephemera.storageNode,
624
+ ephemera.marshaller,
625
+ state.manager.getAssetSubscriber(),
626
+ );
627
+ ephemera.outerUpdater = vaultKit.vaultUpdater;
628
+ helper.updateUiState();
706
629
 
707
- /**
708
- * Called by manager at end of liquidation, at which point all debts have been
709
- * covered.
710
- */
711
- liquidated() {
712
- const { facets } = this;
713
-
714
- const { helper } = facets;
715
- helper.updateDebtSnapshot(
716
- // liquidated vaults retain no debt
717
- AmountMath.makeEmpty(helper.debtBrand()),
718
- );
719
-
720
- helper.assignPhase(Phase.LIQUIDATED);
721
- helper.updateUiState();
630
+ return vaultKit;
631
+ },
722
632
  },
633
+ self: {
634
+ getVaultSeat() {
635
+ return this.state.vaultSeat;
636
+ },
637
+
638
+ /**
639
+ * @param {ZCFSeat} seat
640
+ * @param {ERef<StorageNode>} storageNode
641
+ */
642
+ async initVaultKit(seat, storageNode) {
643
+ const { state, facets } = this;
644
+
645
+ const ephemera = provideEphemera(state.idInManager);
646
+
647
+ const { self, helper } = facets;
648
+
649
+ const normalizedDebtPre = self.getNormalizedDebt();
650
+ const actualDebtPre = self.getCurrentDebt();
651
+ (AmountMath.isEmpty(normalizedDebtPre) &&
652
+ AmountMath.isEmpty(actualDebtPre)) ||
653
+ Fail`vault must be empty initially`;
654
+
655
+ const collateralPre = self.getCollateralAmount();
656
+ trace('initVaultKit start: collateral', state.idInManager, {
657
+ actualDebtPre,
658
+ collateralPre,
659
+ });
723
660
 
724
- makeAdjustBalancesInvitation() {
725
- const { state, facets } = this;
726
- const { helper } = facets;
727
- helper.assertActive();
728
- const { zcf } = provideEphemera(state.idInManager);
729
- return zcf.makeInvitation(
730
- seat => helper.adjustBalancesHook(seat),
731
- 'AdjustBalances',
732
- );
733
- },
661
+ // get the payout to provide access to the collateral if the
662
+ // contract abandons
663
+ const {
664
+ give: { Collateral: giveCollateral },
665
+ want: { Minted: wantMinted },
666
+ } = seat.getProposal();
734
667
 
735
- makeCloseInvitation() {
736
- const { state, facets } = this;
737
- const { helper } = facets;
738
- helper.assertCloseable();
739
- const { zcf } = provideEphemera(state.idInManager);
740
- return zcf.makeInvitation(seat => helper.closeHook(seat), 'CloseVault');
741
- },
668
+ const {
669
+ newDebt: newDebtPre,
670
+ fee,
671
+ toMint,
672
+ } = helper.loanFee(actualDebtPre, helper.emptyDebt(), wantMinted);
673
+ !AmountMath.isEmpty(fee) ||
674
+ Fail`loan requested (${wantMinted}) is too small; cannot accrue interest`;
675
+ AmountMath.isEqual(newDebtPre, toMint) ||
676
+ Fail`fee mismatch for vault`;
677
+ trace(
678
+ 'initVault',
679
+ state.idInManager,
680
+ { wantedRun: wantMinted, fee },
681
+ self.getCollateralAmount(),
682
+ );
742
683
 
743
- /**
744
- * @returns {Promise<Invitation>}
745
- */
746
- makeTransferInvitation() {
747
- const { state, facets } = this;
748
- const ephemera = provideEphemera(state.idInManager);
749
- const { outerUpdater } = ephemera;
750
- const { self, helper } = facets;
751
- // Bring the debt snapshot current for the final report before transfer
752
- helper.updateDebtSnapshot(self.getCurrentDebt());
753
- const { debtSnapshot: debt, interestSnapshot: interest, phase } = state;
754
- if (outerUpdater) {
755
- outerUpdater.finish(helper.getStateSnapshot(Phase.TRANSFER));
756
- ephemera.outerUpdater = null;
757
- }
758
- const transferState = {
759
- debtSnapshot: { debt, interest },
760
- locked: self.getCollateralAmount(),
761
- vaultState: phase,
762
- };
763
- const { zcf } = provideEphemera(state.idInManager);
764
- return zcf.makeInvitation(
765
- seat => helper.makeTransferInvitationHook(seat),
766
- 'TransferVault',
767
- transferState,
768
- );
769
- },
684
+ await helper.assertSufficientCollateral(giveCollateral, newDebtPre);
770
685
 
771
- // for status/debugging
772
-
773
- /**
774
- *
775
- * @returns {Amount<'nat'>}
776
- */
777
- getCollateralAmount() {
778
- const { state, facets } = this;
779
- const { vaultSeat } = state;
780
- const { helper } = facets;
781
- // getCollateralAllocated would return final allocations
782
- return vaultSeat.hasExited()
783
- ? helper.emptyCollateral()
784
- : helper.getCollateralAllocated(vaultSeat);
785
- },
686
+ const { vaultSeat } = state;
687
+ vaultSeat.incrementBy(
688
+ seat.decrementBy(harden({ Collateral: giveCollateral })),
689
+ );
690
+ state.manager.mintAndReallocate(toMint, fee, seat, vaultSeat);
691
+ helper.updateDebtAccounting(
692
+ normalizedDebtPre,
693
+ collateralPre,
694
+ newDebtPre,
695
+ );
786
696
 
787
- /**
788
- * The actual current debt, including accrued interest.
789
- *
790
- * This looks like a simple getter but it does a lot of the heavy lifting for
791
- * interest accrual. Rather than updating all records when interest accrues,
792
- * the vault manager updates just its rolling compounded interest. Here we
793
- * calculate what the current debt is given what's recorded in this vault and
794
- * what interest has compounded since this vault record was written.
795
- *
796
- * @see getNormalizedDebt
797
- *
798
- * @returns {Amount<'nat'>}
799
- */
800
- getCurrentDebt() {
801
- const { state } = this;
802
- return calculateCurrentDebt(
803
- state.debtSnapshot,
804
- state.interestSnapshot,
805
- state.manager.getCompoundedInterest(),
806
- );
807
- },
697
+ const vaultKit = makeVaultKit(
698
+ self,
699
+ storageNode,
700
+ ephemera.marshaller,
701
+ state.manager.getAssetSubscriber(),
702
+ );
703
+ ephemera.outerUpdater = vaultKit.vaultUpdater;
704
+ helper.updateUiState();
705
+ return vaultKit;
706
+ },
707
+
708
+ /**
709
+ * Called by manager at start of liquidation.
710
+ */
711
+ liquidating() {
712
+ const { facets } = this;
713
+
714
+ const { helper } = facets;
715
+ helper.assignPhase(Phase.LIQUIDATING);
716
+ helper.updateUiState();
717
+ },
718
+
719
+ /**
720
+ * Called by manager at end of liquidation, at which point all debts have been
721
+ * covered.
722
+ */
723
+ liquidated() {
724
+ const { facets } = this;
725
+
726
+ const { helper } = facets;
727
+ helper.updateDebtSnapshot(
728
+ // liquidated vaults retain no debt
729
+ AmountMath.makeEmpty(helper.debtBrand()),
730
+ );
808
731
 
809
- /**
810
- * The normalization puts all debts on a common time-independent scale since
811
- * the launch of this vault manager. This allows the manager to order vaults
812
- * by their debt-to-collateral ratios without having to mutate the debts as
813
- * the interest accrues.
814
- *
815
- * @see getActualDebAmount
816
- *
817
- * @returns {import('./storeUtils.js').NormalizedDebt} as if the vault was open at the launch of this manager, before any interest accrued
818
- */
819
- getNormalizedDebt() {
820
- const { state } = this;
821
- // @ts-expect-error cast
822
- return reverseInterest(state.debtSnapshot, state.interestSnapshot);
732
+ helper.assignPhase(Phase.LIQUIDATED);
733
+ helper.updateUiState();
734
+ },
735
+
736
+ makeAdjustBalancesInvitation() {
737
+ const { state, facets } = this;
738
+ const { helper } = facets;
739
+ helper.assertActive();
740
+ const { zcf } = provideEphemera(state.idInManager);
741
+ return zcf.makeInvitation(
742
+ seat => helper.adjustBalancesHook(seat),
743
+ 'AdjustBalances',
744
+ );
745
+ },
746
+
747
+ makeCloseInvitation() {
748
+ const { state, facets } = this;
749
+ const { helper } = facets;
750
+ helper.assertCloseable();
751
+ const { zcf } = provideEphemera(state.idInManager);
752
+ return zcf.makeInvitation(
753
+ seat => helper.closeHook(seat),
754
+ 'CloseVault',
755
+ );
756
+ },
757
+
758
+ /**
759
+ * @returns {Promise<Invitation>}
760
+ */
761
+ makeTransferInvitation() {
762
+ const { state, facets } = this;
763
+ const ephemera = provideEphemera(state.idInManager);
764
+ const { outerUpdater } = ephemera;
765
+ const { self, helper } = facets;
766
+ // Bring the debt snapshot current for the final report before transfer
767
+ helper.updateDebtSnapshot(self.getCurrentDebt());
768
+ const {
769
+ debtSnapshot: debt,
770
+ interestSnapshot: interest,
771
+ phase,
772
+ } = state;
773
+ if (outerUpdater) {
774
+ outerUpdater.finish(helper.getStateSnapshot(Phase.TRANSFER));
775
+ ephemera.outerUpdater = null;
776
+ }
777
+ const transferState = {
778
+ debtSnapshot: { debt, interest },
779
+ locked: self.getCollateralAmount(),
780
+ vaultState: phase,
781
+ };
782
+ const { zcf } = provideEphemera(state.idInManager);
783
+ return zcf.makeInvitation(
784
+ seat => helper.makeTransferInvitationHook(seat),
785
+ 'TransferVault',
786
+ transferState,
787
+ );
788
+ },
789
+
790
+ // for status/debugging
791
+
792
+ /**
793
+ *
794
+ * @returns {Amount<'nat'>}
795
+ */
796
+ getCollateralAmount() {
797
+ const { state, facets } = this;
798
+ const { vaultSeat } = state;
799
+ const { helper } = facets;
800
+ // getCollateralAllocated would return final allocations
801
+ return vaultSeat.hasExited()
802
+ ? helper.emptyCollateral()
803
+ : helper.getCollateralAllocated(vaultSeat);
804
+ },
805
+
806
+ /**
807
+ * The actual current debt, including accrued interest.
808
+ *
809
+ * This looks like a simple getter but it does a lot of the heavy lifting for
810
+ * interest accrual. Rather than updating all records when interest accrues,
811
+ * the vault manager updates just its rolling compounded interest. Here we
812
+ * calculate what the current debt is given what's recorded in this vault and
813
+ * what interest has compounded since this vault record was written.
814
+ *
815
+ * @see getNormalizedDebt
816
+ *
817
+ * @returns {Amount<'nat'>}
818
+ */
819
+ getCurrentDebt() {
820
+ const { state } = this;
821
+ return calculateCurrentDebt(
822
+ state.debtSnapshot,
823
+ state.interestSnapshot,
824
+ state.manager.getCompoundedInterest(),
825
+ );
826
+ },
827
+
828
+ /**
829
+ * The normalization puts all debts on a common time-independent scale since
830
+ * the launch of this vault manager. This allows the manager to order vaults
831
+ * by their debt-to-collateral ratios without having to mutate the debts as
832
+ * the interest accrues.
833
+ *
834
+ * @see getActualDebAmount
835
+ *
836
+ * @returns {import('./storeUtils.js').NormalizedDebt} as if the vault was open at the launch of this manager, before any interest accrued
837
+ */
838
+ getNormalizedDebt() {
839
+ const { state } = this;
840
+ // @ts-expect-error cast
841
+ return reverseInterest(state.debtSnapshot, state.interestSnapshot);
842
+ },
823
843
  },
824
844
  },
825
- },
826
- );
827
-
828
- /**
829
- * @param {ZCF} zcf
830
- * @param {VaultManager} manager
831
- * @param {VaultId} idInManager
832
- */
833
- export const makeVault = pickFacet(makeVaultBase, 'self');
845
+ );
846
+ return maker;
847
+ };
834
848
 
835
- /** @typedef {ReturnType<typeof makeVault>} Vault */
849
+ /** @typedef {ReturnType<ReturnType<typeof vivifyVault>>['self']} Vault */