Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vault manager refactorings #5574

Merged
merged 3 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions packages/run-protocol/src/vaultFactory/liquidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,13 @@ const trace = makeTracer('LIQ', false);
*
* @param {ZCF} zcf
* @param {Vault} vault
* @param {(losses: Amount,
* zcfSeat: ZCFSeat
* ) => void} burnLosses
* @param {Liquidator} liquidator
* @param {Brand} collateralBrand
* @param {Ratio} penaltyRate
*/
const liquidate = async (
zcf,
vault,
burnLosses,
liquidator,
collateralBrand,
penaltyRate,
Expand Down Expand Up @@ -80,16 +76,13 @@ const liquidate = async (
const runToBurn = AmountMath.min(proceeds.RUN, debt);
// debt is fully settled, with runToBurn and shortfall
assert(AmountMath.isEqual(debt, AmountMath.add(runToBurn, shortfall)));
trace('before burn', { debt, proceeds, overage, shortfall, runToBurn });
// TODO why grant this power; we can burn it from the caller
burnLosses(runToBurn, vaultZcfSeat);
Copy link
Member Author

@turadg turadg Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now vault is updated with liquidated before the losses are burned, which updates the consumer of the vault. If burnLosses fails now the vault will still be "liquidated" to the user.

As I think this through I realize this could allow a state in which a vault's phase is liquidated and thus settled, but it's still in the vaultManager. Though that possibility true of master, albeit with one fewer case (failure of burnLosses/burnDebt) that could manifest it. We don't have checked exceptions but maybe there's something to document about what failures must be handled? cc @dtribble


// Accounting complete. Update the vault state.
// Manager accounting changes determined. Update the vault state.
vault.liquidated();
// remaining funds are left on the vault for the user to close and claim

// for accounting
return { proceeds: proceeds.RUN, overage, shortfall };
// for manager's accounting
return { proceeds: proceeds.RUN, overage, runToBurn, shortfall };
};

const liquidationDetailTerms = debtBrand =>
Expand Down
39 changes: 18 additions & 21 deletions packages/run-protocol/src/vaultFactory/prioritizedVaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ export const currentDebtToCollateral = vault =>
* Vaults, ordered by their liquidation ratio so that all the
* vaults below a threshold can be quickly found and liquidated.
*
* @param {() => void} reschedulePriceCheck called when there is a new
* least-collateralized vault
* @param {() => void} highestChanged called when there is a new
* `highestRatio` (least-collateralized vault)
*/
export const makePrioritizedVaults = (reschedulePriceCheck = () => {}) => {
export const makePrioritizedVaults = (highestChanged = () => {}) => {
const vaults = makeOrderedVaultStore();

/**
* Set the callback for when there is a new least-collateralized vault
*
* @param {() => void} rescheduleFn
* @param {() => void} callback
*/
const setRescheduler = rescheduleFn => {
reschedulePriceCheck = rescheduleFn;
const onHighestRatioChanged = callback => {
highestChanged = callback;
};

// To deal with fluctuating prices and varying collateralization, we schedule a
Expand All @@ -79,18 +79,16 @@ export const makePrioritizedVaults = (reschedulePriceCheck = () => {}) => {
*
* @returns {Ratio=} actual debt over collateral
*/
const firstDebtRatio = () => {
const highestRatio = () => {
if (vaults.getSize() === 0) {
return undefined;
}
// Get the first vault.
const [vault] = vaults.values();
const collateralAmount = vault.getCollateralAmount();
if (AmountMath.isEmpty(collateralAmount)) {
// This should only happen when the vault has been added but not funded yet
// TODO remove exceptional case for new vaults; if it's in the store it must be liquidatable
return undefined;
}
assert(
!AmountMath.isEmpty(vault.getCollateralAmount()),
'First vault had no collateral',
);
return currentDebtToCollateral(vault);
};

Expand Down Expand Up @@ -140,15 +138,14 @@ export const makePrioritizedVaults = (reschedulePriceCheck = () => {}) => {
*/
const addVault = (vaultId, vault) => {
const key = vaults.addVault(vaultId, vault);
// TODO refactor to fulfill this invariant
// assert(
// !AmountMath.isEmpty(vault.getCollateralAmount()),
// 'Tracked vaults must have collateral (be liquidatable',
// );
assert(
!AmountMath.isEmpty(vault.getCollateralAmount()),
'Tracked vaults must have collateral (be liquidatable)',
);
trace('addVault', key, 'when first:', firstKey);
if (!firstKey || keyLT(key, firstKey)) {
firstKey = key;
reschedulePriceCheck();
highestChanged();
}
return key;
};
Expand Down Expand Up @@ -186,9 +183,9 @@ export const makePrioritizedVaults = (reschedulePriceCheck = () => {}) => {
entriesPrioritizedGTE,
getCount: vaults.getSize,
hasVaultByAttributes,
highestRatio: firstDebtRatio,
highestRatio,
removeVault,
removeVaultByAttributes,
setRescheduler,
onHighestRatioChanged,
});
};
14 changes: 9 additions & 5 deletions packages/run-protocol/src/vaultFactory/vault.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const validTransitions = {
* @property {MintAndReallocate} mintAndReallocate
* @property {(amount: Amount, seat: ZCFSeat) => void} burnAndRecord
* @property {() => Ratio} getCompoundedInterest
* @property {(oldDebt: import('./storeUtils.js').NormalizedDebt, oldCollateral: Amount<'nat'>, vaultId: VaultId, vaultPhase: VaultPhase) => void} handleBalanceChange
* @property {(oldDebt: import('./storeUtils.js').NormalizedDebt, oldCollateral: Amount<'nat'>, vaultId: VaultId, vaultPhase: VaultPhase, vault: Vault) => void} handleBalanceChange
* @property {() => import('./vaultManager.js').GovernedParamGetters} getGovernedParams
*/

Expand Down Expand Up @@ -260,6 +260,7 @@ const helperBehavior = {
oldCollateral,
state.idInManager,
state.phase,
facets.self,
);
},

Expand Down Expand Up @@ -411,6 +412,7 @@ const helperBehavior = {
oldCollateral,
state.idInManager,
state.phase,
facets.self,
);

return 'your loan is closed, thank you for your business';
Expand Down Expand Up @@ -545,13 +547,15 @@ const selfBehavior = {
*/
initVaultKit: async ({ state, facets }, seat) => {
const { self, helper } = facets;

const normalizedDebtPre = self.getNormalizedDebt();
const actualDebtPre = self.getCurrentDebt();
assert(
AmountMath.isEmpty(state.debtSnapshot),
AmountMath.isEmpty(normalizedDebtPre) &&
AmountMath.isEmpty(actualDebtPre),
X`vault must be empty initially`,
);
// TODO should this be simplified to know that the oldDebt mut be empty?
const normalizedDebtPre = self.getNormalizedDebt();
const actualDebtPre = self.getCurrentDebt();

const collateralPre = self.getCollateralAmount();
trace('initVaultKit start: collateral', state.idInManager, {
actualDebtPre,
Expand Down
62 changes: 46 additions & 16 deletions packages/run-protocol/src/vaultFactory/vaultManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,23 +427,24 @@ const helperBehavior = {
*/
liquidateAndRemove: ({ state, facets }, [key, vault]) => {
const { factoryPowers, prioritizedVaults, zcf } = state;
trace('liquidating', vault.getVaultSeat().getProposal());
const vaultSeat = vault.getVaultSeat();
trace('liquidating', vaultSeat.getProposal());

const collateralPre = vault.getCollateralAmount();

// Start liquidation (vaultState: LIQUIDATING)
const liquidator = state.liquidator;
assert(liquidator);
trace('liquidating 2', vault.getVaultSeat().getProposal());
return liquidate(
zcf,
vault,
(amount, seat) => facets.manager.burnAndRecord(amount, seat),
liquidator,
state.collateralBrand,
factoryPowers.getGovernedParams().getLiquidationPenalty(),
)
.then(accounting => {
facets.manager.burnAndRecord(accounting.runToBurn, vaultSeat);

// current values
state.totalCollateral = AmountMath.subtract(
state.totalCollateral,
Expand Down Expand Up @@ -564,13 +565,15 @@ const managerBehavior = {
* @param {Amount<'nat'>} oldCollateral
* @param {VaultId} vaultId
* @param {import('./vault.js').VaultPhase} vaultPhase at the end of whatever change updatied balances
* @param {Vault} vault
*/
handleBalanceChange: (
{ state, facets },
oldDebtNormalized,
oldCollateral,
vaultId,
vaultPhase,
vault,
) => {
const { prioritizedVaults } = state;

Expand All @@ -590,12 +593,20 @@ const managerBehavior = {
'Settled vaults must not be retained in storage',
);
} else {
// its position in the queue is no longer valid
const vault = prioritizedVaults.removeVaultByAttributes(
oldDebtNormalized,
oldCollateral,
vaultId,
);
const isNew = AmountMath.isEmpty(oldDebtNormalized);
if (!isNew) {
// its position in the queue is no longer valid

const vaultInStore = prioritizedVaults.removeVaultByAttributes(
oldDebtNormalized,
oldCollateral,
vaultId,
);
assert(
vault === vaultInStore,
'handleBalanceChange for two different vaults',
);
}

// replace in queue, but only if it can accrue interest or be liquidated (i.e. has debt).
// getCurrentDebt() would also work (0x = 0) but require more computation.
Expand Down Expand Up @@ -660,19 +671,38 @@ const selfBehavior = {

const vault = makeVault(zcf, manager, vaultId);

// TODO Don't record the vault until it gets opened
const addedVaultKey = prioritizedVaults.addVault(vaultId, vault);

try {
// TODO `await` is allowed until the above ordering is fixed
// eslint-disable-next-line @jessie.js/no-nested-await
const vaultKit = await vault.initVaultKit(seat);
// initVaultKit calls back to handleBalanceChange() which will add the
// vault to prioritizedVaults
seat.exit();
return vaultKit;
} catch (err) {
// remove it from prioritizedVaults
// XXX openLoan shouldn't assume it's already in the prioritizedVaults
prioritizedVaults.removeVault(addedVaultKey);
// ??? do we still need this cleanup? it won't get into the store unless it has collateral,
// which should qualify it to be in the store. If we drop this catch then the nested await
// for `vault.initVaultKit()` goes away.

// remove it from the store if it got in
/** @type {NormalizedDebt} */
// @ts-expect-error cast
const normalizedDebt = AmountMath.makeEmpty(state.debtBrand);
const collateralPre = seat.getCurrentAllocation().Collateral;
try {
prioritizedVaults.removeVaultByAttributes(
normalizedDebt,
collateralPre,
vaultId,
);
console.error('removed vault', vaultId, 'after initVaultKit failure');
} catch {
console.error(
'vault',
vaultId,
'never stored during initVaultKit failure',
);
}
throw err;
}
},
Expand Down Expand Up @@ -740,7 +770,7 @@ const selfBehavior = {

/** @param {MethodContext} context */
const finish = ({ state, facets: { helper } }) => {
state.prioritizedVaults.setRescheduler(helper.reschedulePriceCheck);
state.prioritizedVaults.onHighestRatioChanged(helper.reschedulePriceCheck);

// push initial state of metrics
helper.updateMetrics();
Expand Down
11 changes: 6 additions & 5 deletions packages/run-protocol/test/vaultFactory/test-vaultFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2515,17 +2515,17 @@ test('manager notifiers', async t => {
t.is((await E(vault).getCurrentDebt()).value, DEBT1);

trace('2. Remove collateral');
let taken = aeth.make(50_000n);
const COLL_REMOVED = 50_000n;
const takeCollateralSeat = await E(services.zoe).offer(
await E(vault).makeAdjustBalancesInvitation(),
harden({
give: {},
want: { Collateral: taken },
want: { Collateral: aeth.make(COLL_REMOVED) },
}),
);
await E(takeCollateralSeat).getOfferResult();
await m.assertChange({
totalCollateral: { value: AMPLE - taken.value },
totalCollateral: { value: AMPLE - COLL_REMOVED },
});

trace('3. Liquidate all (1 loan)');
Expand Down Expand Up @@ -2590,7 +2590,7 @@ test('manager notifiers', async t => {
numLiquidationsCompleted: 2,
numVaults: 1,
totalCollateral: { value: AMPLE },
totalDebt: { value: 0n },
totalDebt: { value: DEBT1 },
totalOverageReceived: { value: totalOverageReceived },
totalProceedsReceived: { value: totalProceedsReceived },
});
Expand All @@ -2599,6 +2599,7 @@ test('manager notifiers', async t => {
numLiquidationsCompleted: 3,
numVaults: 0,
totalCollateral: { value: 0n },
totalDebt: { value: 0n },
totalProceedsReceived: { value: totalProceedsReceived },
});
m.assertFullyLiquidated();
Expand Down Expand Up @@ -2693,7 +2694,7 @@ test('manager notifiers', async t => {
numLiquidationsCompleted: 5,
numVaults: 1,
totalCollateral: { value: AMPLE },
totalDebt: { value: DEBT1 + DEBT2 + interestAccrued - nextProceeds - 296n }, // debt changed already with proceeds from next notification
totalDebt: { value: DEBT1 + DEBT2 + interestAccrued - nextProceeds },
totalProceedsReceived: { value: totalProceedsReceived },
});
nextProceeds = 296n;
Expand Down