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