From 546f638efd8e88e203d3b9843f71b6e4674c8f26 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 23 Feb 2022 14:57:09 -0800 Subject: [PATCH] refactor(run-protocol): validate vault phase transitions --- .../run-protocol/src/vaultFactory/vault.js | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 8fc59699b9b3..3e91e3419f40 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -33,19 +33,10 @@ const trace = makeTracer('Vault'); * ACTIVE - vault is in use and can be changed * LIQUIDATING - vault is being liquidated by the vault manager, and cannot be changed by the user * TRANSFER - vault is released from the manager and able to be transferred + * TRANSFER - vault is able to be transferred (payments and debits frozen until it has a new owner) * CLOSED - vault was closed by the user and all assets have been paid out * LIQUIDATED - vault was closed by the manager, with remaining assets paid to owner * - * These are the valid state transitions: - * Active -> Liquidating - * Active -> Transferrable - * Active -> Closed - * Liquidating -> Liquidated - * Transferrable -> Active - * Transferrable -> Liquidating - * - * (Liquidated and Closed cannot be changed) - * * @typedef {VaultPhase[keyof typeof VaultPhase]} VAULT_PHASE */ export const VaultPhase = /** @type {const} */ ({ @@ -56,6 +47,21 @@ export const VaultPhase = /** @type {const} */ ({ TRANSFER: 'transfer', }); +/** + * @type {{[K in VAULT_PHASE]: Array}} + */ +const validTransitions = { + [VaultPhase.ACTIVE]: [ + VaultPhase.LIQUIDATING, + VaultPhase.TRANSFER, + VaultPhase.CLOSED, + ], + [VaultPhase.LIQUIDATING]: [VaultPhase.LIQUIDATED], + [VaultPhase.TRANSFER]: [VaultPhase.ACTIVE, VaultPhase.LIQUIDATING], + [VaultPhase.LIQUIDATED]: [], + [VaultPhase.CLOSED]: [], +}; + const makeOuterKit = inner => { const { updater: uiUpdater, notifier } = makeNotifierKit(); @@ -117,12 +123,31 @@ export const makeInnerVault = ( const { zcfSeat: liquidationZcfSeat, userSeat: liquidationSeat } = zcf.makeEmptySeatKit(undefined); + // #region Phase state /** @type {VAULT_PHASE} */ let phase = VaultPhase.ACTIVE; + /** + * @param {VAULT_PHASE} newPhase + */ + const assignPhase = newPhase => { + const validNewPhases = validTransitions[phase]; + if (!validNewPhases.includes(newPhase)) + throw new Error(`Vault cannot transition from ${phase} to ${newPhase}`); + phase = newPhase; + }; + + const assertPhase = allegedPhase => { + assert( + phase === allegedPhase, + X`vault must be ${allegedPhase}, not ${phase}`, + ); + }; + const assertVaultIsOpen = () => { - assert(phase === VaultPhase.ACTIVE, X`vault must still be active`); + assertPhase(VaultPhase.ACTIVE); }; + // #endregion let outerUpdater; @@ -263,6 +288,8 @@ export const makeInnerVault = ( debtSnapshot, locked: getCollateralAmount(), debt: getDebtAmount(), + // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase + // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415 vaultState: newPhase, }); }; @@ -287,8 +314,11 @@ export const makeInnerVault = ( outerUpdater.finish(uiState); outerUpdater = null; break; + case VaultPhase.TRANSFER: + // Transfer handles finish()/null itself + throw Error('no UI updates from transfer state'); default: - throw Error(`unreachable vaultState: ${phase}`); + throw Error(`unreachable vault phase: ${phase}`); } }; // XXX Echo notifications from the manager through all vaults @@ -309,15 +339,12 @@ export const makeInnerVault = ( const liquidated = newDebt => { updateDebtSnapshot(newDebt); - phase = VaultPhase.LIQUIDATED; + assignPhase(VaultPhase.LIQUIDATED); updateUiState(); }; const liquidating = () => { - if (phase === VaultPhase.LIQUIDATING) { - throw new Error('Vault already liquidating'); - } - phase = VaultPhase.LIQUIDATING; + assignPhase(VaultPhase.LIQUIDATING); updateUiState(); }; @@ -358,7 +385,7 @@ export const makeInnerVault = ( runMint.burnLosses(harden({ RUN: currentDebt }), burnSeat); seat.exit(); burnSeat.exit(); - phase = VaultPhase.CLOSED; + assignPhase(VaultPhase.CLOSED); updateDebtSnapshot(AmountMath.makeEmpty(runBrand)); updateUiState();