Skip to content

Commit d1a142d

Browse files
fix: return funds from liquidation via a seat payout (#3656)
* fix: return funds from liquidation via a seat payout Reduce overuse of vaultSeat for transferring RUN * refactor: add liquidating state; disallow adjustments in liquidating
1 parent e30c934 commit d1a142d

File tree

7 files changed

+115
-72
lines changed

7 files changed

+115
-72
lines changed

packages/treasury/src/liquidation.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ export async function liquidate(
2020
strategy,
2121
collateralBrand,
2222
) {
23+
vaultKit.liquidating();
2324
const runDebt = vaultKit.vault.getDebtAmount();
24-
const { runBrand } = runDebt.brand;
25+
const { brand: runBrand } = runDebt;
26+
const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit;
2527

26-
const vaultSeat = vaultKit.vaultSeat;
2728
const collateralToSell = vaultSeat.getAmountAllocated(
2829
'Collateral',
2930
collateralBrand,
@@ -34,14 +35,15 @@ export async function liquidate(
3435
strategy.keywordMapping(),
3536
strategy.makeProposal(collateralToSell, runDebt),
3637
vaultSeat,
38+
liquidationSeat,
3739
);
3840
trace(` offeredTo`, runDebt);
3941

4042
// await deposited, but we don't need the value.
4143
await Promise.all([deposited, E(liqSeat).getOfferResult()]);
4244

4345
// Now we need to know how much was sold so we can pay off the debt
44-
const runProceedsAmount = vaultSeat.getAmountAllocated('RUN', runBrand);
46+
const runProceedsAmount = liquidationSeat.getAmountAllocated('RUN', runBrand);
4547

4648
trace('RUN PROCEEDS', runProceedsAmount);
4749

@@ -50,11 +52,13 @@ export async function liquidate(
5052

5153
const isUnderwater = !AmountMath.isGTE(runProceedsAmount, runDebt);
5254
const runToBurn = isUnderwater ? runProceedsAmount : runDebt;
53-
burnLosses({ RUN: runToBurn }, vaultSeat);
55+
burnLosses({ RUN: runToBurn }, liquidationSeat);
5456
vaultKit.liquidated(AmountMath.subtract(runDebt, runToBurn));
5557

5658
// any remaining RUN plus anything else leftover from the sale are refunded
5759
vaultSeat.exit();
60+
liquidationSeat.exit();
61+
vaultKit.liquidationPromiseKit.resolve('Liquidated');
5862
}
5963

6064
// The default strategy converts of all the collateral to RUN using autoswap,

packages/treasury/src/types.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,13 @@
9191
* @property {() => Promise<Invitation>} makeCloseInvitation
9292
* @property {() => Amount} getCollateralAmount
9393
* @property {() => Amount} getDebtAmount
94+
* @property {() => ERef<UserSeat>} getLiquidationSeat
95+
* @property {() => Promise<string>} getLiquidationPromise
9496
*/
9597

9698
/**
9799
* @typedef {Object} LoanKit
98100
* @property {Vault} vault
99-
* @property {Promise<PaymentPKeywordRecord>} liquidationPayout
100101
* @property {Notifier<UIState>} uiNotifier
101102
*/
102103

@@ -105,6 +106,9 @@
105106
* @property {Vault} vault
106107
* @property {(ZCFSeat) => Promise<OpenLoanKit>} openLoan
107108
* @property {(Timestamp) => Amount} accrueInterestAndAddToPool
109+
* @property {ZCFSeat} vaultSeat
110+
* @property {PromiseRecord<string>} liquidationPromiseKit
111+
* @property {ZCFSeat} liquidationZcfSeat
108112
*/
109113

110114
/**

packages/treasury/src/vault.js

+56-21
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,25 @@ import { makeNotifierKit } from '@agoric/notifier';
1515
import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js';
1616
import { AmountMath } from '@agoric/ertp';
1717
import { Far } from '@agoric/marshal';
18-
import { makeTracer } from './makeTracer.js';
18+
import { makePromiseKit } from '@agoric/promise-kit';
1919
import { makeInterestCalculator } from './interest.js';
2020

2121
// a Vault is an individual loan, using some collateralType as the
2222
// collateral, and lending RUN to the borrower
2323

24+
/**
25+
* Constants for vault state.
26+
*
27+
* @typedef {'active' | 'liquidating' | 'closed'} VAULT_STATE
28+
*
29+
* @type {{ ACTIVE: 'active', LIQUIDATING: 'liquidating', CLOSED: 'closed' }}
30+
*/
31+
export const VaultState = {
32+
ACTIVE: 'active',
33+
LIQUIDATING: 'liquidating',
34+
CLOSED: 'closed',
35+
};
36+
2437
/** @type {MakeVaultKit} */
2538
export function makeVaultKit(
2639
zcf,
@@ -31,13 +44,18 @@ export function makeVaultKit(
3144
loanParams,
3245
startTimeStamp,
3346
) {
34-
const trace = makeTracer('VV');
3547
const { updater: uiUpdater, notifier } = makeNotifierKit();
48+
const {
49+
zcfSeat: liquidationZcfSeat,
50+
userSeat: liquidationSeat,
51+
} = zcf.makeEmptySeatKit(undefined);
52+
const liquidationPromiseKit = makePromiseKit();
3653

37-
let active = true; // liquidation halts all user actions
54+
/** @type {VAULT_STATE} */
55+
let vaultState = VaultState.ACTIVE;
3856

3957
function assertVaultIsOpen() {
40-
assert(active, 'vault must still be active');
58+
assert(vaultState === VaultState.ACTIVE, 'vault must still be active');
4159
}
4260

4361
const collateralBrand = manager.getCollateralBrand();
@@ -49,9 +67,7 @@ export function makeVaultKit(
4967
// (because the StableCoinMachine vat died), they'll get all their
5068
// collateral back. If that happens, the issuer for the RUN will be dead,
5169
// so their loan will be worthless.
52-
const { zcfSeat: vaultSeat, userSeat } = zcf.makeEmptySeatKit();
53-
54-
trace('vaultSeat proposal', vaultSeat.getProposal());
70+
const { zcfSeat: vaultSeat } = zcf.makeEmptySeatKit();
5571

5672
const { brand: runBrand } = runMint.getIssuerRecord();
5773
let runDebt = AmountMath.makeEmpty(runBrand);
@@ -130,19 +146,31 @@ export function makeVaultKit(
130146
locked: getCollateralAmount(),
131147
debt: runDebt,
132148
collateralizationRatio,
133-
liquidated: !active,
149+
liquidated: vaultState === VaultState.CLOSED,
150+
vaultState,
134151
});
135152

136-
if (active) {
137-
uiUpdater.updateState(uiState);
138-
} else {
139-
uiUpdater.finish(uiState);
153+
switch (vaultState) {
154+
case VaultState.ACTIVE:
155+
case VaultState.LIQUIDATING:
156+
uiUpdater.updateState(uiState);
157+
break;
158+
case VaultState.CLOSED:
159+
uiUpdater.finish(uiState);
160+
break;
161+
default:
162+
throw Error(`unreachable vaultState: ${vaultState}`);
140163
}
141164
}
142165

143166
function liquidated(newDebt) {
144167
runDebt = newDebt;
145-
active = false;
168+
vaultState = VaultState.CLOSED;
169+
updateUiState();
170+
}
171+
172+
function liquidating() {
173+
vaultState = VaultState.LIQUIDATING;
146174
updateUiState();
147175
}
148176

@@ -167,20 +195,24 @@ export function makeVaultKit(
167195
assert(AmountMath.isGTE(runReturned, runDebt));
168196

169197
// Return any overpayment
170-
vaultSeat.incrementBy(seat.decrementBy({ RUN: runDebt }));
198+
199+
const { zcfSeat: burnSeat } = zcf.makeEmptySeatKit();
200+
burnSeat.incrementBy(seat.decrementBy({ RUN: runDebt }));
171201
seat.incrementBy(
172202
vaultSeat.decrementBy({ Collateral: getCollateralAllocated(vaultSeat) }),
173203
);
174-
zcf.reallocate(seat, vaultSeat);
175-
204+
zcf.reallocate(seat, vaultSeat, burnSeat);
205+
runMint.burnLosses({ RUN: runDebt }, burnSeat);
176206
seat.exit();
177-
active = false;
207+
burnSeat.exit();
208+
vaultState = VaultState.CLOSED;
178209
updateUiState();
179210

180-
runMint.burnLosses({ RUN: runDebt }, vaultSeat);
181211
runDebt = AmountMath.makeEmpty(runBrand);
182212
assertVaultHoldsNoRun();
183213
vaultSeat.exit();
214+
liquidationZcfSeat.exit();
215+
liquidationPromiseKit.resolve('Closed');
184216

185217
return 'your loan is closed, thank you for your business';
186218
}
@@ -397,8 +429,6 @@ export function makeVaultKit(
397429
want: { RUN: wantedRun },
398430
} = seat.getProposal();
399431

400-
const collateralPayoutP = E(userSeat).getPayouts();
401-
402432
// todo trigger process() check right away, in case the price dropped while we ran
403433

404434
const fee = multiplyBy(wantedRun, manager.getLoanFee());
@@ -419,7 +449,7 @@ export function makeVaultKit(
419449

420450
updateUiState();
421451

422-
return { notifier, collateralPayoutP };
452+
return { notifier };
423453
}
424454

425455
function accrueInterestAndAddToPool(currentTime) {
@@ -453,13 +483,18 @@ export function makeVaultKit(
453483
// for status/debugging
454484
getCollateralAmount,
455485
getDebtAmount,
486+
getLiquidationSeat: () => liquidationSeat,
487+
getLiquidationPromise: () => liquidationPromiseKit.promise,
456488
});
457489

458490
return harden({
459491
vault,
460492
openLoan,
461493
accrueInterestAndAddToPool,
462494
vaultSeat,
495+
liquidating,
463496
liquidated,
497+
liquidationPromiseKit,
498+
liquidationZcfSeat,
464499
});
465500
}

packages/treasury/src/vaultManager.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -224,21 +224,18 @@ export function makeVaultManager(
224224
);
225225

226226
const { vault, openLoan } = vaultKit;
227-
const { notifier, collateralPayoutP } = await openLoan(seat);
227+
const { notifier } = await openLoan(seat);
228228
sortedVaultKits.addVaultKit(vaultKit, notifier);
229229

230230
seat.exit();
231231

232-
// TODO: nicer to return single objects, find a better way to give them
233-
// the payout object
234232
return harden({
235233
uiNotifier: notifier,
236234
invitationMakers: Far('invitation makers', {
237235
AdjustBalances: vault.makeAdjustBalancesInvitation,
238236
CloseVault: vault.makeCloseInvitation,
239237
}),
240238
vault,
241-
liquidationPayout: collateralPayoutP,
242239
});
243240
}
244241

packages/treasury/test/swingsetTests/vat-alice.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const build = async (log, zoe, brands, payments, timer) => {
2626
}),
2727
);
2828

29-
const { vault, _liquidationPayout } = await E(loanSeat).getOfferResult();
29+
const { vault } = await E(loanSeat).getOfferResult();
3030
log(`Alice owes ${q(await E(vault).getDebtAmount())} after borrowing`);
3131
await E(timer).tick();
3232
log(`Alice owes ${q(await E(vault).getDebtAmount())} after interest`);

0 commit comments

Comments
 (0)