@agoric/inter-protocol 0.13.3 → 0.13.4-dev-800c2bb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +26 -20
  2. package/bundles/bundle-auctioneer-js-meta.json +722 -0
  3. package/bundles/bundle-auctioneer.js +1 -0
  4. package/bundles/bundle-econCommitteeCharter-js-meta.json +514 -0
  5. package/bundles/bundle-econCommitteeCharter.js +1 -0
  6. package/bundles/bundle-feeDistributor-js-meta.json +458 -0
  7. package/bundles/bundle-feeDistributor.js +1 -1
  8. package/bundles/bundle-fluxAggregatorKit-js-meta.json +674 -0
  9. package/bundles/bundle-fluxAggregatorKit.js +1 -0
  10. package/bundles/bundle-psm-js-meta.json +666 -0
  11. package/bundles/bundle-psm.js +1 -1
  12. package/bundles/bundle-reserve-js-meta.json +662 -0
  13. package/bundles/bundle-reserve.js +1 -1
  14. package/bundles/bundle-scaledPriceAuthority-js-meta.json +542 -0
  15. package/bundles/bundle-scaledPriceAuthority.js +1 -1
  16. package/bundles/bundle-vaultFactory-js-meta.json +786 -0
  17. package/bundles/bundle-vaultFactory.js +1 -1
  18. package/exported.js +0 -2
  19. package/package.json +34 -31
  20. package/scripts/add-collateral-core.js +14 -4
  21. package/scripts/build-bundles.js +9 -2
  22. package/scripts/deploy-contracts.js +1 -4
  23. package/scripts/init-core.js +20 -38
  24. package/scripts/invite-committee-core.js +1 -1
  25. package/scripts/manual-price-feed.js +2 -2
  26. package/scripts/price-feed-core.js +7 -6
  27. package/scripts/start-local-chain.sh +84 -0
  28. package/src/auction/auctionBook.js +755 -0
  29. package/src/auction/auctioneer.js +720 -0
  30. package/src/auction/offerBook.js +221 -0
  31. package/src/auction/params.js +174 -0
  32. package/src/auction/scheduleMath.js +167 -0
  33. package/src/auction/scheduler.js +370 -0
  34. package/src/auction/sortedOffers.js +136 -0
  35. package/src/auction/util.js +55 -0
  36. package/src/clientSupport.js +324 -0
  37. package/src/collectFees.js +8 -14
  38. package/src/contractSupport.js +24 -68
  39. package/src/econCommitteeCharter.js +161 -77
  40. package/src/feeDistributor.js +60 -20
  41. package/src/index.js +2 -0
  42. package/src/interest-math.js +1 -4
  43. package/src/interest.js +20 -13
  44. package/src/price/README.md +57 -0
  45. package/src/price/fluxAggregatorContract.js +190 -0
  46. package/src/price/fluxAggregatorKit.js +375 -0
  47. package/src/price/priceOracleKit.js +127 -0
  48. package/src/price/roundsManager.js +726 -0
  49. package/src/proposals/addAssetToVault.js +89 -94
  50. package/src/proposals/committee-proposal.js +141 -40
  51. package/src/proposals/core-proposal.js +100 -167
  52. package/src/proposals/econ-behaviors.js +320 -564
  53. package/src/proposals/price-feed-proposal.js +175 -78
  54. package/src/proposals/startEconCommittee.js +25 -12
  55. package/src/proposals/startPSM.js +231 -211
  56. package/src/proposals/utils.js +31 -17
  57. package/src/psm/README.md +71 -0
  58. package/src/psm/psm.js +272 -160
  59. package/src/psm/types.js +2 -0
  60. package/src/reserve/assetReserve.js +56 -413
  61. package/src/reserve/assetReserveKit.js +303 -0
  62. package/src/reserve/doc.md +1 -3
  63. package/src/reserve/params.js +2 -5
  64. package/src/typeGuards.js +13 -0
  65. package/src/vaultFactory/Liquidation.md +100 -0
  66. package/src/vaultFactory/burn.js +2 -1
  67. package/src/vaultFactory/liquidation.js +270 -108
  68. package/src/vaultFactory/math.js +95 -0
  69. package/src/vaultFactory/orderedVaultStore.js +0 -1
  70. package/src/vaultFactory/params.js +133 -112
  71. package/src/vaultFactory/prioritizedVaults.js +36 -79
  72. package/src/vaultFactory/storeUtils.js +67 -9
  73. package/src/vaultFactory/type-imports.js +2 -0
  74. package/src/vaultFactory/types.js +22 -26
  75. package/src/vaultFactory/vault.js +772 -709
  76. package/src/vaultFactory/vaultDirector.js +480 -483
  77. package/src/vaultFactory/vaultFactory.js +99 -60
  78. package/src/vaultFactory/vaultHolder.js +121 -95
  79. package/src/vaultFactory/vaultKit.js +39 -31
  80. package/src/vaultFactory/vaultManager.js +1249 -868
  81. package/bundles/bundle-amm-meta.js +0 -618
  82. package/bundles/bundle-amm.js +0 -1
  83. package/bundles/bundle-feeDistributor-meta.js +0 -346
  84. package/bundles/bundle-interchainPool-meta.js +0 -534
  85. package/bundles/bundle-interchainPool.js +0 -1
  86. package/bundles/bundle-liquidateIncrementally-meta.js +0 -442
  87. package/bundles/bundle-liquidateIncrementally.js +0 -1
  88. package/bundles/bundle-liquidateMinimum-meta.js +0 -442
  89. package/bundles/bundle-liquidateMinimum.js +0 -1
  90. package/bundles/bundle-psm-meta.js +0 -538
  91. package/bundles/bundle-psmCharter-meta.js +0 -418
  92. package/bundles/bundle-psmCharter.js +0 -1
  93. package/bundles/bundle-reserve-meta.js +0 -542
  94. package/bundles/bundle-scaledPriceAuthority-meta.js +0 -442
  95. package/bundles/bundle-stakeFactory-meta.js +0 -574
  96. package/bundles/bundle-stakeFactory.js +0 -1
  97. package/bundles/bundle-vaultFactory-meta.js +0 -598
  98. package/scripts/add-initial-liquidity.js +0 -180
  99. package/src/collect.js +0 -13
  100. package/src/interchainPool.js +0 -135
  101. package/src/makeTracer.js +0 -36
  102. package/src/my-lien.js +0 -85
  103. package/src/proposals/demoIssuers.js +0 -685
  104. package/src/proposals/sim-behaviors.js +0 -40
  105. package/src/psm/psmCharter.js +0 -139
  106. package/src/stakeFactory/README.md +0 -62
  107. package/src/stakeFactory/attestation.js +0 -267
  108. package/src/stakeFactory/attestationTool.js +0 -66
  109. package/src/stakeFactory/constants.js +0 -16
  110. package/src/stakeFactory/params.js +0 -31
  111. package/src/stakeFactory/stakeFactory.js +0 -236
  112. package/src/stakeFactory/stakeFactoryKit.js +0 -455
  113. package/src/stakeFactory/stakeFactoryManager.js +0 -296
  114. package/src/stakeFactory/types.js +0 -42
  115. package/src/vaultFactory/liquidateIncrementally.js +0 -350
  116. package/src/vaultFactory/liquidateMinimum.js +0 -100
  117. package/src/vpool-xyk-amm/AttackersGuide.md +0 -89
  118. package/src/vpool-xyk-amm/addLiquidity.js +0 -222
  119. package/src/vpool-xyk-amm/addPool.js +0 -197
  120. package/src/vpool-xyk-amm/constantProduct/AMM-Trade.jpeg +0 -0
  121. package/src/vpool-xyk-amm/constantProduct/README.md +0 -204
  122. package/src/vpool-xyk-amm/constantProduct/calcFees.js +0 -99
  123. package/src/vpool-xyk-amm/constantProduct/calcSwapPrices.js +0 -85
  124. package/src/vpool-xyk-amm/constantProduct/core.js +0 -153
  125. package/src/vpool-xyk-amm/constantProduct/defaults.js +0 -5
  126. package/src/vpool-xyk-amm/constantProduct/getXY.js +0 -52
  127. package/src/vpool-xyk-amm/constantProduct/internal-types.js +0 -116
  128. package/src/vpool-xyk-amm/constantProduct/invariants.js +0 -43
  129. package/src/vpool-xyk-amm/constantProduct/swap.js +0 -272
  130. package/src/vpool-xyk-amm/constantProduct/types.js +0 -32
  131. package/src/vpool-xyk-amm/doublePool.js +0 -194
  132. package/src/vpool-xyk-amm/estimate.js +0 -155
  133. package/src/vpool-xyk-amm/multipoolMarketMaker.js +0 -520
  134. package/src/vpool-xyk-amm/params.js +0 -61
  135. package/src/vpool-xyk-amm/pool.js +0 -396
  136. package/src/vpool-xyk-amm/priceAuthority.js +0 -94
  137. package/src/vpool-xyk-amm/removeLiquidity.js +0 -31
  138. package/src/vpool-xyk-amm/singlePool.js +0 -73
  139. package/src/vpool-xyk-amm/swap.js +0 -69
  140. package/src/vpool-xyk-amm/types.js +0 -154
@@ -1,31 +1,23 @@
1
- // @ts-check
2
- import '@agoric/zoe/exported.js';
3
-
4
- import {
5
- assertProposalShape,
6
- makeRatioFromAmounts,
7
- ceilMultiplyBy,
8
- floorMultiplyBy,
9
- } from '@agoric/zoe/src/contractSupport/index.js';
10
- import { AmountMath } from '@agoric/ertp';
11
- import {
12
- defineDurableKindMulti,
13
- makeKindHandle,
14
- pickFacet,
15
- } from '@agoric/vat-data';
16
- import { makeTracer } from '../makeTracer.js';
17
- import { calculateCurrentDebt, reverseInterest } from '../interest-math.js';
18
- import { makeVaultKit } from './vaultKit.js';
1
+ import { AmountMath, AmountShape } from '@agoric/ertp';
2
+ import { makeTracer, StorageNodeShape } from '@agoric/internal';
3
+ import { M, prepareExoClassKit } from '@agoric/vat-data';
4
+ import { atomicTransfer } from '@agoric/zoe/src/contractSupport/index.js';
5
+ import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
19
6
  import {
20
7
  addSubtract,
21
- assertOnlyKeys,
22
- makeEphemeraProvider,
23
- stageDelta,
8
+ allEmpty,
9
+ makeNatAmountShape,
24
10
  } from '../contractSupport.js';
11
+ import { calculateCurrentDebt, reverseInterest } from '../interest-math.js';
12
+ import { UnguardedHelperI } from '../typeGuards.js';
13
+ import { prepareVaultKit } from './vaultKit.js';
14
+
15
+ import '@agoric/zoe/exported.js';
16
+ import { calculateDebtCosts } from './math.js';
25
17
 
26
- const { details: X, quote: q } = assert;
18
+ const { quote: q, Fail } = assert;
27
19
 
28
- const trace = makeTracer('IV', false);
20
+ const trace = makeTracer('Vault', true);
29
21
 
30
22
  /** @typedef {import('./storeUtils.js').NormalizedDebt} NormalizedDebt */
31
23
 
@@ -70,7 +62,7 @@ export const Phase = /** @type {const} */ ({
70
62
  */
71
63
  const validTransitions = {
72
64
  [Phase.ACTIVE]: [Phase.LIQUIDATING, Phase.CLOSED],
73
- [Phase.LIQUIDATING]: [Phase.LIQUIDATED],
65
+ [Phase.LIQUIDATING]: [Phase.LIQUIDATED, Phase.ACTIVE],
74
66
  [Phase.LIQUIDATED]: [Phase.CLOSED],
75
67
  [Phase.CLOSED]: [],
76
68
  };
@@ -88,11 +80,12 @@ const validTransitions = {
88
80
  /**
89
81
  * @typedef {object} VaultManager
90
82
  * @property {() => Subscriber<import('./vaultManager').AssetState>} getAssetSubscriber
91
- * @property {(collateralAmount: Amount) => ERef<Amount<'nat'>>} maxDebtFor
83
+ * @property {(collateralAmount: Amount) => Amount<'nat'>} maxDebtFor
92
84
  * @property {() => Brand} getCollateralBrand
85
+ * @property {(base: string) => string} scopeDescription
93
86
  * @property {() => Brand<'nat'>} getDebtBrand
94
- * @property {MintAndReallocate} mintAndReallocate
95
- * @property {(amount: Amount, seat: ZCFSeat) => void} burnAndRecord
87
+ * @property {MintAndTransfer} mintAndTransfer
88
+ * @property {(amount: Amount, seat: ZCFSeat) => void} burn
96
89
  * @property {() => Ratio} getCompoundedInterest
97
90
  * @property {(oldDebt: import('./storeUtils.js').NormalizedDebt, oldCollateral: Amount<'nat'>, vaultId: VaultId, vaultPhase: VaultPhase, vault: Vault) => void} handleBalanceChange
98
91
  * @property {() => import('./vaultManager.js').GovernedParamGetters} getGovernedParams
@@ -102,6 +95,7 @@ const validTransitions = {
102
95
  * @typedef {Readonly<{
103
96
  * idInManager: VaultId,
104
97
  * manager: VaultManager,
98
+ * storageNode: StorageNode,
105
99
  * vaultSeat: ZCFSeat,
106
100
  * }>} ImmutableState
107
101
  */
@@ -113,696 +107,765 @@ const validTransitions = {
113
107
  * interestSnapshot: Ratio,
114
108
  * phase: VaultPhase,
115
109
  * debtSnapshot: Amount<'nat'>,
110
+ * outerUpdater: import('@agoric/zoe/src/contractSupport/recorder.js').Recorder<VaultNotification> | null,
116
111
  * }} MutableState
117
112
  */
118
113
 
119
- /**
120
- * @typedef {Readonly<{
121
- * state: ImmutableState & MutableState,
122
- * facets: {
123
- * self: import('@agoric/vat-data/src/types').KindFacet<typeof selfBehavior>,
124
- * helper: import('@agoric/vat-data/src/types').KindFacet<typeof helperBehavior>,
125
- * },
126
- * }>} MethodContext
127
- */
114
+ export const VaultI = M.interface('Vault', {
115
+ getCollateralAmount: M.call().returns(AmountShape),
116
+ getCurrentDebt: M.call().returns(AmountShape),
117
+ getNormalizedDebt: M.call().returns(AmountShape),
118
+ getVaultSeat: M.call().returns(SeatShape),
119
+ initVaultKit: M.call(SeatShape, StorageNodeShape).returns(M.promise()),
120
+ liquidated: M.call().returns(undefined),
121
+ liquidating: M.call().returns(undefined),
122
+ makeAdjustBalancesInvitation: M.call().returns(M.promise()),
123
+ makeCloseInvitation: M.call().returns(M.promise()),
124
+ makeTransferInvitation: M.call().returns(M.promise()),
125
+ abortLiquidation: M.call().returns(M.string()),
126
+ });
128
127
 
129
- /**
130
- * Ephemera are the elements of state that cannot (or need not) be durable.
131
- * When there's a single instance it can be held in a closure, but there are
132
- * many vault manaager objects. So we hold their ephemera keyed by the durable
133
- * vault manager object reference.
134
- *
135
- * @type {(key: VaultId) => {
136
- * storageNode: ERef<StorageNode>,
137
- * marshaller: ERef<Marshaller>,
138
- * outerUpdater: Publisher<VaultNotification> | null,
139
- * zcf: ZCF,
140
- * }} */
141
- // @ts-expect-error not yet defined
142
- const provideEphemera = makeEphemeraProvider(() => ({}));
128
+ const VaultStateShape = harden({
129
+ idInManager: M.any(),
130
+ manager: M.any(),
131
+ outerUpdater: M.any(),
132
+ phase: M.any(),
133
+ storageNode: M.any(),
134
+ vaultSeat: M.any(),
135
+ interestSnapshot: M.any(),
136
+ debtSnapshot: M.any(),
137
+ });
143
138
 
144
139
  /**
140
+ * @param {import('@agoric/ertp').Baggage} baggage
141
+ * @param {import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit} makeRecorderKit
145
142
  * @param {ZCF} zcf
146
- * @param {VaultManager} manager
147
- * @param {VaultId} idInManager
148
- * @param {ERef<StorageNode>} storageNode
149
- * @param {ERef<Marshaller>} marshaller
150
143
  */
151
- const initState = (zcf, manager, idInManager, storageNode, marshaller) => {
152
- const ephemera = provideEphemera(idInManager);
153
- ephemera.storageNode = storageNode;
154
- ephemera.marshaller = marshaller;
155
- ephemera.zcf = zcf;
156
-
157
- /**
158
- * @type {ImmutableState & MutableState}
159
- */
160
- return harden({
161
- idInManager,
162
- manager,
163
- outerUpdater: null,
164
- phase: Phase.ACTIVE,
165
-
166
- // vaultSeat will hold the collateral until the loan is retired. The
167
- // payout from it will be handed to the user: if the vault dies early
168
- // (because the vaultFactory vat died), they'll get all their
169
- // collateral back. If that happens, the issuer for the Minted will be dead,
170
- // so their loan will be worthless.
171
- vaultSeat: zcf.makeEmptySeatKit().zcfSeat,
172
-
173
- // Two values from the same moment
174
- interestSnapshot: manager.getCompoundedInterest(),
175
- debtSnapshot: AmountMath.makeEmpty(manager.getDebtBrand()),
176
- });
177
- };
178
-
179
- /**
180
- * Check whether we can proceed with an `adjustBalances`.
181
- *
182
- * @param {Amount} newCollateralPre
183
- * @param {Amount} maxDebtPre
184
- * @param {Amount} newCollateral
185
- * @param {Amount} newDebt
186
- * @returns {boolean}
187
- */
188
- const checkRestart = (newCollateralPre, maxDebtPre, newCollateral, newDebt) => {
189
- if (AmountMath.isGTE(newCollateralPre, newCollateral)) {
190
- // The collateral did not go up. If the collateral decreased, we pro-rate maxDebt.
191
- // We can pro-rate maxDebt because the quote is either linear (price is
192
- // unchanging) or super-linear (also called "convex"). Super-linear is from
193
- // AMMs: selling less collateral would mean an even smaller price impact, so
194
- // this is a conservative choice.
195
- const debtPerCollateral = makeRatioFromAmounts(
196
- maxDebtPre,
197
- newCollateralPre,
198
- );
199
- // `floorMultiply` because the debt ceiling should be tight
200
- const maxDebtAfter = floorMultiplyBy(newCollateral, debtPerCollateral);
201
- assert(
202
- AmountMath.isGTE(maxDebtAfter, newDebt),
203
- X`The requested debt ${q(
204
- newDebt,
205
- )} is more than the collateralization ratio allows: ${q(maxDebtAfter)}`,
206
- );
207
- // The `collateralAfter` can still cover the `newDebt`, so don't restart.
208
- return false;
209
- }
210
- // The collateral went up. Restart if the debt *also* went up because
211
- // the price quote might not apply at the higher numbers.
212
- return !AmountMath.isGTE(maxDebtPre, newDebt);
213
- };
214
-
215
- const helperBehavior = {
216
- // #region Computed constants
217
- collateralBrand: ({ state }) => state.manager.getCollateralBrand(),
218
- debtBrand: ({ state }) => state.manager.getDebtBrand(),
219
-
220
- emptyCollateral: ({ facets }) =>
221
- AmountMath.makeEmpty(facets.helper.collateralBrand()),
222
- emptyDebt: ({ facets }) => AmountMath.makeEmpty(facets.helper.debtBrand()),
223
- // #endregion
224
-
225
- // #region Phase logic
226
- /**
227
- * @param {MethodContext} context
228
- * @param {VaultPhase} newPhase
229
- */
230
- assignPhase: ({ state }, newPhase) => {
231
- const { phase } = state;
232
- const validNewPhases = validTransitions[phase];
233
- validNewPhases.includes(newPhase) ||
234
- assert.fail(
235
- X`Vault cannot transition from ${q(phase)} to ${q(newPhase)}`,
236
- );
237
- state.phase = newPhase;
238
- },
239
-
240
- assertActive: ({ state }) => {
241
- const { phase } = state;
242
- assert(phase === Phase.ACTIVE);
243
- },
244
-
245
- assertCloseable: ({ state }) => {
246
- const { phase } = state;
247
- phase === Phase.ACTIVE ||
248
- phase === Phase.LIQUIDATED ||
249
- assert.fail(
250
- X`to be closed a vault must be active or liquidated, not ${phase}`,
251
- );
252
- },
253
- // #endregion
254
-
255
- /**
256
- * Called whenever the debt is paid or created through a transaction,
257
- * but not for interest accrual.
258
- *
259
- * @param {MethodContext} context
260
- * @param {Amount} newDebt - principal and all accrued interest
261
- */
262
- updateDebtSnapshot: ({ state }, newDebt) => {
263
- // update local state
264
- state.debtSnapshot = newDebt;
265
- state.interestSnapshot = state.manager.getCompoundedInterest();
266
- },
267
-
268
- /**
269
- * Update the debt balance and propagate upwards to
270
- * maintain aggregate debt and liquidation order.
271
- *
272
- * @param {MethodContext} context
273
- * @param {NormalizedDebt} oldDebtNormalized - prior principal and all accrued interest, normalized to the launch of the vaultManager
274
- * @param {Amount<'nat'>} oldCollateral - actual collateral
275
- * @param {Amount<'nat'>} newDebtActual - actual principal and all accrued interest
276
- */
277
- updateDebtAccounting: (
278
- { state, facets },
279
- oldDebtNormalized,
280
- oldCollateral,
281
- newDebtActual,
282
- ) => {
283
- const { helper } = facets;
284
- helper.updateDebtSnapshot(newDebtActual);
285
- // notify manager so it can notify and clean up as appropriate
286
- state.manager.handleBalanceChange(
287
- oldDebtNormalized,
288
- oldCollateral,
289
- state.idInManager,
290
- state.phase,
291
- facets.self,
292
- );
293
- },
294
-
295
- /**
296
- *
297
- * @param {MethodContext} context
298
- * @param {ZCFSeat} seat
299
- */
300
- getCollateralAllocated: ({ facets }, seat) =>
301
- seat.getAmountAllocated('Collateral', facets.helper.collateralBrand()),
302
- getMintedAllocated: ({ facets }, seat) =>
303
- seat.getAmountAllocated('Minted', facets.helper.debtBrand()),
304
-
305
- assertVaultHoldsNoMinted: ({ state, facets }) => {
306
- const { vaultSeat } = state;
307
- AmountMath.isEmpty(facets.helper.getMintedAllocated(vaultSeat)) ||
308
- assert.fail(X`Vault should be empty of Minted`);
309
- },
310
-
311
- /**
312
- *
313
- * @param {MethodContext} context
314
- * @param {Amount<'nat'>} collateralAmount
315
- * @param {Amount<'nat'>} proposedRunDebt
316
- */
317
- assertSufficientCollateral: async (
318
- { state, facets },
319
- collateralAmount,
320
- proposedRunDebt,
321
- ) => {
322
- const maxRun = await state.manager.maxDebtFor(collateralAmount);
323
- assert(
324
- AmountMath.isGTE(maxRun, proposedRunDebt, facets.helper.debtBrand()),
325
- X`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)} for ${q(
326
- collateralAmount,
327
- )} collateral`,
328
- );
329
- },
330
-
331
- /**
332
- *
333
- * @param {MethodContext} context
334
- * @param {HolderPhase} newPhase
335
- */
336
- getStateSnapshot: ({ state, facets }, newPhase) => {
337
- const { debtSnapshot: debt, interestSnapshot: interest } = state;
338
- /** @type {VaultNotification} */
339
- return harden({
340
- debtSnapshot: { debt, interest },
341
- locked: facets.self.getCollateralAmount(),
342
- // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase
343
- // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415
344
- vaultState: newPhase,
345
- });
346
- },
347
-
348
- /**
349
- * call this whenever anything changes!
350
- *
351
- * @param {MethodContext} context
352
- */
353
- updateUiState: ({ state, facets }) => {
354
- const ephemera = provideEphemera(state.idInManager);
355
- const { outerUpdater } = ephemera;
356
- if (!outerUpdater) {
357
- // It's not an error to change to liquidating during transfer
358
- return;
359
- }
360
- const { phase } = state;
361
- const uiState = facets.helper.getStateSnapshot(phase);
362
- trace('updateUiState', state.idInManager, uiState);
363
-
364
- switch (phase) {
365
- case Phase.ACTIVE:
366
- case Phase.LIQUIDATING:
367
- case Phase.LIQUIDATED:
368
- outerUpdater.publish(uiState);
369
- break;
370
- case Phase.CLOSED:
371
- outerUpdater.finish(uiState);
372
- ephemera.outerUpdater = null;
373
- break;
374
- default:
375
- throw Error(`unreachable vault phase: ${phase}`);
376
- }
377
- },
378
-
379
- /**
380
- * @param {MethodContext} context
381
- * @param {ZCFSeat} seat
382
- */
383
- closeHook: async ({ state, facets }, seat) => {
384
- const { self, helper } = facets;
385
- helper.assertCloseable();
386
- const { phase, vaultSeat } = state;
387
- const { zcf } = provideEphemera(state.idInManager);
388
-
389
- // Held as keys for cleanup in the manager
390
- const oldDebtNormalized = self.getNormalizedDebt();
391
- const oldCollateral = self.getCollateralAmount();
392
-
393
- if (phase === Phase.ACTIVE) {
394
- assertProposalShape(seat, {
395
- give: { Minted: null },
144
+ export const prepareVault = (baggage, makeRecorderKit, zcf) => {
145
+ const makeVaultKit = prepareVaultKit(baggage, makeRecorderKit);
146
+
147
+ const maker = prepareExoClassKit(
148
+ baggage,
149
+ 'Vault',
150
+ {
151
+ helper: UnguardedHelperI,
152
+ self: VaultI,
153
+ },
154
+ /**
155
+ * @param {VaultManager} manager
156
+ * @param {VaultId} idInManager
157
+ * @param {StorageNode} storageNode
158
+ * @returns {ImmutableState & MutableState}
159
+ */
160
+ (manager, idInManager, storageNode) => {
161
+ return harden({
162
+ idInManager,
163
+ manager,
164
+ outerUpdater: null,
165
+ phase: Phase.ACTIVE,
166
+
167
+ storageNode,
168
+
169
+ // vaultSeat will hold the collateral until the position is retired. The
170
+ // payout from it will be handed to the user: if the vault dies early
171
+ // (because the vaultFactory vat died), they'll get all their
172
+ // collateral back. If that happens, the issuer for the Minted will be dead,
173
+ // so their position will be worthless.
174
+ vaultSeat: zcf.makeEmptySeatKit().zcfSeat,
175
+
176
+ // Two values from the same moment
177
+ interestSnapshot: manager.getCompoundedInterest(),
178
+ debtSnapshot: AmountMath.makeEmpty(manager.getDebtBrand()),
396
179
  });
397
-
398
- // you're paying off the debt, you get everything back.
399
- const debt = self.getCurrentDebt();
400
- const {
401
- give: { Minted: given },
402
- } = seat.getProposal();
403
-
404
- // you must pay off the entire remainder but if you offer too much, we won't
405
- // take more than you owe
406
- AmountMath.isGTE(given, debt) ||
407
- assert.fail(
408
- X`Offer ${given} is not sufficient to pay off debt ${debt}`,
409
- );
410
-
411
- // Return any overpayment
412
- seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation()));
413
- zcf.reallocate(seat, vaultSeat);
414
- state.manager.burnAndRecord(debt, seat);
415
- } else if (phase === Phase.LIQUIDATED) {
416
- // Simply reallocate vault assets to the offer seat.
417
- // Don't take anything from the offer, even if vault is underwater.
418
- // TODO verify that returning Minted here doesn't mess up debt limits
419
- seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation()));
420
- zcf.reallocate(seat, vaultSeat);
421
- } else {
422
- throw new Error('only active and liquidated vaults can be closed');
423
- }
424
-
425
- seat.exit();
426
- helper.assignPhase(Phase.CLOSED);
427
- helper.updateDebtSnapshot(helper.emptyDebt());
428
- helper.updateUiState();
429
-
430
- helper.assertVaultHoldsNoMinted();
431
- vaultSeat.exit();
432
-
433
- state.manager.handleBalanceChange(
434
- oldDebtNormalized,
435
- oldCollateral,
436
- state.idInManager,
437
- state.phase,
438
- facets.self,
439
- );
440
-
441
- return 'your loan is closed, thank you for your business';
442
- },
443
-
444
- /**
445
- * Calculate the fee, the amount to mint and the resulting debt.
446
- * The give and the want together reflect a delta, where typically
447
- * one is zero because they come from the gave/want of an offer
448
- * proposal. If the `want` is zero, the `fee` will also be zero,
449
- * so the simple math works.
450
- *
451
- * @param {MethodContext} context
452
- * @param {Amount} currentDebt
453
- * @param {Amount} giveAmount
454
- * @param {Amount} wantAmount
455
- */
456
- loanFee: ({ state }, currentDebt, giveAmount, wantAmount) => {
457
- const fee = ceilMultiplyBy(
458
- wantAmount,
459
- state.manager.getGovernedParams().getLoanFee(),
460
- );
461
- const toMint = AmountMath.add(wantAmount, fee);
462
- const newDebt = addSubtract(currentDebt, toMint, giveAmount);
463
- return { newDebt, toMint, fee };
464
- },
465
-
466
- /**
467
- * Adjust principal and collateral (atomically for offer safety)
468
- *
469
- * @param {MethodContext} context
470
- * @param {ZCFSeat} clientSeat
471
- * @returns {Promise<string>} success message
472
- */
473
- adjustBalancesHook: async ({ state, facets }, clientSeat) => {
474
- const { self, helper } = facets;
475
- const ephemera = provideEphemera(state.idInManager);
476
- const { outerUpdater: updaterPre } = ephemera;
477
- const { vaultSeat } = state;
478
- const proposal = clientSeat.getProposal();
479
- assertOnlyKeys(proposal, ['Collateral', 'Minted']);
480
-
481
- const allEmpty = amounts => {
482
- return amounts.every(a => AmountMath.isEmpty(a));
483
- };
484
-
485
- const normalizedDebtPre = self.getNormalizedDebt();
486
- const collateralPre = helper.getCollateralAllocated(vaultSeat);
487
-
488
- const giveColl = proposal.give.Collateral || helper.emptyCollateral();
489
- const wantColl = proposal.want.Collateral || helper.emptyCollateral();
490
-
491
- const newCollateralPre = addSubtract(collateralPre, giveColl, wantColl);
492
- // max debt supported by current Collateral as modified by proposal
493
- const maxDebtPre = await state.manager.maxDebtFor(newCollateralPre);
494
- updaterPre === ephemera.outerUpdater ||
495
- assert.fail(X`Transfer during vault adjustment`);
496
- helper.assertActive();
497
-
498
- // After the `await`, we retrieve the vault's allocations again,
499
- // so we can compare to the debt limit based on the new values.
500
- const collateral = helper.getCollateralAllocated(vaultSeat);
501
- const newCollateral = addSubtract(collateral, giveColl, wantColl);
502
-
503
- const debt = self.getCurrentDebt();
504
- const giveMinted = AmountMath.min(
505
- proposal.give.Minted || helper.emptyDebt(),
506
- debt,
507
- );
508
- const wantMinted = proposal.want.Minted || helper.emptyDebt();
509
- if (allEmpty([giveColl, giveMinted, wantColl, wantMinted])) {
510
- clientSeat.exit();
511
- return 'no transaction, as requested';
512
- }
513
-
514
- // Calculate the fee, the amount to mint and the resulting debt. We'll
515
- // verify that the target debt doesn't violate the collateralization ratio,
516
- // then mint, reallocate, and burn.
517
- const { newDebt, fee, toMint } = helper.loanFee(
518
- debt,
519
- giveMinted,
520
- wantMinted,
521
- );
522
-
523
- trace('adjustBalancesHook', state.idInManager, {
524
- newCollateralPre,
525
- newCollateral,
526
- fee,
527
- toMint,
528
- newDebt,
529
- });
530
-
531
- if (checkRestart(newCollateralPre, maxDebtPre, newCollateral, newDebt)) {
532
- return helper.adjustBalancesHook(clientSeat);
533
- }
534
-
535
- stageDelta(clientSeat, vaultSeat, giveColl, wantColl, 'Collateral');
536
- // `wantMinted` is allocated in the reallocate and mint operation, and so not here
537
- stageDelta(clientSeat, vaultSeat, giveMinted, helper.emptyDebt(), 'Minted');
538
-
539
- /** @type {Array<ZCFSeat>} */
540
- const vaultSeatOpt = allEmpty([giveColl, giveMinted, wantColl])
541
- ? []
542
- : [vaultSeat];
543
- state.manager.mintAndReallocate(toMint, fee, clientSeat, ...vaultSeatOpt);
544
-
545
- // parent needs to know about the change in debt
546
- helper.updateDebtAccounting(normalizedDebtPre, collateralPre, newDebt);
547
- state.manager.burnAndRecord(giveMinted, vaultSeat);
548
- helper.assertVaultHoldsNoMinted();
549
-
550
- helper.updateUiState();
551
- clientSeat.exit();
552
- return 'We have adjusted your balances, thank you for your business';
553
- },
554
-
555
- /**
556
- *
557
- * @param {MethodContext} context
558
- * @param {ZCFSeat} seat
559
- * @returns {VaultKit}
560
- */
561
- makeTransferInvitationHook: ({ state, facets }, seat) => {
562
- const { self, helper } = facets;
563
- helper.assertCloseable();
564
- seat.exit();
565
-
566
- const ephemera = provideEphemera(state.idInManager);
567
-
568
- // eslint-disable-next-line no-use-before-define
569
- const vaultKit = makeVaultKit(
570
- self,
571
- ephemera.storageNode,
572
- ephemera.marshaller,
573
- state.manager.getAssetSubscriber(),
574
- );
575
- ephemera.outerUpdater = vaultKit.vaultUpdater;
576
- helper.updateUiState();
577
-
578
- return vaultKit;
579
- },
180
+ },
181
+ {
182
+ helper: {
183
+ //#region Computed constants
184
+ collateralBrand() {
185
+ return this.state.manager.getCollateralBrand();
186
+ },
187
+ debtBrand() {
188
+ return this.state.manager.getDebtBrand();
189
+ },
190
+
191
+ emptyCollateral() {
192
+ return AmountMath.makeEmpty(this.facets.helper.collateralBrand());
193
+ },
194
+ emptyDebt() {
195
+ return AmountMath.makeEmpty(this.facets.helper.debtBrand());
196
+ },
197
+ /**
198
+ * @typedef {{ give: { Collateral: Amount<'nat'>, Minted: Amount<'nat'> }, want: { Collateral: Amount<'nat'>, Minted: Amount<'nat'> } }} FullProposal
199
+ */
200
+ /**
201
+ * @param {ProposalRecord} partial
202
+ * @returns {FullProposal}
203
+ */
204
+ fullProposal(partial) {
205
+ return {
206
+ give: {
207
+ Collateral:
208
+ partial.give?.Collateral ||
209
+ this.facets.helper.emptyCollateral(),
210
+ Minted: partial.give?.Minted || this.facets.helper.emptyDebt(),
211
+ },
212
+ want: {
213
+ Collateral:
214
+ partial.want?.Collateral ||
215
+ this.facets.helper.emptyCollateral(),
216
+ Minted: partial.want?.Minted || this.facets.helper.emptyDebt(),
217
+ },
218
+ };
219
+ },
220
+ //#endregion
221
+
222
+ //#region Phase logic
223
+ /**
224
+ * @param {VaultPhase} newPhase
225
+ */
226
+ assignPhase(newPhase) {
227
+ const { state } = this;
228
+
229
+ const { phase } = state;
230
+ const validNewPhases = validTransitions[phase];
231
+ validNewPhases.includes(newPhase) ||
232
+ Fail`Vault cannot transition from ${q(phase)} to ${q(newPhase)}`;
233
+ state.phase = newPhase;
234
+ },
235
+
236
+ assertActive() {
237
+ const { phase } = this.state;
238
+ phase === Phase.ACTIVE || Fail`vault not active`;
239
+ },
240
+
241
+ assertCloseable() {
242
+ const { phase } = this.state;
243
+ phase === Phase.ACTIVE ||
244
+ phase === Phase.LIQUIDATED ||
245
+ Fail`to be closed a vault must be active or liquidated, not ${phase}`;
246
+ },
247
+ //#endregion
248
+
249
+ /**
250
+ * Called whenever the debt is paid or created through a transaction,
251
+ * but not for interest accrual.
252
+ *
253
+ * @param {Amount<'nat'>} newDebt - principal and all accrued interest
254
+ */
255
+ updateDebtSnapshot(newDebt) {
256
+ const { state } = this;
257
+
258
+ // update local state
259
+ state.debtSnapshot = newDebt;
260
+ state.interestSnapshot = state.manager.getCompoundedInterest();
261
+ },
262
+
263
+ /**
264
+ * Update the debt balance and propagate upwards to
265
+ * maintain aggregate debt and liquidation order.
266
+ *
267
+ * @param {NormalizedDebt} oldDebtNormalized - prior principal and all accrued interest, normalized to the launch of the vaultManager
268
+ * @param {Amount<'nat'>} oldCollateral - actual collateral
269
+ * @param {Amount<'nat'>} newDebtActual - actual principal and all accrued interest
270
+ */
271
+ updateDebtAccounting(oldDebtNormalized, oldCollateral, newDebtActual) {
272
+ const { state, facets } = this;
273
+ const { helper } = facets;
274
+ helper.updateDebtSnapshot(newDebtActual);
275
+ // notify manager so it can notify and clean up as appropriate
276
+ state.manager.handleBalanceChange(
277
+ oldDebtNormalized,
278
+ oldCollateral,
279
+ state.idInManager,
280
+ state.phase,
281
+ facets.self,
282
+ );
283
+ },
284
+
285
+ /**
286
+ *
287
+ * @param {ZCFSeat} seat
288
+ */
289
+ getCollateralAllocated(seat) {
290
+ return seat.getAmountAllocated(
291
+ 'Collateral',
292
+ this.facets.helper.collateralBrand(),
293
+ );
294
+ },
295
+ getMintedAllocated(seat) {
296
+ return seat.getAmountAllocated(
297
+ 'Minted',
298
+ this.facets.helper.debtBrand(),
299
+ );
300
+ },
301
+
302
+ assertVaultHoldsNoMinted() {
303
+ const { state, facets } = this;
304
+ const { vaultSeat } = state;
305
+ AmountMath.isEmpty(facets.helper.getMintedAllocated(vaultSeat)) ||
306
+ Fail`Vault should be empty of Minted`;
307
+ },
308
+
309
+ /**
310
+ *
311
+ * @param {Amount<'nat'>} collateralAmount
312
+ * @param {Amount<'nat'>} proposedDebt
313
+ */
314
+ assertSufficientCollateral(collateralAmount, proposedDebt) {
315
+ const { state, facets } = this;
316
+ const maxDebt = state.manager.maxDebtFor(collateralAmount);
317
+ AmountMath.isGTE(maxDebt, proposedDebt, facets.helper.debtBrand()) ||
318
+ Fail`Proposed debt ${q(proposedDebt)} exceeds max ${q(
319
+ maxDebt,
320
+ )} for ${q(collateralAmount)} collateral`;
321
+ },
322
+
323
+ /**
324
+ *
325
+ * @param {HolderPhase} newPhase
326
+ */
327
+ getStateSnapshot(newPhase) {
328
+ const { state, facets } = this;
329
+
330
+ const { debtSnapshot: debt, interestSnapshot: interest } = state;
331
+ /** @type {VaultNotification} */
332
+ return harden({
333
+ debtSnapshot: { debt, interest },
334
+ locked: facets.self.getCollateralAmount(),
335
+ // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase
336
+ // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415
337
+ vaultState: newPhase,
338
+ });
339
+ },
340
+
341
+ /**
342
+ * call this whenever anything changes!
343
+ */
344
+ updateUiState() {
345
+ const { state, facets } = this;
346
+ const { outerUpdater } = state;
347
+ if (!outerUpdater) {
348
+ // It's not an error to change to liquidating during transfer
349
+ return;
350
+ }
351
+ const { phase } = state;
352
+ const uiState = facets.helper.getStateSnapshot(phase);
353
+ const brand = facets.helper.collateralBrand();
354
+ trace(brand, 'updateUiState', state.idInManager, uiState);
355
+
356
+ switch (phase) {
357
+ case Phase.ACTIVE:
358
+ case Phase.LIQUIDATING:
359
+ case Phase.LIQUIDATED:
360
+ void outerUpdater.write(uiState);
361
+ break;
362
+ case Phase.CLOSED:
363
+ void outerUpdater.writeFinal(uiState);
364
+ state.outerUpdater = null;
365
+ break;
366
+ default:
367
+ throw Error(`unreachable vault phase: ${phase}`);
368
+ }
369
+ },
370
+
371
+ /**
372
+ * @param {ZCFSeat} seat
373
+ */
374
+ async closeHook(seat) {
375
+ const { state, facets } = this;
376
+
377
+ const { self, helper } = facets;
378
+ helper.assertCloseable();
379
+ const { phase, vaultSeat } = state;
380
+
381
+ // Held as keys for cleanup in the manager
382
+ const oldDebtNormalized = self.getNormalizedDebt();
383
+ const oldCollateral = self.getCollateralAmount();
384
+
385
+ if (phase === Phase.ACTIVE) {
386
+ // you're paying off the debt, you get everything back.
387
+ const debt = self.getCurrentDebt();
388
+ const {
389
+ give: { Minted: given },
390
+ } = seat.getProposal();
391
+ given || Fail`closing an active vault requires a give`;
392
+
393
+ // you must pay off the entire remainder but if you offer too much, we won't
394
+ // take more than you owe
395
+ AmountMath.isGTE(given, debt) ||
396
+ Fail`Offer ${q(given)} is not sufficient to pay off debt ${q(
397
+ debt,
398
+ )}`;
399
+
400
+ // Return any overpayment
401
+ atomicTransfer(
402
+ zcf,
403
+ vaultSeat,
404
+ seat,
405
+ vaultSeat.getCurrentAllocation(),
406
+ );
407
+
408
+ state.manager.burn(debt, seat);
409
+ } else if (phase === Phase.LIQUIDATED) {
410
+ // Simply reallocate vault assets to the offer seat.
411
+ // Don't take anything from the offer, even if vault is underwater.
412
+ // TODO verify that returning Minted here doesn't mess up debt limits
413
+
414
+ atomicTransfer(
415
+ zcf,
416
+ vaultSeat,
417
+ seat,
418
+ vaultSeat.getCurrentAllocation(),
419
+ );
420
+ } else {
421
+ throw Error('only active and liquidated vaults can be closed');
422
+ }
423
+
424
+ seat.exit();
425
+ helper.assignPhase(Phase.CLOSED);
426
+ helper.updateDebtSnapshot(helper.emptyDebt());
427
+ helper.updateUiState();
428
+ helper.assertVaultHoldsNoMinted();
429
+ vaultSeat.exit();
430
+
431
+ state.manager.handleBalanceChange(
432
+ oldDebtNormalized,
433
+ oldCollateral,
434
+ state.idInManager,
435
+ state.phase,
436
+ facets.self,
437
+ );
438
+
439
+ return 'your vault is closed, thank you for your business';
440
+ },
441
+
442
+ /**
443
+ * Calculate the fee, the amount to mint and the resulting debt.
444
+ * The give and the want together reflect a delta, where typically
445
+ * one is zero because they come from the gave/want of an offer
446
+ * proposal. If the `want` is zero, the `fee` will also be zero,
447
+ * so the simple math works.
448
+ *
449
+ * @param {Amount<'nat'>} currentDebt
450
+ * @param {Amount<'nat'>} giveAmount
451
+ * @param {Amount<'nat'>} wantAmount
452
+ */
453
+ debtFee(currentDebt, giveAmount, wantAmount) {
454
+ const { state } = this;
455
+
456
+ return calculateDebtCosts(
457
+ currentDebt,
458
+ giveAmount,
459
+ wantAmount,
460
+ state.manager.getGovernedParams().getMintFee(),
461
+ );
462
+ },
463
+
464
+ /**
465
+ * Adjust principal and collateral (atomically for offer safety)
466
+ *
467
+ * @param {ZCFSeat} clientSeat
468
+ * @returns {string} success message
469
+ */
470
+ adjustBalancesHook(clientSeat) {
471
+ const { state, facets } = this;
472
+
473
+ const { self, helper } = facets;
474
+ const { vaultSeat } = state;
475
+ const fp = helper.fullProposal(clientSeat.getProposal());
476
+
477
+ if (
478
+ allEmpty([
479
+ fp.give.Collateral,
480
+ fp.give.Minted,
481
+ fp.want.Collateral,
482
+ fp.want.Minted,
483
+ ])
484
+ ) {
485
+ clientSeat.exit();
486
+ return 'no transaction, as requested';
487
+ }
488
+
489
+ // Calculate the fee, the amount to mint and the resulting debt. We'll
490
+ // verify that the target debt doesn't violate the collateralization ratio,
491
+ // then mint, reallocate, and burn.
492
+ const { newDebt, fee, surplus, toMint } = helper.debtFee(
493
+ self.getCurrentDebt(),
494
+ fp.give.Minted,
495
+ fp.want.Minted,
496
+ );
497
+
498
+ const normalizedDebtPre = self.getNormalizedDebt();
499
+ const collateralPre = helper.getCollateralAllocated(vaultSeat);
500
+
501
+ const hasWants = !allEmpty([fp.want.Collateral, fp.want.Minted]);
502
+ if (hasWants) {
503
+ const newCollateral = addSubtract(
504
+ collateralPre,
505
+ fp.give.Collateral,
506
+ fp.want.Collateral,
507
+ );
508
+ helper.assertSufficientCollateral(newCollateral, newDebt);
509
+ }
510
+
511
+ return helper.commitBalanceAdjustment(
512
+ clientSeat,
513
+ fp,
514
+ {
515
+ newDebt,
516
+ fee,
517
+ surplus,
518
+ toMint,
519
+ },
520
+ { normalizedDebtPre, collateralPre },
521
+ );
522
+ },
523
+
524
+ /**
525
+ *
526
+ * @param {ZCFSeat} clientSeat
527
+ * @param {FullProposal} fp
528
+ * @param {ReturnType<typeof calculateDebtCosts>} costs
529
+ * @param {object} accounting
530
+ * @param {NormalizedDebt} accounting.normalizedDebtPre
531
+ * @param {Amount<'nat'>} accounting.collateralPre
532
+ * @returns {string} success message
533
+ */
534
+ commitBalanceAdjustment(
535
+ clientSeat,
536
+ fp,
537
+ { newDebt, fee, surplus, toMint },
538
+ { normalizedDebtPre, collateralPre },
539
+ ) {
540
+ const { state, facets } = this;
541
+ const { helper } = facets;
542
+ const { vaultSeat } = state;
543
+
544
+ const giveMintedTaken = AmountMath.subtract(fp.give.Minted, surplus);
545
+
546
+ /** @type {import('@agoric/zoe/src/contractSupport/atomicTransfer.js').TransferPart[]} */
547
+ const transfers = harden([
548
+ [clientSeat, vaultSeat, { Collateral: fp.give.Collateral }],
549
+ [vaultSeat, clientSeat, { Collateral: fp.want.Collateral }],
550
+ [clientSeat, vaultSeat, { Minted: giveMintedTaken }],
551
+ // Minted into vaultSeat requires minting and so is done by mintAndTransfer
552
+ ]);
553
+
554
+ state.manager.mintAndTransfer(clientSeat, toMint, fee, transfers);
555
+
556
+ // parent needs to know about the change in debt
557
+ helper.updateDebtAccounting(
558
+ normalizedDebtPre,
559
+ collateralPre,
560
+ newDebt,
561
+ );
562
+ state.manager.burn(giveMintedTaken, vaultSeat);
563
+ helper.assertVaultHoldsNoMinted();
564
+
565
+ helper.updateUiState();
566
+ clientSeat.exit();
567
+ return 'We have adjusted your balances, thank you for your business';
568
+ },
569
+
570
+ /**
571
+ *
572
+ * @param {ZCFSeat} seat
573
+ * @returns {VaultKit}
574
+ */
575
+ makeTransferInvitationHook(seat) {
576
+ const { state, facets } = this;
577
+
578
+ const { self, helper } = facets;
579
+ helper.assertCloseable();
580
+ seat.exit();
581
+
582
+ // eslint-disable-next-line no-use-before-define
583
+ const vaultKit = makeVaultKit(self, state.storageNode);
584
+ state.outerUpdater = vaultKit.vaultUpdater;
585
+ helper.updateUiState();
586
+
587
+ return vaultKit;
588
+ },
589
+ },
590
+ self: {
591
+ getVaultSeat() {
592
+ return this.state.vaultSeat;
593
+ },
594
+
595
+ /**
596
+ * @param {ZCFSeat} seat
597
+ * @param {StorageNode} storageNode
598
+ */
599
+ async initVaultKit(seat, storageNode) {
600
+ const { state, facets } = this;
601
+
602
+ const { self, helper } = facets;
603
+
604
+ const normalizedDebtPre = self.getNormalizedDebt();
605
+ const actualDebtPre = self.getCurrentDebt();
606
+ (AmountMath.isEmpty(normalizedDebtPre) &&
607
+ AmountMath.isEmpty(actualDebtPre)) ||
608
+ Fail`vault must be empty initially`;
609
+
610
+ const collateralPre = self.getCollateralAmount();
611
+ trace('initVaultKit start: collateral', state.idInManager, {
612
+ actualDebtPre,
613
+ collateralPre,
614
+ });
615
+
616
+ // get the payout to provide access to the collateral if the
617
+ // contract abandons
618
+ const {
619
+ give: { Collateral: giveCollateral },
620
+ want: { Minted: wantMinted },
621
+ } = seat.getProposal();
622
+
623
+ const minInitialDebt = state.manager
624
+ .getGovernedParams()
625
+ .getMinInitialDebt();
626
+ AmountMath.isGTE(wantMinted, minInitialDebt) ||
627
+ Fail`Vault creation requires a minInitialDebt of ${q(
628
+ minInitialDebt,
629
+ )}`;
630
+
631
+ const {
632
+ newDebt: newDebtPre,
633
+ fee,
634
+ toMint,
635
+ } = helper.debtFee(actualDebtPre, helper.emptyDebt(), wantMinted);
636
+ !AmountMath.isEmpty(fee) ||
637
+ Fail`debt requested (${wantMinted}) too small to accrue interest`;
638
+ AmountMath.isEqual(newDebtPre, toMint) ||
639
+ Fail`fee mismatch for vault`;
640
+ trace(
641
+ 'initVault',
642
+ state.idInManager,
643
+ { wantedRun: wantMinted, fee },
644
+ self.getCollateralAmount(),
645
+ );
646
+
647
+ helper.assertSufficientCollateral(giveCollateral, newDebtPre);
648
+
649
+ const { vaultSeat } = state;
650
+ state.manager.mintAndTransfer(
651
+ seat,
652
+ toMint,
653
+ fee,
654
+ harden([[seat, vaultSeat, { Collateral: giveCollateral }]]),
655
+ );
656
+ seat.exit();
657
+
658
+ helper.updateDebtAccounting(
659
+ normalizedDebtPre,
660
+ collateralPre,
661
+ newDebtPre,
662
+ );
663
+ trace('initVault updateDebtAccounting fired');
664
+
665
+ // So that makeVaultKit can be synchronous
666
+ const vaultKit = makeVaultKit(self, storageNode);
667
+ state.outerUpdater = vaultKit.vaultUpdater;
668
+ helper.updateUiState();
669
+ return vaultKit;
670
+ },
671
+
672
+ /**
673
+ * Called by manager at start of liquidation.
674
+ */
675
+ liquidating() {
676
+ const { facets } = this;
677
+
678
+ const { helper } = facets;
679
+ helper.assignPhase(Phase.LIQUIDATING);
680
+ helper.updateUiState();
681
+ },
682
+
683
+ /**
684
+ * Called by manager at end of liquidation, at which point all debts have been
685
+ * covered.
686
+ */
687
+ liquidated() {
688
+ const { facets } = this;
689
+
690
+ const { helper } = facets;
691
+ helper.updateDebtSnapshot(
692
+ // liquidated vaults retain no debt
693
+ AmountMath.makeEmpty(helper.debtBrand()),
694
+ );
695
+
696
+ helper.assignPhase(Phase.LIQUIDATED);
697
+ helper.updateUiState();
698
+ },
699
+
700
+ /**
701
+ * Called by vaultManager when the auction wasn't able to sell the
702
+ * collateral. The liquidation fee was charged against the collateral,
703
+ * but the debt will be restored and the vault will be active again.
704
+ * Liquidation.md has details on the liquidation approach.
705
+ */
706
+ abortLiquidation() {
707
+ const { state, facets } = this;
708
+
709
+ const { helper } = facets;
710
+
711
+ helper.assignPhase(Phase.ACTIVE);
712
+ helper.updateUiState();
713
+ return state.idInManager;
714
+ },
715
+
716
+ makeAdjustBalancesInvitation() {
717
+ const { state, facets } = this;
718
+ const { helper } = facets;
719
+ helper.assertActive();
720
+
721
+ return zcf.makeInvitation(
722
+ seat => helper.adjustBalancesHook(seat),
723
+ state.manager.scopeDescription('AdjustBalances'),
724
+ undefined,
725
+ M.splitRecord({
726
+ give: M.splitRecord(
727
+ {},
728
+ {
729
+ // It may seem odd to give both at once but there is use case:
730
+ // To rescue a vault that's on the verge of being liquidated
731
+ // when you have limited resources, you might add collateral
732
+ // at the same time that you're repaying IST.
733
+ Collateral: makeNatAmountShape(helper.collateralBrand()),
734
+ Minted: makeNatAmountShape(helper.debtBrand()),
735
+ },
736
+ {},
737
+ ),
738
+ want: M.splitRecord(
739
+ {},
740
+ {
741
+ Collateral: makeNatAmountShape(helper.collateralBrand()),
742
+ Minted: makeNatAmountShape(helper.debtBrand()),
743
+ },
744
+ {},
745
+ ),
746
+ }),
747
+ );
748
+ },
749
+
750
+ makeCloseInvitation() {
751
+ const { state, facets } = this;
752
+ const { helper } = facets;
753
+ helper.assertCloseable();
754
+ return zcf.makeInvitation(
755
+ seat => helper.closeHook(seat),
756
+ state.manager.scopeDescription('CloseVault'),
757
+ undefined,
758
+ M.splitRecord({
759
+ give: M.splitRecord(
760
+ {},
761
+ {
762
+ Minted: makeNatAmountShape(helper.debtBrand()),
763
+ },
764
+ {},
765
+ ),
766
+ want: M.splitRecord(
767
+ {},
768
+ {
769
+ Collateral: makeNatAmountShape(helper.collateralBrand()),
770
+ },
771
+ {},
772
+ ),
773
+ }),
774
+ );
775
+ },
776
+
777
+ /**
778
+ * @returns {Promise<Invitation>}
779
+ */
780
+ makeTransferInvitation() {
781
+ const { state, facets } = this;
782
+ const { outerUpdater } = state;
783
+ const { self, helper } = facets;
784
+ // Bring the debt snapshot current for the final report before transfer
785
+ helper.updateDebtSnapshot(self.getCurrentDebt());
786
+ const {
787
+ debtSnapshot: debt,
788
+ interestSnapshot: interest,
789
+ phase,
790
+ } = state;
791
+ if (outerUpdater) {
792
+ void outerUpdater.writeFinal(
793
+ helper.getStateSnapshot(Phase.TRANSFER),
794
+ );
795
+ state.outerUpdater = null;
796
+ }
797
+ const transferState = {
798
+ debtSnapshot: { debt, interest },
799
+ locked: self.getCollateralAmount(),
800
+ vaultState: phase,
801
+ };
802
+ return zcf.makeInvitation(
803
+ seat => helper.makeTransferInvitationHook(seat),
804
+ state.manager.scopeDescription('TransferVault'),
805
+ transferState,
806
+ );
807
+ },
808
+
809
+ // for status/debugging
810
+
811
+ /**
812
+ *
813
+ * @returns {Amount<'nat'>}
814
+ */
815
+ getCollateralAmount() {
816
+ const { state, facets } = this;
817
+ const { vaultSeat } = state;
818
+ const { helper } = facets;
819
+ // getCollateralAllocated would return final allocations
820
+ return vaultSeat.hasExited()
821
+ ? helper.emptyCollateral()
822
+ : helper.getCollateralAllocated(vaultSeat);
823
+ },
824
+
825
+ /**
826
+ * The actual current debt, including accrued interest.
827
+ *
828
+ * This looks like a simple getter but it does a lot of the heavy lifting for
829
+ * interest accrual. Rather than updating all records when interest accrues,
830
+ * the vault manager updates just its rolling compounded interest. Here we
831
+ * calculate what the current debt is given what's recorded in this vault and
832
+ * what interest has compounded since this vault record was written.
833
+ *
834
+ * @see getNormalizedDebt
835
+ *
836
+ * @returns {Amount<'nat'>}
837
+ */
838
+ getCurrentDebt() {
839
+ const { state } = this;
840
+ return calculateCurrentDebt(
841
+ state.debtSnapshot,
842
+ state.interestSnapshot,
843
+ state.manager.getCompoundedInterest(),
844
+ );
845
+ },
846
+
847
+ /**
848
+ * The normalization puts all debts on a common time-independent scale since
849
+ * the launch of this vault manager. This allows the manager to order vaults
850
+ * by their debt-to-collateral ratios without having to mutate the debts as
851
+ * the interest accrues.
852
+ *
853
+ * @see getActualDebAmount
854
+ *
855
+ * @returns {import('./storeUtils.js').NormalizedDebt} as if the vault was open at the launch of this manager, before any interest accrued
856
+ */
857
+ getNormalizedDebt() {
858
+ const { state } = this;
859
+ // @ts-expect-error cast
860
+ return reverseInterest(state.debtSnapshot, state.interestSnapshot);
861
+ },
862
+ },
863
+ },
864
+ {
865
+ stateShape: VaultStateShape,
866
+ },
867
+ );
868
+ return maker;
580
869
  };
581
870
 
582
- const selfBehavior = {
583
- /**
584
- * @param {MethodContext} context
585
- */
586
- getVaultSeat: ({ state }) => state.vaultSeat,
587
-
588
- /**
589
- * @param {MethodContext} context
590
- * @param {ZCFSeat} seat
591
- * @param {ERef<StorageNode>} storageNode
592
- * @param {ERef<Marshaller>} marshaller
593
- */
594
- initVaultKit: async ({ state, facets }, seat, storageNode, marshaller) => {
595
- assert(seat && storageNode && marshaller, 'initVaultKit missing argument');
596
- const ephemera = provideEphemera(state.idInManager);
597
-
598
- const { self, helper } = facets;
599
-
600
- const normalizedDebtPre = self.getNormalizedDebt();
601
- const actualDebtPre = self.getCurrentDebt();
602
- assert(
603
- AmountMath.isEmpty(normalizedDebtPre) &&
604
- AmountMath.isEmpty(actualDebtPre),
605
- X`vault must be empty initially`,
606
- );
607
-
608
- const collateralPre = self.getCollateralAmount();
609
- trace('initVaultKit start: collateral', state.idInManager, {
610
- actualDebtPre,
611
- collateralPre,
612
- });
613
-
614
- // get the payout to provide access to the collateral if the
615
- // contract abandons
616
- const {
617
- give: { Collateral: giveCollateral },
618
- want: { Minted: wantMinted },
619
- } = seat.getProposal();
620
-
621
- const {
622
- newDebt: newDebtPre,
623
- fee,
624
- toMint,
625
- } = helper.loanFee(actualDebtPre, helper.emptyDebt(), wantMinted);
626
- !AmountMath.isEmpty(fee) ||
627
- assert.fail(
628
- X`loan requested (${wantMinted}) is too small; cannot accrue interest`,
629
- );
630
- assert(AmountMath.isEqual(newDebtPre, toMint), X`fee mismatch for vault`);
631
- trace(
632
- 'initVault',
633
- state.idInManager,
634
- { wantedRun: wantMinted, fee },
635
- self.getCollateralAmount(),
636
- );
637
-
638
- await helper.assertSufficientCollateral(giveCollateral, newDebtPre);
639
-
640
- const { vaultSeat } = state;
641
- vaultSeat.incrementBy(
642
- seat.decrementBy(harden({ Collateral: giveCollateral })),
643
- );
644
- state.manager.mintAndReallocate(toMint, fee, seat, vaultSeat);
645
- helper.updateDebtAccounting(normalizedDebtPre, collateralPre, newDebtPre);
646
-
647
- const vaultKit = makeVaultKit(
648
- self,
649
- storageNode,
650
- marshaller,
651
- state.manager.getAssetSubscriber(),
652
- );
653
- ephemera.outerUpdater = vaultKit.vaultUpdater;
654
- helper.updateUiState();
655
- return vaultKit;
656
- },
657
-
658
- /**
659
- * Called by manager at start of liquidation.
660
- *
661
- * @param {MethodContext} context
662
- */
663
- liquidating: ({ facets }) => {
664
- const { helper } = facets;
665
- helper.assignPhase(Phase.LIQUIDATING);
666
- helper.updateUiState();
667
- },
668
-
669
- /**
670
- * Called by manager at end of liquidation, at which point all debts have been
671
- * covered.
672
- *
673
- * @param {MethodContext} context
674
- */
675
- liquidated: ({ facets }) => {
676
- const { helper } = facets;
677
- helper.updateDebtSnapshot(
678
- // liquidated vaults retain no debt
679
- AmountMath.makeEmpty(helper.debtBrand()),
680
- );
681
-
682
- helper.assignPhase(Phase.LIQUIDATED);
683
- helper.updateUiState();
684
- },
685
-
686
- /**
687
- * @param {MethodContext} context
688
- */
689
- makeAdjustBalancesInvitation: ({ state, facets }) => {
690
- const { helper } = facets;
691
- helper.assertActive();
692
- const { zcf } = provideEphemera(state.idInManager);
693
- return zcf.makeInvitation(
694
- seat => helper.adjustBalancesHook(seat),
695
- 'AdjustBalances',
696
- );
697
- },
698
-
699
- /**
700
- * @param {MethodContext} context
701
- */
702
- makeCloseInvitation: ({ state, facets }) => {
703
- const { helper } = facets;
704
- helper.assertCloseable();
705
- const { zcf } = provideEphemera(state.idInManager);
706
- return zcf.makeInvitation(seat => helper.closeHook(seat), 'CloseVault');
707
- },
708
-
709
- /**
710
- * @param {MethodContext} context
711
- * @returns {Promise<Invitation>}
712
- */
713
- makeTransferInvitation: ({ state, facets }) => {
714
- const ephemera = provideEphemera(state.idInManager);
715
- const { outerUpdater } = ephemera;
716
- const { self, helper } = facets;
717
- // Bring the debt snapshot current for the final report before transfer
718
- helper.updateDebtSnapshot(self.getCurrentDebt());
719
- const { debtSnapshot: debt, interestSnapshot: interest, phase } = state;
720
- if (outerUpdater) {
721
- outerUpdater.finish(helper.getStateSnapshot(Phase.TRANSFER));
722
- ephemera.outerUpdater = null;
723
- }
724
- const transferState = {
725
- debtSnapshot: { debt, interest },
726
- locked: self.getCollateralAmount(),
727
- vaultState: phase,
728
- };
729
- const { zcf } = provideEphemera(state.idInManager);
730
- return zcf.makeInvitation(
731
- seat => helper.makeTransferInvitationHook(seat),
732
- 'TransferVault',
733
- transferState,
734
- );
735
- },
736
-
737
- // for status/debugging
738
-
739
- /**
740
- *
741
- * @param {MethodContext} context
742
- * @returns {Amount<'nat'>}
743
- */
744
- getCollateralAmount: ({ state, facets }) => {
745
- const { vaultSeat } = state;
746
- const { helper } = facets;
747
- // getCollateralAllocated would return final allocations
748
- return vaultSeat.hasExited()
749
- ? helper.emptyCollateral()
750
- : helper.getCollateralAllocated(vaultSeat);
751
- },
752
-
753
- /**
754
- * The actual current debt, including accrued interest.
755
- *
756
- * This looks like a simple getter but it does a lot of the heavy lifting for
757
- * interest accrual. Rather than updating all records when interest accrues,
758
- * the vault manager updates just its rolling compounded interest. Here we
759
- * calculate what the current debt is given what's recorded in this vault and
760
- * what interest has compounded since this vault record was written.
761
- *
762
- * @see getNormalizedDebt
763
- *
764
- * @param {MethodContext} context
765
- * @returns {Amount<'nat'>}
766
- */
767
- getCurrentDebt: ({ state }) => {
768
- return calculateCurrentDebt(
769
- state.debtSnapshot,
770
- state.interestSnapshot,
771
- state.manager.getCompoundedInterest(),
772
- );
773
- },
774
-
775
- /**
776
- * The normalization puts all debts on a common time-independent scale since
777
- * the launch of this vault manager. This allows the manager to order vaults
778
- * by their debt-to-collateral ratios without having to mutate the debts as
779
- * the interest accrues.
780
- *
781
- * @see getActualDebAmount
782
- *
783
- * @param {MethodContext} context
784
- * @returns {import('./storeUtils.js').NormalizedDebt} as if the vault was open at the launch of this manager, before any interest accrued
785
- */
786
- getNormalizedDebt: ({ state }) => {
787
- // @ts-expect-error cast
788
- return reverseInterest(state.debtSnapshot, state.interestSnapshot);
789
- },
790
- };
791
-
792
- const makeVaultBase = defineDurableKindMulti(
793
- makeKindHandle('Vault'),
794
- initState,
795
- {
796
- self: selfBehavior,
797
- helper: helperBehavior,
798
- },
799
- );
800
-
801
- /**
802
- * @param {ZCF} zcf
803
- * @param {VaultManager} manager
804
- * @param {VaultId} idInManager
805
- */
806
- export const makeVault = pickFacet(makeVaultBase, 'self');
807
-
808
- /** @typedef {ReturnType<typeof makeVault>} Vault */
871
+ /** @typedef {ReturnType<ReturnType<typeof prepareVault>>['self']} Vault */