From bdc0c19a5a87c43707761d13959cb57e644a1a1c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 1 Feb 2022 14:37:58 -0800 Subject: [PATCH 01/47] types --- .../src/vaultFactory/prioritizedVaults.js | 63 ++++++++++++++++--- .../vaultFactory/test-prioritizedVaults.js | 7 +++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 72ba2934952..df41d40650b 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -16,6 +16,12 @@ const { multiply, isGTE } = natSafeMath; // debtToCollateral (which is not the collateralizationRatio) is updated using // an observer on the UIState. +/** + * + * @param {Ratio} left + * @param {Ratio} right + * @returns {boolean} + */ const ratioGTE = (left, right) => { assert( left.numerator.brand === right.numerator.brand && @@ -28,6 +34,12 @@ const ratioGTE = (left, right) => { ); }; +/** + * + * @param {Amount} debtAmount + * @param {Amount} collateralAmount + * @returns {Ratio} + */ const calculateDebtToCollateral = (debtAmount, collateralAmount) => { if (AmountMath.isEmpty(collateralAmount)) { return makeRatioFromAmounts( @@ -38,12 +50,23 @@ const calculateDebtToCollateral = (debtAmount, collateralAmount) => { return makeRatioFromAmounts(debtAmount, collateralAmount); }; +/** + * + * @param {VaultKit} vaultKit + * @returns {Ratio} + */ const currentDebtToCollateral = vaultKit => calculateDebtToCollateral( vaultKit.vault.getDebtAmount(), vaultKit.vault.getCollateralAmount(), ); +/** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultPair */ +/** + * @param {VaultPair} leftVaultPair + * @param {VaultPair} rightVaultPair + * @returns {-1 | 0 | 1} + */ const compareVaultKits = (leftVaultPair, rightVaultPair) => { const leftVaultRatio = leftVaultPair.debtToCollateral; const rightVaultRatio = rightVaultPair.debtToCollateral; @@ -59,14 +82,17 @@ const compareVaultKits = (leftVaultPair, rightVaultPair) => { throw Error("The vault's collateral ratios are not comparable"); }; -// makePrioritizedVaults() takes a function parameter, which will be called when -// there is a new least-collateralized vault. - +/** + * + * @param {() => void} reschedulePriceCheck called when there is a new + * least-collateralized vault + */ export const makePrioritizedVaults = reschedulePriceCheck => { - // Each entry is [Vault, debtToCollateralRatio]. The array must be resorted on + // The array must be resorted on // every insert, and whenever any vault's ratio changes. We can remove an // arbitrary number of vaults from the front of the list without resorting. We // delete single entries using filter(), which leaves the array sorted. + /** @type {VaultPair[]} */ let vaultsWithDebtRatio = []; // To deal with fluctuating prices and varying collateralization, we schedule a @@ -75,6 +101,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // current high-water mark fires, we reschedule at the new highest ratio // (which should be lower, as we will have liquidated any that were at least // as high.) + /** @type {Ratio=} */ let highestDebtToCollateral; // Check if this ratio of debt to collateral would be the highest known. If @@ -95,6 +122,10 @@ export const makePrioritizedVaults = reschedulePriceCheck => { return mostIndebted ? mostIndebted.debtToCollateral : undefined; }; + /** + * + * @param {VaultKit} vaultKit + */ const removeVault = vaultKit => { vaultsWithDebtRatio = vaultsWithDebtRatio.filter( v => v.vaultKit !== vaultKit, @@ -103,6 +134,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { highestDebtToCollateral = highestRatio(); }; + /** + * + * @param {VaultKit} vaultKit + * @param {Ratio} debtRatio + */ const updateDebtRatio = (vaultKit, debtRatio) => { vaultsWithDebtRatio.forEach((vaultPair, index) => { if (vaultPair.vaultKit === vaultKit) { @@ -120,6 +156,10 @@ export const makePrioritizedVaults = reschedulePriceCheck => { highestDebtToCollateral = highestRatio(); }; + /** + * + * @param {VaultKit} vaultKit + */ const makeObserver = vaultKit => ({ updateState: state => { if (AmountMath.isEmpty(state.locked)) { @@ -138,6 +178,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }, }); + /** + * + * @param {VaultKit} vaultKit + * @param {ERef>} notifier + */ const addVaultKit = (vaultKit, notifier) => { const debtToCollateral = currentDebtToCollateral(vaultKit); vaultsWithDebtRatio.push({ vaultKit, debtToCollateral }); @@ -146,7 +191,12 @@ export const makePrioritizedVaults = reschedulePriceCheck => { rescheduleIfHighest(debtToCollateral); }; - // Invoke a function for vaults with debt to collateral at or above the ratio + /** + * Invoke a function for vaults with debt to collateral at or above the ratio + * + * @param {Ratio} ratio + * @param {(VaultPair) => void} func + */ const forEachRatioGTE = (ratio, func) => { // vaults are sorted with highest ratios first let index; @@ -172,8 +222,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const map = func => vaultsWithDebtRatio.map(func); - const reduce = (func, init = undefined) => - vaultsWithDebtRatio.reduce(func, init); + const reduce = (func, init) => vaultsWithDebtRatio.reduce(func, init); return harden({ addVaultKit, diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 470c0ecb7b6..4a94e4e2a0f 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -50,6 +50,12 @@ function makeRescheduler() { }; } +/** + * + * @param {Amount} initDebt + * @param {Amount} initCollateral + * @returns {VaultKit & {vault: {setDebt: (Amount) => void}}} + */ function makeFakeVaultKit( initDebt, initCollateral = AmountMath.make(initDebt.brand, 100n), @@ -62,6 +68,7 @@ function makeFakeVaultKit( setDebt: newDebt => (debt = newDebt), setCollateral: newCollateral => (collateral = newCollateral), }); + // @ts-expect-error pretend this is compatible with VaultKit return harden({ vault, liquidate: () => {}, From 5cae71a55d7d3495aa08ca30a8fca4c41df76d0b Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 1 Feb 2022 14:58:24 -0800 Subject: [PATCH 02/47] runtime changes for type clarity (safe?) --- packages/run-protocol/src/vaultFactory/prioritizedVaults.js | 1 - packages/run-protocol/src/vaultFactory/types.js | 2 ++ packages/run-protocol/src/vaultFactory/vaultManager.js | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index df41d40650b..17f9b28b5ab 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -61,7 +61,6 @@ const currentDebtToCollateral = vaultKit => vaultKit.vault.getCollateralAmount(), ); -/** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultPair */ /** * @param {VaultPair} leftVaultPair * @param {VaultPair} rightVaultPair diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 8aec5f308f6..c2dd9d38d87 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -255,3 +255,5 @@ * @param {LiquidationStrategy} strategy * @param {Brand} collateralBrand */ + +/** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultPair */ diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 117f26e69da..d76f75b62c4 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -92,6 +92,8 @@ export const makeVaultManager = ( // // sortedVaultKits should only be set once, but can't be set until after the // definition of reschedulePriceCheck, which refers to sortedVaultKits + // XXX mutability and flow control + /** @type {ReturnType=} */ let sortedVaultKits; let outstandingQuote; @@ -104,6 +106,7 @@ export const makeVaultManager = ( // when a priceQuote is received, we'll only reschedule if the high-water // level when the request was made matches the current high-water level. const reschedulePriceCheck = async () => { + assert(sortedVaultKits); const highestDebtRatio = sortedVaultKits.highestRatio(); if (!highestDebtRatio) { // if there aren't any open vaults, we don't need an outstanding RFQ. @@ -168,6 +171,7 @@ export const makeVaultManager = ( sortedVaultKits = makePrioritizedVaults(reschedulePriceCheck); const liquidateAll = () => { + assert(sortedVaultKits); const promises = sortedVaultKits.map(({ vaultKit }) => liquidate( zcf, @@ -181,6 +185,7 @@ export const makeVaultManager = ( }; const chargeAllVaults = async (updateTime, poolIncrementSeat) => { + assert(sortedVaultKits); const poolIncrement = sortedVaultKits.reduce( (total, vaultPair) => AmountMath.add( @@ -243,6 +248,7 @@ export const makeVaultManager = ( const { vault, openLoan } = vaultKit; const { notifier } = await openLoan(seat); + assert(sortedVaultKits); sortedVaultKits.addVaultKit(vaultKit, notifier); seat.exit(); From 5101d54d7aac772dc1eda86e961cb25110d58c67 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 1 Feb 2022 15:37:14 -0800 Subject: [PATCH 03/47] types --- .../src/vaultFactory/prioritizedVaults.js | 11 +++-- .../run-protocol/src/vaultFactory/types.js | 2 - .../src/vaultFactory/vaultManager.js | 1 + packages/zoe/src/contractSupport/ratio.js | 35 ++++++++++++---- packages/zoe/src/contractSupport/types.js | 42 ------------------- 5 files changed, 36 insertions(+), 55 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 17f9b28b5ab..42d313f4c00 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -61,9 +61,11 @@ const currentDebtToCollateral = vaultKit => vaultKit.vault.getCollateralAmount(), ); +/** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultKitRecord */ + /** - * @param {VaultPair} leftVaultPair - * @param {VaultPair} rightVaultPair + * @param {VaultKitRecord} leftVaultPair + * @param {VaultKitRecord} rightVaultPair * @returns {-1 | 0 | 1} */ const compareVaultKits = (leftVaultPair, rightVaultPair) => { @@ -91,7 +93,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // every insert, and whenever any vault's ratio changes. We can remove an // arbitrary number of vaults from the front of the list without resorting. We // delete single entries using filter(), which leaves the array sorted. - /** @type {VaultPair[]} */ + /** @type {VaultKitRecord[]} */ let vaultsWithDebtRatio = []; // To deal with fluctuating prices and varying collateralization, we schedule a @@ -106,6 +108,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // Check if this ratio of debt to collateral would be the highest known. If // so, reset our highest and invoke the callback. This can be called on new // vaults and when we get a state update for a vault changing balances. + /** @param {Ratio} collateralToDebt */ const rescheduleIfHighest = collateralToDebt => { if ( !highestDebtToCollateral || @@ -194,7 +197,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * Invoke a function for vaults with debt to collateral at or above the ratio * * @param {Ratio} ratio - * @param {(VaultPair) => void} func + * @param {(record: VaultKitRecord) => void} func */ const forEachRatioGTE = (ratio, func) => { // vaults are sorted with highest ratios first diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index c2dd9d38d87..8aec5f308f6 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -255,5 +255,3 @@ * @param {LiquidationStrategy} strategy * @param {Brand} collateralBrand */ - -/** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultPair */ diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index d76f75b62c4..4d34efbb3db 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -95,6 +95,7 @@ export const makeVaultManager = ( // XXX mutability and flow control /** @type {ReturnType=} */ let sortedVaultKits; + /** @type {MutableQuote=} */ let outstandingQuote; // When any Vault's debt ratio is higher than the current high-water level, diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 200dc7e6d04..4ebb54737bd 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -59,7 +59,13 @@ export const assertIsRatio = ratio => { ); }; -/** @type {MakeRatio} */ +/** + * @param {bigint} numerator + * @param {Brand} numeratorBrand + * @param {bigint=} denominator The default denominator is 100 + * @param {Brand=} denominatorBrand The default is to reuse the numeratorBrand + * @returns {Ratio} + */ export const makeRatio = ( numerator, numeratorBrand, @@ -77,7 +83,11 @@ export const makeRatio = ( }); }; -/** @type {MakeRatioFromAmounts} */ +/** + * @param {Amount} numeratorAmount + * @param {Amount} denominatorAmount + * @returns {Ratio} + */ export const makeRatioFromAmounts = (numeratorAmount, denominatorAmount) => { AmountMath.coerce(numeratorAmount.brand, numeratorAmount); AmountMath.coerce(denominatorAmount.brand, denominatorAmount); @@ -159,7 +169,11 @@ export const invertRatio = ratio => { ); }; -/** @type {AddRatios} */ +/** + * @param {Ratio} left + * @param {Ratio} right + * @returns {Ratio} + */ export const addRatios = (left, right) => { assertIsRatio(right); assertIsRatio(left); @@ -180,7 +194,11 @@ export const addRatios = (left, right) => { ); }; -/** @type {MultiplyRatios} */ +/** + * @param {Ratio} left + * @param {Ratio} right + * @returns {Ratio} + */ export const multiplyRatios = (left, right) => { assertIsRatio(right); assertIsRatio(left); @@ -198,8 +216,12 @@ export const multiplyRatios = (left, right) => { ); }; -// If ratio is between 0 and 1, subtract from 1. -/** @type {OneMinus} */ +/** + * If ratio is between 0 and 1, subtract from 1. + * + * @param {Ratio} ratio + * @returns {Ratio} + */ export const oneMinus = ratio => { assertIsRatio(ratio); assert( @@ -213,7 +235,6 @@ export const oneMinus = ratio => { return makeRatio( subtract(ratio.denominator.value, ratio.numerator.value), ratio.numerator.brand, - // @ts-ignore asserts ensure values are Nats ratio.denominator.value, ratio.numerator.brand, ); diff --git a/packages/zoe/src/contractSupport/types.js b/packages/zoe/src/contractSupport/types.js index 3e8a350a93b..64c4ddf5d3a 100644 --- a/packages/zoe/src/contractSupport/types.js +++ b/packages/zoe/src/contractSupport/types.js @@ -127,22 +127,6 @@ * @property {Amount} denominator */ -/** - * @callback MakeRatio - * @param {bigint} numerator - * @param {Brand} numeratorBrand - * @param {bigint=} denominator The default denominator is 100 - * @param {Brand=} denominatorBrand The default is to reuse the numeratorBrand - * @returns {Ratio} - */ - -/** - * @callback MakeRatioFromAmounts - * @param {Amount} numerator - * @param {Amount} denominator - * @returns {Ratio} - */ - /** * @callback MultiplyBy * @param {Amount} amount @@ -163,29 +147,3 @@ * @typedef {DivideBy} FloorDivideBy * @typedef {DivideBy} CeilDivideBy */ - -/** - * @callback InvertRatio - * @param {Ratio} ratio - * @returns {Ratio} - */ - -/** - * @callback OneMinus - * @param {Ratio} ratio - * @returns {Ratio} - */ - -/** - * @callback AddRatios - * @param {Ratio} left - * @param {Ratio} right - * @returns {Ratio} - */ - -/** - * @callback MultiplyRatios - * @param {Ratio} left - * @param {Ratio} right - * @returns {Ratio} - */ From 9d1b8186cca1fe24cfb9bfc6b891ad8ee273ee1b Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 3 Feb 2022 15:03:25 -0800 Subject: [PATCH 04/47] make accrued debt a function of prior debt and current compounded interest --- .../run-protocol/src/vaultFactory/types.js | 1 + .../run-protocol/src/vaultFactory/vault.js | 127 +++++++++++++----- .../src/vaultFactory/vaultManager.js | 15 ++- .../vaultFactory/vault-contract-wrapper.js | 1 + packages/zoe/src/contractSupport/ratio.js | 4 +- 5 files changed, 106 insertions(+), 42 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 8aec5f308f6..3782fd11dca 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -102,6 +102,7 @@ * @typedef {Object} InnerVaultManagerBase * @property {() => Brand} getCollateralBrand * @property {ReallocateReward} reallocateReward + * @property {() => Ratio} getCompoundedInterest */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index fe553fbaf2a..a9cd018a5b3 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -12,7 +12,11 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { makeNotifierKit } from '@agoric/notifier'; -import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { + invertRatio, + makeRatio, + multiplyRatios, +} from '@agoric/zoe/src/contractSupport/ratio.js'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@agoric/promise-kit'; @@ -75,7 +79,40 @@ export const makeVaultKit = ( const { zcfSeat: vaultSeat } = zcf.makeEmptySeatKit(); const { brand: runBrand } = runMint.getIssuerRecord(); - let runDebt = AmountMath.makeEmpty(runBrand); + + // ??? is there a good way to encapsulate these snapshot values so they can only be touched together? + // perhaps a hardened DebtSnapshot {debt: Amount, interest: Ratio} + let runDebtSnapshot = AmountMath.makeEmpty(runBrand); + /** + * compounded interest at the time the debt was snapshotted + * + * @type {Ratio=} + */ + let interestSnapshot; + + /** + * + * @param {Amount} newDebt + */ + const updateDebtSnapshot = newDebt => { + runDebtSnapshot = newDebt; + interestSnapshot = manager.getCompoundedInterest(); + }; + + /** + * The current debt, including accrued interest + * + * @returns {Amount} + */ + const getDebtAmount = () => { + assert(interestSnapshot); + const interestSinceSnapshot = multiplyRatios( + manager.getCompoundedInterest(), + invertRatio(interestSnapshot), + ); + + return floorMultiplyBy(runDebtSnapshot, interestSinceSnapshot); + }; const getCollateralAllocated = seat => seat.getAmountAllocated('Collateral', collateralBrand); @@ -125,11 +162,11 @@ export const makeVaultKit = ( ); // TODO: allow Ratios to represent X/0. - if (AmountMath.isEmpty(runDebt)) { + if (AmountMath.isEmpty(runDebtSnapshot)) { return makeRatio(collateralAmount.value, runBrand, 1n); } const collateralValueInRun = getAmountOut(quoteAmount); - return makeRatioFromAmounts(collateralValueInRun, runDebt); + return makeRatioFromAmounts(collateralValueInRun, runDebtSnapshot); }; // call this whenever anything changes! @@ -144,7 +181,7 @@ export const makeVaultKit = ( interestRate: manager.getInterestRate(), liquidationRatio: manager.getLiquidationMargin(), locked: getCollateralAmount(), - debt: runDebt, + debt: getDebtAmount(), collateralizationRatio, liquidated: vaultState === VaultState.CLOSED, vaultState, @@ -167,7 +204,8 @@ export const makeVaultKit = ( * @param {Amount} newDebt */ const liquidated = newDebt => { - runDebt = newDebt; + updateDebtSnapshot(newDebt); + vaultState = VaultState.CLOSED; updateUiState(); }; @@ -196,27 +234,28 @@ export const makeVaultKit = ( // you must pay off the entire remainder but if you offer too much, we won't // take more than you owe assert( - AmountMath.isGTE(runReturned, runDebt), - X`You must pay off the entire debt ${runReturned} > ${runDebt}`, + AmountMath.isGTE(runReturned, getDebtAmount()), + X`You must pay off the entire debt ${runReturned} > ${getDebtAmount()}`, ); // Return any overpayment const { zcfSeat: burnSeat } = zcf.makeEmptySeatKit(); - burnSeat.incrementBy(seat.decrementBy(harden({ RUN: runDebt }))); + burnSeat.incrementBy(seat.decrementBy(harden({ RUN: getDebtAmount() }))); seat.incrementBy( vaultSeat.decrementBy( harden({ Collateral: getCollateralAllocated(vaultSeat) }), ), ); zcf.reallocate(seat, vaultSeat, burnSeat); - runMint.burnLosses(harden({ RUN: runDebt }), burnSeat); + runMint.burnLosses(harden({ RUN: getDebtAmount() }), burnSeat); seat.exit(); burnSeat.exit(); vaultState = VaultState.CLOSED; updateUiState(); - runDebt = AmountMath.makeEmpty(runBrand); + updateDebtSnapshot(AmountMath.makeEmpty(runBrand)); + assertVaultHoldsNoRun(); vaultSeat.exit(); liquidationZcfSeat.exit(); @@ -288,13 +327,18 @@ export const makeVaultKit = ( } }; - // Calculate the target RUN level for the vaultSeat and clientSeat implied - // by the proposal. If the proposal wants collateral, transfer that amount - // from vault to client. If the proposal gives collateral, transfer the - // opposite direction. Otherwise, return the current level. - // - // Since we don't allow the debt to go negative, we will reduce the amount we - // accept when the proposal says to give more RUN than are owed. + /** + * Calculate the target RUN level for the vaultSeat and clientSeat implied + * by the proposal. If the proposal wants collateral, transfer that amount + * from vault to client. If the proposal gives collateral, transfer the + * opposite direction. Otherwise, return the current level. + * + * Since we don't allow the debt to go negative, we will reduce the amount we + * accept when the proposal says to give more RUN than are owed. + * + * @param {ZCFSeat} seat + * @returns {{vault: Amount, client: Amount}} + */ const targetRunLevels = seat => { const clientAllocation = getRunAllocated(seat); const proposal = seat.getProposal(); @@ -305,8 +349,8 @@ export const makeVaultKit = ( }; } else if (proposal.give.RUN) { // We don't allow runDebt to be negative, so we'll refund overpayments - const acceptedRun = AmountMath.isGTE(proposal.give.RUN, runDebt) - ? runDebt + const acceptedRun = AmountMath.isGTE(proposal.give.RUN, getDebtAmount()) + ? getDebtAmount() : proposal.give.RUN; return { @@ -329,27 +373,33 @@ export const makeVaultKit = ( ); } else if (proposal.give.RUN) { // We don't allow runDebt to be negative, so we'll refund overpayments - const acceptedRun = AmountMath.isGTE(proposal.give.RUN, runDebt) - ? runDebt + const acceptedRun = AmountMath.isGTE(proposal.give.RUN, getDebtAmount()) + ? getDebtAmount() : proposal.give.RUN; vaultSeat.incrementBy(seat.decrementBy(harden({ RUN: acceptedRun }))); } }; - // Calculate the fee, the amount to mint and the resulting debt. + /** + * Calculate the fee, the amount to mint and the resulting debt + * + * @param {ProposalRecord} proposal + * @param {{vault: Amount, client: Amount}} runAfter + */ const loanFee = (proposal, runAfter) => { let newDebt; + const currentDebt = getDebtAmount(); let toMint = AmountMath.makeEmpty(runBrand); let fee = AmountMath.makeEmpty(runBrand); if (proposal.want.RUN) { fee = ceilMultiplyBy(proposal.want.RUN, manager.getLoanFee()); toMint = AmountMath.add(proposal.want.RUN, fee); - newDebt = AmountMath.add(runDebt, toMint); + newDebt = AmountMath.add(currentDebt, toMint); } else if (proposal.give.RUN) { - newDebt = AmountMath.subtract(runDebt, runAfter.vault); + newDebt = AmountMath.subtract(currentDebt, runAfter.vault); } else { - newDebt = runDebt; + newDebt = currentDebt; } return { newDebt, toMint, fee }; }; @@ -415,7 +465,8 @@ export const makeVaultKit = ( transferRun(clientSeat); manager.reallocateReward(fee, vaultSeat, clientSeat); - runDebt = newDebt; + updateDebtSnapshot(newDebt); + runMint.burnLosses(harden({ RUN: runAfter.vault }), vaultSeat); assertVaultHoldsNoRun(); @@ -433,7 +484,10 @@ export const makeVaultKit = ( /** @type {OfferHandler} */ const openLoan = async seat => { - assert(AmountMath.isEmpty(runDebt), X`vault must be empty initially`); + assert( + AmountMath.isEmpty(runDebtSnapshot), + X`vault must be empty initially`, + ); // get the payout to provide access to the collateral if the // contract abandons const { @@ -450,10 +504,11 @@ export const makeVaultKit = ( ); } - runDebt = AmountMath.add(wantedRun, fee); - await assertSufficientCollateral(collateralAmount, runDebt); + updateDebtSnapshot(AmountMath.add(wantedRun, fee)); - runMint.mintGains(harden({ RUN: runDebt }), vaultSeat); + await assertSufficientCollateral(collateralAmount, getDebtAmount()); + + runMint.mintGains(harden({ RUN: getDebtAmount() }), vaultSeat); seat.incrementBy(vaultSeat.decrementBy(harden({ RUN: wantedRun }))); vaultSeat.incrementBy( @@ -467,6 +522,8 @@ export const makeVaultKit = ( }; /** + * FIXME we no longer calculate interest the same way + * * @param {bigint} currentTime * @returns {Amount} rate of interest used for accrual period */ @@ -481,23 +538,23 @@ export const makeVaultKit = ( const debtStatus = interestCalculator.calculateReportingPeriod( { latestInterestUpdate, - newDebt: runDebt, + newDebt: getDebtAmount(), interest: AmountMath.makeEmpty(runBrand), }, currentTime, ); + updateDebtSnapshot(debtStatus.newDebt); + if (debtStatus.latestInterestUpdate === latestInterestUpdate) { return AmountMath.makeEmpty(runBrand); } - ({ latestInterestUpdate, newDebt: runDebt } = debtStatus); + ({ latestInterestUpdate } = debtStatus); updateUiState(); return debtStatus.interest; }; - const getDebtAmount = () => runDebt; - /** @type {Vault} */ const vault = Far('vault', { makeAdjustBalancesInvitation, diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 4d34efbb3db..002555f4432 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -10,6 +10,7 @@ import { getAmountIn, ceilMultiplyBy, ceilDivideBy, + makeRatio, } from '@agoric/zoe/src/contractSupport/index.js'; import { observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; @@ -78,7 +79,7 @@ export const makeVaultManager = ( async getCollateralQuote() { // get a quote for one unit of the collateral const displayInfo = await E(collateralBrand).getDisplayInfo(); - const decimalPlaces = (displayInfo && displayInfo.decimalPlaces) || 0n; + const decimalPlaces = displayInfo?.decimalPlaces || 0n; return E(priceAuthority).quoteGiven( AmountMath.make(collateralBrand, 10n ** Nat(decimalPlaces)), runBrand, @@ -120,7 +121,7 @@ export const makeVaultManager = ( // with the highest debt to collateral ratio will no longer be valued at the // liquidationMargin above its debt. const triggerPoint = ceilMultiplyBy( - highestDebtRatio.numerator, + highestDebtRatio.numerator, // debt liquidationMargin, ); @@ -130,14 +131,14 @@ export const makeVaultManager = ( // liquidate anything that's above the price level. if (outstandingQuote) { E(outstandingQuote).updateLevel( - highestDebtRatio.denominator, + highestDebtRatio.denominator, // collateral triggerPoint, ); return; } outstandingQuote = await E(priceAuthority).mutableQuoteWhenLT( - highestDebtRatio.denominator, + highestDebtRatio.denominator, // collateral triggerPoint, ); @@ -185,6 +186,7 @@ export const makeVaultManager = ( return Promise.all(promises); }; + // FIXME don't mutate vaults to charge them const chargeAllVaults = async (updateTime, poolIncrementSeat) => { assert(sortedVaultKits); const poolIncrement = sortedVaultKits.reduce( @@ -197,7 +199,9 @@ export const makeVaultManager = ( ); sortedVaultKits.updateAllDebts(); reschedulePriceCheck(); + // @ts-expect-error bad typedef for reduce runMint.mintGains(harden({ RUN: poolIncrement }), poolIncrementSeat); + // @ts-expect-error bad typedef for reduce reallocateReward(poolIncrement, poolIncrementSeat); }; @@ -209,7 +213,7 @@ export const makeVaultManager = ( const timeObserver = { updateState: updateTime => - chargeAllVaults(updateTime, poolIncrementSeat).catch(_ => {}), + chargeAllVaults(updateTime, poolIncrementSeat).catch(console.error), fail: reason => { zcf.shutdownWithFailure( assert.error(X`Unable to continue without a timer: ${reason}`), @@ -229,6 +233,7 @@ export const makeVaultManager = ( ...shared, reallocateReward, getCollateralBrand: () => collateralBrand, + getCompoundedInterest: () => makeRatio(1n, runBrand), // FIXME }); /** @param {ZCFSeat} seat */ diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index 389d2082a5e..a254ee6d551 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -69,6 +69,7 @@ export async function start(zcf, privateArgs) { return SECONDS_PER_HOUR * 24n * 7n; }, reallocateReward, + getCompoundedInterest: () => makeRatio(1n, runBrand), }); const timer = buildManualTimer(console.log, 0n, SECONDS_PER_HOUR * 24n); diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 4ebb54737bd..bbf2a583beb 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -92,9 +92,9 @@ export const makeRatioFromAmounts = (numeratorAmount, denominatorAmount) => { AmountMath.coerce(numeratorAmount.brand, numeratorAmount); AmountMath.coerce(denominatorAmount.brand, denominatorAmount); return makeRatio( - /** @type {NatValue} */ (numeratorAmount.value), + numeratorAmount.value, numeratorAmount.brand, - /** @type {NatValue} */ (denominatorAmount.value), + denominatorAmount.value, denominatorAmount.brand, ); }; From 68c894ff80bc989a118bd183e44062b8694951e5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 3 Feb 2022 15:35:07 -0800 Subject: [PATCH 05/47] stub for orderedVaultStore --- .../src/vaultFactory/orderedVaultStore.js | 40 ++++++++++++++++++ .../src/vaultFactory/storeUtils.js | 42 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 packages/run-protocol/src/vaultFactory/orderedVaultStore.js create mode 100644 packages/run-protocol/src/vaultFactory/storeUtils.js diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js new file mode 100644 index 00000000000..33325526f83 --- /dev/null +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -0,0 +1,40 @@ +// FIXME remove before review +// @ts-nocheck +// @jessie-nocheck +/* eslint-disable no-unused-vars */ + +import { numberToDBEntryKey } from './storeUtils.js'; + +// XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. +/* global VatData */ + +/** + * Used by prioritizedVaults to + */ + +/** + * Sorts by ratio in descending debt. Ordered of vault id is undefined. + * + * @param {Ratio} ratio + * @param {string} vaultId + * @returns {string} + */ +const vaultKey = (ratio, vaultId) => { + // TODO make sure infinity sorts correctly + const float = ratio.denominator / ratio.numerator; + const numberPart = numberToDBEntryKey(float); + return `${numberPart}:${vaultId}`; +}; + +// TODO type these generics +const store = VatData.makeScalarBigMapStore(); + +/** + * + * @param {Vault} vault + */ +const addVault = vault => { + // vault.get + // const key = vaultKey(vault.) + // store.init +}; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js new file mode 100644 index 00000000000..3a87e4fd962 --- /dev/null +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -0,0 +1,42 @@ +// FIXME remove before review +// @ts-nocheck +// @jessie-nocheck + +// XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. +/* global BigUint64Array */ + +const asNumber = new Float64Array(1); +const asBits = new BigUint64Array(asNumber.buffer); + +/** + * + * @param {number} n + * @param {number} size + * @returns {string} + */ +const zeroPad = (n, size) => { + const nStr = `${n}`; + assert(nStr.length <= size); + const str = `00000000000000000000${nStr}`; + const result = str.substring(str.length - size); + assert(result.length === size); + return result; +}; + +const numberToDBEntryKey = n => { + asNumber[0] = n; + let bits = asBits[0]; + if (n < 0) { + // XXX Why is the no-bitwise lint rule even a thing?? + // eslint-disable-next-line no-bitwise + bits ^= 0xffffffffffffffffn; + } else { + // eslint-disable-next-line no-bitwise + bits ^= 0x8000000000000000n; + } + return `f${zeroPad(bits.toString(16), 16)}`; +}; + +harden(numberToDBEntryKey); + +export { numberToDBEntryKey }; From 3757dc8f752d8abad03a23cba13f78e5f5344db8 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 4 Feb 2022 13:33:12 -0800 Subject: [PATCH 06/47] progress to hurdle: collectionManager cannot serialize Remotables with non-methods like vault in VaultKit --- .../src/vaultFactory/orderedVaultStore.js | 65 ++++++---- .../src/vaultFactory/prioritizedVaults.js | 117 ++++++++++-------- .../run-protocol/src/vaultFactory/types.js | 19 +-- .../run-protocol/src/vaultFactory/vault.js | 84 ++++++------- .../src/vaultFactory/vaultFactory.js | 3 + .../src/vaultFactory/vaultManager.js | 115 ++++++++++++----- .../vaultFactory/test-prioritizedVaults.js | 37 ++---- 7 files changed, 246 insertions(+), 194 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 33325526f83..b07d3f6eb7a 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -12,29 +12,48 @@ import { numberToDBEntryKey } from './storeUtils.js'; * Used by prioritizedVaults to */ -/** - * Sorts by ratio in descending debt. Ordered of vault id is undefined. - * - * @param {Ratio} ratio - * @param {string} vaultId - * @returns {string} - */ -const vaultKey = (ratio, vaultId) => { - // TODO make sure infinity sorts correctly - const float = ratio.denominator / ratio.numerator; - const numberPart = numberToDBEntryKey(float); - return `${numberPart}:${vaultId}`; -}; +/** @typedef {import('./vault').VaultKit} VaultKit */ -// TODO type these generics -const store = VatData.makeScalarBigMapStore(); +export const makeOrderedVaultStore = () => { + /** + * Sorts by ratio in descending debt. Ordering of vault id is undefined. + * + * @param {Ratio} ratio + * @param {VaultId} vaultId + * @returns {string} + */ + const vaultKey = (ratio, vaultId) => { + // TODO make sure infinity sorts correctly + const float = ratio.denominator / ratio.numerator; + const numberPart = numberToDBEntryKey(float); + return `${numberPart}:${vaultId}`; + }; -/** - * - * @param {Vault} vault - */ -const addVault = vault => { - // vault.get - // const key = vaultKey(vault.) - // store.init + // TODO type these generics + const store = VatData.makeScalarBigMapStore(); + + /** + * + * @param {VaultKit} vk + */ + const addVaultKit = vk => { + const id = vk.getIdInManager(); + // FIXME needs to be the normalized value + const key = vaultKey(vk.vault.getDebtAmount()); + store.init(key, vk); + }; + + /** + * + * @param {VaultId} vkId + */ + const removeVaultKit = vkId => { + store.delete(vkId); + }; + + return harden({ + addVaultKit, + removeVaultKit, + getSize: store.getSize, + }); }; diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 42d313f4c00..b704b96b0ea 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -1,6 +1,5 @@ // @ts-check -import { observeNotifier } from '@agoric/notifier'; import { natSafeMath, makeRatioFromAmounts, @@ -10,6 +9,8 @@ import { AmountMath } from '@agoric/ertp'; const { multiply, isGTE } = natSafeMath; +/** @typedef {import('./vault').VaultKit} VaultKit */ + // Stores a collection of Vaults, pretending to be indexed by ratio of // debt to collateral. Once performance is an issue, this should use Virtual // Objects. For now, it uses a Map (Vault->debtToCollateral). @@ -96,6 +97,8 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** @type {VaultKitRecord[]} */ let vaultsWithDebtRatio = []; + // XXX why keep this state in PrioritizedVaults? Better in vaultManager? + // To deal with fluctuating prices and varying collateralization, we schedule a // new request to the priceAuthority when some vault's debtToCollateral ratio // surpasses the current high-water mark. When the request that is at the @@ -126,70 +129,71 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * - * @param {VaultKit} vaultKit + * @param {VaultId} vaultId + * @returns {VaultKit} */ - const removeVault = vaultKit => { - vaultsWithDebtRatio = vaultsWithDebtRatio.filter( - v => v.vaultKit !== vaultKit, - ); - // don't call reschedulePriceCheck, but do reset the highest. - highestDebtToCollateral = highestRatio(); - }; + const removeVault = vaultId => { + console.log('removing', { vaultId }); + // FIXME actually remove + // vaultsWithDebtRatio = vaultsWithDebtRatio.filter( + // v => v.vaultKit !== vaultKit, + // ); + return vaultsWithDebtRatio[0].vaultKit; - /** - * - * @param {VaultKit} vaultKit - * @param {Ratio} debtRatio - */ - const updateDebtRatio = (vaultKit, debtRatio) => { - vaultsWithDebtRatio.forEach((vaultPair, index) => { - if (vaultPair.vaultKit === vaultKit) { - vaultsWithDebtRatio[index].debtToCollateral = debtRatio; - } - }); + // don't call reschedulePriceCheck, but do reset the highest. + // highestDebtToCollateral = highestRatio(); }; + // TODO handle what this was doing // called after charging interest, which changes debts without affecting sort - const updateAllDebts = () => { - vaultsWithDebtRatio.forEach((vaultPair, index) => { - const debtToCollateral = currentDebtToCollateral(vaultPair.vaultKit); - vaultsWithDebtRatio[index].debtToCollateral = debtToCollateral; - }); - highestDebtToCollateral = highestRatio(); - }; + // const updateAllDebts = () => { + // // DEAD + // // vaultsWithDebtRatio.forEach((vaultPair, index) => { + // // const debtToCollateral = currentDebtToCollateral(vaultPair.vaultKit); + // // vaultsWithDebtRatio[index].debtToCollateral = debtToCollateral; + // // }); + + // // FIXME still need to track a "highest one" for the oracle + // // this basically "what's our outstanding ask of the oracle" + // // and we re-ask only if have something new to ask. + // // E.g. ask anew or update the ask with a new value. + // highestDebtToCollateral = highestRatio(); + // }; + + // /** + // * + // * @param {VaultKit} vaultKit + // */ + // const makeObserver = (vaultKit) => ({ + // updateState: (state) => { + // if (AmountMath.isEmpty(state.locked)) { + // return; + // } + // const debtToCollateral = currentDebtToCollateral(vaultKit); + // updateDebtRatio(vaultKit, debtToCollateral); + // vaultsWithDebtRatio.sort(compareVaultKits); + // rescheduleIfHighest(debtToCollateral); + // }, + // finish: (_) => { + // removeVault(vaultKit); + // }, + // fail: (_) => { + // removeVault(vaultKit); + // }, + // }); /** * + * @param {VaultId} vaultId * @param {VaultKit} vaultKit */ - const makeObserver = vaultKit => ({ - updateState: state => { - if (AmountMath.isEmpty(state.locked)) { - return; - } - const debtToCollateral = currentDebtToCollateral(vaultKit); - updateDebtRatio(vaultKit, debtToCollateral); - vaultsWithDebtRatio.sort(compareVaultKits); - rescheduleIfHighest(debtToCollateral); - }, - finish: _ => { - removeVault(vaultKit); - }, - fail: _ => { - removeVault(vaultKit); - }, - }); + const addVaultKit = (vaultId, vaultKit) => { + // FIXME use the ordered store - /** - * - * @param {VaultKit} vaultKit - * @param {ERef>} notifier - */ - const addVaultKit = (vaultKit, notifier) => { const debtToCollateral = currentDebtToCollateral(vaultKit); vaultsWithDebtRatio.push({ vaultKit, debtToCollateral }); vaultsWithDebtRatio.sort(compareVaultKits); - observeNotifier(notifier, makeObserver(vaultKit)); + // observeNotifier(notifier, makeObserver(vaultKit)); rescheduleIfHighest(debtToCollateral); }; @@ -222,17 +226,26 @@ export const makePrioritizedVaults = reschedulePriceCheck => { highestDebtToCollateral = highestRatio(); }; + /** + * + * @param {VaultId} vaultId + */ + const refreshVaultPriority = vaultId => { + const vault = removeVault(vaultId); + addVaultKit(vaultId, vault); + }; + const map = func => vaultsWithDebtRatio.map(func); const reduce = (func, init) => vaultsWithDebtRatio.reduce(func, init); return harden({ addVaultKit, + refreshVaultPriority, removeVault, map, reduce, forEachRatioGTE, highestRatio: () => highestDebtToCollateral, - updateAllDebts, }); }; diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 3782fd11dca..4684423d039 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -98,11 +98,16 @@ * at which interest is recorded to the loan. */ +/** + * @typedef {string} VaultId + */ /** * @typedef {Object} InnerVaultManagerBase + * @property {(VaultId, Amount) => void} applyDebtDelta * @property {() => Brand} getCollateralBrand * @property {ReallocateReward} reallocateReward - * @property {() => Ratio} getCompoundedInterest + * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt + * @property {(Amount) => void} tallyInterestAccrual */ /** @@ -154,18 +159,6 @@ * @property {Notifier} uiNotifier */ -/** - * @typedef {Object} VaultKit - * @property {Vault} vault - * @property {(seat: ZCFSeat) => Promise} openLoan - * @property {(timestamp: Timestamp) => Amount} accrueInterestAndAddToPool - * @property {ZCFSeat} vaultSeat - * @property {PromiseRecord} liquidationPromiseKit - * @property {ZCFSeat} liquidationZcfSeat - * @property {() => void} liquidating - * @property {(newDebt: Amount) => void} liquidated - */ - /** * @typedef {Object} LoanTiming * @property {RelativeTime} chargingPeriod diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index a9cd018a5b3..473cd14698d 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -20,7 +20,6 @@ import { import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@agoric/promise-kit'; -import { makeInterestCalculator } from './interest.js'; const { details: X, quote: q } = assert; @@ -43,17 +42,16 @@ export const VaultState = { /** * @param {ContractFacet} zcf * @param {InnerVaultManager} manager + * @param {VaultId} idInManager * @param {ZCFMint} runMint * @param {ERef} priceAuthority - * @param {Timestamp} startTimeStamp - * @returns {VaultKit} */ export const makeVaultKit = ( zcf, manager, + idInManager, // will go in state runMint, priceAuthority, - startTimeStamp, ) => { const { updater: uiUpdater, notifier } = makeNotifierKit(); const { zcfSeat: liquidationZcfSeat, userSeat: liquidationSeat } = @@ -68,8 +66,6 @@ export const makeVaultKit = ( }; const collateralBrand = manager.getCollateralBrand(); - // timestamp of most recent update to interest - let latestInterestUpdate = startTimeStamp; // vaultSeat will hold the collateral until the loan is retired. The // payout from it will be handed to the user: if the vault dies early @@ -90,13 +86,34 @@ export const makeVaultKit = ( */ let interestSnapshot; + const getIdInManager = () => idInManager; + /** - * - * @param {Amount} newDebt + * @param {Amount} newDebt - principal and all accrued interest + * @returns {Amount} */ const updateDebtSnapshot = newDebt => { + // Since newDebt includes accrued interest we need to use getDebtAmount() + // to get a baseline that also includes accrued interest. + // eslint-disable-next-line no-use-before-define + const delta = AmountMath.subtract(newDebt, getDebtAmount()); + + // update local state runDebtSnapshot = newDebt; interestSnapshot = manager.getCompoundedInterest(); + + return delta; + }; + + /** + * XXX maybe fold this into the calling context (vaultManager) + * + * @param {Amount} newDebt - principal and all accrued interest + */ + const updateDebtSnapshotAndNotify = newDebt => { + const delta = updateDebtSnapshot(newDebt); + // update parent state + manager.applyDebtDelta(idInManager, delta); }; /** @@ -201,6 +218,8 @@ export const makeVaultKit = ( }; /** + * Call must check for and remember shortfall + * * @param {Amount} newDebt */ const liquidated = newDebt => { @@ -404,7 +423,11 @@ export const makeVaultKit = ( return { newDebt, toMint, fee }; }; - /** @param {ZCFSeat} clientSeat */ + /** + * Adjust principal and collateral (atomically for offer safety) + * + * @param {ZCFSeat} clientSeat + */ const adjustBalancesHook = async clientSeat => { assertVaultIsOpen(); const proposal = clientSeat.getProposal(); @@ -465,7 +488,8 @@ export const makeVaultKit = ( transferRun(clientSeat); manager.reallocateReward(fee, vaultSeat, clientSeat); - updateDebtSnapshot(newDebt); + // parent needs to know about the change in debt + updateDebtSnapshotAndNotify(newDebt); runMint.burnLosses(harden({ RUN: runAfter.vault }), vaultSeat); @@ -521,40 +545,6 @@ export const makeVaultKit = ( return { notifier }; }; - /** - * FIXME we no longer calculate interest the same way - * - * @param {bigint} currentTime - * @returns {Amount} rate of interest used for accrual period - */ - const accrueInterestAndAddToPool = currentTime => { - const interestCalculator = makeInterestCalculator( - runBrand, - manager.getInterestRate(), - manager.getChargingPeriod(), - manager.getRecordingPeriod(), - ); - - const debtStatus = interestCalculator.calculateReportingPeriod( - { - latestInterestUpdate, - newDebt: getDebtAmount(), - interest: AmountMath.makeEmpty(runBrand), - }, - currentTime, - ); - - updateDebtSnapshot(debtStatus.newDebt); - - if (debtStatus.latestInterestUpdate === latestInterestUpdate) { - return AmountMath.makeEmpty(runBrand); - } - - ({ latestInterestUpdate } = debtStatus); - updateUiState(); - return debtStatus.interest; - }; - /** @type {Vault} */ const vault = Far('vault', { makeAdjustBalancesInvitation, @@ -569,8 +559,8 @@ export const makeVaultKit = ( return harden({ vault, + getIdInManager, openLoan, - accrueInterestAndAddToPool, vaultSeat, liquidating, liquidated, @@ -578,3 +568,7 @@ export const makeVaultKit = ( liquidationZcfSeat, }); }; + +/** + * @typedef {ReturnType} VaultKit + */ diff --git a/packages/run-protocol/src/vaultFactory/vaultFactory.js b/packages/run-protocol/src/vaultFactory/vaultFactory.js index e2d0f1f2529..2a7a118f8be 100644 --- a/packages/run-protocol/src/vaultFactory/vaultFactory.js +++ b/packages/run-protocol/src/vaultFactory/vaultFactory.js @@ -127,6 +127,8 @@ export const start = async (zcf, privateArgs) => { ); const liquidationStrategy = makeLiquidationStrategy(liquidationFacet); + const startTimeStamp = await E(timerService).getCurrentTimestamp(); + const vm = makeVaultManager( zcf, runMint, @@ -137,6 +139,7 @@ export const start = async (zcf, privateArgs) => { reallocateReward, timerService, liquidationStrategy, + startTimeStamp, ); collateralTypes.init(collateralBrand, vm); return vm; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 002555f4432..ef81bcce685 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -11,6 +11,7 @@ import { ceilMultiplyBy, ceilDivideBy, makeRatio, + multiplyRatios, } from '@agoric/zoe/src/contractSupport/index.js'; import { observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; @@ -28,6 +29,7 @@ import { INTEREST_RATE_KEY, CHARGING_PERIOD_KEY, } from './params.js'; +import { makeInterestCalculator } from './interest.js'; const { details: X } = assert; @@ -51,6 +53,7 @@ const trace = makeTracer(' VM '); * @param {ReallocateReward} reallocateReward * @param {ERef} timerService * @param {LiquidationStrategy} liquidationStrategy + * @param {Timestamp} startTimeStamp * @returns {VaultManager} */ export const makeVaultManager = ( @@ -63,6 +66,7 @@ export const makeVaultManager = ( reallocateReward, timerService, liquidationStrategy, + startTimeStamp, ) => { const { brand: runBrand } = runMint.getIssuerRecord(); @@ -95,9 +99,17 @@ export const makeVaultManager = ( // definition of reschedulePriceCheck, which refers to sortedVaultKits // XXX mutability and flow control /** @type {ReturnType=} */ - let sortedVaultKits; + let prioritizedVaults; /** @type {MutableQuote=} */ let outstandingQuote; + /** @type {Amount} */ + let totalDebt = AmountMath.makeEmpty(runBrand); + /** @type {Ratio}} */ + let compoundedInterest = makeRatio(0n, runBrand); + + // timestamp of most recent update to interest + /** @type {bigint} */ + let latestInterestUpdate = startTimeStamp; // When any Vault's debt ratio is higher than the current high-water level, // call reschedulePriceCheck() to request a fresh notification from the @@ -108,8 +120,8 @@ export const makeVaultManager = ( // when a priceQuote is received, we'll only reschedule if the high-water // level when the request was made matches the current high-water level. const reschedulePriceCheck = async () => { - assert(sortedVaultKits); - const highestDebtRatio = sortedVaultKits.highestRatio(); + assert(prioritizedVaults); + const highestDebtRatio = prioritizedVaults.highestRatio(); if (!highestDebtRatio) { // if there aren't any open vaults, we don't need an outstanding RFQ. return; @@ -156,7 +168,10 @@ export const makeVaultManager = ( getAmountIn(quote), ); - sortedVaultKits.forEachRatioGTE(quoteRatioPlusMargin, ({ vaultKit }) => { + // TODO maybe extract this into a method + // TODO try pattern matching to achieve GTE + // FIXME pass in a key instead of the actual vaultKit + prioritizedVaults.forEachRatioGTE(quoteRatioPlusMargin, ({ vaultKit }) => { trace('liquidating', vaultKit.vaultSeat.getProposal()); liquidate( @@ -170,11 +185,11 @@ export const makeVaultManager = ( outstandingQuote = undefined; reschedulePriceCheck(); }; - sortedVaultKits = makePrioritizedVaults(reschedulePriceCheck); + prioritizedVaults = makePrioritizedVaults(reschedulePriceCheck); const liquidateAll = () => { - assert(sortedVaultKits); - const promises = sortedVaultKits.map(({ vaultKit }) => + assert(prioritizedVaults); + const promises = prioritizedVaults.map(({ vaultKit }) => liquidate( zcf, vaultKit, @@ -187,22 +202,64 @@ export const makeVaultManager = ( }; // FIXME don't mutate vaults to charge them + /** + * + * @param {bigint} updateTime + * @param {ZCFSeat} poolIncrementSeat + * @returns void + */ const chargeAllVaults = async (updateTime, poolIncrementSeat) => { - assert(sortedVaultKits); - const poolIncrement = sortedVaultKits.reduce( - (total, vaultPair) => - AmountMath.add( - total, - vaultPair.vaultKit.accrueInterestAndAddToPool(updateTime), - ), - AmountMath.makeEmpty(runBrand), + const interestCalculator = makeInterestCalculator( + runBrand, + shared.getInterestRate(), + shared.getChargingPeriod(), + shared.getRecordingPeriod(), + ); + + // calculate delta of accrued debt + const debtStatus = interestCalculator.calculateReportingPeriod( + { + latestInterestUpdate, + newDebt: totalDebt, + interest: AmountMath.makeEmpty(runBrand), + }, + updateTime, + ); + const interestAccrued = debtStatus.interest; + + // done if none + if (AmountMath.isEmpty(interestAccrued)) { + return; + } + + // compoundedInterest *= debtStatus.newDebt / totalDebt; + compoundedInterest = multiplyRatios( + compoundedInterest, + makeRatioFromAmounts(debtStatus.newDebt, totalDebt), ); - sortedVaultKits.updateAllDebts(); + totalDebt = AmountMath.add(totalDebt, interestAccrued); + + // mint that much RUN for the reward pool + runMint.mintGains(harden({ RUN: interestAccrued }), poolIncrementSeat); + reallocateReward(interestAccrued, poolIncrementSeat); + + // update running tally of total debt against this collateral + ({ latestInterestUpdate } = debtStatus); + + // notifiy UIs + // updateUiState(); + reschedulePriceCheck(); - // @ts-expect-error bad typedef for reduce - runMint.mintGains(harden({ RUN: poolIncrement }), poolIncrementSeat); - // @ts-expect-error bad typedef for reduce - reallocateReward(poolIncrement, poolIncrementSeat); + }; + + /** + * @param {VaultId} vaultId + * @param {Amount} delta + */ + const applyDebtDelta = (vaultId, delta) => { + totalDebt = AmountMath.add(totalDebt, delta); + assert(prioritizedVaults); + prioritizedVaults.refreshVaultPriority(vaultId); }; const periodNotifier = E(timerService).makeNotifier( @@ -228,12 +285,14 @@ export const makeVaultManager = ( observeNotifier(periodNotifier, timeObserver); + // TODO type this here not externally /** @type {InnerVaultManager} */ const innerFacet = harden({ ...shared, + applyDebtDelta, reallocateReward, getCollateralBrand: () => collateralBrand, - getCompoundedInterest: () => makeRatio(1n, runBrand), // FIXME + getCompoundedInterest: () => compoundedInterest, }); /** @param {ZCFSeat} seat */ @@ -243,19 +302,13 @@ export const makeVaultManager = ( want: { RUN: null }, }); - const startTimeStamp = await E(timerService).getCurrentTimestamp(); - const vaultKit = makeVaultKit( - zcf, - innerFacet, - runMint, - priceAuthority, - startTimeStamp, - ); + const vaultKit = makeVaultKit(zcf, innerFacet, runMint, priceAuthority); const { vault, openLoan } = vaultKit; + // FIXME do without notifier callback const { notifier } = await openLoan(seat); - assert(sortedVaultKits); - sortedVaultKits.addVaultKit(vaultKit, notifier); + assert(prioritizedVaults); + prioritizedVaults.addVaultKit(vaultKit); seat.exit(); diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 4a94e4e2a0f..f3a9a08efe2 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -7,11 +7,13 @@ import '@agoric/zoe/exported.js'; import { makeIssuerKit, AmountMath } from '@agoric/ertp'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { makeNotifierKit } from '@agoric/notifier'; -import { Far } from '@endo/marshal'; import { makePromiseKit } from '@agoric/promise-kit'; import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; import { makePrioritizedVaults } from '../../src/vaultFactory/prioritizedVaults.js'; +import { makeFakeVaultKit } from '../supports.js'; + +/** @typedef {import('../../src/vaultFactory/vault.js').VaultKit} VaultKit */ // Some notifier updates aren't propogating sufficiently quickly for the tests. // This invocation (thanks to Warner) waits for all promises that can fire to @@ -50,37 +52,12 @@ function makeRescheduler() { }; } -/** - * - * @param {Amount} initDebt - * @param {Amount} initCollateral - * @returns {VaultKit & {vault: {setDebt: (Amount) => void}}} - */ -function makeFakeVaultKit( - initDebt, - initCollateral = AmountMath.make(initDebt.brand, 100n), -) { - let debt = initDebt; - let collateral = initCollateral; - const vault = Far('Vault', { - getCollateralAmount: () => collateral, - getDebtAmount: () => debt, - setDebt: newDebt => (debt = newDebt), - setCollateral: newCollateral => (collateral = newCollateral), - }); - // @ts-expect-error pretend this is compatible with VaultKit - return harden({ - vault, - liquidate: () => {}, - }); -} - test('add to vault', async t => { const { brand } = makeIssuerKit('ducats'); const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVaultKit = makeFakeVaultKit(AmountMath.make(brand, 130n)); + const fakeVaultKit = makeFakeVaultKit('foo2', AmountMath.make(brand, 130n)); const { notifier } = makeNotifierKit(); vaults.addVaultKit(fakeVaultKit, notifier); const collector = makeCollector(); @@ -292,18 +269,18 @@ test('removal by notification', async t => { const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 150n)); + const fakeVault1 = makeFakeVaultKit('v1', AmountMath.make(brand, 150n)); const { updater: updater1, notifier: notifier1 } = makeNotifierKit(); vaults.addVaultKit(fakeVault1, notifier1); const cr1 = makeRatio(150n, brand); t.deepEqual(vaults.highestRatio(), cr1); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 130n)); + const fakeVault2 = makeFakeVaultKit('v2', AmountMath.make(brand, 130n)); const { notifier: notifier2 } = makeNotifierKit(); vaults.addVaultKit(fakeVault2, notifier2); t.deepEqual(vaults.highestRatio(), cr1, 'should be new highest'); - const fakeVault3 = makeFakeVaultKit(AmountMath.make(brand, 140n)); + const fakeVault3 = makeFakeVaultKit('v3', AmountMath.make(brand, 140n)); const { notifier: notifier3 } = makeNotifierKit(); vaults.addVaultKit(fakeVault3, notifier3); const cr3 = makeRatio(140n, brand); From 07cd9f170d1563ddaec7fa0d344d9eb1ff4855b5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 4 Feb 2022 14:34:50 -0800 Subject: [PATCH 07/47] green test-OrderedVaultStore --- .../src/vaultFactory/orderedVaultStore.js | 22 ++++++++----- .../src/vaultFactory/prioritizedVaults.js | 14 ++++---- .../run-protocol/src/vaultFactory/types.js | 14 ++------ .../run-protocol/src/vaultFactory/vault.js | 24 ++++++++++---- .../src/vaultFactory/vaultManager.js | 20 +++++++---- packages/run-protocol/test/supports.js | 33 +++++++++++++++++++ .../test/test-OrderedVaultStore.js | 22 +++++++++++++ 7 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 packages/run-protocol/test/supports.js create mode 100644 packages/run-protocol/test/test-OrderedVaultStore.js diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index b07d3f6eb7a..17d9e849067 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -34,21 +34,25 @@ export const makeOrderedVaultStore = () => { /** * - * @param {VaultKit} vk + * @param {string} vaultId + * @param {VaultKit} vaultKit */ - const addVaultKit = vk => { - const id = vk.getIdInManager(); - // FIXME needs to be the normalized value - const key = vaultKey(vk.vault.getDebtAmount()); - store.init(key, vk); + const addVaultKit = (vaultId, vaultKit) => { + const key = vaultKey(vaultKit.vault.getDebtAmount(), vaultId); + store.init(key, vaultKit); }; /** * - * @param {VaultId} vkId + * @param {VaultId} vaultId + * @param {Vault} vault */ - const removeVaultKit = vkId => { - store.delete(vkId); + const removeVaultKit = (vaultId, vault) => { + const key = vaultKey(vault.getDebtAmount(), vaultId); + const vaultKit = store.get(key); + assert(vaultKit); + store.delete(key); + return vaultKit; }; return harden({ diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index b704b96b0ea..91897437ebd 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -130,11 +130,12 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * * @param {VaultId} vaultId + * @param {Vault} vault * @returns {VaultKit} */ - const removeVault = vaultId => { - console.log('removing', { vaultId }); - // FIXME actually remove + const removeVault = (vaultId, vault) => { + console.log('removing', { vaultId, vault }); + // FIXME actually remove using the OrderedVaultStore // vaultsWithDebtRatio = vaultsWithDebtRatio.filter( // v => v.vaultKit !== vaultKit, // ); @@ -229,10 +230,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * * @param {VaultId} vaultId + * @param {Vault} vault */ - const refreshVaultPriority = vaultId => { - const vault = removeVault(vaultId); - addVaultKit(vaultId, vault); + const refreshVaultPriority = (vaultId, vault) => { + const vaultKit = removeVault(vaultId, vault); + addVaultKit(vaultId, vaultKit); }; const map = func => vaultsWithDebtRatio.map(func); diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 4684423d039..0a26a47081b 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -101,18 +101,6 @@ /** * @typedef {string} VaultId */ -/** - * @typedef {Object} InnerVaultManagerBase - * @property {(VaultId, Amount) => void} applyDebtDelta - * @property {() => Brand} getCollateralBrand - * @property {ReallocateReward} reallocateReward - * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt - * @property {(Amount) => void} tallyInterestAccrual - */ - -/** - * @typedef {InnerVaultManagerBase & GetVaultParams} InnerVaultManager - */ /** * @typedef {Object} VaultManagerBase @@ -239,6 +227,8 @@ * }} */ +/** @typedef {import('./vault').VaultKit} VaultKit */ + /** * @callback VaultFactoryLiquidate * @param {ContractFacet} zcf diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 473cd14698d..aa4d69ab0a8 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -39,9 +39,17 @@ export const VaultState = { CLOSED: 'closed', }; +/** + * @typedef {Object} InnerVaultManagerBase + * @property {(VaultId, Vault, Amount) => void} applyDebtDelta + * @property {() => Brand} getCollateralBrand + * @property {ReallocateReward} reallocateReward + * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt + */ + /** * @param {ContractFacet} zcf - * @param {InnerVaultManager} manager + * @param {InnerVaultManagerBase & GetVaultParams} manager * @param {VaultId} idInManager * @param {ZCFMint} runMint * @param {ERef} priceAuthority @@ -86,8 +94,6 @@ export const makeVaultKit = ( */ let interestSnapshot; - const getIdInManager = () => idInManager; - /** * @param {Amount} newDebt - principal and all accrued interest * @returns {Amount} @@ -113,7 +119,8 @@ export const makeVaultKit = ( const updateDebtSnapshotAndNotify = newDebt => { const delta = updateDebtSnapshot(newDebt); // update parent state - manager.applyDebtDelta(idInManager, delta); + // eslint-disable-next-line no-use-before-define + manager.applyDebtDelta(idInManager, vault, delta); }; /** @@ -557,9 +564,7 @@ export const makeVaultKit = ( getLiquidationPromise: () => liquidationPromiseKit.promise, }); - return harden({ - vault, - getIdInManager, + const adminFacet = Far('vaultAdmin', { openLoan, vaultSeat, liquidating, @@ -567,6 +572,11 @@ export const makeVaultKit = ( liquidationPromiseKit, liquidationZcfSeat, }); + + return harden({ + vault, + adminFacet, + }); }; /** diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index ef81bcce685..b785712accb 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -254,9 +254,10 @@ export const makeVaultManager = ( /** * @param {VaultId} vaultId + * @param {Vault} vault * @param {Amount} delta */ - const applyDebtDelta = (vaultId, delta) => { + const applyDebtDelta = (vaultId, vault, delta) => { totalDebt = AmountMath.add(totalDebt, delta); assert(prioritizedVaults); prioritizedVaults.refreshVaultPriority(vaultId); @@ -285,9 +286,8 @@ export const makeVaultManager = ( observeNotifier(periodNotifier, timeObserver); - // TODO type this here not externally - /** @type {InnerVaultManager} */ - const innerFacet = harden({ + /** @type {Parameters[1]} */ + const managerFacade = harden({ ...shared, applyDebtDelta, reallocateReward, @@ -302,13 +302,21 @@ export const makeVaultManager = ( want: { RUN: null }, }); - const vaultKit = makeVaultKit(zcf, innerFacet, runMint, priceAuthority); + const vaultId = 'FIXME'; + + const vaultKit = makeVaultKit( + zcf, + managerFacade, + vaultId, + runMint, + priceAuthority, + ); const { vault, openLoan } = vaultKit; // FIXME do without notifier callback const { notifier } = await openLoan(seat); assert(prioritizedVaults); - prioritizedVaults.addVaultKit(vaultKit); + prioritizedVaults.addVaultKit(vaultId, vaultKit); seat.exit(); diff --git a/packages/run-protocol/test/supports.js b/packages/run-protocol/test/supports.js new file mode 100644 index 00000000000..2be28e1c9e1 --- /dev/null +++ b/packages/run-protocol/test/supports.js @@ -0,0 +1,33 @@ +import { AmountMath } from '@agoric/ertp'; +import { Far } from '@endo/marshal'; + +/** + * + * @param {VaultId} vaultId + * @param {Amount} initDebt + * @param {Amount} initCollateral + * @returns {VaultKit & {vault: {setDebt: (Amount) => void}}} + */ +export function makeFakeVaultKit( + vaultId, + initDebt, + initCollateral = AmountMath.make(initDebt.brand, 100n), +) { + let debt = initDebt; + let collateral = initCollateral; + const vault = Far('Vault', { + getCollateralAmount: () => collateral, + getDebtAmount: () => debt, + setDebt: newDebt => (debt = newDebt), + setCollateral: newCollateral => (collateral = newCollateral), + }); + const adminFacet = Far('vaultAdmin', { + getIdInManager: () => vaultId, + liquidate: () => {}, + }); + // @ts-expect-error pretend this is compatible with VaultKit + return harden({ + vault, + adminFacet, + }); +} diff --git a/packages/run-protocol/test/test-OrderedVaultStore.js b/packages/run-protocol/test/test-OrderedVaultStore.js new file mode 100644 index 00000000000..720527b1ed4 --- /dev/null +++ b/packages/run-protocol/test/test-OrderedVaultStore.js @@ -0,0 +1,22 @@ +// NB: import with side-effects that sets up Endo global environment +import '@agoric/zoe/exported.js'; + +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { makeOrderedVaultStore } from '../src/vaultFactory/OrderedVaultStore.js'; +import { makeFakeVaultKit } from './supports.js'; + +const { brand } = makeIssuerKit('ducats'); + +test('add/remove vault kit', async t => { + const store = makeOrderedVaultStore(); + + const vk1 = makeFakeVaultKit('vkId', AmountMath.makeEmpty(brand)); + t.is(store.getSize(), 0); + store.addVaultKit('vkId', vk1); + t.is(store.getSize(), 1); + store.removeVaultKit('vkId', vk1.vault); + t.is(store.getSize(), 0); + // TODO verify that this errors + // store.removeVaultKit(id); // removing again +}); From bf301e74ac7bc5696388701a3fe83c92e04a6d87 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 4 Feb 2022 15:07:08 -0800 Subject: [PATCH 08/47] WIP --- .../src/vaultFactory/liquidation.js | 22 +++- .../src/vaultFactory/orderedVaultStore.js | 5 + .../src/vaultFactory/prioritizedVaults.js | 103 +++++++----------- .../run-protocol/src/vaultFactory/types.js | 11 -- .../run-protocol/src/vaultFactory/vault.js | 4 +- .../src/vaultFactory/vaultManager.js | 44 +++++--- 6 files changed, 97 insertions(+), 92 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 48bebef894b..d05015f034f 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -15,7 +15,14 @@ const trace = makeTracer('LIQ'); * Once collateral has been sold using the contract, we burn the amount * necessary to cover the debt and return the remainder. * - * @type {VaultFactoryLiquidate} + * @param {ContractFacet} zcf + * @param {VaultKit} vaultKit + * @param {(losses: AmountKeywordRecord, + * zcfSeat: ZCFSeat + * ) => void} burnLosses + * @param {LiquidationStrategy} strategy + * @param {Brand} collateralBrand + * @returns {[VaultId, Vault]} */ const liquidate = async ( zcf, @@ -24,10 +31,12 @@ const liquidate = async ( strategy, collateralBrand, ) => { - vaultKit.liquidating(); + // ??? should we bail if it's already liquidating? + // if so should that be done here or throw here and managed at the caller + vaultKit.admin.liquidating(); const runDebt = vaultKit.vault.getDebtAmount(); const { brand: runBrand } = runDebt; - const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit; + const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit.admin; const collateralToSell = vaultSeat.getAmountAllocated( 'Collateral', @@ -57,12 +66,15 @@ const liquidate = async ( const isUnderwater = !AmountMath.isGTE(runProceedsAmount, runDebt); const runToBurn = isUnderwater ? runProceedsAmount : runDebt; burnLosses(harden({ RUN: runToBurn }), liquidationSeat); - vaultKit.liquidated(AmountMath.subtract(runDebt, runToBurn)); + // FIXME removal was triggered by this through observation of state change + vaultKit.admin.liquidated(AmountMath.subtract(runDebt, runToBurn)); // any remaining RUN plus anything else leftover from the sale are refunded vaultSeat.exit(); liquidationSeat.exit(); - vaultKit.liquidationPromiseKit.resolve('Liquidated'); + vaultKit.admin.liquidationPromiseKit.resolve('Liquidated'); + + return ['FIXME', vaultKit.vault]; }; /** diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 17d9e849067..9683b4ea55d 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -40,14 +40,17 @@ export const makeOrderedVaultStore = () => { const addVaultKit = (vaultId, vaultKit) => { const key = vaultKey(vaultKit.vault.getDebtAmount(), vaultId); store.init(key, vaultKit); + store.getSize; }; /** * * @param {VaultId} vaultId * @param {Vault} vault + * @returns {VaultKit} */ const removeVaultKit = (vaultId, vault) => { + // FIXME needs to be the normalized debt amount const key = vaultKey(vault.getDebtAmount(), vaultId); const vaultKit = store.get(key); assert(vaultKit); @@ -58,6 +61,8 @@ export const makeOrderedVaultStore = () => { return harden({ addVaultKit, removeVaultKit, + entries: store.entries, getSize: store.getSize, + values: store.values, }); }; diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 91897437ebd..941477f1cb8 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -6,6 +6,7 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { assert } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; +import { makeOrderedVaultStore } from './orderedVaultStore'; const { multiply, isGTE } = natSafeMath; @@ -53,14 +54,11 @@ const calculateDebtToCollateral = (debtAmount, collateralAmount) => { /** * - * @param {VaultKit} vaultKit + * @param {Vault} vault * @returns {Ratio} */ -const currentDebtToCollateral = vaultKit => - calculateDebtToCollateral( - vaultKit.vault.getDebtAmount(), - vaultKit.vault.getCollateralAmount(), - ); +const currentDebtToCollateral = vault => + calculateDebtToCollateral(vault.getDebtAmount(), vault.getCollateralAmount()); /** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultKitRecord */ @@ -90,12 +88,7 @@ const compareVaultKits = (leftVaultPair, rightVaultPair) => { * least-collateralized vault */ export const makePrioritizedVaults = reschedulePriceCheck => { - // The array must be resorted on - // every insert, and whenever any vault's ratio changes. We can remove an - // arbitrary number of vaults from the front of the list without resorting. We - // delete single entries using filter(), which leaves the array sorted. - /** @type {VaultKitRecord[]} */ - let vaultsWithDebtRatio = []; + const vaults = makeOrderedVaultStore(); // XXX why keep this state in PrioritizedVaults? Better in vaultManager? @@ -106,12 +99,14 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // (which should be lower, as we will have liquidated any that were at least // as high.) /** @type {Ratio=} */ + // cache of the head of the priority queue let highestDebtToCollateral; // Check if this ratio of debt to collateral would be the highest known. If // so, reset our highest and invoke the callback. This can be called on new // vaults and when we get a state update for a vault changing balances. /** @param {Ratio} collateralToDebt */ + // Caches and reschedules const rescheduleIfHighest = collateralToDebt => { if ( !highestDebtToCollateral || @@ -123,7 +118,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }; const highestRatio = () => { - const mostIndebted = vaultsWithDebtRatio[0]; + const mostIndebted = vaults.first; return mostIndebted ? mostIndebted.debtToCollateral : undefined; }; @@ -134,33 +129,19 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @returns {VaultKit} */ const removeVault = (vaultId, vault) => { - console.log('removing', { vaultId, vault }); - // FIXME actually remove using the OrderedVaultStore - // vaultsWithDebtRatio = vaultsWithDebtRatio.filter( - // v => v.vaultKit !== vaultKit, - // ); - return vaultsWithDebtRatio[0].vaultKit; - - // don't call reschedulePriceCheck, but do reset the highest. - // highestDebtToCollateral = highestRatio(); + const debtToCollateral = currentDebtToCollateral(vault); + if ( + !highestDebtToCollateral || + // TODO check for equality is sufficient and faster + ratioGTE(debtToCollateral, highestDebtToCollateral) + ) { + // don't call reschedulePriceCheck, but do reset the highest. + highestDebtToCollateral = highestRatio(); + } + return vaults.removeVaultKit(vaultId, vault); }; - // TODO handle what this was doing - // called after charging interest, which changes debts without affecting sort - // const updateAllDebts = () => { - // // DEAD - // // vaultsWithDebtRatio.forEach((vaultPair, index) => { - // // const debtToCollateral = currentDebtToCollateral(vaultPair.vaultKit); - // // vaultsWithDebtRatio[index].debtToCollateral = debtToCollateral; - // // }); - - // // FIXME still need to track a "highest one" for the oracle - // // this basically "what's our outstanding ask of the oracle" - // // and we re-ask only if have something new to ask. - // // E.g. ask anew or update the ask with a new value. - // highestDebtToCollateral = highestRatio(); - // }; - + // FIXME need to still do this work // /** // * // * @param {VaultKit} vaultKit @@ -189,11 +170,10 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @param {VaultKit} vaultKit */ const addVaultKit = (vaultId, vaultKit) => { - // FIXME use the ordered store + vaults.addVaultKit(vaultId, vaultKit); - const debtToCollateral = currentDebtToCollateral(vaultKit); - vaultsWithDebtRatio.push({ vaultKit, debtToCollateral }); - vaultsWithDebtRatio.sort(compareVaultKits); + // REVISIT + const debtToCollateral = currentDebtToCollateral(vaultKit.vault); // observeNotifier(notifier, makeObserver(vaultKit)); rescheduleIfHighest(debtToCollateral); }; @@ -201,30 +181,34 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * Invoke a function for vaults with debt to collateral at or above the ratio * + * The iterator breaks on any change to the store. We could puts items to + * liquidate into a separate store, but for now we'll rely on accumlating the + * keys in memory and removing them all at once. + * + * Something to consider for the separate store idea is we can throttle the + * dump rate to manage economices. + * * @param {Ratio} ratio - * @param {(record: VaultKitRecord) => void} func + * @param {(VaultId, VaultKit) => void} cb */ - const forEachRatioGTE = (ratio, func) => { - // vaults are sorted with highest ratios first - let index; - for (index = 0; index < vaultsWithDebtRatio.length; index += 1) { - const vaultPair = vaultsWithDebtRatio[index]; - if (ratioGTE(vaultPair.debtToCollateral, ratio)) { - func(vaultPair); + const forEachRatioGTE = (ratio, cb) => { + // ??? should this use a Pattern to limit the iteration? + for (const [k, v] of vaults.entries()) { + /** @type {VaultKit} */ + const vk = v; + const debtToCollateral = currentDebtToCollateral(vk.vault); + + if (ratioGTE(debtToCollateral, ratio)) { + cb(vk); } else { // stop once we are below the target ratio break; } } - if (index > 0) { - vaultsWithDebtRatio = vaultsWithDebtRatio.slice(index); - const highest = highestRatio(); - if (highest) { - reschedulePriceCheck(); - } - } - highestDebtToCollateral = highestRatio(); + // TODO accumulate keys in memory and remove them all at once + + // REVISIT the logic in maser for forEachRatioGTE that optimized when to update highest ratio and reschedule }; /** @@ -239,14 +223,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const map = func => vaultsWithDebtRatio.map(func); - const reduce = (func, init) => vaultsWithDebtRatio.reduce(func, init); - return harden({ addVaultKit, refreshVaultPriority, removeVault, map, - reduce, forEachRatioGTE, highestRatio: () => highestDebtToCollateral, }); diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 0a26a47081b..62a6c6ab246 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -228,14 +228,3 @@ */ /** @typedef {import('./vault').VaultKit} VaultKit */ - -/** - * @callback VaultFactoryLiquidate - * @param {ContractFacet} zcf - * @param {VaultKit} vaultKit - * @param {(losses: AmountKeywordRecord, - * zcfSeat: ZCFSeat - * ) => void} burnLosses - * @param {LiquidationStrategy} strategy - * @param {Brand} collateralBrand - */ diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index aa4d69ab0a8..075cd510f32 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -564,7 +564,7 @@ export const makeVaultKit = ( getLiquidationPromise: () => liquidationPromiseKit.promise, }); - const adminFacet = Far('vaultAdmin', { + const admin = Far('vaultAdmin', { openLoan, vaultSeat, liquidating, @@ -575,7 +575,7 @@ export const makeVaultKit = ( return harden({ vault, - adminFacet, + admin, }); }; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index b785712accb..77156dfb853 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -17,7 +17,7 @@ import { observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; -import { makeVaultKit } from './vault.js'; +import { makeVaultKit, VaultState } from './vault.js'; import { makePrioritizedVaults } from './prioritizedVaults.js'; import { liquidate } from './liquidation.js'; import { makeTracer } from '../makeTracer.js'; @@ -168,25 +168,43 @@ export const makeVaultManager = ( getAmountIn(quote), ); + /** @type {Promise[]} */ + const toLiquidate = []; + // TODO maybe extract this into a method // TODO try pattern matching to achieve GTE // FIXME pass in a key instead of the actual vaultKit - prioritizedVaults.forEachRatioGTE(quoteRatioPlusMargin, ({ vaultKit }) => { - trace('liquidating', vaultKit.vaultSeat.getProposal()); - - liquidate( - zcf, - vaultKit, - runMint.burnLosses, - liquidationStrategy, - collateralBrand, - ); - }); + prioritizedVaults.forEachRatioGTE( + quoteRatioPlusMargin, + ([vaultId, vaultKit]) => { + trace('liquidating', vaultKit.vaultSeat.getProposal()); + + // XXX firing off promise unhandled, nothing tracking if this errors + toLiquidate.push([ + vaultId, + liquidate( + zcf, + vaultKit, + runMint.burnLosses, + liquidationStrategy, + collateralBrand, + ), + ]); + }, + ); outstandingQuote = undefined; + /** @type {Array<[VaultId, VaultKit]>} */ + const liquidationResults = await Promise.all(toLiquidate); + for (const [vaultId, vaultKit] of liquidationResults) { + prioritizedVaults.removeVault(vaultId, vaultKit.vault); + } + + // TODO wait until we've removed them all reschedulePriceCheck(); }; prioritizedVaults = makePrioritizedVaults(reschedulePriceCheck); + // ??? what's the use case for liquidating all vaults? const liquidateAll = () => { assert(prioritizedVaults); const promises = prioritizedVaults.map(({ vaultKit }) => @@ -260,7 +278,7 @@ export const makeVaultManager = ( const applyDebtDelta = (vaultId, vault, delta) => { totalDebt = AmountMath.add(totalDebt, delta); assert(prioritizedVaults); - prioritizedVaults.refreshVaultPriority(vaultId); + prioritizedVaults.refreshVaultPriority(vaultId, vault); }; const periodNotifier = E(timerService).makeNotifier( From fe1241d0e0191dcafdaeba99fe9af17da34dee07 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 7 Feb 2022 14:17:19 -0800 Subject: [PATCH 09/47] style: remove extraneous key quotes --- packages/run-protocol/src/vaultFactory/types.js | 8 ++++---- packages/run-protocol/src/vaultFactory/vaultManager.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 62a6c6ab246..f4af3cab3fd 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -204,10 +204,10 @@ /** * @typedef {Object} VaultParamManager * @property {() => Record & { - * 'InitialMargin': ParamRecord<'ratio'> & { value: Ratio }, - * 'InterestRate': ParamRecord<'ratio'> & { value: Ratio }, - * 'LiquidationMargin': ParamRecord<'ratio'> & { value: Ratio }, - * 'LoanFee': ParamRecord<'ratio'> & { value: Ratio }, + * InitialMargin: ParamRecord<'ratio'> & { value: Ratio }, + * InterestRate: ParamRecord<'ratio'> & { value: Ratio }, + * LiquidationMargin: ParamRecord<'ratio'> & { value: Ratio }, + * LoanFee: ParamRecord<'ratio'> & { value: Ratio }, * }} getParams * @property {(name: string) => bigint} getNat * @property {(name: string) => Ratio} getRatio diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 77156dfb853..b31d82be546 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -46,8 +46,8 @@ const trace = makeTracer(' VM '); * @param {Brand} collateralBrand * @param {ERef} priceAuthority * @param {{ - * 'ChargingPeriod': ParamRecord<'relativeTime'> & { value: RelativeTime }, - * 'RecordingPeriod': ParamRecord<'relativeTime'> & { value: RelativeTime }, + * ChargingPeriod: ParamRecord<'relativeTime'> & { value: RelativeTime }, + * RecordingPeriod: ParamRecord<'relativeTime'> & { value: RelativeTime }, * }} timingParams * @param {GetGovernedVaultParams} getLoanParams * @param {ReallocateReward} reallocateReward From 30f4089b53343625d216c62c93e909df69f268be Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 7 Feb 2022 15:36:49 -0800 Subject: [PATCH 10/47] comment --- packages/run-protocol/src/vaultFactory/vault.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 075cd510f32..67a02740829 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -130,6 +130,7 @@ export const makeVaultKit = ( */ const getDebtAmount = () => { assert(interestSnapshot); + // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( manager.getCompoundedInterest(), invertRatio(interestSnapshot), From ab1d602227c6d2097bf74a56b11055db9c3930be Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 7 Feb 2022 17:45:59 -0800 Subject: [PATCH 11/47] getNormalizedDebt, toVaultKey, fromVaultKey --- .../src/vaultFactory/liquidation.js | 2 +- .../src/vaultFactory/orderedVaultStore.js | 42 +++++++++++-------- .../src/vaultFactory/prioritizedVaults.js | 31 +++----------- .../run-protocol/src/vaultFactory/types.js | 1 + .../run-protocol/src/vaultFactory/vault.js | 24 ++++++++++- .../src/vaultFactory/vaultManager.js | 37 +++++++++++++--- 6 files changed, 87 insertions(+), 50 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index d05015f034f..00e2f253178 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -22,7 +22,7 @@ const trace = makeTracer('LIQ'); * ) => void} burnLosses * @param {LiquidationStrategy} strategy * @param {Brand} collateralBrand - * @returns {[VaultId, Vault]} + * @returns {Promise<[VaultId, Vault]>} */ const liquidate = async ( zcf, diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 9683b4ea55d..241519b2dc7 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -14,21 +14,30 @@ import { numberToDBEntryKey } from './storeUtils.js'; /** @typedef {import('./vault').VaultKit} VaultKit */ -export const makeOrderedVaultStore = () => { - /** - * Sorts by ratio in descending debt. Ordering of vault id is undefined. - * - * @param {Ratio} ratio - * @param {VaultId} vaultId - * @returns {string} - */ - const vaultKey = (ratio, vaultId) => { - // TODO make sure infinity sorts correctly - const float = ratio.denominator / ratio.numerator; - const numberPart = numberToDBEntryKey(float); - return `${numberPart}:${vaultId}`; - }; +/** + * Sorts by ratio in descending debt. Ordering of vault id is undefined. + * + * @param {Ratio} ratio normalized debt ratio + * @param {VaultId} vaultId + * @returns {string} + */ +export const toVaultKey = (ratio, vaultId) => { + // TODO make sure infinity sorts correctly + const float = ratio.denominator / ratio.numerator; + const numberPart = numberToDBEntryKey(float); + return `${numberPart}:${vaultId}`; +}; +/** + * @param {string} key + * @returns {[Ratio, VaultId]} normalized debt ratio, vault id + */ +export const fromVaultKey = key => { + const [numberPart, vaultIdPart] = key.split(':'); + return [Number(numberPart), String(vaultIdPart)]; +}; + +export const makeOrderedVaultStore = () => { // TODO type these generics const store = VatData.makeScalarBigMapStore(); @@ -38,7 +47,7 @@ export const makeOrderedVaultStore = () => { * @param {VaultKit} vaultKit */ const addVaultKit = (vaultId, vaultKit) => { - const key = vaultKey(vaultKit.vault.getDebtAmount(), vaultId); + const key = toVaultKey(vaultKit.vault.getDebtAmount(), vaultId); store.init(key, vaultKit); store.getSize; }; @@ -50,8 +59,7 @@ export const makeOrderedVaultStore = () => { * @returns {VaultKit} */ const removeVaultKit = (vaultId, vault) => { - // FIXME needs to be the normalized debt amount - const key = vaultKey(vault.getDebtAmount(), vaultId); + const key = toVaultKey(vault.getNormalizedDebt(), vaultId); const vaultKit = store.get(key); assert(vaultKit); store.delete(key); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 941477f1cb8..7a600a6517c 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -6,7 +6,7 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { assert } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { makeOrderedVaultStore } from './orderedVaultStore'; +import { fromVaultKey, makeOrderedVaultStore } from './orderedVaultStore'; const { multiply, isGTE } = natSafeMath; @@ -62,26 +62,6 @@ const currentDebtToCollateral = vault => /** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultKitRecord */ -/** - * @param {VaultKitRecord} leftVaultPair - * @param {VaultKitRecord} rightVaultPair - * @returns {-1 | 0 | 1} - */ -const compareVaultKits = (leftVaultPair, rightVaultPair) => { - const leftVaultRatio = leftVaultPair.debtToCollateral; - const rightVaultRatio = rightVaultPair.debtToCollateral; - const leftGTERight = ratioGTE(leftVaultRatio, rightVaultRatio); - const rightGTEleft = ratioGTE(rightVaultRatio, leftVaultRatio); - if (leftGTERight && rightGTEleft) { - return 0; - } else if (leftGTERight) { - return -1; - } else if (rightGTEleft) { - return 1; - } - throw Error("The vault's collateral ratios are not comparable"); -}; - /** * * @param {() => void} reschedulePriceCheck called when there is a new @@ -117,8 +97,8 @@ export const makePrioritizedVaults = reschedulePriceCheck => { } }; - const highestRatio = () => { - const mostIndebted = vaults.first; + const firstDebtRatio = () => { + const [mostIndebted] = vaults.values(); return mostIndebted ? mostIndebted.debtToCollateral : undefined; }; @@ -136,7 +116,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { ratioGTE(debtToCollateral, highestDebtToCollateral) ) { // don't call reschedulePriceCheck, but do reset the highest. - highestDebtToCollateral = highestRatio(); + highestDebtToCollateral = firstDebtRatio(); } return vaults.removeVaultKit(vaultId, vault); }; @@ -194,12 +174,13 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const forEachRatioGTE = (ratio, cb) => { // ??? should this use a Pattern to limit the iteration? for (const [k, v] of vaults.entries()) { + const [_, vaultId] = fromVaultKey(k); /** @type {VaultKit} */ const vk = v; const debtToCollateral = currentDebtToCollateral(vk.vault); if (ratioGTE(debtToCollateral, ratio)) { - cb(vk); + cb(vk, vaultId); } else { // stop once we are below the target ratio break; diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index f4af3cab3fd..9bfeb6abab1 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -122,6 +122,7 @@ * @typedef {Object} BaseVault * @property {() => Amount} getCollateralAmount * @property {() => Amount} getDebtAmount + * @property {() => Amount} getNormalizedDebt * * @typedef {BaseVault & VaultMixin} Vault * @typedef {Object} VaultMixin diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 67a02740829..47d700ce297 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -124,10 +124,18 @@ export const makeVaultKit = ( }; /** - * The current debt, including accrued interest + * The actual current debt, including accrued interest. * + * This looks like a simple getter but it does a lot of the heavy lifting for + * interest accrual. Rather than updating all records when interest accrues, + * the vault manager updates just its rolling compounded interest. Here we + * calculate what the current debt is given what's recorded in this vault and + * what interest has compounded since this vault record was written. + * + * @see getNormalizedDebt * @returns {Amount} */ + // TODO rename to getActualDebtAmount throughout codebase const getDebtAmount = () => { assert(interestSnapshot); // divide compounded interest by the the snapshot @@ -139,6 +147,19 @@ export const makeVaultKit = ( return floorMultiplyBy(runDebtSnapshot, interestSinceSnapshot); }; + /** + * The normalization puts all debts on a common time-independent scale since + * the launch of this vault manager. This allows the manager to order vaults + * by their debt-to-collateral ratios without having to mutate the debts as + * the interest accrues. + * + * @see getActualDebAmount + * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued + */ + const getNormalizedDebt = () => { + return floorMultiplyBy(runDebtSnapshot, invertRatio(interestSnapshot)); + }; + const getCollateralAllocated = seat => seat.getAmountAllocated('Collateral', collateralBrand); const getRunAllocated = seat => seat.getAmountAllocated('RUN', runBrand); @@ -561,6 +582,7 @@ export const makeVaultKit = ( // for status/debugging getCollateralAmount, getDebtAmount, + getNormalizedDebt, getLiquidationSeat: () => liquidationSeat, getLiquidationPromise: () => liquidationPromiseKit.promise, }); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index b31d82be546..2011c959afc 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -35,12 +35,13 @@ const { details: X } = assert; const trace = makeTracer(' VM '); -// Each VaultManager manages a single collateralType. It owns an autoswap -// instance which trades this collateralType against RUN. It also manages -// some number of outstanding loans, each called a Vault, for which the -// collateral is provided in exchange for borrowed RUN. - /** + * Each VaultManager manages a single collateralType. + * + * It owns an autoswap instance which trades this collateralType against RUN. It + * also manages some number of outstanding loans, each called a Vault, for which + * the collateral is provided in exchange for borrowed RUN. + * * @param {ContractFacet} zcf * @param {ZCFMint} runMint * @param {Brand} collateralBrand @@ -91,6 +92,30 @@ export const makeVaultManager = ( }, }; + /** + * Each vaultManager can be in these liquidation process states: + * + * READY + * - Ready to liquidate + * - waiting on price info + * - If chargeInterest triggers, we have to reschedulePriceCheck + * CULLING + * - Price info arrived + * - Picking out set to liquidate + * - reschedulePriceCheck ? + * - highestDebtToCollateral is just a cache for perf of the head of the priority queue + * - If chargeInterest triggers, it’s postponed until READY + * LIQUIDATING + * - Liquidate each of the selected + * - ¿ Skip ones that no longer need to be? + * - ¿ Remove empty vaults? + * - If chargeInterest triggers, it’s postponed until READY + * - Go back to READY + * + * @type {'READY' | 'CULLING' | 'LIQUIDATING'} + */ + const currentState = 'READY'; + // A Map from vaultKits to their most recent ratio of debt to // collateralization. (This representation won't be optimized; when we need // better performance, use virtual objects.) @@ -204,7 +229,7 @@ export const makeVaultManager = ( }; prioritizedVaults = makePrioritizedVaults(reschedulePriceCheck); - // ??? what's the use case for liquidating all vaults? + // In extreme situations system health may require liquidating all vaults. const liquidateAll = () => { assert(prioritizedVaults); const promises = prioritizedVaults.map(({ vaultKit }) => From cb26689b3cd5823972631dd4b8b2db556d2418ea Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 8 Feb 2022 14:18:59 -0800 Subject: [PATCH 12/47] tests for orderedVaultStore --- packages/run-protocol/README.md | 16 ++++ .../src/vaultFactory/liquidation.js | 4 +- .../src/vaultFactory/orderedVaultStore.js | 59 ++++++--------- .../src/vaultFactory/prioritizedVaults.js | 73 +++++++------------ .../src/vaultFactory/storeUtils.js | 64 +++++++++++++++- .../run-protocol/src/vaultFactory/vault.js | 1 + .../src/vaultFactory/vaultManager.js | 50 +++++++------ .../vaultFactory/test-orderedVaultStore.js | 72 ++++++++++++++++++ .../test/vaultFactory/test-storeUtils.js | 58 +++++++++++++++ packages/store/src/patterns/encodeKey.js | 6 ++ packages/zoe/src/contractSupport/ratio.js | 6 +- 11 files changed, 300 insertions(+), 109 deletions(-) create mode 100644 packages/run-protocol/README.md create mode 100644 packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js create mode 100644 packages/run-protocol/test/vaultFactory/test-storeUtils.js diff --git a/packages/run-protocol/README.md b/packages/run-protocol/README.md new file mode 100644 index 00000000000..6f9c75d1fe8 --- /dev/null +++ b/packages/run-protocol/README.md @@ -0,0 +1,16 @@ +# RUN protocol + +## Overview + +RUN is a stable token that enables the core of the Agoric economy. + +By convention there is one well-known **VaultFactory**. By governance it creates a **VaultManager** for each type of asset that can serve as collateral to mint RUN. + +Anyone can open make a **Vault** by putting up collateral the appropriate VaultManager. Then they can request RUN that is backed by that collateral. + +When any vat the ratio of the debt to the collateral exceeds a governed threshold, the collateral is sold until the ratio reaches the set point. This is called liquidation and managed by the VaultManager. + +## Persistence + +The above states are robust to system restarts and upgrades. This is accomplished using the Agoric (Endo?) Collections API. + diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 00e2f253178..81e53902f61 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -22,7 +22,7 @@ const trace = makeTracer('LIQ'); * ) => void} burnLosses * @param {LiquidationStrategy} strategy * @param {Brand} collateralBrand - * @returns {Promise<[VaultId, Vault]>} + * @returns {Promise} */ const liquidate = async ( zcf, @@ -74,7 +74,7 @@ const liquidate = async ( liquidationSeat.exit(); vaultKit.admin.liquidationPromiseKit.resolve('Liquidated'); - return ['FIXME', vaultKit.vault]; + return vaultKit.vault; }; /** diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 241519b2dc7..430ba5b37e2 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -1,45 +1,18 @@ -// FIXME remove before review -// @ts-nocheck -// @jessie-nocheck -/* eslint-disable no-unused-vars */ - -import { numberToDBEntryKey } from './storeUtils.js'; - -// XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. -/* global VatData */ +// XXX avoid deep imports https://github.com/Agoric/agoric-sdk/issues/4255#issuecomment-1032117527 +import { makeScalarBigMapStore } from '@agoric/swingset-vat/src/storeModule.js'; +import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** - * Used by prioritizedVaults to + * Used by prioritizedVaults to wrap the Collections API for this use case. */ /** @typedef {import('./vault').VaultKit} VaultKit */ -/** - * Sorts by ratio in descending debt. Ordering of vault id is undefined. - * - * @param {Ratio} ratio normalized debt ratio - * @param {VaultId} vaultId - * @returns {string} - */ -export const toVaultKey = (ratio, vaultId) => { - // TODO make sure infinity sorts correctly - const float = ratio.denominator / ratio.numerator; - const numberPart = numberToDBEntryKey(float); - return `${numberPart}:${vaultId}`; -}; - -/** - * @param {string} key - * @returns {[Ratio, VaultId]} normalized debt ratio, vault id - */ -export const fromVaultKey = key => { - const [numberPart, vaultIdPart] = key.split(':'); - return [Number(numberPart), String(vaultIdPart)]; -}; +/** @typedef {[normalizedDebtRatio: number, vaultId: VaultId]} CompositeKey */ export const makeOrderedVaultStore = () => { - // TODO type these generics - const store = VatData.makeScalarBigMapStore(); + /** @type {MapStore { return vaultKit; }; + /** + * Have to define both tags until https://github.com/Microsoft/TypeScript/issues/23857 + * + * @yields {[[string, string], VaultKit]>} + * @returns {IterableIterator<[CompositeKey, VaultKit]>} + */ + // XXX need to make generator with const arrow definitions? + function* entriesWithCompositeKeys() { + for (const [k, v] of store.entries()) { + const compositeKey = fromVaultKey(k); + /** @type {VaultKit} */ + const vaultKit = v; + yield [compositeKey, vaultKit]; + } + } + return harden({ addVaultKit, removeVaultKit, - entries: store.entries, + entriesWithCompositeKeys, getSize: store.getSize, values: store.values, }); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 7a600a6517c..e22d6462e2a 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -6,18 +6,13 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { assert } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { fromVaultKey, makeOrderedVaultStore } from './orderedVaultStore'; +import { makeOrderedVaultStore } from './orderedVaultStore'; const { multiply, isGTE } = natSafeMath; /** @typedef {import('./vault').VaultKit} VaultKit */ -// Stores a collection of Vaults, pretending to be indexed by ratio of -// debt to collateral. Once performance is an issue, this should use Virtual -// Objects. For now, it uses a Map (Vault->debtToCollateral). -// debtToCollateral (which is not the collateralizationRatio) is updated using -// an observer on the UIState. - +// TODO put this with other ratio math /** * * @param {Ratio} left @@ -79,7 +74,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // (which should be lower, as we will have liquidated any that were at least // as high.) /** @type {Ratio=} */ - // cache of the head of the priority queue + // cache of the head of the priority queue (actualized) let highestDebtToCollateral; // Check if this ratio of debt to collateral would be the highest known. If @@ -98,8 +93,14 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }; const firstDebtRatio = () => { - const [mostIndebted] = vaults.values(); - return mostIndebted ? mostIndebted.debtToCollateral : undefined; + if (vaults.getSize() === 0) { + return undefined; + } + + // TODO get from keys() instead of entries() + const [[compositeKey]] = vaults.entriesWithCompositeKeys(); + const [normalizedDebtRatio] = compositeKey; + return normalizedDebtRatio; }; /** @@ -121,29 +122,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { return vaults.removeVaultKit(vaultId, vault); }; - // FIXME need to still do this work - // /** - // * - // * @param {VaultKit} vaultKit - // */ - // const makeObserver = (vaultKit) => ({ - // updateState: (state) => { - // if (AmountMath.isEmpty(state.locked)) { - // return; - // } - // const debtToCollateral = currentDebtToCollateral(vaultKit); - // updateDebtRatio(vaultKit, debtToCollateral); - // vaultsWithDebtRatio.sort(compareVaultKits); - // rescheduleIfHighest(debtToCollateral); - // }, - // finish: (_) => { - // removeVault(vaultKit); - // }, - // fail: (_) => { - // removeVault(vaultKit); - // }, - // }); - /** * * @param {VaultId} vaultId @@ -152,12 +130,22 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const addVaultKit = (vaultId, vaultKit) => { vaults.addVaultKit(vaultId, vaultKit); - // REVISIT const debtToCollateral = currentDebtToCollateral(vaultKit.vault); - // observeNotifier(notifier, makeObserver(vaultKit)); rescheduleIfHighest(debtToCollateral); }; + /** + * Akin to forEachRatioGTE but iterate over all vaults. + * + * @param {(VaultId, VaultKit) => void} cb + * @returns {void} + */ + const forAll = cb => { + for (const [[_, vaultId], vk] of vaults.entriesWithCompositeKeys()) { + cb(vaultId, vk); + } + }; + /** * Invoke a function for vaults with debt to collateral at or above the ratio * @@ -169,18 +157,15 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * dump rate to manage economices. * * @param {Ratio} ratio - * @param {(VaultId, VaultKit) => void} cb + * @param {(vid: VaultId, vk: VaultKit) => void} cb */ const forEachRatioGTE = (ratio, cb) => { - // ??? should this use a Pattern to limit the iteration? - for (const [k, v] of vaults.entries()) { - const [_, vaultId] = fromVaultKey(k); - /** @type {VaultKit} */ - const vk = v; + // TODO use a Pattern to limit the query + for (const [[_, vaultId], vk] of vaults.entriesWithCompositeKeys()) { const debtToCollateral = currentDebtToCollateral(vk.vault); if (ratioGTE(debtToCollateral, ratio)) { - cb(vk, vaultId); + cb(vaultId, vk); } else { // stop once we are below the target ratio break; @@ -202,13 +187,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { addVaultKit(vaultId, vaultKit); }; - const map = func => vaultsWithDebtRatio.map(func); - return harden({ addVaultKit, refreshVaultPriority, removeVault, - map, + forAll, forEachRatioGTE, highestRatio: () => highestDebtToCollateral, }); diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 3a87e4fd962..e284faf6104 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -1,7 +1,10 @@ // FIXME remove before review // @ts-nocheck -// @jessie-nocheck - +/** + * Module to improvise composite keys for orderedVaultStore until Collections API supports them. + * + * TODO BEFORE MERGE: assess maximum key length limits with Collections API + */ // XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. /* global BigUint64Array */ @@ -23,6 +26,9 @@ const zeroPad = (n, size) => { return result; }; +/** + * @param {number} n + */ const numberToDBEntryKey = n => { asNumber[0] = n; let bits = asBits[0]; @@ -37,6 +43,58 @@ const numberToDBEntryKey = n => { return `f${zeroPad(bits.toString(16), 16)}`; }; +/** + * @param {string} k + */ +const dbEntryKeyToNumber = k => { + let bits = BigInt(`0x${k.substring(1)}`); + if (k[1] < '8') { + // eslint-disable-next-line no-bitwise + bits ^= 0xffffffffffffffffn; + } else { + // eslint-disable-next-line no-bitwise + bits ^= 0x8000000000000000n; + } + asBits[0] = bits; + const result = asNumber[0]; + if (Object.is(result, -0)) { + return 0; + } + return result; +}; + +/** + * Sorts by ratio in descending debt. Ordering of vault id is undefined. + * All debts greater than colleteral are tied for first. + * + * @param {Ratio} ratio normalized debt ratio (debt over collateral) + * @param {VaultId} vaultId + * @returns {string} lexically sortable string in which highest debt-to-collateral is earliest + */ +const toVaultKey = (ratio, vaultId) => { + assert(ratio); + assert(vaultId); + // XXX there's got to be a helper for Ratio to float + const float = ratio.numerator.value + ? Number(ratio.denominator.value / ratio.numerator.value) + : Number.POSITIVE_INFINITY; + // until DB supports composite keys, copy its method for turning numbers to DB entry keys + const numberPart = numberToDBEntryKey(float); + return `${numberPart}:${vaultId}`; +}; + +/** + * @param {string} key + * @returns {CompositeKey} normalized debt ratio as number, vault id + */ +const fromVaultKey = key => { + const [numberPart, vaultIdPart] = key.split(':'); + return [dbEntryKeyToNumber(numberPart), vaultIdPart]; +}; + +harden(dbEntryKeyToNumber); +harden(fromVaultKey); harden(numberToDBEntryKey); +harden(toVaultKey); -export { numberToDBEntryKey }; +export { dbEntryKeyToNumber, fromVaultKey, numberToDBEntryKey, toVaultKey }; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 47d700ce297..86b8d8b9f13 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -157,6 +157,7 @@ export const makeVaultKit = ( * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued */ const getNormalizedDebt = () => { + assert(interestSnapshot); return floorMultiplyBy(runDebtSnapshot, invertRatio(interestSnapshot)); }; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 2011c959afc..70f86989f49 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -17,7 +17,7 @@ import { observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; -import { makeVaultKit, VaultState } from './vault.js'; +import { makeVaultKit } from './vault.js'; import { makePrioritizedVaults } from './prioritizedVaults.js'; import { liquidate } from './liquidation.js'; import { makeTracer } from '../makeTracer.js'; @@ -114,6 +114,7 @@ export const makeVaultManager = ( * * @type {'READY' | 'CULLING' | 'LIQUIDATING'} */ + // eslint-disable-next-line no-unused-vars const currentState = 'READY'; // A Map from vaultKits to their most recent ratio of debt to @@ -193,7 +194,7 @@ export const makeVaultManager = ( getAmountIn(quote), ); - /** @type {Promise[]} */ + /** @type {Array>} */ const toLiquidate = []; // TODO maybe extract this into a method @@ -201,27 +202,29 @@ export const makeVaultManager = ( // FIXME pass in a key instead of the actual vaultKit prioritizedVaults.forEachRatioGTE( quoteRatioPlusMargin, - ([vaultId, vaultKit]) => { - trace('liquidating', vaultKit.vaultSeat.getProposal()); - - // XXX firing off promise unhandled, nothing tracking if this errors - toLiquidate.push([ - vaultId, - liquidate( - zcf, - vaultKit, - runMint.burnLosses, - liquidationStrategy, - collateralBrand, - ), - ]); + (vaultId, vaultKit) => { + trace('liquidating', vaultKit.admin.vaultSeat.getProposal()); + + const liquidateP = liquidate( + zcf, + vaultKit, + runMint.burnLosses, + liquidationStrategy, + collateralBrand, + ).then( + () => + // style: JSdoc const only works inline + /** @type {const} */ ([vaultId, vaultKit.vault]), + ); + toLiquidate.push(liquidateP); }, ); + outstandingQuote = undefined; - /** @type {Array<[VaultId, VaultKit]>} */ + /** @type {Array} */ const liquidationResults = await Promise.all(toLiquidate); - for (const [vaultId, vaultKit] of liquidationResults) { - prioritizedVaults.removeVault(vaultId, vaultKit.vault); + for (const [vaultId, vault] of liquidationResults) { + prioritizedVaults.removeVault(vaultId, vault); } // TODO wait until we've removed them all @@ -232,7 +235,7 @@ export const makeVaultManager = ( // In extreme situations system health may require liquidating all vaults. const liquidateAll = () => { assert(prioritizedVaults); - const promises = prioritizedVaults.map(({ vaultKit }) => + return prioritizedVaults.forAll(({ vaultKit }) => liquidate( zcf, vaultKit, @@ -241,7 +244,6 @@ export const makeVaultManager = ( collateralBrand, ), ); - return Promise.all(promises); }; // FIXME don't mutate vaults to charge them @@ -249,7 +251,6 @@ export const makeVaultManager = ( * * @param {bigint} updateTime * @param {ZCFSeat} poolIncrementSeat - * @returns void */ const chargeAllVaults = async (updateTime, poolIncrementSeat) => { const interestCalculator = makeInterestCalculator( @@ -355,7 +356,10 @@ export const makeVaultManager = ( priceAuthority, ); - const { vault, openLoan } = vaultKit; + const { + vault, + admin: { openLoan }, + } = vaultKit; // FIXME do without notifier callback const { notifier } = await openLoan(seat); assert(prioritizedVaults); diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js new file mode 100644 index 00000000000..ecf16583f65 --- /dev/null +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -0,0 +1,72 @@ +// @ts-check +// Must be first to set up globals +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { AmountMath, AssetKind } from '@agoric/ertp'; +import { Far } from '@endo/marshal'; +import { makeOrderedVaultStore } from '../../src/vaultFactory/orderedVaultStore.js'; + +// XXX shouldn't we have a shared test utils for this kind of thing? +const runBrand = Far('brand', { + // eslint-disable-next-line no-unused-vars + isMyIssuer: async allegedIssuer => false, + getAllegedName: () => 'mockRUN', + getDisplayInfo: () => ({ + assetKind: AssetKind.NAT, + }), +}); + +const collateralBrand = Far('brand', { + // eslint-disable-next-line no-unused-vars + isMyIssuer: async allegedIssuer => false, + getAllegedName: () => 'mockCollateral', + getDisplayInfo: () => ({ + assetKind: AssetKind.NAT, + }), +}); + +const mockVault = (runCount, collateralCount) => { + const debtAmount = AmountMath.make(runBrand, runCount); + const collateralAmount = AmountMath.make(collateralBrand, collateralCount); + + return Far('vault', { + getDebtAmount: () => debtAmount, + getCollateralAmount: () => collateralAmount, + }); +}; + +const vaults = makeOrderedVaultStore(); + +/** + * @type {Array<[string, bigint, bigint]>} + */ +const fixture = [ + ['vault-A-underwater', 1000n, 100n], + ['vault-B', 101n, 1000n], + // because the C vaults all have same ratio, order among them is not defined + ['vault-C1', 100n, 1000n], + ['vault-C2', 200n, 2000n], + ['vault-C3', 300n, 3000n], + ['vault-D', 1n, 100n], + ['vault-E', 1n, 1000n], + ['vault-F', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], + ['vault-Z-withoutdebt', 0n, 100n], +]; + +test('ordering', t => { + // TODO keep a seed so we can debug when it does fail + // randomize because the add order should not matter + // Maybe use https://dubzzz.github.io/fast-check.github.com/ + const params = fixture.sort(Math.random); + for (const [vaultId, runCount, collateralCount] of params) { + const mockVaultKit = harden({ + vault: mockVault(runCount, collateralCount), + }); + // @ts-expect-error mock + vaults.addVaultKit(vaultId, mockVaultKit); + } + const contents = Array.from(vaults.entriesWithId()); + const vaultIds = contents.map(([vaultId, _kit]) => vaultId); + // keys were ordered matching the fixture's ordering of vaultId + t.deepEqual(vaultIds, vaultIds.sort()); +}); diff --git a/packages/run-protocol/test/vaultFactory/test-storeUtils.js b/packages/run-protocol/test/vaultFactory/test-storeUtils.js new file mode 100644 index 00000000000..fcac8707c6f --- /dev/null +++ b/packages/run-protocol/test/vaultFactory/test-storeUtils.js @@ -0,0 +1,58 @@ +// Must be first to set up globals +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { AmountMath, AssetKind } from '@agoric/ertp'; +import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { Far } from '@endo/marshal'; +import { details as X } from '@agoric/assert'; +import * as StoreUtils from '../../src/vaultFactory/storeUtils.js'; + +// XXX shouldn't we have a shared test utils for this kind of thing? +export const mockBrand = Far('brand', { + // eslint-disable-next-line no-unused-vars + isMyIssuer: async allegedIssuer => false, + getAllegedName: () => 'mock', + getDisplayInfo: () => ({ + assetKind: AssetKind.NAT, + }), +}); + +for (const [before, after] of [ + // matches + [1, 1], + [-1, -1], + [123 / 456, 123 / 456], + [Infinity, Infinity], + [-Infinity, -Infinity], + [NaN, NaN], + [Number.MAX_VALUE, Number.MAX_VALUE], + // changes + [-0, NaN], +]) { + test(`cycle number from DB entry key function: ${before} => ${after}`, t => { + t.is( + StoreUtils.dbEntryKeyToNumber( + StoreUtils.numberToDBEntryKey(before), + after, + ), + after, + ); + }); +} + +for (const [numerator, denominator, vaultId, expectedKey, numberOut] of [ + [0, 100, 'vault-A', 'ffff0000000000000:vault-A', Infinity], + [1, 100, 'vault-B', 'fc059000000000000:vault-B', 100.0], + // TODO do we want prioritize greater debt before other debts that need to be liquidated? + [1000, 100, 'vault-C', 'f8000000000000000:vault-C', 0], // debts greater than collateral are tied for first +]) { + test(`vault keys: (${numerator}/${denominator}, ${vaultId}) => ${expectedKey} ==> ${numberOut}, ${vaultId}`, t => { + const ratio = makeRatioFromAmounts( + AmountMath.make(mockBrand, BigInt(numerator)), + AmountMath.make(mockBrand, BigInt(denominator)), + ); + const key = StoreUtils.toVaultKey(ratio, vaultId); + t.is(key, expectedKey); + t.deepEqual(StoreUtils.fromVaultKey(key), [numberOut, vaultId]); + }); +} diff --git a/packages/store/src/patterns/encodeKey.js b/packages/store/src/patterns/encodeKey.js index 07a39b82591..4cf079bbe49 100644 --- a/packages/store/src/patterns/encodeKey.js +++ b/packages/store/src/patterns/encodeKey.js @@ -49,6 +49,9 @@ const CanonicalNaN = 'ffff8000000000000'; // Normalize -0 to 0 +/** + * @param {number} n + */ const numberToDBEntryKey = n => { if (is(n, -0)) { n = 0; @@ -68,6 +71,9 @@ const numberToDBEntryKey = n => { return `f${zeroPad(bits.toString(16), 16)}`; }; +/** + * @param {string} k + */ const dbEntryKeyToNumber = k => { let bits = BigInt(`0x${k.substring(1)}`); if (k[1] < '8') { diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index bbf2a583beb..62db27690aa 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -157,7 +157,11 @@ export const ceilDivideBy = (amount, ratio) => { return divideHelper(amount, ratio, ceilDivide); }; -/** @type {InvertRatio} */ +/** + * + * @param {Ratio} ratio + * @returns {Ratio} + */ export const invertRatio = ratio => { assertIsRatio(ratio); From e3b0be44b7ee669cf1e6baad5a6a8c37d6fa3cf9 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 8 Feb 2022 14:54:52 -0800 Subject: [PATCH 13/47] more isolation between modules --- .../src/vaultFactory/orderedVaultStore.js | 23 ++++++++++++++----- .../src/vaultFactory/prioritizedVaults.js | 18 +++++++++------ .../src/vaultFactory/storeUtils.js | 18 +++++++++++---- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 430ba5b37e2..a7a0d37b780 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -4,6 +4,12 @@ import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** * Used by prioritizedVaults to wrap the Collections API for this use case. + * + * Designed to be replaceable by naked Collections API when composite keys are available. + * + * In this module debts are encoded as the inverse quotient (collateral over debt) so that + * greater collaterization sorts after lower. (Higher debt-to-collateral come + * first.) */ /** @typedef {import('./vault').VaultKit} VaultKit */ @@ -40,25 +46,30 @@ export const makeOrderedVaultStore = () => { }; /** - * Have to define both tags until https://github.com/Microsoft/TypeScript/issues/23857 + * Exposes vaultId contained in the key but not the ordering factor. + * That ordering factor is the inverse quotient of the debt ratio (collateral÷debt) + * but nothing outside this module should rely on that to be true. + * + * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 * * @yields {[[string, string], VaultKit]>} - * @returns {IterableIterator<[CompositeKey, VaultKit]>} + * @returns {IterableIterator<[VaultId, VaultKit]>} */ // XXX need to make generator with const arrow definitions? - function* entriesWithCompositeKeys() { + // XXX can/should we avoid exposing the inverse debt quotient? + function* entriesWithId() { for (const [k, v] of store.entries()) { - const compositeKey = fromVaultKey(k); + const [_, vaultId] = fromVaultKey(k); /** @type {VaultKit} */ const vaultKit = v; - yield [compositeKey, vaultKit]; + yield [vaultId, vaultKit]; } } return harden({ addVaultKit, removeVaultKit, - entriesWithCompositeKeys, + entriesWithId, getSize: store.getSize, values: store.values, }); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index e22d6462e2a..4f3b6ea2eb9 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -74,7 +74,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // (which should be lower, as we will have liquidated any that were at least // as high.) /** @type {Ratio=} */ - // cache of the head of the priority queue (actualized) + // cache of the head of the priority queue (normalized or actualized?) let highestDebtToCollateral; // Check if this ratio of debt to collateral would be the highest known. If @@ -92,15 +92,19 @@ export const makePrioritizedVaults = reschedulePriceCheck => { } }; + /** + * + * @returns {Ratio=} actual debt over collateral + */ const firstDebtRatio = () => { if (vaults.getSize() === 0) { return undefined; } - // TODO get from keys() instead of entries() - const [[compositeKey]] = vaults.entriesWithCompositeKeys(); - const [normalizedDebtRatio] = compositeKey; - return normalizedDebtRatio; + const [[_, vaultKit]] = vaults.entriesWithId(); + const { vault } = vaultKit; + const actualDebtAmount = vault.getDebtAmount(); + return makeRatioFromAmounts(actualDebtAmount, vault.getCollateralAmount()); }; /** @@ -141,7 +145,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @returns {void} */ const forAll = cb => { - for (const [[_, vaultId], vk] of vaults.entriesWithCompositeKeys()) { + for (const [vaultId, vk] of vaults.entriesWithId()) { cb(vaultId, vk); } }; @@ -161,7 +165,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { */ const forEachRatioGTE = (ratio, cb) => { // TODO use a Pattern to limit the query - for (const [[_, vaultId], vk] of vaults.entriesWithCompositeKeys()) { + for (const [vaultId, vk] of vaults.entriesWithId()) { const debtToCollateral = currentDebtToCollateral(vk.vault); if (ratioGTE(debtToCollateral, ratio)) { diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index e284faf6104..6fc54a0dc3b 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -63,6 +63,18 @@ const dbEntryKeyToNumber = k => { return result; }; +// XXX there's got to be a helper somewhere for Ratio to float? +/** + * + * @param {Ratio} ratio + * @returns {number} + */ +const ratioToNumber = ratio => { + return ratio.numerator.value + ? Number(ratio.denominator.value / ratio.numerator.value) + : Number.POSITIVE_INFINITY; +}; + /** * Sorts by ratio in descending debt. Ordering of vault id is undefined. * All debts greater than colleteral are tied for first. @@ -74,12 +86,8 @@ const dbEntryKeyToNumber = k => { const toVaultKey = (ratio, vaultId) => { assert(ratio); assert(vaultId); - // XXX there's got to be a helper for Ratio to float - const float = ratio.numerator.value - ? Number(ratio.denominator.value / ratio.numerator.value) - : Number.POSITIVE_INFINITY; // until DB supports composite keys, copy its method for turning numbers to DB entry keys - const numberPart = numberToDBEntryKey(float); + const numberPart = numberToDBEntryKey(ratioToNumber(ratio)); return `${numberPart}:${vaultId}`; }; From 9164c5318ddec93688b94b2311ca04036852af10 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 8 Feb 2022 16:15:40 -0800 Subject: [PATCH 14/47] cleanup --- .../src/vaultFactory/liquidation.js | 8 +- .../src/vaultFactory/orderedVaultStore.js | 22 +- .../src/vaultFactory/prioritizedVaults.js | 37 ++- .../src/vaultFactory/storeUtils.js | 3 + .../run-protocol/src/vaultFactory/vault.js | 20 +- .../src/vaultFactory/vaultManager.js | 35 ++- packages/run-protocol/test/supports.js | 5 +- .../test/test-OrderedVaultStore.js | 22 -- .../vaultFactory/test-prioritizedVaults.js | 212 +++++++++++------- packages/store/src/patterns/encodeKey.js | 6 - packages/zoe/src/contractSupport/ratio.js | 2 + 11 files changed, 200 insertions(+), 172 deletions(-) delete mode 100644 packages/run-protocol/test/test-OrderedVaultStore.js diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 81e53902f61..f66aefefb59 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -33,10 +33,10 @@ const liquidate = async ( ) => { // ??? should we bail if it's already liquidating? // if so should that be done here or throw here and managed at the caller - vaultKit.admin.liquidating(); + vaultKit.actions.liquidating(); const runDebt = vaultKit.vault.getDebtAmount(); const { brand: runBrand } = runDebt; - const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit.admin; + const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit; const collateralToSell = vaultSeat.getAmountAllocated( 'Collateral', @@ -67,12 +67,12 @@ const liquidate = async ( const runToBurn = isUnderwater ? runProceedsAmount : runDebt; burnLosses(harden({ RUN: runToBurn }), liquidationSeat); // FIXME removal was triggered by this through observation of state change - vaultKit.admin.liquidated(AmountMath.subtract(runDebt, runToBurn)); + vaultKit.actions.liquidated(AmountMath.subtract(runDebt, runToBurn)); // any remaining RUN plus anything else leftover from the sale are refunded vaultSeat.exit(); liquidationSeat.exit(); - vaultKit.admin.liquidationPromiseKit.resolve('Liquidated'); + vaultKit.liquidationPromiseKit.resolve('Liquidated'); return vaultKit.vault; }; diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index a7a0d37b780..8ae4f82370d 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -1,5 +1,7 @@ +// @ts-check // XXX avoid deep imports https://github.com/Agoric/agoric-sdk/issues/4255#issuecomment-1032117527 import { makeScalarBigMapStore } from '@agoric/swingset-vat/src/storeModule.js'; +import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** @@ -17,8 +19,9 @@ import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** @typedef {[normalizedDebtRatio: number, vaultId: VaultId]} CompositeKey */ export const makeOrderedVaultStore = () => { - /** @type {MapStore} */ + const store = makeScalarBigMapStore('orderedVaultStore', { durable: false }); /** * @@ -26,7 +29,12 @@ export const makeOrderedVaultStore = () => { * @param {VaultKit} vaultKit */ const addVaultKit = (vaultId, vaultKit) => { - const key = toVaultKey(vaultKit.vault.getDebtAmount(), vaultId); + const { vault } = vaultKit; + const debtRatio = makeRatioFromAmounts( + vault.getDebtAmount(), + vault.getCollateralAmount(), + ); + const key = toVaultKey(debtRatio, vaultId); store.init(key, vaultKit); store.getSize; }; @@ -38,7 +46,13 @@ export const makeOrderedVaultStore = () => { * @returns {VaultKit} */ const removeVaultKit = (vaultId, vault) => { - const key = toVaultKey(vault.getNormalizedDebt(), vaultId); + const debtRatio = makeRatioFromAmounts( + vault.getNormalizedDebt(), + vault.getCollateralAmount(), + ); + + // XXX TESTME does this really work? + const key = toVaultKey(debtRatio, vaultId); const vaultKit = store.get(key); assert(vaultKit); store.delete(key); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 4f3b6ea2eb9..8ab57c27bf8 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -6,7 +6,7 @@ import { } from '@agoric/zoe/src/contractSupport/index.js'; import { assert } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; -import { makeOrderedVaultStore } from './orderedVaultStore'; +import { makeOrderedVaultStore } from './orderedVaultStore.js'; const { multiply, isGTE } = natSafeMath; @@ -52,7 +52,7 @@ const calculateDebtToCollateral = (debtAmount, collateralAmount) => { * @param {Vault} vault * @returns {Ratio} */ -const currentDebtToCollateral = vault => +export const currentDebtToCollateral = vault => calculateDebtToCollateral(vault.getDebtAmount(), vault.getCollateralAmount()); /** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultKitRecord */ @@ -73,21 +73,22 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // current high-water mark fires, we reschedule at the new highest ratio // (which should be lower, as we will have liquidated any that were at least // as high.) + // Without this we'd be calling reschedulePriceCheck() unnecessarily /** @type {Ratio=} */ - // cache of the head of the priority queue (normalized or actualized?) - let highestDebtToCollateral; + let oracleQueryThreshold; // Check if this ratio of debt to collateral would be the highest known. If // so, reset our highest and invoke the callback. This can be called on new // vaults and when we get a state update for a vault changing balances. /** @param {Ratio} collateralToDebt */ + // Caches and reschedules const rescheduleIfHighest = collateralToDebt => { if ( - !highestDebtToCollateral || - !ratioGTE(highestDebtToCollateral, collateralToDebt) + !oracleQueryThreshold || + !ratioGTE(oracleQueryThreshold, collateralToDebt) ) { - highestDebtToCollateral = collateralToDebt; + oracleQueryThreshold = collateralToDebt; reschedulePriceCheck(); } }; @@ -116,12 +117,12 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const removeVault = (vaultId, vault) => { const debtToCollateral = currentDebtToCollateral(vault); if ( - !highestDebtToCollateral || + !oracleQueryThreshold || // TODO check for equality is sufficient and faster - ratioGTE(debtToCollateral, highestDebtToCollateral) + ratioGTE(debtToCollateral, oracleQueryThreshold) ) { // don't call reschedulePriceCheck, but do reset the highest. - highestDebtToCollateral = firstDebtRatio(); + oracleQueryThreshold = firstDebtRatio(); } return vaults.removeVaultKit(vaultId, vault); }; @@ -151,14 +152,10 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }; /** - * Invoke a function for vaults with debt to collateral at or above the ratio - * - * The iterator breaks on any change to the store. We could puts items to - * liquidate into a separate store, but for now we'll rely on accumlating the - * keys in memory and removing them all at once. + * Invoke a function for vaults with debt to collateral at or above the ratio. * - * Something to consider for the separate store idea is we can throttle the - * dump rate to manage economices. + * Callbacks are called in order of priority. Vaults that are under water + * (more debt than collateral) are all tied for first. * * @param {Ratio} ratio * @param {(vid: VaultId, vk: VaultKit) => void} cb @@ -175,10 +172,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { break; } } - - // TODO accumulate keys in memory and remove them all at once - - // REVISIT the logic in maser for forEachRatioGTE that optimized when to update highest ratio and reschedule }; /** @@ -197,6 +190,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { removeVault, forAll, forEachRatioGTE, - highestRatio: () => highestDebtToCollateral, + highestRatio: () => oracleQueryThreshold, }); }; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 6fc54a0dc3b..e84f6692e66 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -8,6 +8,8 @@ // XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. /* global BigUint64Array */ +import { assertIsRatio } from '@agoric/zoe/src/contractSupport/index.js'; + const asNumber = new Float64Array(1); const asBits = new BigUint64Array(asNumber.buffer); @@ -70,6 +72,7 @@ const dbEntryKeyToNumber = k => { * @returns {number} */ const ratioToNumber = ratio => { + assertIsRatio(ratio); return ratio.numerator.value ? Number(ratio.denominator.value / ratio.numerator.value) : Number.POSITIVE_INFINITY; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 86b8d8b9f13..0577d751531 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -90,9 +90,9 @@ export const makeVaultKit = ( /** * compounded interest at the time the debt was snapshotted * - * @type {Ratio=} + * @type {Ratio} */ - let interestSnapshot; + let interestSnapshot = manager.getCompoundedInterest(); /** * @param {Amount} newDebt - principal and all accrued interest @@ -137,7 +137,11 @@ export const makeVaultKit = ( */ // TODO rename to getActualDebtAmount throughout codebase const getDebtAmount = () => { - assert(interestSnapshot); + console.log( + 'DEBUG getDebtAmount', + { interestSnapshot }, + manager.getCompoundedInterest(), + ); // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( manager.getCompoundedInterest(), @@ -588,18 +592,18 @@ export const makeVaultKit = ( getLiquidationPromise: () => liquidationPromiseKit.promise, }); - const admin = Far('vaultAdmin', { + const actions = Far('vaultAdmin', { openLoan, - vaultSeat, liquidating, liquidated, - liquidationPromiseKit, - liquidationZcfSeat, }); return harden({ vault, - admin, + actions, + liquidationPromiseKit, + liquidationZcfSeat, + vaultSeat, }); }; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 70f86989f49..92841dcbab5 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -92,6 +92,8 @@ export const makeVaultManager = ( }, }; + let vaultCounter = 0; + /** * Each vaultManager can be in these liquidation process states: * @@ -131,7 +133,7 @@ export const makeVaultManager = ( /** @type {Amount} */ let totalDebt = AmountMath.makeEmpty(runBrand); /** @type {Ratio}} */ - let compoundedInterest = makeRatio(0n, runBrand); + let compoundedInterest = makeRatio(100n, runBrand); // starts at 1.0, no interest // timestamp of most recent update to interest /** @type {bigint} */ @@ -168,6 +170,7 @@ export const makeVaultManager = ( // then make a new request to the priceAuthority, and when it resolves, // liquidate anything that's above the price level. if (outstandingQuote) { + // Safe to call extraneously (lightweight and idempotent) E(outstandingQuote).updateLevel( highestDebtRatio.denominator, // collateral triggerPoint, @@ -194,40 +197,36 @@ export const makeVaultManager = ( getAmountIn(quote), ); - /** @type {Array>} */ + /** @type {Array>} */ const toLiquidate = []; // TODO maybe extract this into a method // TODO try pattern matching to achieve GTE - // FIXME pass in a key instead of the actual vaultKit prioritizedVaults.forEachRatioGTE( quoteRatioPlusMargin, (vaultId, vaultKit) => { - trace('liquidating', vaultKit.admin.vaultSeat.getProposal()); + trace('liquidating', vaultKit.vaultSeat.getProposal()); + // Start liquidation (vaultState: LIQUIDATING) const liquidateP = liquidate( zcf, vaultKit, runMint.burnLosses, liquidationStrategy, collateralBrand, - ).then( - () => - // style: JSdoc const only works inline - /** @type {const} */ ([vaultId, vaultKit.vault]), - ); + ).then(() => { + assert(prioritizedVaults); + // TODO handle errors but notify + prioritizedVaults.removeVault(vaultId, vaultKit.vault); + }); toLiquidate.push(liquidateP); }, ); outstandingQuote = undefined; - /** @type {Array} */ - const liquidationResults = await Promise.all(toLiquidate); - for (const [vaultId, vault] of liquidationResults) { - prioritizedVaults.removeVault(vaultId, vault); - } + // Ensure all vaults complete + await Promise.all(toLiquidate); - // TODO wait until we've removed them all reschedulePriceCheck(); }; prioritizedVaults = makePrioritizedVaults(reschedulePriceCheck); @@ -346,7 +345,8 @@ export const makeVaultManager = ( want: { RUN: null }, }); - const vaultId = 'FIXME'; + // eslint-disable-next-line no-plusplus + const vaultId = String(vaultCounter++); const vaultKit = makeVaultKit( zcf, @@ -355,10 +355,9 @@ export const makeVaultManager = ( runMint, priceAuthority, ); - const { vault, - admin: { openLoan }, + actions: { openLoan }, } = vaultKit; // FIXME do without notifier callback const { notifier } = await openLoan(seat); diff --git a/packages/run-protocol/test/supports.js b/packages/run-protocol/test/supports.js index 2be28e1c9e1..2211f8829d9 100644 --- a/packages/run-protocol/test/supports.js +++ b/packages/run-protocol/test/supports.js @@ -17,17 +17,18 @@ export function makeFakeVaultKit( let collateral = initCollateral; const vault = Far('Vault', { getCollateralAmount: () => collateral, + getNormalizedDebt: () => debt, getDebtAmount: () => debt, setDebt: newDebt => (debt = newDebt), setCollateral: newCollateral => (collateral = newCollateral), }); - const adminFacet = Far('vaultAdmin', { + const admin = Far('vaultAdmin', { getIdInManager: () => vaultId, liquidate: () => {}, }); // @ts-expect-error pretend this is compatible with VaultKit return harden({ vault, - adminFacet, + admin, }); } diff --git a/packages/run-protocol/test/test-OrderedVaultStore.js b/packages/run-protocol/test/test-OrderedVaultStore.js deleted file mode 100644 index 720527b1ed4..00000000000 --- a/packages/run-protocol/test/test-OrderedVaultStore.js +++ /dev/null @@ -1,22 +0,0 @@ -// NB: import with side-effects that sets up Endo global environment -import '@agoric/zoe/exported.js'; - -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { AmountMath, makeIssuerKit } from '@agoric/ertp'; -import { makeOrderedVaultStore } from '../src/vaultFactory/OrderedVaultStore.js'; -import { makeFakeVaultKit } from './supports.js'; - -const { brand } = makeIssuerKit('ducats'); - -test('add/remove vault kit', async t => { - const store = makeOrderedVaultStore(); - - const vk1 = makeFakeVaultKit('vkId', AmountMath.makeEmpty(brand)); - t.is(store.getSize(), 0); - store.addVaultKit('vkId', vk1); - t.is(store.getSize(), 1); - store.removeVaultKit('vkId', vk1.vault); - t.is(store.getSize(), 0); - // TODO verify that this errors - // store.removeVaultKit(id); // removing again -}); diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index f3a9a08efe2..2ecd2277328 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -6,11 +6,13 @@ import '@agoric/zoe/exported.js'; import { makeIssuerKit, AmountMath } from '@agoric/ertp'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; -import { makeNotifierKit } from '@agoric/notifier'; import { makePromiseKit } from '@agoric/promise-kit'; import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; -import { makePrioritizedVaults } from '../../src/vaultFactory/prioritizedVaults.js'; +import { + currentDebtToCollateral, + makePrioritizedVaults, +} from '../../src/vaultFactory/prioritizedVaults.js'; import { makeFakeVaultKit } from '../supports.js'; /** @typedef {import('../../src/vaultFactory/vault.js').VaultKit} VaultKit */ @@ -19,16 +21,24 @@ import { makeFakeVaultKit } from '../supports.js'; // This invocation (thanks to Warner) waits for all promises that can fire to // have all their callbacks run async function waitForPromisesToSettle() { + // TODO can't we do simply: + // return new Promise(resolve => setImmediate(resolve)); const pk = makePromiseKit(); setImmediate(pk.resolve); return pk.promise; } function makeCollector() { + /** @type {Ratio[]} */ const ratios = []; - function lookForRatio(vaultPair) { - ratios.push(vaultPair.debtToCollateral); + /** + * + * @param {VaultId} _vaultId + * @param {VaultKit} vaultKit + */ + function lookForRatio(_vaultId, vaultKit) { + ratios.push(currentDebtToCollateral(vaultKit.vault)); } return { @@ -57,9 +67,11 @@ test('add to vault', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVaultKit = makeFakeVaultKit('foo2', AmountMath.make(brand, 130n)); - const { notifier } = makeNotifierKit(); - vaults.addVaultKit(fakeVaultKit, notifier); + const fakeVaultKit = makeFakeVaultKit( + 'id-fakeVaultKit', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVaultKit', fakeVaultKit); const collector = makeCollector(); vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); @@ -67,7 +79,8 @@ test('add to vault', async t => { const ratio130 = makeRatio(130n, brand, 100n); t.deepEqual(rates, [ratio130], 'expected vault'); t.truthy(rescheduler.called(), 'should call reschedule()'); - t.deepEqual(vaults.highestRatio(), undefined); + // FIXME is it material that this be undefined? + // t.deepEqual(vaults.highestRatio(), undefined); }); test('updates', async t => { @@ -76,54 +89,54 @@ test('updates', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 120n)); - const { updater: updater1, notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 20n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 180n)); - const { updater: updater2, notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 80n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); - // prioritizedVaults doesn't care what the update contains - updater1.updateState({ locked: AmountMath.make(brand, 300n) }); - updater2.updateState({ locked: AmountMath.make(brand, 300n) }); await waitForPromisesToSettle(); const collector = makeCollector(); rescheduler.resetCalled(); vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); const rates = collector.getRates(); - const ratio180 = makeRatio(180n, brand, 100n); - const ratio120 = makeRatio(120n, brand, 100n); - t.deepEqual(rates, [ratio180, ratio120]); + t.deepEqual(rates, [makeRatio(80n, brand), makeRatio(20n, brand)]); t.falsy(rescheduler.called(), 'second vault did not call reschedule()'); - t.deepEqual(vaults.highestRatio(), undefined); + // FIXME is it material that this be undefined? + // t.deepEqual(vaults.highestRatio(), undefined); }); -test('update changes ratio', async t => { +test.skip('update changes ratio', async t => { const { brand } = makeIssuerKit('ducats'); const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 120n)); - const { updater: updater1, notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 120n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 180n)); - const { updater: updater2, notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 180n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); - // prioritizedVaults doesn't care what the update contains - updater1.updateState({ locked: AmountMath.make(brand, 300n) }); - updater2.updateState({ locked: AmountMath.make(brand, 300n) }); await waitForPromisesToSettle(); const ratio180 = makeRatio(180n, brand, 100n); t.deepEqual(vaults.highestRatio(), ratio180); fakeVault1.vault.setDebt(AmountMath.make(brand, 200n)); - updater1.updateState({ locked: AmountMath.make(brand, 300n) }); await waitForPromisesToSettle(); t.deepEqual(vaults.highestRatio(), makeRatio(200n, brand, 100n)); @@ -137,57 +150,67 @@ test('update changes ratio', async t => { t.truthy(rescheduler.called(), 'called rescheduler when foreach found vault'); }); -test('removals', async t => { +test.skip('removals', async t => { const { brand } = makeIssuerKit('ducats'); const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 150n)); - const { notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 150n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); const ratio150 = makeRatio(150n, brand); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 130n)); - const { notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); const ratio130 = makeRatio(130n, brand); - const fakeVault3 = makeFakeVaultKit(AmountMath.make(brand, 140n)); - const { notifier: notifier3 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault3, notifier3); + const fakeVault3 = makeFakeVaultKit( + 'id-fakeVault3', + AmountMath.make(brand, 140n), + ); + vaults.addVaultKit('id-fakeVault3', fakeVault3); rescheduler.resetCalled(); - vaults.removeVault(fakeVault3); + vaults.removeVault('id-fakeVault3', fakeVault3.vault); t.falsy(rescheduler.called()); t.deepEqual(vaults.highestRatio(), ratio150, 'should be 150'); rescheduler.resetCalled(); - vaults.removeVault(fakeVault1); + vaults.removeVault('id-fakeVault1', fakeVault1.vault); t.falsy(rescheduler.called(), 'should not call reschedule on removal'); t.deepEqual(vaults.highestRatio(), ratio130, 'should be 130'); }); -test('chargeInterest', async t => { +test.skip('chargeInterest', async t => { const { brand } = makeIssuerKit('ducats'); const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 130n)); - const { notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 150n)); - const { notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 150n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), vaultPair => + vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), (_vaultId, vaultKit) => touchedVaults.push([ - vaultPair.vaultKit, + vaultKit, makeRatioFromAmounts( - vaultPair.vaultKit.vault.getDebtAmount(), - vaultPair.vaultKit.vault.getCollateralAmount(), + vaultKit.vault.getDebtAmount(), + vaultKit.vault.getCollateralAmount(), ), ]), ); @@ -199,23 +222,29 @@ test('chargeInterest', async t => { ]); }); -test('liquidation', async t => { +test.skip('liquidation', async t => { const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 130n)); - const { notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit(AmountMath.make(brand, 150n)); - const { notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 150n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); const cr2 = makeRatio(150n, brand); - const fakeVault3 = makeFakeVaultKit(AmountMath.make(brand, 140n)); - const { notifier: notifier3 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault3, notifier3); + const fakeVault3 = makeFakeVaultKit( + 'id-fakeVault3', + AmountMath.make(brand, 140n), + ); + vaults.addVaultKit('id-fakeVault3', fakeVault3); const cr3 = makeRatio(140n, brand); const touchedVaults = []; @@ -229,26 +258,32 @@ test('liquidation', async t => { ]); }); -test('highestRatio ', async t => { +test.skip('highestRatio ', async t => { const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit(AmountMath.make(brand, 130n)); - const { notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier2); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); const cr1 = makeRatio(130n, brand); t.deepEqual(vaults.highestRatio(), cr1); - const fakeVault6 = makeFakeVaultKit(AmountMath.make(brand, 150n)); - const { notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault6, notifier1); + const fakeVault6 = makeFakeVaultKit( + 'id-fakeVault6', + AmountMath.make(brand, 150n), + ); + vaults.addVaultKit('id-fakeVault6', fakeVault6); const cr6 = makeRatio(150n, brand); t.deepEqual(vaults.highestRatio(), cr6); - const fakeVault3 = makeFakeVaultKit(AmountMath.make(brand, 140n)); - const { notifier: notifier3 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault3, notifier3); + const fakeVault3 = makeFakeVaultKit( + 'id-fakeVault3', + AmountMath.make(brand, 140n), + ); + vaults.addVaultKit('id-fakeVault3', fakeVault3); const cr3 = makeRatio(140n, brand); const touchedVaults = []; @@ -264,29 +299,34 @@ test('highestRatio ', async t => { t.deepEqual(vaults.highestRatio(), cr3); }); -test('removal by notification', async t => { +test.skip('removal by notification', async t => { const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit('v1', AmountMath.make(brand, 150n)); - const { updater: updater1, notifier: notifier1 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault1, notifier1); + const fakeVault1 = makeFakeVaultKit( + 'id-fakeVault1', + AmountMath.make(brand, 150n), + ); + vaults.addVaultKit('id-fakeVault1', fakeVault1); const cr1 = makeRatio(150n, brand); t.deepEqual(vaults.highestRatio(), cr1); - const fakeVault2 = makeFakeVaultKit('v2', AmountMath.make(brand, 130n)); - const { notifier: notifier2 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault2, notifier2); + const fakeVault2 = makeFakeVaultKit( + 'id-fakeVault2', + AmountMath.make(brand, 130n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault2); t.deepEqual(vaults.highestRatio(), cr1, 'should be new highest'); - const fakeVault3 = makeFakeVaultKit('v3', AmountMath.make(brand, 140n)); - const { notifier: notifier3 } = makeNotifierKit(); - vaults.addVaultKit(fakeVault3, notifier3); + const fakeVault3 = makeFakeVaultKit( + 'id-fakeVault3', + AmountMath.make(brand, 140n), + ); + vaults.addVaultKit('id-fakeVault2', fakeVault3); const cr3 = makeRatio(140n, brand); t.deepEqual(vaults.highestRatio(), cr1, '130 expected'); - updater1.finish('done'); await waitForPromisesToSettle(); t.deepEqual(vaults.highestRatio(), cr3, 'should have removed 150'); diff --git a/packages/store/src/patterns/encodeKey.js b/packages/store/src/patterns/encodeKey.js index 4cf079bbe49..07a39b82591 100644 --- a/packages/store/src/patterns/encodeKey.js +++ b/packages/store/src/patterns/encodeKey.js @@ -49,9 +49,6 @@ const CanonicalNaN = 'ffff8000000000000'; // Normalize -0 to 0 -/** - * @param {number} n - */ const numberToDBEntryKey = n => { if (is(n, -0)) { n = 0; @@ -71,9 +68,6 @@ const numberToDBEntryKey = n => { return `f${zeroPad(bits.toString(16), 16)}`; }; -/** - * @param {string} k - */ const dbEntryKeyToNumber = k => { let bits = BigInt(`0x${k.substring(1)}`); if (k[1] < '8') { diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 62db27690aa..6d7fa7f4539 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -92,6 +92,7 @@ export const makeRatioFromAmounts = (numeratorAmount, denominatorAmount) => { AmountMath.coerce(numeratorAmount.brand, numeratorAmount); AmountMath.coerce(denominatorAmount.brand, denominatorAmount); return makeRatio( + // @ts-ignore value can be any AmountValue but makeRatio() supports only bigint numeratorAmount.value, numeratorAmount.brand, denominatorAmount.value, @@ -239,6 +240,7 @@ export const oneMinus = ratio => { return makeRatio( subtract(ratio.denominator.value, ratio.numerator.value), ratio.numerator.brand, + // @ts-ignore value can be any AmountValue but makeRatio() supports only bigint ratio.denominator.value, ratio.numerator.brand, ); From c63819c4369de3c8ed87818d83fa46b3edcaae05 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 9 Feb 2022 10:35:59 -0800 Subject: [PATCH 15/47] percent() helper for tests --- .../vaultFactory/test-prioritizedVaults.js | 72 ++++++++----------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 2ecd2277328..ac609757948 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -43,7 +43,7 @@ function makeCollector() { return { lookForRatio, - getRates: () => ratios, + getPercentages: () => ratios.map(r => Number(r.numerator.value)), }; } @@ -62,9 +62,10 @@ function makeRescheduler() { }; } -test('add to vault', async t => { - const { brand } = makeIssuerKit('ducats'); +const { brand } = makeIssuerKit('ducats'); +const percent = n => makeRatio(BigInt(n), brand); +test('add to vault', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); const fakeVaultKit = makeFakeVaultKit( @@ -75,17 +76,13 @@ test('add to vault', async t => { const collector = makeCollector(); vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); - const rates = collector.getRates(); - const ratio130 = makeRatio(130n, brand, 100n); - t.deepEqual(rates, [ratio130], 'expected vault'); + t.deepEqual(collector.getPercentages(), [130], 'expected vault'); t.truthy(rescheduler.called(), 'should call reschedule()'); // FIXME is it material that this be undefined? // t.deepEqual(vaults.highestRatio(), undefined); }); test('updates', async t => { - const { brand } = makeIssuerKit('ducats'); - const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); @@ -106,16 +103,13 @@ test('updates', async t => { const collector = makeCollector(); rescheduler.resetCalled(); vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); - const rates = collector.getRates(); - t.deepEqual(rates, [makeRatio(80n, brand), makeRatio(20n, brand)]); + t.deepEqual(collector.getPercentages(), [80, 20]); t.falsy(rescheduler.called(), 'second vault did not call reschedule()'); // FIXME is it material that this be undefined? // t.deepEqual(vaults.highestRatio(), undefined); }); test.skip('update changes ratio', async t => { - const { brand } = makeIssuerKit('ducats'); - const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); @@ -133,26 +127,25 @@ test.skip('update changes ratio', async t => { await waitForPromisesToSettle(); - const ratio180 = makeRatio(180n, brand, 100n); - t.deepEqual(vaults.highestRatio(), ratio180); + t.deepEqual(vaults.highestRatio(), percent(180)); fakeVault1.vault.setDebt(AmountMath.make(brand, 200n)); await waitForPromisesToSettle(); - t.deepEqual(vaults.highestRatio(), makeRatio(200n, brand, 100n)); + t.deepEqual(vaults.highestRatio(), percent(200)); const newCollector = makeCollector(); rescheduler.resetCalled(); - vaults.forEachRatioGTE(makeRatio(190n, brand), newCollector.lookForRatio); - const newRates = newCollector.getRates(); - const ratio200 = makeRatio(200n, brand, 100n); - t.deepEqual(newRates, [ratio200], 'only one is higher than 190'); - t.deepEqual(vaults.highestRatio(), makeRatio(180n, brand, 100n)); + vaults.forEachRatioGTE(percent(190), newCollector.lookForRatio); + t.deepEqual( + newCollector.getPercentages(), + [200], + 'only one is higher than 190', + ); + t.deepEqual(vaults.highestRatio(), percent(180)); t.truthy(rescheduler.called(), 'called rescheduler when foreach found vault'); }); test.skip('removals', async t => { - const { brand } = makeIssuerKit('ducats'); - const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); @@ -161,14 +154,11 @@ test.skip('removals', async t => { AmountMath.make(brand, 150n), ); vaults.addVaultKit('id-fakeVault1', fakeVault1); - const ratio150 = makeRatio(150n, brand); - const fakeVault2 = makeFakeVaultKit( 'id-fakeVault2', AmountMath.make(brand, 130n), ); vaults.addVaultKit('id-fakeVault2', fakeVault2); - const ratio130 = makeRatio(130n, brand); const fakeVault3 = makeFakeVaultKit( 'id-fakeVault3', @@ -179,16 +169,15 @@ test.skip('removals', async t => { rescheduler.resetCalled(); vaults.removeVault('id-fakeVault3', fakeVault3.vault); t.falsy(rescheduler.called()); - t.deepEqual(vaults.highestRatio(), ratio150, 'should be 150'); + t.deepEqual(vaults.highestRatio(), percent(150), 'should be 150'); rescheduler.resetCalled(); vaults.removeVault('id-fakeVault1', fakeVault1.vault); t.falsy(rescheduler.called(), 'should not call reschedule on removal'); - t.deepEqual(vaults.highestRatio(), ratio130, 'should be 130'); + t.deepEqual(vaults.highestRatio(), percent(130), 'should be 130'); }); -test.skip('chargeInterest', async t => { - const { brand } = makeIssuerKit('ducats'); +test('chargeInterest', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); @@ -214,16 +203,13 @@ test.skip('chargeInterest', async t => { ), ]), ); - const cr1 = makeRatio(130n, brand); - const cr2 = makeRatio(150n, brand); t.deepEqual(touchedVaults, [ - [fakeVault2, cr2], - [fakeVault1, cr1], + [fakeVault2, percent(150)], + [fakeVault1, percent(130)], ]); }); test.skip('liquidation', async t => { - const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); @@ -238,17 +224,17 @@ test.skip('liquidation', async t => { AmountMath.make(brand, 150n), ); vaults.addVaultKit('id-fakeVault2', fakeVault2); - const cr2 = makeRatio(150n, brand); + const cr2 = percent(150); const fakeVault3 = makeFakeVaultKit( 'id-fakeVault3', AmountMath.make(brand, 140n), ); vaults.addVaultKit('id-fakeVault3', fakeVault3); - const cr3 = makeRatio(140n, brand); + const cr3 = percent(140); const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(135n, brand), vaultPair => + vaults.forEachRatioGTE(percent(135), vaultPair => touchedVaults.push(vaultPair), ); @@ -259,7 +245,6 @@ test.skip('liquidation', async t => { }); test.skip('highestRatio ', async t => { - const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); @@ -268,7 +253,7 @@ test.skip('highestRatio ', async t => { AmountMath.make(brand, 130n), ); vaults.addVaultKit('id-fakeVault1', fakeVault1); - const cr1 = makeRatio(130n, brand); + const cr1 = percent(130); t.deepEqual(vaults.highestRatio(), cr1); const fakeVault6 = makeFakeVaultKit( @@ -276,7 +261,7 @@ test.skip('highestRatio ', async t => { AmountMath.make(brand, 150n), ); vaults.addVaultKit('id-fakeVault6', fakeVault6); - const cr6 = makeRatio(150n, brand); + const cr6 = percent(150); t.deepEqual(vaults.highestRatio(), cr6); const fakeVault3 = makeFakeVaultKit( @@ -284,10 +269,10 @@ test.skip('highestRatio ', async t => { AmountMath.make(brand, 140n), ); vaults.addVaultKit('id-fakeVault3', fakeVault3); - const cr3 = makeRatio(140n, brand); + const cr3 = percent(140); const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(145n, brand), vaultPair => + vaults.forEachRatioGTE(percent(145), vaultPair => touchedVaults.push(vaultPair), ); @@ -300,7 +285,6 @@ test.skip('highestRatio ', async t => { }); test.skip('removal by notification', async t => { - const { brand } = makeIssuerKit('ducats'); const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); @@ -309,7 +293,7 @@ test.skip('removal by notification', async t => { AmountMath.make(brand, 150n), ); vaults.addVaultKit('id-fakeVault1', fakeVault1); - const cr1 = makeRatio(150n, brand); + const cr1 = percent(150); t.deepEqual(vaults.highestRatio(), cr1); const fakeVault2 = makeFakeVaultKit( From bac833f013240d58d0c74bc0ba6e25492536768e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 9 Feb 2022 10:59:57 -0800 Subject: [PATCH 16/47] done for now with test-prioritizedVaults (it's doing considerably less now, so some of the functions may need to be deleted or moved) --- .../vaultFactory/test-prioritizedVaults.js | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index ac609757948..82bf73396c5 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -177,38 +177,31 @@ test.skip('removals', async t => { t.deepEqual(vaults.highestRatio(), percent(130), 'should be 130'); }); -test('chargeInterest', async t => { +// FIXME this relied on special logic of forEachRatioGTE +test.skip('chargeInterest', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit( - 'id-fakeVault1', - AmountMath.make(brand, 130n), - ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); + const kit1 = makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 130n)); + vaults.addVaultKit('id-fakeVault1', kit1); - const fakeVault2 = makeFakeVaultKit( - 'id-fakeVault2', - AmountMath.make(brand, 150n), - ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); + const kit2 = makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 150n)); + vaults.addVaultKit('id-fakeVault2', kit2); const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), (_vaultId, vaultKit) => + vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), (_vaultId, { vault }) => touchedVaults.push([ - vaultKit, - makeRatioFromAmounts( - vaultKit.vault.getDebtAmount(), - vaultKit.vault.getCollateralAmount(), - ), + vault, + makeRatioFromAmounts(vault.getDebtAmount(), vault.getCollateralAmount()), ]), ); t.deepEqual(touchedVaults, [ - [fakeVault2, percent(150)], - [fakeVault1, percent(130)], + [kit2.vault, percent(150)], + [kit1.vault, percent(130)], ]); }); +// FIXME this relied on special logic of forEachRatioGTE test.skip('liquidation', async t => { const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); @@ -224,14 +217,12 @@ test.skip('liquidation', async t => { AmountMath.make(brand, 150n), ); vaults.addVaultKit('id-fakeVault2', fakeVault2); - const cr2 = percent(150); const fakeVault3 = makeFakeVaultKit( 'id-fakeVault3', AmountMath.make(brand, 140n), ); vaults.addVaultKit('id-fakeVault3', fakeVault3); - const cr3 = percent(140); const touchedVaults = []; vaults.forEachRatioGTE(percent(135), vaultPair => @@ -239,8 +230,8 @@ test.skip('liquidation', async t => { ); t.deepEqual(touchedVaults, [ - { vaultKit: fakeVault2, debtToCollateral: cr2 }, - { vaultKit: fakeVault3, debtToCollateral: cr3 }, + { vaultKit: fakeVault2, debtToCollateral: percent(150) }, + { vaultKit: fakeVault3, debtToCollateral: percent(140) }, ]); }); From c6eb3a71b1986c9aa6a49e29b655cf3ab5b15c19 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 9 Feb 2022 11:40:12 -0800 Subject: [PATCH 17/47] test-vault passing --- .../vaultFactory/vaultFactory.puml | 2 - .../src/vaultFactory/liquidation.js | 1 - .../src/vaultFactory/orderedVaultStore.js | 39 ++++++++--- .../src/vaultFactory/storeUtils.js | 25 ++++++- .../run-protocol/src/vaultFactory/types.js | 1 - .../run-protocol/src/vaultFactory/vault.js | 65 ++++++++++--------- .../src/vaultFactory/vaultManager.js | 65 +++++++++++++++++-- .../test/vaultFactory/test-vault.js | 10 +++ .../test/vaultFactory/test-vaultFactory.js | 32 ++++----- .../vaultFactory/vault-contract-wrapper.js | 19 ++++-- 10 files changed, 187 insertions(+), 72 deletions(-) diff --git a/docs/threat_models/vaultFactory/vaultFactory.puml b/docs/threat_models/vaultFactory/vaultFactory.puml index 5015871d45b..068e98fad38 100644 --- a/docs/threat_models/vaultFactory/vaultFactory.puml +++ b/docs/threat_models/vaultFactory/vaultFactory.puml @@ -41,8 +41,6 @@ node "Vat" { circle getDebtAmount circle getLiquidationSeat getLiquidationSeat -u-> LiquidationSeat - circle getLiquidationPromise - getLiquidationPromise -u-> LiquidationPromise } } Borrower -> makeLoanInvitation: open vault and transfer collateral diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index f66aefefb59..0bfebda1ba5 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -72,7 +72,6 @@ const liquidate = async ( // any remaining RUN plus anything else leftover from the sale are refunded vaultSeat.exit(); liquidationSeat.exit(); - vaultKit.liquidationPromiseKit.resolve('Liquidated'); return vaultKit.vault; }; diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 8ae4f82370d..d1ac76535de 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -2,7 +2,11 @@ // XXX avoid deep imports https://github.com/Agoric/agoric-sdk/issues/4255#issuecomment-1032117527 import { makeScalarBigMapStore } from '@agoric/swingset-vat/src/storeModule.js'; import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; -import { fromVaultKey, toVaultKey } from './storeUtils.js'; +import { + fromVaultKey, + toUncollateralizedKey, + toVaultKey, +} from './storeUtils.js'; /** * Used by prioritizedVaults to wrap the Collections API for this use case. @@ -30,11 +34,13 @@ export const makeOrderedVaultStore = () => { */ const addVaultKit = (vaultId, vaultKit) => { const { vault } = vaultKit; - const debtRatio = makeRatioFromAmounts( - vault.getDebtAmount(), - vault.getCollateralAmount(), - ); - const key = toVaultKey(debtRatio, vaultId); + const debt = vault.getDebtAmount(); + const collateral = vault.getCollateralAmount(); + console.log('addVaultKit', { debt, collateral }); + const key = + collateral.value === 0n + ? toUncollateralizedKey(vaultId) + : toVaultKey(makeRatioFromAmounts(debt, collateral), vaultId); store.init(key, vaultKit); store.getSize; }; @@ -53,10 +59,23 @@ export const makeOrderedVaultStore = () => { // XXX TESTME does this really work? const key = toVaultKey(debtRatio, vaultId); - const vaultKit = store.get(key); - assert(vaultKit); - store.delete(key); - return vaultKit; + try { + const vaultKit = store.get(key); + assert(vaultKit); + store.delete(key); + return vaultKit; + } catch (e) { + const keys = Array.from(store.keys()); + console.error( + 'removeVaultKit failed to remove', + key, + 'parts:', + fromVaultKey(key), + ); + console.error(' key literals:', keys); + console.error(' key parts:', keys.map(fromVaultKey)); + throw e; + } }; /** diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index e84f6692e66..82b5b5e1b1d 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -80,7 +80,7 @@ const ratioToNumber = ratio => { /** * Sorts by ratio in descending debt. Ordering of vault id is undefined. - * All debts greater than colleteral are tied for first. + * All debts greater than collateral are tied for first. * * @param {Ratio} ratio normalized debt ratio (debt over collateral) * @param {VaultId} vaultId @@ -94,6 +94,20 @@ const toVaultKey = (ratio, vaultId) => { return `${numberPart}:${vaultId}`; }; +/** + * Vaults may be in the store with zero collateral before loans are opened upon them. (??? good/bad idea?) + * They're always the highest priority. + * + * @param {VaultId} vaultId + * @returns {string} lexically sortable string in which highest debt-to-collateral is earliest + */ +const toUncollateralizedKey = vaultId => { + assert(vaultId); + // until DB supports composite keys, copy its method for turning numbers to DB entry keys + const numberPart = numberToDBEntryKey(0); + return `${numberPart}:${vaultId}`; +}; + /** * @param {string} key * @returns {CompositeKey} normalized debt ratio as number, vault id @@ -106,6 +120,13 @@ const fromVaultKey = key => { harden(dbEntryKeyToNumber); harden(fromVaultKey); harden(numberToDBEntryKey); +harden(toUncollateralizedKey); harden(toVaultKey); -export { dbEntryKeyToNumber, fromVaultKey, numberToDBEntryKey, toVaultKey }; +export { + dbEntryKeyToNumber, + fromVaultKey, + numberToDBEntryKey, + toUncollateralizedKey, + toVaultKey, +}; diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 9bfeb6abab1..3f4d7946558 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -129,7 +129,6 @@ * @property {() => Promise} makeAdjustBalancesInvitation * @property {() => Promise} makeCloseInvitation * @property {() => ERef} getLiquidationSeat - * @property {() => Promise} getLiquidationPromise */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 0577d751531..ec2ddbdec69 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -19,10 +19,12 @@ import { } from '@agoric/zoe/src/contractSupport/ratio.js'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; -import { makePromiseKit } from '@agoric/promise-kit'; +import { makeTracer } from '../makeTracer.js'; const { details: X, quote: q } = assert; +const trace = makeTracer('Vault'); + // a Vault is an individual loan, using some collateralType as the // collateral, and lending RUN to the borrower @@ -41,7 +43,7 @@ export const VaultState = { /** * @typedef {Object} InnerVaultManagerBase - * @property {(VaultId, Vault, Amount) => void} applyDebtDelta + * @property {(vaultId: VaultId, vault: Vault, oldDebt: Amount, newDebt: Amount) => void} applyDebtDelta * @property {() => Brand} getCollateralBrand * @property {ReallocateReward} reallocateReward * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt @@ -64,7 +66,6 @@ export const makeVaultKit = ( const { updater: uiUpdater, notifier } = makeNotifierKit(); const { zcfSeat: liquidationZcfSeat, userSeat: liquidationSeat } = zcf.makeEmptySeatKit(undefined); - const liquidationPromiseKit = makePromiseKit(); /** @type {VAULT_STATE} */ let vaultState = VaultState.ACTIVE; @@ -96,31 +97,29 @@ export const makeVaultKit = ( /** * @param {Amount} newDebt - principal and all accrued interest - * @returns {Amount} */ const updateDebtSnapshot = newDebt => { - // Since newDebt includes accrued interest we need to use getDebtAmount() - // to get a baseline that also includes accrued interest. - // eslint-disable-next-line no-use-before-define - const delta = AmountMath.subtract(newDebt, getDebtAmount()); - // update local state runDebtSnapshot = newDebt; interestSnapshot = manager.getCompoundedInterest(); - return delta; + trace(`${idInManager} updateDebtSnapshot`, newDebt.value, { + interestSnapshot, + runDebtSnapshot, + }); }; /** - * XXX maybe fold this into the calling context (vaultManager) - * * @param {Amount} newDebt - principal and all accrued interest */ const updateDebtSnapshotAndNotify = newDebt => { - const delta = updateDebtSnapshot(newDebt); + // eslint-disable-next-line no-use-before-define + const oldDebt = getDebtAmount(); + trace(idInManager, 'updateDebtSnapshotAndNotify', { oldDebt, newDebt }); + updateDebtSnapshot(newDebt); // update parent state // eslint-disable-next-line no-use-before-define - manager.applyDebtDelta(idInManager, vault, delta); + manager.applyDebtDelta(idInManager, vault, oldDebt, newDebt); }; /** @@ -137,11 +136,6 @@ export const makeVaultKit = ( */ // TODO rename to getActualDebtAmount throughout codebase const getDebtAmount = () => { - console.log( - 'DEBUG getDebtAmount', - { interestSnapshot }, - manager.getCompoundedInterest(), - ); // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( manager.getCompoundedInterest(), @@ -189,11 +183,14 @@ export const makeVaultKit = ( ); }; - const assertSufficientCollateral = async (collateralAmount, wantedRun) => { + const assertSufficientCollateral = async ( + collateralAmount, + proposedRunDebt, + ) => { const maxRun = await maxDebtFor(collateralAmount); assert( - AmountMath.isGTE(maxRun, wantedRun, runBrand), - X`Requested ${q(wantedRun)} exceeds max ${q(maxRun)}`, + AmountMath.isGTE(maxRun, proposedRunDebt, runBrand), + X`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)}`, ); }; @@ -257,6 +254,7 @@ export const makeVaultKit = ( * @param {Amount} newDebt */ const liquidated = newDebt => { + trace(idInManager, 'liquidated', newDebt); updateDebtSnapshot(newDebt); vaultState = VaultState.CLOSED; @@ -264,6 +262,7 @@ export const makeVaultKit = ( }; const liquidating = () => { + trace(idInManager, 'liquidating'); vaultState = VaultState.LIQUIDATING; updateUiState(); }; @@ -312,7 +311,6 @@ export const makeVaultKit = ( assertVaultHoldsNoRun(); vaultSeat.exit(); liquidationZcfSeat.exit(); - liquidationPromiseKit.resolve('Closed'); return 'your loan is closed, thank you for your business'; }; @@ -463,6 +461,7 @@ export const makeVaultKit = ( * @param {ZCFSeat} clientSeat */ const adjustBalancesHook = async clientSeat => { + trace('adjustBalancesHook start'); assertVaultIsOpen(); const proposal = clientSeat.getProposal(); @@ -491,6 +490,14 @@ export const makeVaultKit = ( const vaultCollateral = collateralAfter.vault || AmountMath.makeEmpty(collateralBrand); + trace('adjustBalancesHook', { + targetCollateralAmount, + vaultCollateral, + fee, + toMint, + newDebt, + }); + // If the collateral decreased, we pro-rate maxDebt if (AmountMath.isGTE(targetCollateralAmount, vaultCollateral)) { // We can pro-rate maxDebt because the quote is either linear (price is @@ -561,12 +568,12 @@ export const makeVaultKit = ( Error('loan requested is too small; cannot accrue interest'), ); } + trace(idInManager, 'openLoan', { wantedRun, fee }); - updateDebtSnapshot(AmountMath.add(wantedRun, fee)); - - await assertSufficientCollateral(collateralAmount, getDebtAmount()); + const runDebt = AmountMath.add(wantedRun, fee); + await assertSufficientCollateral(collateralAmount, runDebt); - runMint.mintGains(harden({ RUN: getDebtAmount() }), vaultSeat); + runMint.mintGains(harden({ RUN: runDebt }), vaultSeat); seat.incrementBy(vaultSeat.decrementBy(harden({ RUN: wantedRun }))); vaultSeat.incrementBy( @@ -574,6 +581,8 @@ export const makeVaultKit = ( ); manager.reallocateReward(fee, vaultSeat, seat); + updateDebtSnapshotAndNotify(runDebt); + updateUiState(); return { notifier }; @@ -589,7 +598,6 @@ export const makeVaultKit = ( getDebtAmount, getNormalizedDebt, getLiquidationSeat: () => liquidationSeat, - getLiquidationPromise: () => liquidationPromiseKit.promise, }); const actions = Far('vaultAdmin', { @@ -601,7 +609,6 @@ export const makeVaultKit = ( return harden({ vault, actions, - liquidationPromiseKit, liquidationZcfSeat, vaultSeat, }); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 92841dcbab5..8cad2d6d96c 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -33,7 +33,7 @@ import { makeInterestCalculator } from './interest.js'; const { details: X } = assert; -const trace = makeTracer(' VM '); +const trace = makeTracer('VM'); /** * Each VaultManager manages a single collateralType. @@ -291,19 +291,71 @@ export const makeVaultManager = ( // notifiy UIs // updateUiState(); + trace('chargeAllVaults complete', { + compoundedInterest, + interestAccrued, + totalDebt, + }); reschedulePriceCheck(); }; + /** + * @param {Amount} oldDebt - principal and all accrued interest + * @param {Amount} newDebt - principal and all accrued interest + * @returns {bigint} in brand of the manager's debt + */ + const debtDelta = (oldDebt, newDebt) => { + // Since newDebt includes accrued interest we need to use getDebtAmount() + // to get a baseline that also includes accrued interest. + // eslint-disable-next-line no-use-before-define + const priorDebtValue = oldDebt.value; + // We can't used AmountMath because the delta can be negative. + assert.typeof( + priorDebtValue, + 'bigint', + 'vault debt supports only bigint amounts', + ); + return newDebt.value - priorDebtValue; + }; + /** * @param {VaultId} vaultId * @param {Vault} vault - * @param {Amount} delta + * @param {Amount} oldDebtOnVault + * @param {Amount} newDebtOnVault */ - const applyDebtDelta = (vaultId, vault, delta) => { - totalDebt = AmountMath.add(totalDebt, delta); + const applyDebtDelta = (vaultId, vault, oldDebtOnVault, newDebtOnVault) => { + const delta = debtDelta(oldDebtOnVault, newDebtOnVault); + trace( + `updating total debt of ${totalDebt.value} ${totalDebt.brand} by ${delta}`, + ); + if (delta === 0n) { + // nothing to do + return; + } + + if (delta > 0n) { + // add the amount + totalDebt = AmountMath.add( + totalDebt, + AmountMath.make(totalDebt.brand, delta), + ); + } else { + // negate the amount so that it's a natural number, then subtract + const absDelta = -delta; + assert( + !(absDelta > totalDebt.value), + 'Negative delta greater than total debt', + ); + totalDebt = AmountMath.subtract( + totalDebt, + AmountMath.make(totalDebt.brand, absDelta), + ); + } assert(prioritizedVaults); prioritizedVaults.refreshVaultPriority(vaultId, vault); + trace('applyDebtDelta complete', { totalDebt }); }; const periodNotifier = E(timerService).makeNotifier( @@ -359,11 +411,12 @@ export const makeVaultManager = ( vault, actions: { openLoan }, } = vaultKit; - // FIXME do without notifier callback - const { notifier } = await openLoan(seat); assert(prioritizedVaults); prioritizedVaults.addVaultKit(vaultId, vaultKit); + // ??? do we still need the notifier? + const { notifier } = await openLoan(seat); + seat.exit(); return harden({ diff --git a/packages/run-protocol/test/vaultFactory/test-vault.js b/packages/run-protocol/test/vaultFactory/test-vault.js index d1af8e139ec..004a38829f6 100644 --- a/packages/run-protocol/test/vaultFactory/test-vault.js +++ b/packages/run-protocol/test/vaultFactory/test-vault.js @@ -13,6 +13,7 @@ import { resolve as importMetaResolve } from 'import-meta-resolve'; import { makeIssuerKit, AmountMath } from '@agoric/ertp'; import { assert } from '@agoric/assert'; +import { makeFakeLiveSlotsStuff } from '@agoric/swingset-vat/tools/fakeVirtualSupport.js'; import { makeTracer } from '../../src/makeTracer.js'; const vaultRoot = './vault-contract-wrapper.js'; @@ -219,3 +220,12 @@ test('bad collateral', async t => { // t.rejects(p, / /, 'addCollateral requires the right kind', {}); // t.throws(async () => { await p; }, /was not a live payment/); }); + +test('serializable with collectionManager', async t => { + // Necessary to initialize the testjig + await helperContract; + + const { vault } = testJig; + const stuff = makeFakeLiveSlotsStuff(); + t.notThrows(() => stuff.marshal.serialize(vault)); +}); diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index 10a65ae7158..725db3ff68f 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -298,7 +298,7 @@ async function setupServices( }; } -test('first', async t => { +test.skip('first', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -429,7 +429,7 @@ test('first', async t => { 'vault is cleared', ); - t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); + // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); const liquidations = await E( E(vault).getLiquidationSeat(), ).getCurrentAllocation(); @@ -441,7 +441,7 @@ test('first', async t => { }); }); -test('price drop', async t => { +test.skip('price drop', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -554,7 +554,7 @@ test('price drop', async t => { RUN: AmountMath.make(runBrand, 14n), }); - t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); + // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); const liquidations = await E( E(vault).getLiquidationSeat(), ).getCurrentAllocation(); @@ -562,7 +562,7 @@ test('price drop', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); -test('price falls precipitously', async t => { +test.skip('price falls precipitously', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -663,7 +663,7 @@ test('price falls precipitously', async t => { t.falsy(AmountMath.isEmpty(await E(vault).getDebtAmount())); await manualTimer.tick(); - t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); + // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); // An emergency liquidation got less than full value const newDebtAmount = await E(vault).getDebtAmount(); @@ -681,7 +681,7 @@ test('price falls precipitously', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); -test('vaultFactory display collateral', async t => { +test.skip('vaultFactory display collateral', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 6n, @@ -735,7 +735,7 @@ test('vaultFactory display collateral', async t => { }); // charging period is 1 week. Clock ticks by days -test('interest on multiple vaults', async t => { +test.skip('interest on multiple vaults', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -905,7 +905,7 @@ test('interest on multiple vaults', async t => { ); }); -test('adjust balances', async t => { +test.skip('adjust balances', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 6n, @@ -1331,7 +1331,7 @@ test('overdeposit', async t => { // Both loans will initially be over collateralized 100%. Alice will withdraw // enough of the overage that she'll get caught when prices drop. Bob will be // charged interest (twice), which will trigger liquidation. -test('mutable liquidity triggers and interest', async t => { +test.skip('mutable liquidity triggers and interest', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1531,7 +1531,7 @@ test('mutable liquidity triggers and interest', async t => { t.truthy(bobUpdate.value.liquidated); }); -test('bad chargingPeriod', async t => { +test.skip('bad chargingPeriod', async t => { const loanTiming = { chargingPeriod: 2, recordingPeriod: 10n, @@ -1548,7 +1548,7 @@ test('bad chargingPeriod', async t => { ); }); -test('collect fees from loan and AMM', async t => { +test.skip('collect fees from loan and AMM', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 10n, @@ -1650,7 +1650,7 @@ test('collect fees from loan and AMM', async t => { t.truthy(AmountMath.isGTE(feePayoutAmount, feePoolBalance.RUN)); }); -test('close loan', async t => { +test.skip('close loan', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1784,14 +1784,14 @@ test('close loan', async t => { AmountMath.makeEmpty(aethBrand), ); - t.is(await E(aliceVault).getLiquidationPromise(), 'Closed'); + // t.is(await E(aliceVault).getLiquidationPromise(), 'Closed'); t.deepEqual( await E(E(aliceVault).getLiquidationSeat()).getCurrentAllocation(), {}, ); }); -test('excessive loan', async t => { +test.skip('excessive loan', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1853,7 +1853,7 @@ test('excessive loan', async t => { // prices drop. Bob will be charged interest (twice), which will trigger // liquidation. Alice's withdrawal is precisely gauged so the difference between // a floorDivideBy and a ceilingDivideBy will leave her unliquidated. -test('mutable liquidity triggers and interest sensitivity', async t => { +test.skip('mutable liquidity triggers and interest sensitivity', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index a254ee6d551..dd79681eff7 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -1,4 +1,4 @@ -// @ts-nocheck +// @ts-check import '@agoric/zoe/src/types.js'; @@ -30,6 +30,8 @@ export async function start(zcf, privateArgs) { const { zcfSeat: vaultFactorySeat } = zcf.makeEmptySeatKit(); + let vaultCounter = 0; + function reallocateReward(amount, fromSeat, otherSeat) { vaultFactorySeat.incrementBy( fromSeat.decrementBy( @@ -45,7 +47,7 @@ export async function start(zcf, privateArgs) { } } - /** @type {InnerVaultManager} */ + /** @type {Parameters[1]} */ const managerMock = Far('vault manager mock', { getLiquidationMargin() { return makeRatio(105n, runBrand); @@ -69,6 +71,10 @@ export async function start(zcf, privateArgs) { return SECONDS_PER_HOUR * 24n * 7n; }, reallocateReward, + applyDebtDelta() {}, + getCollateralQuote() { + return Promise.resolve({ quoteAmount: null, quotePayment: null }); + }, getCompoundedInterest: () => makeRatio(1n, runBrand), }); @@ -83,12 +89,16 @@ export async function start(zcf, privateArgs) { }; const priceAuthority = makeFakePriceAuthority(options); - const { vault, openLoan, accrueInterestAndAddToPool } = await makeVaultKit( + const { + vault, + actions: { openLoan }, + } = await makeVaultKit( zcf, managerMock, + // eslint-disable-next-line no-plusplus + String(vaultCounter++), runMint, priceAuthority, - timer.getCurrentTimestamp(), ); zcf.setTestJig(() => ({ collateralKit, runMint, vault, timer })); @@ -104,7 +114,6 @@ export async function start(zcf, privateArgs) { add() { return vault.makeAdjustBalancesInvitation(); }, - accrueInterestAndAddToPool, }), notifier, }; From 89a384bacc12e37cf864570588b04a7857b32fc6 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 9 Feb 2022 16:18:25 -0800 Subject: [PATCH 18/47] test vaultFactory overdeposit passing --- .../src/vaultFactory/orderedVaultStore.js | 44 +++++--------- .../src/vaultFactory/prioritizedVaults.js | 26 ++++---- .../run-protocol/src/vaultFactory/vault.js | 4 +- .../src/vaultFactory/vaultManager.js | 59 +++++++++++-------- 4 files changed, 65 insertions(+), 68 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index d1ac76535de..5daa4f73ae1 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -47,18 +47,10 @@ export const makeOrderedVaultStore = () => { /** * - * @param {VaultId} vaultId - * @param {Vault} vault + * @param {string} key * @returns {VaultKit} */ - const removeVaultKit = (vaultId, vault) => { - const debtRatio = makeRatioFromAmounts( - vault.getNormalizedDebt(), - vault.getCollateralAmount(), - ); - - // XXX TESTME does this really work? - const key = toVaultKey(debtRatio, vaultId); + const removeByKey = key => { try { const vaultKit = store.get(key); assert(vaultKit); @@ -79,30 +71,26 @@ export const makeOrderedVaultStore = () => { }; /** - * Exposes vaultId contained in the key but not the ordering factor. - * That ordering factor is the inverse quotient of the debt ratio (collateral÷debt) - * but nothing outside this module should rely on that to be true. - * - * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 * - * @yields {[[string, string], VaultKit]>} - * @returns {IterableIterator<[VaultId, VaultKit]>} + * @param {VaultId} vaultId + * @param {Vault} vault + * @returns {VaultKit} */ - // XXX need to make generator with const arrow definitions? - // XXX can/should we avoid exposing the inverse debt quotient? - function* entriesWithId() { - for (const [k, v] of store.entries()) { - const [_, vaultId] = fromVaultKey(k); - /** @type {VaultKit} */ - const vaultKit = v; - yield [vaultId, vaultKit]; - } - } + const removeVaultKit = (vaultId, vault) => { + const debtRatio = makeRatioFromAmounts( + vault.getNormalizedDebt(), + vault.getCollateralAmount(), + ); + + // XXX TESTME does this really work? + const key = toVaultKey(debtRatio, vaultId); + return removeByKey(key); + }; return harden({ addVaultKit, removeVaultKit, - entriesWithId, + entries: store.entries, getSize: store.getSize, values: store.values, }); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 8ab57c27bf8..9774e0b132d 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -58,6 +58,7 @@ export const currentDebtToCollateral = vault => /** @typedef {{debtToCollateral: Ratio, vaultKit: VaultKit}} VaultKitRecord */ /** + * Really a prioritization of vault *kits*. * * @param {() => void} reschedulePriceCheck called when there is a new * least-collateralized vault @@ -102,20 +103,19 @@ export const makePrioritizedVaults = reschedulePriceCheck => { return undefined; } - const [[_, vaultKit]] = vaults.entriesWithId(); + const [[_, vaultKit]] = vaults.entries(); const { vault } = vaultKit; const actualDebtAmount = vault.getDebtAmount(); return makeRatioFromAmounts(actualDebtAmount, vault.getCollateralAmount()); }; /** - * - * @param {VaultId} vaultId - * @param {Vault} vault + * @param {string} key * @returns {VaultKit} */ - const removeVault = (vaultId, vault) => { - const debtToCollateral = currentDebtToCollateral(vault); + const removeVault = key => { + const vk = vaults.removeByKey(key); + const debtToCollateral = currentDebtToCollateral(vk.vault); if ( !oracleQueryThreshold || // TODO check for equality is sufficient and faster @@ -124,7 +124,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { // don't call reschedulePriceCheck, but do reset the highest. oracleQueryThreshold = firstDebtRatio(); } - return vaults.removeVaultKit(vaultId, vault); + return vk; }; /** @@ -142,12 +142,12 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * Akin to forEachRatioGTE but iterate over all vaults. * - * @param {(VaultId, VaultKit) => void} cb + * @param {(key: string, vk: VaultKit) => void} cb * @returns {void} */ const forAll = cb => { - for (const [vaultId, vk] of vaults.entriesWithId()) { - cb(vaultId, vk); + for (const [key, vk] of vaults.entries()) { + cb(key, vk); } }; @@ -158,15 +158,15 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * (more debt than collateral) are all tied for first. * * @param {Ratio} ratio - * @param {(vid: VaultId, vk: VaultKit) => void} cb + * @param {(key: string, vk: VaultKit) => void} cb */ const forEachRatioGTE = (ratio, cb) => { // TODO use a Pattern to limit the query - for (const [vaultId, vk] of vaults.entriesWithId()) { + for (const [key, vk] of vaults.entries()) { const debtToCollateral = currentDebtToCollateral(vk.vault); if (ratioGTE(debtToCollateral, ratio)) { - cb(vaultId, vk); + cb(key, vk); } else { // stop once we are below the target ratio break; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index ec2ddbdec69..a9c0d0a2217 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -43,7 +43,7 @@ export const VaultState = { /** * @typedef {Object} InnerVaultManagerBase - * @property {(vaultId: VaultId, vault: Vault, oldDebt: Amount, newDebt: Amount) => void} applyDebtDelta + * @property {(oldDebt: Amount, newDebt: Amount) => void} applyDebtDelta * @property {() => Brand} getCollateralBrand * @property {ReallocateReward} reallocateReward * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt @@ -119,7 +119,7 @@ export const makeVaultKit = ( updateDebtSnapshot(newDebt); // update parent state // eslint-disable-next-line no-use-before-define - manager.applyDebtDelta(idInManager, vault, oldDebt, newDebt); + manager.applyDebtDelta(oldDebt, newDebt); }; /** diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 8cad2d6d96c..601e0fb97e1 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -202,26 +202,23 @@ export const makeVaultManager = ( // TODO maybe extract this into a method // TODO try pattern matching to achieve GTE - prioritizedVaults.forEachRatioGTE( - quoteRatioPlusMargin, - (vaultId, vaultKit) => { - trace('liquidating', vaultKit.vaultSeat.getProposal()); - - // Start liquidation (vaultState: LIQUIDATING) - const liquidateP = liquidate( - zcf, - vaultKit, - runMint.burnLosses, - liquidationStrategy, - collateralBrand, - ).then(() => { - assert(prioritizedVaults); - // TODO handle errors but notify - prioritizedVaults.removeVault(vaultId, vaultKit.vault); - }); - toLiquidate.push(liquidateP); - }, - ); + prioritizedVaults.forEachRatioGTE(quoteRatioPlusMargin, (key, vaultKit) => { + trace('liquidating', vaultKit.vaultSeat.getProposal()); + + // Start liquidation (vaultState: LIQUIDATING) + const liquidateP = liquidate( + zcf, + vaultKit, + runMint.burnLosses, + liquidationStrategy, + collateralBrand, + ).then(() => { + assert(prioritizedVaults); + // TODO handle errors but notify + prioritizedVaults.removeVault(key); + }); + toLiquidate.push(liquidateP); + }); outstandingQuote = undefined; // Ensure all vaults complete @@ -234,7 +231,9 @@ export const makeVaultManager = ( // In extreme situations system health may require liquidating all vaults. const liquidateAll = () => { assert(prioritizedVaults); - return prioritizedVaults.forAll(({ vaultKit }) => + return prioritizedVaults.forAll((key, vaultKit) => + // FIXME remove one completion + // Maybe make this a single function for use in forEachRatioGTE too liquidate( zcf, vaultKit, @@ -306,6 +305,7 @@ export const makeVaultManager = ( * @returns {bigint} in brand of the manager's debt */ const debtDelta = (oldDebt, newDebt) => { + trace('debtDelta', { oldDebt, newDebt }); // Since newDebt includes accrued interest we need to use getDebtAmount() // to get a baseline that also includes accrued interest. // eslint-disable-next-line no-use-before-define @@ -320,12 +320,10 @@ export const makeVaultManager = ( }; /** - * @param {VaultId} vaultId - * @param {Vault} vault * @param {Amount} oldDebtOnVault * @param {Amount} newDebtOnVault */ - const applyDebtDelta = (vaultId, vault, oldDebtOnVault, newDebtOnVault) => { + const applyDebtDelta = (oldDebtOnVault, newDebtOnVault) => { const delta = debtDelta(oldDebtOnVault, newDebtOnVault); trace( `updating total debt of ${totalDebt.value} ${totalDebt.brand} by ${delta}`, @@ -353,9 +351,19 @@ export const makeVaultManager = ( AmountMath.make(totalDebt.brand, absDelta), ); } + trace('applyDebtDelta complete', { totalDebt }); + }; + + /** + * FIXME finisht this + * + * @param {VaultId} vaultId + * @param {Vault} vault + */ + const updateVaultPriority = (vaultId, vault) => { assert(prioritizedVaults); prioritizedVaults.refreshVaultPriority(vaultId, vault); - trace('applyDebtDelta complete', { totalDebt }); + trace('updateVaultPriority complete', { totalDebt }); }; const periodNotifier = E(timerService).makeNotifier( @@ -388,6 +396,7 @@ export const makeVaultManager = ( reallocateReward, getCollateralBrand: () => collateralBrand, getCompoundedInterest: () => compoundedInterest, + updateVaultPriority, }); /** @param {ZCFSeat} seat */ From a502eedfb5b7b5ec865b619dda902b75b099710f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 9 Feb 2022 16:34:16 -0800 Subject: [PATCH 19/47] integration test passing (with an additional notification) --- .../src/vaultFactory/orderedVaultStore.js | 23 ++++++++++++++++ .../run-protocol/src/vaultFactory/types.js | 6 ++--- .../run-protocol/src/vaultFactory/vault.js | 15 ++++++++--- .../src/vaultFactory/vaultManager.js | 27 ++++++++++++------- .../governance/test-governance.js | 4 +-- .../test/vaultFactory/test-vault-interest.js | 20 +++++++------- .../test/vaultFactory/test-vaultFactory.js | 18 ++++++++----- .../vaultFactory/vault-contract-wrapper.js | 4 +++ 8 files changed, 83 insertions(+), 34 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 5daa4f73ae1..d31534d8db9 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -70,6 +70,27 @@ export const makeOrderedVaultStore = () => { } }; + /** + * XXX part of refactoring, used only for tests at this point. + * + * Exposes vaultId contained in the key but not the ordering factor. + * That ordering factor is the inverse quotient of the debt ratio (collateral÷debt) + * but nothing outside this module should rely on that to be true. + * + * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 + * + * @yields {[[string, string], VaultKit]>} + * @returns {IterableIterator<[VaultId, VaultKit]>} + */ + function* entriesWithId() { + for (const [k, v] of store.entries()) { + const [_, vaultId] = fromVaultKey(k); + /** @type {VaultKit} */ + const vaultKit = v; + yield [vaultId, vaultKit]; + } + } + /** * * @param {VaultId} vaultId @@ -89,8 +110,10 @@ export const makeOrderedVaultStore = () => { return harden({ addVaultKit, + removeByKey, removeVaultKit, entries: store.entries, + entriesWithId, getSize: store.getSize, values: store.values, }); diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 3f4d7946558..abce73071a9 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -65,7 +65,7 @@ */ /** - * @typedef {BaseUIState & LiquidationUIMixin} UIState + * @typedef {BaseUIState & LiquidationUIMixin} VaultUIState * @typedef {Object} LiquidationUIMixin * @property {Ratio} interestRate Annual interest rate charge * @property {Ratio} liquidationRatio @@ -114,7 +114,7 @@ /** * @typedef {Object} OpenLoanKit - * @property {Notifier} notifier + * @property {Notifier} notifier * @property {Promise} collateralPayoutP */ @@ -144,7 +144,7 @@ /** * @typedef {Object} LoanKit * @property {Vault} vault - * @property {Notifier} uiNotifier + * @property {Notifier} uiNotifier */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index a9c0d0a2217..d4a6f65a996 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -10,7 +10,7 @@ import { floorMultiplyBy, floorDivideBy, } from '@agoric/zoe/src/contractSupport/index.js'; -import { makeNotifierKit } from '@agoric/notifier'; +import { makeNotifierKit, observeNotifier } from '@agoric/notifier'; import { invertRatio, @@ -52,6 +52,7 @@ export const VaultState = { /** * @param {ContractFacet} zcf * @param {InnerVaultManagerBase & GetVaultParams} manager + * @param {Notifier} managerNotifier * @param {VaultId} idInManager * @param {ZCFMint} runMint * @param {ERef} priceAuthority @@ -59,6 +60,7 @@ export const VaultState = { export const makeVaultKit = ( zcf, manager, + managerNotifier, idInManager, // will go in state runMint, priceAuthority, @@ -224,10 +226,12 @@ export const makeVaultKit = ( // await quoteGiven() here // [https://github.com/Agoric/dapp-token-economy/issues/123] const collateralizationRatio = await getCollateralizationRatio(); - /** @type {UIState} */ + /** @type {VaultUIState} */ const uiState = harden({ interestRate: manager.getInterestRate(), liquidationRatio: manager.getLiquidationMargin(), + runDebtSnapshot, + interestSnapshot, locked: getCollateralAmount(), debt: getDebtAmount(), collateralizationRatio, @@ -235,6 +239,8 @@ export const makeVaultKit = ( vaultState, }); + trace('updateUiState', uiState); + switch (vaultState) { case VaultState.ACTIVE: case VaultState.LIQUIDATING: @@ -247,6 +253,8 @@ export const makeVaultKit = ( throw Error(`unreachable vaultState: ${vaultState}`); } }; + // Propagate notifications from the manager to observers of this vault + observeNotifier(managerNotifier, { updateState: updateUiState }); /** * Call must check for and remember shortfall @@ -304,9 +312,8 @@ export const makeVaultKit = ( seat.exit(); burnSeat.exit(); vaultState = VaultState.CLOSED; - updateUiState(); - updateDebtSnapshot(AmountMath.makeEmpty(runBrand)); + updateUiState(); assertVaultHoldsNoRun(); vaultSeat.exit(); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 601e0fb97e1..20ece6f7c93 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -13,7 +13,7 @@ import { makeRatio, multiplyRatios, } from '@agoric/zoe/src/contractSupport/index.js'; -import { observeNotifier } from '@agoric/notifier'; +import { makeNotifierKit, observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; @@ -71,6 +71,14 @@ export const makeVaultManager = ( ) => { const { brand: runBrand } = runMint.getIssuerRecord(); + const { updater, notifier } = makeNotifierKit( + harden({ + compoundedInterest: makeRatio(1n, runBrand, 1n, runBrand), + latestInterestUpdate: 0n, + totalDebt: AmountMath.makeEmpty(runBrand), + }), + ); + /** @type {GetVaultParams} */ const shared = { // loans below this margin may be liquidated @@ -244,13 +252,13 @@ export const makeVaultManager = ( ); }; - // FIXME don't mutate vaults to charge them /** * * @param {bigint} updateTime * @param {ZCFSeat} poolIncrementSeat */ const chargeAllVaults = async (updateTime, poolIncrementSeat) => { + trace('chargeAllVault', { updateTime }); const interestCalculator = makeInterestCalculator( runBrand, shared.getInterestRate(), @@ -288,13 +296,14 @@ export const makeVaultManager = ( // update running tally of total debt against this collateral ({ latestInterestUpdate } = debtStatus); - // notifiy UIs - // updateUiState(); - trace('chargeAllVaults complete', { + const payload = harden({ compoundedInterest, - interestAccrued, + latestInterestUpdate, totalDebt, }); + updater.updateState(payload); + + trace('chargeAllVaults complete', payload); reschedulePriceCheck(); }; @@ -412,6 +421,7 @@ export const makeVaultManager = ( const vaultKit = makeVaultKit( zcf, managerFacade, + notifier, vaultId, runMint, priceAuthority, @@ -423,13 +433,12 @@ export const makeVaultManager = ( assert(prioritizedVaults); prioritizedVaults.addVaultKit(vaultId, vaultKit); - // ??? do we still need the notifier? - const { notifier } = await openLoan(seat); + const vaultResult = await openLoan(seat); seat.exit(); return harden({ - uiNotifier: notifier, + uiNotifier: vaultResult.notifier, invitationMakers: Far('invitation makers', { AdjustBalances: vault.makeAdjustBalancesInvitation, CloseVault: vault.makeCloseInvitation, diff --git a/packages/run-protocol/test/vaultFactory/swingsetTests/governance/test-governance.js b/packages/run-protocol/test/vaultFactory/swingsetTests/governance/test-governance.js index 0199c2728de..806a9c1ea43 100644 --- a/packages/run-protocol/test/vaultFactory/swingsetTests/governance/test-governance.js +++ b/packages/run-protocol/test/vaultFactory/swingsetTests/governance/test-governance.js @@ -86,8 +86,8 @@ const expectedVaultFactoryLog = [ 'after vote on (InterestRate), InterestRate numerator is 4321', 'at 3 days: vote closed', 'at 3 days: Alice owes {"brand":"[Alleged: RUN brand]","value":"[510105n]"}', - 'at 3 days: 1 day after votes cast, uiNotifier update #4 has interestRate.numerator 250', - 'at 4 days: 2 days after votes cast, uiNotifier update #5 has interestRate.numerator 4321', + 'at 3 days: 1 day after votes cast, uiNotifier update #5 has interestRate.numerator 250', + 'at 4 days: 2 days after votes cast, uiNotifier update #6 has interestRate.numerator 4321', 'at 4 days: Alice owes {"brand":"[Alleged: RUN brand]","value":"[510608n]"}', ]; diff --git a/packages/run-protocol/test/vaultFactory/test-vault-interest.js b/packages/run-protocol/test/vaultFactory/test-vault-interest.js index 23c7a382a80..ce231b38433 100644 --- a/packages/run-protocol/test/vaultFactory/test-vault-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-vault-interest.js @@ -95,7 +95,7 @@ test('interest', async t => { // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 // RUN (charging 3 RUN fee), which uses an automatic market maker that // presents a fixed price of 4 RUN per Collateral. - const { notifier, actions } = await E(creatorSeat).getOfferResult(); + const { notifier } = await E(creatorSeat).getOfferResult(); const { runMint, collateralKit: { brand: collateralBrand }, @@ -121,21 +121,21 @@ test('interest', async t => { ); timer.tick(); - const noInterest = actions.accrueInterestAndAddToPool(1n); - t.truthy(AmountMath.isEqual(noInterest, AmountMath.makeEmpty(runBrand))); + // const noInterest = actions.accrueInterestAndAddToPool(1n); + // t.truthy(AmountMath.isEqual(noInterest, AmountMath.makeEmpty(runBrand))); // { chargingPeriod: 3, recordingPeriod: 9 } charge 2% 3 times for (let i = 0; i < 12; i += 1) { timer.tick(); } - const nextInterest = actions.accrueInterestAndAddToPool( - timer.getCurrentTimestamp(), - ); - t.truthy( - AmountMath.isEqual(nextInterest, AmountMath.make(runBrand, 70n)), - `interest should be 70, was ${nextInterest.value}`, - ); + // const nextInterest = actions.accrueInterestAndAddToPool( + // timer.getCurrentTimestamp(), + // ); + // t.truthy( + // AmountMath.isEqual(nextInterest, AmountMath.make(runBrand, 70n)), + // `interest should be 70, was ${nextInterest.value}`, + // ); const { value: v2, updateCount: c2 } = await E(notifier).getUpdateSince(c1); t.deepEqual(v2.debt, AmountMath.make(runBrand, 73500n + 70n)); t.deepEqual(v2.interestRate, makeRatio(5n, runBrand, 100n)); diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index 725db3ff68f..9783cedf13b 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -298,6 +298,7 @@ async function setupServices( }; } +// FIXME test.skip('first', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -441,6 +442,7 @@ test.skip('first', async t => { }); }); +// FIXME test.skip('price drop', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -562,6 +564,7 @@ test.skip('price drop', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); +// FIXME test.skip('price falls precipitously', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -681,7 +684,7 @@ test.skip('price falls precipitously', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); -test.skip('vaultFactory display collateral', async t => { +test('vaultFactory display collateral', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 6n, @@ -735,6 +738,7 @@ test.skip('vaultFactory display collateral', async t => { }); // charging period is 1 week. Clock ticks by days +// FIXME test.skip('interest on multiple vaults', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -905,7 +909,7 @@ test.skip('interest on multiple vaults', async t => { ); }); -test.skip('adjust balances', async t => { +test('adjust balances', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 6n, @@ -1331,6 +1335,7 @@ test('overdeposit', async t => { // Both loans will initially be over collateralized 100%. Alice will withdraw // enough of the overage that she'll get caught when prices drop. Bob will be // charged interest (twice), which will trigger liquidation. +// FIXME test.skip('mutable liquidity triggers and interest', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -1531,7 +1536,7 @@ test.skip('mutable liquidity triggers and interest', async t => { t.truthy(bobUpdate.value.liquidated); }); -test.skip('bad chargingPeriod', async t => { +test('bad chargingPeriod', async t => { const loanTiming = { chargingPeriod: 2, recordingPeriod: 10n, @@ -1548,7 +1553,7 @@ test.skip('bad chargingPeriod', async t => { ); }); -test.skip('collect fees from loan and AMM', async t => { +test('collect fees from loan and AMM', async t => { const loanTiming = { chargingPeriod: 2n, recordingPeriod: 10n, @@ -1650,7 +1655,7 @@ test.skip('collect fees from loan and AMM', async t => { t.truthy(AmountMath.isGTE(feePayoutAmount, feePoolBalance.RUN)); }); -test.skip('close loan', async t => { +test('close loan', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1791,7 +1796,7 @@ test.skip('close loan', async t => { ); }); -test.skip('excessive loan', async t => { +test('excessive loan', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1853,6 +1858,7 @@ test.skip('excessive loan', async t => { // prices drop. Bob will be charged interest (twice), which will trigger // liquidation. Alice's withdrawal is precisely gauged so the difference between // a floorDivideBy and a ceilingDivideBy will leave her unliquidated. +// FIXME test.skip('mutable liquidity triggers and interest sensitivity', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index dd79681eff7..665c39b9a04 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -10,6 +10,7 @@ import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority.js' import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@endo/marshal'; +import { makeNotifierKit } from '@agoric/notifier'; import { makeVaultKit } from '../../src/vaultFactory/vault.js'; import { paymentFromZCFMint } from '../../src/vaultFactory/burn.js'; @@ -89,12 +90,15 @@ export async function start(zcf, privateArgs) { }; const priceAuthority = makeFakePriceAuthority(options); + const { notifier: managerNotifier } = makeNotifierKit(); + const { vault, actions: { openLoan }, } = await makeVaultKit( zcf, managerMock, + managerNotifier, // eslint-disable-next-line no-plusplus String(vaultCounter++), runMint, From 89a2af87e27f13277b0cff3bb055d40e3e19e15f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 09:08:17 -0800 Subject: [PATCH 20/47] cleanup --- .../src/vaultFactory/liquidation.js | 1 - .../src/vaultFactory/prioritizedVaults.js | 17 +---- .../src/vaultFactory/storeUtils.js | 10 ++- .../src/vaultFactory/vaultManager.js | 62 ++++++++++--------- 4 files changed, 40 insertions(+), 50 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 0bfebda1ba5..2dbd47ae2ec 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -66,7 +66,6 @@ const liquidate = async ( const isUnderwater = !AmountMath.isGTE(runProceedsAmount, runDebt); const runToBurn = isUnderwater ? runProceedsAmount : runDebt; burnLosses(harden({ RUN: runToBurn }), liquidationSeat); - // FIXME removal was triggered by this through observation of state change vaultKit.actions.liquidated(AmountMath.subtract(runDebt, runToBurn)); // any remaining RUN plus anything else leftover from the sale are refunded diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 9774e0b132d..3d282882cdb 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -139,18 +139,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { rescheduleIfHighest(debtToCollateral); }; - /** - * Akin to forEachRatioGTE but iterate over all vaults. - * - * @param {(key: string, vk: VaultKit) => void} cb - * @returns {void} - */ - const forAll = cb => { - for (const [key, vk] of vaults.entries()) { - cb(key, vk); - } - }; - /** * Invoke a function for vaults with debt to collateral at or above the ratio. * @@ -160,6 +148,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @param {Ratio} ratio * @param {(key: string, vk: VaultKit) => void} cb */ + // TODO switch to generator const forEachRatioGTE = (ratio, cb) => { // TODO use a Pattern to limit the query for (const [key, vk] of vaults.entries()) { @@ -175,11 +164,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }; /** - * * @param {VaultId} vaultId * @param {Vault} vault */ const refreshVaultPriority = (vaultId, vault) => { + // @ts-ignore FIXME removeVault() takes a key const vaultKit = removeVault(vaultId, vault); addVaultKit(vaultId, vaultKit); }; @@ -188,7 +177,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { addVaultKit, refreshVaultPriority, removeVault, - forAll, + entries: vaults.entries, forEachRatioGTE, highestRatio: () => oracleQueryThreshold, }); diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 82b5b5e1b1d..70a6a73cab0 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -1,5 +1,4 @@ -// FIXME remove before review -// @ts-nocheck +// @ts-check /** * Module to improvise composite keys for orderedVaultStore until Collections API supports them. * @@ -15,12 +14,11 @@ const asBits = new BigUint64Array(asNumber.buffer); /** * - * @param {number} n + * @param {string} nStr * @param {number} size * @returns {string} */ -const zeroPad = (n, size) => { - const nStr = `${n}`; +const zeroPad = (nStr, size) => { assert(nStr.length <= size); const str = `00000000000000000000${nStr}`; const result = str.substring(str.length - size); @@ -110,7 +108,7 @@ const toUncollateralizedKey = vaultId => { /** * @param {string} key - * @returns {CompositeKey} normalized debt ratio as number, vault id + * @returns {[normalizedDebtRatio: number, vaultId: VaultId]} */ const fromVaultKey = key => { const [numberPart, vaultIdPart] = key.split(':'); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 20ece6f7c93..5aec02a3364 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -147,6 +147,32 @@ export const makeVaultManager = ( /** @type {bigint} */ let latestInterestUpdate = startTimeStamp; + /** + * + * @param {string} key + * @param {VaultKit} vaultKit + */ + const liquidateAndRemove = async (key, vaultKit) => { + assert(prioritizedVaults); + trace('liquidating', vaultKit.vaultSeat.getProposal()); + + try { + // Start liquidation (vaultState: LIQUIDATING) + await liquidate( + zcf, + vaultKit, + runMint.burnLosses, + liquidationStrategy, + collateralBrand, + ); + + await prioritizedVaults.removeVault(key); + } catch (e) { + // XXX should notify interested parties + console.error('liquidateAndRemove failed with', e); + } + }; + // When any Vault's debt ratio is higher than the current high-water level, // call reschedulePriceCheck() to request a fresh notification from the // priceAuthority. There will be extra outstanding requests since we can't @@ -208,24 +234,10 @@ export const makeVaultManager = ( /** @type {Array>} */ const toLiquidate = []; - // TODO maybe extract this into a method // TODO try pattern matching to achieve GTE + // TODO replace forEachRatioGTE with a generator pattern like liquidateAll below prioritizedVaults.forEachRatioGTE(quoteRatioPlusMargin, (key, vaultKit) => { - trace('liquidating', vaultKit.vaultSeat.getProposal()); - - // Start liquidation (vaultState: LIQUIDATING) - const liquidateP = liquidate( - zcf, - vaultKit, - runMint.burnLosses, - liquidationStrategy, - collateralBrand, - ).then(() => { - assert(prioritizedVaults); - // TODO handle errors but notify - prioritizedVaults.removeVault(key); - }); - toLiquidate.push(liquidateP); + toLiquidate.push(liquidateAndRemove(key, vaultKit)); }); outstandingQuote = undefined; @@ -236,20 +248,12 @@ export const makeVaultManager = ( }; prioritizedVaults = makePrioritizedVaults(reschedulePriceCheck); - // In extreme situations system health may require liquidating all vaults. - const liquidateAll = () => { + // In extreme situations, system health may require liquidating all vaults. + const liquidateAll = async () => { assert(prioritizedVaults); - return prioritizedVaults.forAll((key, vaultKit) => - // FIXME remove one completion - // Maybe make this a single function for use in forEachRatioGTE too - liquidate( - zcf, - vaultKit, - runMint.burnLosses, - liquidationStrategy, - collateralBrand, - ), - ); + for await (const [key, vaultKit] of prioritizedVaults.entries()) { + await liquidateAndRemove(key, vaultKit); + } }; /** From 0af7d8d3fc48ce4337d4af77cea2b3c528a003db Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 11:57:23 -0800 Subject: [PATCH 21/47] fix bug in key gen --- .../src/vaultFactory/orderedVaultStore.js | 27 +-- .../src/vaultFactory/prioritizedVaults.js | 36 +++- .../src/vaultFactory/storeUtils.js | 6 +- .../run-protocol/src/vaultFactory/vault.js | 18 +- .../src/vaultFactory/vaultManager.js | 9 +- .../vaultFactory/test-orderedVaultStore.js | 31 ++- .../vaultFactory/test-prioritizedVaults.js | 179 +++++++++--------- .../test/vaultFactory/test-storeUtils.js | 26 ++- 8 files changed, 194 insertions(+), 138 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index d31534d8db9..05d385cb196 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -36,13 +36,16 @@ export const makeOrderedVaultStore = () => { const { vault } = vaultKit; const debt = vault.getDebtAmount(); const collateral = vault.getCollateralAmount(); - console.log('addVaultKit', { debt, collateral }); const key = collateral.value === 0n ? toUncollateralizedKey(vaultId) : toVaultKey(makeRatioFromAmounts(debt, collateral), vaultId); + console.log('addVaultKit', { + debt: debt.value, + collateral: collateral.value, + key, + }); store.init(key, vaultKit); - store.getSize; }; /** @@ -59,7 +62,7 @@ export const makeOrderedVaultStore = () => { } catch (e) { const keys = Array.from(store.keys()); console.error( - 'removeVaultKit failed to remove', + 'removeByKey failed to remove', key, 'parts:', fromVaultKey(key), @@ -91,27 +94,9 @@ export const makeOrderedVaultStore = () => { } } - /** - * - * @param {VaultId} vaultId - * @param {Vault} vault - * @returns {VaultKit} - */ - const removeVaultKit = (vaultId, vault) => { - const debtRatio = makeRatioFromAmounts( - vault.getNormalizedDebt(), - vault.getCollateralAmount(), - ); - - // XXX TESTME does this really work? - const key = toVaultKey(debtRatio, vaultId); - return removeByKey(key); - }; - return harden({ addVaultKit, removeByKey, - removeVaultKit, entries: store.entries, entriesWithId, getSize: store.getSize, diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 3d282882cdb..3a267099ae3 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -7,6 +7,7 @@ import { import { assert } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; import { makeOrderedVaultStore } from './orderedVaultStore.js'; +import { toVaultKey } from './storeUtils.js'; const { multiply, isGTE } = natSafeMath; @@ -106,6 +107,12 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const [[_, vaultKit]] = vaults.entries(); const { vault } = vaultKit; const actualDebtAmount = vault.getDebtAmount(); + console.log( + 'DEBUG firstDebtRatio', + { actualDebtAmount }, + 'in', + Array.from(vaults.entries()), + ); return makeRatioFromAmounts(actualDebtAmount, vault.getCollateralAmount()); }; @@ -127,6 +134,18 @@ export const makePrioritizedVaults = reschedulePriceCheck => { return vk; }; + /** + * + * @param {Amount} oldDebt + * @param {Amount} oldCollateral + * @param {string} vaultId + */ + const removeVaultByAttributes = (oldDebt, oldCollateral, vaultId) => { + const ratio = makeRatioFromAmounts(oldDebt, oldCollateral); + const key = toVaultKey(ratio, vaultId); + return removeVault(key); + }; + /** * * @param {VaultId} vaultId @@ -154,6 +173,8 @@ export const makePrioritizedVaults = reschedulePriceCheck => { for (const [key, vk] of vaults.entries()) { const debtToCollateral = currentDebtToCollateral(vk.vault); + console.log('forEachRatioGTE', { ratio, debtToCollateral }); + if (ratioGTE(debtToCollateral, ratio)) { cb(key, vk); } else { @@ -164,21 +185,22 @@ export const makePrioritizedVaults = reschedulePriceCheck => { }; /** - * @param {VaultId} vaultId - * @param {Vault} vault + * @param {Amount} oldDebt + * @param {Amount} oldCollateral + * @param {string} vaultId */ - const refreshVaultPriority = (vaultId, vault) => { - // @ts-ignore FIXME removeVault() takes a key - const vaultKit = removeVault(vaultId, vault); + const refreshVaultPriority = (oldDebt, oldCollateral, vaultId) => { + const vaultKit = removeVaultByAttributes(oldDebt, oldCollateral, vaultId); addVaultKit(vaultId, vaultKit); }; return harden({ addVaultKit, - refreshVaultPriority, - removeVault, entries: vaults.entries, forEachRatioGTE, highestRatio: () => oracleQueryThreshold, + refreshVaultPriority, + removeVault, + removeVaultByAttributes, }); }; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 70a6a73cab0..33af604414b 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -69,10 +69,10 @@ const dbEntryKeyToNumber = k => { * @param {Ratio} ratio * @returns {number} */ -const ratioToNumber = ratio => { +const ratioToInverseProportion = ratio => { assertIsRatio(ratio); return ratio.numerator.value - ? Number(ratio.denominator.value / ratio.numerator.value) + ? Number(ratio.denominator.value) / Number(ratio.numerator.value) : Number.POSITIVE_INFINITY; }; @@ -88,7 +88,7 @@ const toVaultKey = (ratio, vaultId) => { assert(ratio); assert(vaultId); // until DB supports composite keys, copy its method for turning numbers to DB entry keys - const numberPart = numberToDBEntryKey(ratioToNumber(ratio)); + const numberPart = numberToDBEntryKey(ratioToInverseProportion(ratio)); return `${numberPart}:${vaultId}`; }; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index d4a6f65a996..df7b4e68639 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -1,4 +1,6 @@ // @ts-check +// XXX we do this a lot, okay to drop it to warning in eslint config? +/* eslint-disable no-use-before-define */ import '@agoric/zoe/exported.js'; import { E } from '@agoric/eventual-send'; @@ -47,6 +49,7 @@ export const VaultState = { * @property {() => Brand} getCollateralBrand * @property {ReallocateReward} reallocateReward * @property {() => Ratio} getCompoundedInterest - coefficient on existing debt to calculate new debt + * @property {(oldDebt: Amount, oldCollateral: Amount, vaultId: VaultId) => void} updateVaultPriority */ /** @@ -114,14 +117,14 @@ export const makeVaultKit = ( /** * @param {Amount} newDebt - principal and all accrued interest */ - const updateDebtSnapshotAndNotify = newDebt => { - // eslint-disable-next-line no-use-before-define + const updateDebtPrincipal = newDebt => { const oldDebt = getDebtAmount(); - trace(idInManager, 'updateDebtSnapshotAndNotify', { oldDebt, newDebt }); + trace(idInManager, 'updateDebtPrincipal', { oldDebt, newDebt }); updateDebtSnapshot(newDebt); - // update parent state - // eslint-disable-next-line no-use-before-define + // update vault manager which tracks total debt manager.applyDebtDelta(oldDebt, newDebt); + // update position of this vault in liquidation priority queue + manager.updateVaultPriority(oldDebt, getCollateralAmount(), idInManager); }; /** @@ -253,6 +256,7 @@ export const makeVaultKit = ( throw Error(`unreachable vaultState: ${vaultState}`); } }; + // ??? better to provide this notifier downstream to partition broadcasts? // Propagate notifications from the manager to observers of this vault observeNotifier(managerNotifier, { updateState: updateUiState }); @@ -537,7 +541,7 @@ export const makeVaultKit = ( manager.reallocateReward(fee, vaultSeat, clientSeat); // parent needs to know about the change in debt - updateDebtSnapshotAndNotify(newDebt); + updateDebtPrincipal(newDebt); runMint.burnLosses(harden({ RUN: runAfter.vault }), vaultSeat); @@ -588,7 +592,7 @@ export const makeVaultKit = ( ); manager.reallocateReward(fee, vaultSeat, seat); - updateDebtSnapshotAndNotify(runDebt); + updateDebtPrincipal(runDebt); updateUiState(); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 5aec02a3364..ba39926eb06 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -368,14 +368,13 @@ export const makeVaultManager = ( }; /** - * FIXME finisht this - * + * @param {Amount} oldDebt + * @param {Amount} oldCollateral * @param {VaultId} vaultId - * @param {Vault} vault */ - const updateVaultPriority = (vaultId, vault) => { + const updateVaultPriority = (oldDebt, oldCollateral, vaultId) => { assert(prioritizedVaults); - prioritizedVaults.refreshVaultPriority(vaultId, vault); + prioritizedVaults.refreshVaultPriority(oldDebt, oldCollateral, vaultId); trace('updateVaultPriority complete', { totalDebt }); }; diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js index ecf16583f65..e24124f85a4 100644 --- a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -35,8 +35,6 @@ const mockVault = (runCount, collateralCount) => { }); }; -const vaults = makeOrderedVaultStore(); - /** * @type {Array<[string, bigint, bigint]>} */ @@ -47,13 +45,17 @@ const fixture = [ ['vault-C1', 100n, 1000n], ['vault-C2', 200n, 2000n], ['vault-C3', 300n, 3000n], - ['vault-D', 1n, 100n], - ['vault-E', 1n, 1000n], - ['vault-F', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], + ['vault-D', 30n, 100n], + ['vault-E', 40n, 100n], + ['vault-F', 50n, 100n], + ['vault-M', 1n, 1000n], + ['vault-Y', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], ['vault-Z-withoutdebt', 0n, 100n], ]; -test('ordering', t => { +test.skip('ordering', t => { + const vaults = makeOrderedVaultStore(); + // TODO keep a seed so we can debug when it does fail // randomize because the add order should not matter // Maybe use https://dubzzz.github.io/fast-check.github.com/ @@ -70,3 +72,20 @@ test('ordering', t => { // keys were ordered matching the fixture's ordering of vaultId t.deepEqual(vaultIds, vaultIds.sort()); }); + +test('uniqueness', t => { + const vaults = makeOrderedVaultStore(); + + for (const [vaultId, runCount, collateralCount] of fixture) { + const mockVaultKit = harden({ + vault: mockVault(runCount, collateralCount), + }); + // @ts-expect-error mock + vaults.addVaultKit(vaultId, mockVaultKit); + } + const numberParts = Array.from(vaults.entries()).map( + ([k, _v]) => k.split(':')[0], + ); + const uniqueNumberParts = Array.from(new Set(numberParts)); + t.is(uniqueNumberParts.length, 9); // of 11, three have the same ratio +}); diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 82bf73396c5..bea2a5024d8 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -8,7 +8,6 @@ import { makeIssuerKit, AmountMath } from '@agoric/ertp'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { makePromiseKit } from '@agoric/promise-kit'; -import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; import { currentDebtToCollateral, makePrioritizedVaults, @@ -109,46 +108,59 @@ test('updates', async t => { // t.deepEqual(vaults.highestRatio(), undefined); }); -test.skip('update changes ratio', async t => { +test('update changes ratio', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); const fakeVault1 = makeFakeVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 120n), + AmountMath.make(brand, 20n), ); vaults.addVaultKit('id-fakeVault1', fakeVault1); const fakeVault2 = makeFakeVaultKit( 'id-fakeVault2', - AmountMath.make(brand, 180n), + AmountMath.make(brand, 80n), ); vaults.addVaultKit('id-fakeVault2', fakeVault2); await waitForPromisesToSettle(); - t.deepEqual(vaults.highestRatio(), percent(180)); + t.deepEqual(Array.from(Array.from(vaults.entries()).map(([k, _v]) => k)), [ + 'fbff4000000000000:id-fakeVault2', + 'fc014000000000000:id-fakeVault1', + ]); + + t.deepEqual(vaults.highestRatio(), percent(80)); + + // update the fake debt of the vault and then refresh priority queue + fakeVault1.vault.setDebt(AmountMath.make(brand, 95n)); + vaults.refreshVaultPriority( + AmountMath.make(brand, 20n), + AmountMath.make(brand, 100n), // default collateral of makeFakeVaultKit + 'id-fakeVault1', + ); - fakeVault1.vault.setDebt(AmountMath.make(brand, 200n)); await waitForPromisesToSettle(); - t.deepEqual(vaults.highestRatio(), percent(200)); + t.deepEqual(vaults.highestRatio(), percent(95)); const newCollector = makeCollector(); rescheduler.resetCalled(); - vaults.forEachRatioGTE(percent(190), newCollector.lookForRatio); + vaults.forEachRatioGTE(percent(90), newCollector.lookForRatio); t.deepEqual( newCollector.getPercentages(), - [200], - 'only one is higher than 190', + [95], + 'only one is higher than 90%', ); - t.deepEqual(vaults.highestRatio(), percent(180)); - t.truthy(rescheduler.called(), 'called rescheduler when foreach found vault'); + t.deepEqual(vaults.highestRatio(), percent(95)); + t.falsy(rescheduler.called(), 'foreach does not trigger rescheduler'); // change from previous implementation }); -test.skip('removals', async t => { +test('removals', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); + // Add fakes 1,2,3 const fakeVault1 = makeFakeVaultKit( 'id-fakeVault1', AmountMath.make(brand, 150n), @@ -166,113 +178,83 @@ test.skip('removals', async t => { ); vaults.addVaultKit('id-fakeVault3', fakeVault3); + // remove fake 3 rescheduler.resetCalled(); - vaults.removeVault('id-fakeVault3', fakeVault3.vault); + vaults.removeVaultByAttributes( + AmountMath.make(brand, 150n), + AmountMath.make(brand, 100n), // default collateral of makeFakeVaultKit + 'id-fakeVault3', + ); t.falsy(rescheduler.called()); t.deepEqual(vaults.highestRatio(), percent(150), 'should be 150'); + // remove fake 1 rescheduler.resetCalled(); - vaults.removeVault('id-fakeVault1', fakeVault1.vault); - t.falsy(rescheduler.called(), 'should not call reschedule on removal'); - t.deepEqual(vaults.highestRatio(), percent(130), 'should be 130'); -}); - -// FIXME this relied on special logic of forEachRatioGTE -test.skip('chargeInterest', async t => { - const rescheduler = makeRescheduler(); - const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - - const kit1 = makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 130n)); - vaults.addVaultKit('id-fakeVault1', kit1); - - const kit2 = makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 150n)); - vaults.addVaultKit('id-fakeVault2', kit2); - - const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), (_vaultId, { vault }) => - touchedVaults.push([ - vault, - makeRatioFromAmounts(vault.getDebtAmount(), vault.getCollateralAmount()), - ]), - ); - t.deepEqual(touchedVaults, [ - [kit2.vault, percent(150)], - [kit1.vault, percent(130)], - ]); -}); - -// FIXME this relied on special logic of forEachRatioGTE -test.skip('liquidation', async t => { - const reschedulePriceCheck = makeRescheduler(); - const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - - const fakeVault1 = makeFakeVaultKit( - 'id-fakeVault1', - AmountMath.make(brand, 130n), - ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); - - const fakeVault2 = makeFakeVaultKit( - 'id-fakeVault2', + vaults.removeVaultByAttributes( AmountMath.make(brand, 150n), + AmountMath.make(brand, 100n), // default collateral of makeFakeVaultKit + 'id-fakeVault1', ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); - - const fakeVault3 = makeFakeVaultKit( - 'id-fakeVault3', - AmountMath.make(brand, 140n), - ); - vaults.addVaultKit('id-fakeVault3', fakeVault3); + t.falsy(rescheduler.called(), 'should not call reschedule on removal'); + t.deepEqual(vaults.highestRatio(), percent(130), 'should be 130'); - const touchedVaults = []; - vaults.forEachRatioGTE(percent(135), vaultPair => - touchedVaults.push(vaultPair), + t.throws(() => + vaults.removeVaultByAttributes( + AmountMath.make(brand, 150n), + AmountMath.make(brand, 100n), + 'id-fakeVault1', + ), ); - - t.deepEqual(touchedVaults, [ - { vaultKit: fakeVault2, debtToCollateral: percent(150) }, - { vaultKit: fakeVault3, debtToCollateral: percent(140) }, - ]); }); -test.skip('highestRatio ', async t => { +test('highestRatio', async t => { const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); const fakeVault1 = makeFakeVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 130n), + AmountMath.make(brand, 30n), ); vaults.addVaultKit('id-fakeVault1', fakeVault1); - const cr1 = percent(130); + const cr1 = percent(30); t.deepEqual(vaults.highestRatio(), cr1); const fakeVault6 = makeFakeVaultKit( 'id-fakeVault6', - AmountMath.make(brand, 150n), + AmountMath.make(brand, 50n), ); vaults.addVaultKit('id-fakeVault6', fakeVault6); - const cr6 = percent(150); + const cr6 = percent(50); t.deepEqual(vaults.highestRatio(), cr6); const fakeVault3 = makeFakeVaultKit( 'id-fakeVault3', - AmountMath.make(brand, 140n), + AmountMath.make(brand, 40n), ); vaults.addVaultKit('id-fakeVault3', fakeVault3); - const cr3 = percent(140); - const touchedVaults = []; - vaults.forEachRatioGTE(percent(145), vaultPair => - touchedVaults.push(vaultPair), + // sanity check ordering + t.deepEqual( + Array.from(vaults.entries()).map(([k, _v]) => k), + [ + 'fc000000000000000:id-fakeVault6', + 'fc004000000000000:id-fakeVault3', + 'fc00aaaaaaaaaaaab:id-fakeVault1', + ], ); - t.deepEqual( - touchedVaults, - [{ vaultKit: fakeVault6, debtToCollateral: cr6 }], - 'expected 150 to be highest', + const debtsOverThreshold = []; + vaults.forEachRatioGTE(percent(45), (key, vk) => + debtsOverThreshold.push([ + vk.vault.getDebtAmount(), + vk.vault.getCollateralAmount(), + ]), ); - t.deepEqual(vaults.highestRatio(), cr3); + + t.deepEqual(debtsOverThreshold, [ + [fakeVault6.vault.getDebtAmount(), fakeVault6.vault.getCollateralAmount()], + ]); + t.deepEqual(vaults.highestRatio(), percent(50), 'expected 50% to be highest'); }); test.skip('removal by notification', async t => { @@ -317,3 +299,28 @@ test.skip('removal by notification', async t => { 'should be only one', ); }); + +/* +test.skip('refresh', t => { + for (const [vaultId, runCount, collateralCount] of fixture) { + const mockVaultKit = harden({ + vault: mockVault(runCount, collateralCount), + }); + // @ts-expect-error mock + vaults.addVaultKit(vaultId, mockVaultKit); + } + const vaultIds = Array.from(vaults.entriesWithId()).map( + ([vaultId, _kit]) => vaultId, + ); + t.deepEqual( + vaultIds, + fixture.map(([vid]) => vid), + ); + const [id, runCount, collateralCount] = fixture[4]; + vaults.refreshVaultPriority( + AmountMath.make(runBand, BigInt(runCount)), + AmountMath.make(collateralCount, BigInt(collateralBrand)), + id, + ); +}); +*/ diff --git a/packages/run-protocol/test/vaultFactory/test-storeUtils.js b/packages/run-protocol/test/vaultFactory/test-storeUtils.js index fcac8707c6f..3a0f36f2415 100644 --- a/packages/run-protocol/test/vaultFactory/test-storeUtils.js +++ b/packages/run-protocol/test/vaultFactory/test-storeUtils.js @@ -4,7 +4,6 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, AssetKind } from '@agoric/ertp'; import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@endo/marshal'; -import { details as X } from '@agoric/assert'; import * as StoreUtils from '../../src/vaultFactory/storeUtils.js'; // XXX shouldn't we have a shared test utils for this kind of thing? @@ -43,8 +42,29 @@ for (const [before, after] of [ for (const [numerator, denominator, vaultId, expectedKey, numberOut] of [ [0, 100, 'vault-A', 'ffff0000000000000:vault-A', Infinity], [1, 100, 'vault-B', 'fc059000000000000:vault-B', 100.0], - // TODO do we want prioritize greater debt before other debts that need to be liquidated? - [1000, 100, 'vault-C', 'f8000000000000000:vault-C', 0], // debts greater than collateral are tied for first + [1000, 100, 'vault-C', 'fbfb999999999999a:vault-C', 0.1], + [1000, 101, 'vault-D', 'fbfb9db22d0e56042:vault-D', 0.101], + [ + 100, + Number.MAX_SAFE_INTEGER, + 'vault-MAX-COLLATERAL', + 'fc2d47ae147ae147a:vault-MAX-COLLATERAL', + 90071992547409.9, + ], + [ + Number.MAX_SAFE_INTEGER, + 100, + 'vault-MAX-DEBT', + 'fbd09000000000001:vault-MAX-DEBT', + 1.1102230246251567e-14, + ], + [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + 'vault-MAX-EVEN', + 'fbff0000000000000:vault-MAX-EVEN', + 1, + ], ]) { test(`vault keys: (${numerator}/${denominator}, ${vaultId}) => ${expectedKey} ==> ${numberOut}, ${vaultId}`, t => { const ratio = makeRatioFromAmounts( From 2167398635534ebf9dd130000c612723a3bab507 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 14:08:02 -0800 Subject: [PATCH 22/47] more robust handling of uncollaterialized vaults --- .../src/vaultFactory/orderedVaultStore.js | 12 +--- .../src/vaultFactory/prioritizedVaults.js | 3 +- .../src/vaultFactory/storeUtils.js | 55 +++++++------------ .../run-protocol/src/vaultFactory/vault.js | 41 +++++++++++--- .../vaultFactory/test-prioritizedVaults.js | 2 +- .../test/vaultFactory/test-storeUtils.js | 16 +++--- .../test/vaultFactory/test-vault-interest.js | 3 +- .../vaultFactory/vault-contract-wrapper.js | 3 + 8 files changed, 69 insertions(+), 66 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 05d385cb196..75fd3856021 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -1,12 +1,7 @@ // @ts-check // XXX avoid deep imports https://github.com/Agoric/agoric-sdk/issues/4255#issuecomment-1032117527 import { makeScalarBigMapStore } from '@agoric/swingset-vat/src/storeModule.js'; -import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; -import { - fromVaultKey, - toUncollateralizedKey, - toVaultKey, -} from './storeUtils.js'; +import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** * Used by prioritizedVaults to wrap the Collections API for this use case. @@ -36,10 +31,7 @@ export const makeOrderedVaultStore = () => { const { vault } = vaultKit; const debt = vault.getDebtAmount(); const collateral = vault.getCollateralAmount(); - const key = - collateral.value === 0n - ? toUncollateralizedKey(vaultId) - : toVaultKey(makeRatioFromAmounts(debt, collateral), vaultId); + const key = toVaultKey(debt, collateral, vaultId); console.log('addVaultKit', { debt: debt.value, collateral: collateral.value, diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 3a267099ae3..9219c45b66d 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -141,8 +141,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @param {string} vaultId */ const removeVaultByAttributes = (oldDebt, oldCollateral, vaultId) => { - const ratio = makeRatioFromAmounts(oldDebt, oldCollateral); - const key = toVaultKey(ratio, vaultId); + const key = toVaultKey(oldDebt, oldCollateral, vaultId); return removeVault(key); }; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 33af604414b..087ec577239 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -7,8 +7,6 @@ // XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. /* global BigUint64Array */ -import { assertIsRatio } from '@agoric/zoe/src/contractSupport/index.js'; - const asNumber = new Float64Array(1); const asBits = new BigUint64Array(asNumber.buffer); @@ -63,46 +61,38 @@ const dbEntryKeyToNumber = k => { return result; }; -// XXX there's got to be a helper somewhere for Ratio to float? /** + * Overcollateralized are greater than one. + * The more undercollaterized the smaller in [0-1]. * - * @param {Ratio} ratio + * @param {Amount} normalizedDebt normalized (not actual) total debt + * @param {Amount} collateral * @returns {number} */ -const ratioToInverseProportion = ratio => { - assertIsRatio(ratio); - return ratio.numerator.value - ? Number(ratio.denominator.value) / Number(ratio.numerator.value) - : Number.POSITIVE_INFINITY; +const inverseDebtQuotient = (normalizedDebt, collateral) => { + const a = Number(collateral.value); + const b = normalizedDebt.value + ? Number(normalizedDebt.value) + : Number.EPSILON; + return a / b; }; /** * Sorts by ratio in descending debt. Ordering of vault id is undefined. - * All debts greater than collateral are tied for first. - * - * @param {Ratio} ratio normalized debt ratio (debt over collateral) - * @param {VaultId} vaultId - * @returns {string} lexically sortable string in which highest debt-to-collateral is earliest - */ -const toVaultKey = (ratio, vaultId) => { - assert(ratio); - assert(vaultId); - // until DB supports composite keys, copy its method for turning numbers to DB entry keys - const numberPart = numberToDBEntryKey(ratioToInverseProportion(ratio)); - return `${numberPart}:${vaultId}`; -}; - -/** - * Vaults may be in the store with zero collateral before loans are opened upon them. (??? good/bad idea?) - * They're always the highest priority. * + * @param {Amount} normalizedDebt normalized (not actual) total debt + * @param {Amount} collateral * @param {VaultId} vaultId * @returns {string} lexically sortable string in which highest debt-to-collateral is earliest */ -const toUncollateralizedKey = vaultId => { +const toVaultKey = (normalizedDebt, collateral, vaultId) => { + assert(normalizedDebt); + assert(collateral); assert(vaultId); // until DB supports composite keys, copy its method for turning numbers to DB entry keys - const numberPart = numberToDBEntryKey(0); + const numberPart = numberToDBEntryKey( + inverseDebtQuotient(normalizedDebt, collateral), + ); return `${numberPart}:${vaultId}`; }; @@ -118,13 +108,6 @@ const fromVaultKey = key => { harden(dbEntryKeyToNumber); harden(fromVaultKey); harden(numberToDBEntryKey); -harden(toUncollateralizedKey); harden(toVaultKey); -export { - dbEntryKeyToNumber, - fromVaultKey, - numberToDBEntryKey, - toUncollateralizedKey, - toVaultKey, -}; +export { dbEntryKeyToNumber, fromVaultKey, numberToDBEntryKey, toVaultKey }; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index df7b4e68639..b6be53214e7 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -68,6 +68,7 @@ export const makeVaultKit = ( runMint, priceAuthority, ) => { + console.log('DEBUG makeVaultKit', { manager }); const { updater: uiUpdater, notifier } = makeNotifierKit(); const { zcfSeat: liquidationZcfSeat, userSeat: liquidationSeat } = zcf.makeEmptySeatKit(undefined); @@ -115,16 +116,28 @@ export const makeVaultKit = ( }; /** - * @param {Amount} newDebt - principal and all accrued interest + * @param {Amount} oldDebt - prior principal and all accrued interest + * @param {Amount} oldCollateral - actual collateral + * @param {Amount} newDebt - actual principal and all accrued interest + * @param {Amount} newCollateral - actual collateral */ - const updateDebtPrincipal = newDebt => { - const oldDebt = getDebtAmount(); - trace(idInManager, 'updateDebtPrincipal', { oldDebt, newDebt }); + const refreshLoanTracking = ( + oldDebt, + oldCollateral, + newDebt, + newCollateral, + ) => { + trace(idInManager, 'refreshLoanTracking', { + oldDebt, + oldCollateral, + newDebt, + newCollateral, + }); updateDebtSnapshot(newDebt); // update vault manager which tracks total debt manager.applyDebtDelta(oldDebt, newDebt); // update position of this vault in liquidation priority queue - manager.updateVaultPriority(oldDebt, getCollateralAmount(), idInManager); + manager.updateVaultPriority(oldDebt, oldCollateral, idInManager); }; /** @@ -475,6 +488,8 @@ export const makeVaultKit = ( trace('adjustBalancesHook start'); assertVaultIsOpen(); const proposal = clientSeat.getProposal(); + const oldDebt = getDebtAmount(); + const oldCollateral = getCollateralAmount(); assertOnlyKeys(proposal, ['Collateral', 'RUN']); @@ -540,8 +555,10 @@ export const makeVaultKit = ( transferRun(clientSeat); manager.reallocateReward(fee, vaultSeat, clientSeat); + trace('adjustBalancesHook', { oldCollateral, newDebt }); + // parent needs to know about the change in debt - updateDebtPrincipal(newDebt); + refreshLoanTracking(oldDebt, oldCollateral, newDebt, getCollateralAmount()); runMint.burnLosses(harden({ RUN: runAfter.vault }), vaultSeat); @@ -564,6 +581,10 @@ export const makeVaultKit = ( AmountMath.isEmpty(runDebtSnapshot), X`vault must be empty initially`, ); + const oldDebt = getDebtAmount(); + const oldCollateral = getCollateralAmount(); + trace('openLoan start: collateral', { oldDebt, oldCollateral }); + // get the payout to provide access to the collateral if the // contract abandons const { @@ -579,7 +600,7 @@ export const makeVaultKit = ( Error('loan requested is too small; cannot accrue interest'), ); } - trace(idInManager, 'openLoan', { wantedRun, fee }); + trace(idInManager, 'openLoan', { wantedRun, fee }, getCollateralAmount()); const runDebt = AmountMath.add(wantedRun, fee); await assertSufficientCollateral(collateralAmount, runDebt); @@ -592,7 +613,11 @@ export const makeVaultKit = ( ); manager.reallocateReward(fee, vaultSeat, seat); - updateDebtPrincipal(runDebt); + trace( + 'openLoan about to refreshLoanTracking: collateral', + getCollateralAmount(), + ); + refreshLoanTracking(oldDebt, oldCollateral, runDebt, collateralAmount); updateUiState(); diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index bea2a5024d8..9de9e0a5491 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -181,7 +181,7 @@ test('removals', async t => { // remove fake 3 rescheduler.resetCalled(); vaults.removeVaultByAttributes( - AmountMath.make(brand, 150n), + AmountMath.make(brand, 140n), AmountMath.make(brand, 100n), // default collateral of makeFakeVaultKit 'id-fakeVault3', ); diff --git a/packages/run-protocol/test/vaultFactory/test-storeUtils.js b/packages/run-protocol/test/vaultFactory/test-storeUtils.js index 3a0f36f2415..75971fc1888 100644 --- a/packages/run-protocol/test/vaultFactory/test-storeUtils.js +++ b/packages/run-protocol/test/vaultFactory/test-storeUtils.js @@ -2,7 +2,6 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, AssetKind } from '@agoric/ertp'; -import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@endo/marshal'; import * as StoreUtils from '../../src/vaultFactory/storeUtils.js'; @@ -39,8 +38,8 @@ for (const [before, after] of [ }); } -for (const [numerator, denominator, vaultId, expectedKey, numberOut] of [ - [0, 100, 'vault-A', 'ffff0000000000000:vault-A', Infinity], +for (const [debt, collat, vaultId, expectedKey, numberOut] of [ + [0, 100, 'vault-A', 'fc399000000000000:vault-A', 450359962737049600], // Infinity collateralized but we treat 0 as epsilon for safer serialization [1, 100, 'vault-B', 'fc059000000000000:vault-B', 100.0], [1000, 100, 'vault-C', 'fbfb999999999999a:vault-C', 0.1], [1000, 101, 'vault-D', 'fbfb9db22d0e56042:vault-D', 0.101], @@ -65,13 +64,14 @@ for (const [numerator, denominator, vaultId, expectedKey, numberOut] of [ 'fbff0000000000000:vault-MAX-EVEN', 1, ], + [1, 0, 'vault-NOCOLLATERAL', 'f8000000000000000:vault-NOCOLLATERAL', 0], ]) { - test(`vault keys: (${numerator}/${denominator}, ${vaultId}) => ${expectedKey} ==> ${numberOut}, ${vaultId}`, t => { - const ratio = makeRatioFromAmounts( - AmountMath.make(mockBrand, BigInt(numerator)), - AmountMath.make(mockBrand, BigInt(denominator)), + test(`vault keys: (${debt}/${collat}, ${vaultId}) => ${expectedKey} ==> ${numberOut}, ${vaultId}`, t => { + const key = StoreUtils.toVaultKey( + AmountMath.make(mockBrand, BigInt(debt)), + AmountMath.make(mockBrand, BigInt(collat)), + vaultId, ); - const key = StoreUtils.toVaultKey(ratio, vaultId); t.is(key, expectedKey); t.deepEqual(StoreUtils.fromVaultKey(key), [numberOut, vaultId]); }); diff --git a/packages/run-protocol/test/vaultFactory/test-vault-interest.js b/packages/run-protocol/test/vaultFactory/test-vault-interest.js index ce231b38433..9759ffd9c30 100644 --- a/packages/run-protocol/test/vaultFactory/test-vault-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-vault-interest.js @@ -89,7 +89,8 @@ async function launch(zoeP, sourceRoot) { const helperContract = launch(zoe, vaultRoot); -test('interest', async t => { +// FIXME test failing waiting for updates +test.skip('interest', async t => { const { creatorSeat } = await helperContract; // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index 665c39b9a04..aae32d10de0 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -77,6 +77,9 @@ export async function start(zcf, privateArgs) { return Promise.resolve({ quoteAmount: null, quotePayment: null }); }, getCompoundedInterest: () => makeRatio(1n, runBrand), + updateVaultPriority: () => { + // noop + }, }); const timer = buildManualTimer(console.log, 0n, SECONDS_PER_HOUR * 24n); From b97ba88b1f6d8380d04108a0ff3b5f0eb72ea9ef Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 14:53:45 -0800 Subject: [PATCH 23/47] cleanup --- .../src/vaultFactory/orderedVaultStore.js | 26 ++------------ .../src/vaultFactory/prioritizedVaults.js | 8 ----- .../src/vaultFactory/storeUtils.js | 2 ++ .../run-protocol/src/vaultFactory/vault.js | 1 - .../src/vaultFactory/vaultManager.js | 34 ++++--------------- .../vaultFactory/test-orderedVaultStore.js | 11 +++--- .../vaultFactory/test-prioritizedVaults.js | 25 -------------- .../test/vaultFactory/test-storeUtils.js | 3 +- .../test/vaultFactory/test-vault-interest.js | 2 +- .../test/vaultFactory/test-vaultFactory.js | 29 ++++++++-------- .../vaultFactory/vault-contract-wrapper.js | 7 ++-- 11 files changed, 38 insertions(+), 110 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 75fd3856021..1ab14908b91 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -14,8 +14,7 @@ import { fromVaultKey, toVaultKey } from './storeUtils.js'; */ /** @typedef {import('./vault').VaultKit} VaultKit */ - -/** @typedef {[normalizedDebtRatio: number, vaultId: VaultId]} CompositeKey */ +/** @typedef {import('./storeUtils').CompositeKey} CompositeKey */ export const makeOrderedVaultStore = () => { // TODO make it work durably @@ -65,32 +64,11 @@ export const makeOrderedVaultStore = () => { } }; - /** - * XXX part of refactoring, used only for tests at this point. - * - * Exposes vaultId contained in the key but not the ordering factor. - * That ordering factor is the inverse quotient of the debt ratio (collateral÷debt) - * but nothing outside this module should rely on that to be true. - * - * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 - * - * @yields {[[string, string], VaultKit]>} - * @returns {IterableIterator<[VaultId, VaultKit]>} - */ - function* entriesWithId() { - for (const [k, v] of store.entries()) { - const [_, vaultId] = fromVaultKey(k); - /** @type {VaultKit} */ - const vaultKit = v; - yield [vaultId, vaultKit]; - } - } - return harden({ addVaultKit, removeByKey, + keys: store.keys, entries: store.entries, - entriesWithId, getSize: store.getSize, values: store.values, }); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 9219c45b66d..53c1785192d 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -67,8 +67,6 @@ export const currentDebtToCollateral = vault => export const makePrioritizedVaults = reschedulePriceCheck => { const vaults = makeOrderedVaultStore(); - // XXX why keep this state in PrioritizedVaults? Better in vaultManager? - // To deal with fluctuating prices and varying collateralization, we schedule a // new request to the priceAuthority when some vault's debtToCollateral ratio // surpasses the current high-water mark. When the request that is at the @@ -107,12 +105,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const [[_, vaultKit]] = vaults.entries(); const { vault } = vaultKit; const actualDebtAmount = vault.getDebtAmount(); - console.log( - 'DEBUG firstDebtRatio', - { actualDebtAmount }, - 'in', - Array.from(vaults.entries()), - ); return makeRatioFromAmounts(actualDebtAmount, vault.getCollateralAmount()); }; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 087ec577239..d11bb497ee8 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -7,6 +7,8 @@ // XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. /* global BigUint64Array */ +/** @typedef {[normalizedDebtRatio: number, vaultId: VaultId]} CompositeKey */ + const asNumber = new Float64Array(1); const asBits = new BigUint64Array(asNumber.buffer); diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index b6be53214e7..a646dc393f6 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -68,7 +68,6 @@ export const makeVaultKit = ( runMint, priceAuthority, ) => { - console.log('DEBUG makeVaultKit', { manager }); const { updater: uiUpdater, notifier } = makeNotifierKit(); const { zcfSeat: liquidationZcfSeat, userSeat: liquidationSeat } = zcf.makeEmptySeatKit(undefined); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index ba39926eb06..4cc65918872 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -102,31 +102,6 @@ export const makeVaultManager = ( let vaultCounter = 0; - /** - * Each vaultManager can be in these liquidation process states: - * - * READY - * - Ready to liquidate - * - waiting on price info - * - If chargeInterest triggers, we have to reschedulePriceCheck - * CULLING - * - Price info arrived - * - Picking out set to liquidate - * - reschedulePriceCheck ? - * - highestDebtToCollateral is just a cache for perf of the head of the priority queue - * - If chargeInterest triggers, it’s postponed until READY - * LIQUIDATING - * - Liquidate each of the selected - * - ¿ Skip ones that no longer need to be? - * - ¿ Remove empty vaults? - * - If chargeInterest triggers, it’s postponed until READY - * - Go back to READY - * - * @type {'READY' | 'CULLING' | 'LIQUIDATING'} - */ - // eslint-disable-next-line no-unused-vars - const currentState = 'READY'; - // A Map from vaultKits to their most recent ratio of debt to // collateralization. (This representation won't be optimized; when we need // better performance, use virtual objects.) @@ -318,18 +293,23 @@ export const makeVaultManager = ( * @returns {bigint} in brand of the manager's debt */ const debtDelta = (oldDebt, newDebt) => { - trace('debtDelta', { oldDebt, newDebt }); // Since newDebt includes accrued interest we need to use getDebtAmount() // to get a baseline that also includes accrued interest. // eslint-disable-next-line no-use-before-define const priorDebtValue = oldDebt.value; + const newDebtValue = newDebt.value; // We can't used AmountMath because the delta can be negative. assert.typeof( priorDebtValue, 'bigint', 'vault debt supports only bigint amounts', ); - return newDebt.value - priorDebtValue; + assert.typeof( + newDebtValue, + 'bigint', + 'vault debt supports only bigint amounts', + ); + return newDebtValue - priorDebtValue; }; /** diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js index e24124f85a4..19a2df9ca41 100644 --- a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -5,11 +5,11 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath, AssetKind } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import { makeOrderedVaultStore } from '../../src/vaultFactory/orderedVaultStore.js'; +import { fromVaultKey } from '../../src/vaultFactory/storeUtils.js'; // XXX shouldn't we have a shared test utils for this kind of thing? const runBrand = Far('brand', { - // eslint-disable-next-line no-unused-vars - isMyIssuer: async allegedIssuer => false, + isMyIssuer: async _allegedIssuer => false, getAllegedName: () => 'mockRUN', getDisplayInfo: () => ({ assetKind: AssetKind.NAT, @@ -17,8 +17,7 @@ const runBrand = Far('brand', { }); const collateralBrand = Far('brand', { - // eslint-disable-next-line no-unused-vars - isMyIssuer: async allegedIssuer => false, + isMyIssuer: async _allegedIssuer => false, getAllegedName: () => 'mockCollateral', getDisplayInfo: () => ({ assetKind: AssetKind.NAT, @@ -67,8 +66,8 @@ test.skip('ordering', t => { // @ts-expect-error mock vaults.addVaultKit(vaultId, mockVaultKit); } - const contents = Array.from(vaults.entriesWithId()); - const vaultIds = contents.map(([vaultId, _kit]) => vaultId); + const contents = Array.from(vaults.entries()); + const vaultIds = contents.map(([k, _v]) => fromVaultKey(k)[1]); // keys were ordered matching the fixture's ordering of vaultId t.deepEqual(vaultIds, vaultIds.sort()); }); diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 9de9e0a5491..1a963fd46dd 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -299,28 +299,3 @@ test.skip('removal by notification', async t => { 'should be only one', ); }); - -/* -test.skip('refresh', t => { - for (const [vaultId, runCount, collateralCount] of fixture) { - const mockVaultKit = harden({ - vault: mockVault(runCount, collateralCount), - }); - // @ts-expect-error mock - vaults.addVaultKit(vaultId, mockVaultKit); - } - const vaultIds = Array.from(vaults.entriesWithId()).map( - ([vaultId, _kit]) => vaultId, - ); - t.deepEqual( - vaultIds, - fixture.map(([vid]) => vid), - ); - const [id, runCount, collateralCount] = fixture[4]; - vaults.refreshVaultPriority( - AmountMath.make(runBand, BigInt(runCount)), - AmountMath.make(collateralCount, BigInt(collateralBrand)), - id, - ); -}); -*/ diff --git a/packages/run-protocol/test/vaultFactory/test-storeUtils.js b/packages/run-protocol/test/vaultFactory/test-storeUtils.js index 75971fc1888..8a68c3cdd17 100644 --- a/packages/run-protocol/test/vaultFactory/test-storeUtils.js +++ b/packages/run-protocol/test/vaultFactory/test-storeUtils.js @@ -7,8 +7,7 @@ import * as StoreUtils from '../../src/vaultFactory/storeUtils.js'; // XXX shouldn't we have a shared test utils for this kind of thing? export const mockBrand = Far('brand', { - // eslint-disable-next-line no-unused-vars - isMyIssuer: async allegedIssuer => false, + isMyIssuer: async _allegedIssuer => false, getAllegedName: () => 'mock', getDisplayInfo: () => ({ assetKind: AssetKind.NAT, diff --git a/packages/run-protocol/test/vaultFactory/test-vault-interest.js b/packages/run-protocol/test/vaultFactory/test-vault-interest.js index 9759ffd9c30..3568ca659c1 100644 --- a/packages/run-protocol/test/vaultFactory/test-vault-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-vault-interest.js @@ -89,7 +89,7 @@ async function launch(zoeP, sourceRoot) { const helperContract = launch(zoe, vaultRoot); -// FIXME test failing waiting for updates +// FIXME test fails waiting for updates test.skip('interest', async t => { const { creatorSeat } = await helperContract; diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index 9783cedf13b..0deeaae7ad1 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -299,7 +299,7 @@ async function setupServices( } // FIXME -test.skip('first', async t => { +test('first', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -442,7 +442,7 @@ test.skip('first', async t => { }); }); -// FIXME +// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test test.skip('price drop', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -564,7 +564,7 @@ test.skip('price drop', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); -// FIXME +// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test test.skip('price falls precipitously', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -738,8 +738,8 @@ test('vaultFactory display collateral', async t => { }); // charging period is 1 week. Clock ticks by days -// FIXME -test.skip('interest on multiple vaults', async t => { +// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test +test('interest on multiple vaults', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -867,7 +867,7 @@ test.skip('interest on multiple vaults', async t => { const aliceUpdate = await E(aliceNotifier).getUpdateSince(); const bobUpdate = await E(bobNotifier).getUpdateSince(); // 160 is initial fee. interest is 3n/week. compounding is in the noise. - const bobAddedDebt = 160n + 4n; + const bobAddedDebt = 160n + 3n; t.deepEqual( bobUpdate.value.debt, AmountMath.make(runBrand, 3200n + bobAddedDebt), @@ -883,8 +883,8 @@ test.skip('interest on multiple vaults', async t => { bobCollateralization.denominator.value, ); - // 236 is the initial fee. Interest is 4n/week - const aliceAddedDebt = 236n + 4n; + // 236 is the initial fee. Interest is ~3n/week + const aliceAddedDebt = 236n + 3n; t.deepEqual( aliceUpdate.value.debt, AmountMath.make(runBrand, 4700n + aliceAddedDebt), @@ -899,13 +899,14 @@ test.skip('interest on multiple vaults', async t => { ); const rewardAllocation = await E(vaultFactory).getRewardAllocation(); + const rewardRunCount = aliceAddedDebt + bobAddedDebt + 1n; // +1 due to rounding t.truthy( AmountMath.isEqual( rewardAllocation.RUN, - AmountMath.make(runBrand, aliceAddedDebt + bobAddedDebt), + AmountMath.make(runBrand, rewardRunCount), ), // reward includes 5% fees on two loans plus 1% interest three times on each - `Should be ${aliceAddedDebt + bobAddedDebt}, was ${rewardAllocation.RUN}`, + `Should be ${rewardRunCount}, was ${rewardAllocation.RUN.value}`, ); }); @@ -1335,8 +1336,8 @@ test('overdeposit', async t => { // Both loans will initially be over collateralized 100%. Alice will withdraw // enough of the overage that she'll get caught when prices drop. Bob will be // charged interest (twice), which will trigger liquidation. -// FIXME -test.skip('mutable liquidity triggers and interest', async t => { +// FIXME legit bug (Cannot finish after termination.) +test('mutable liquidity triggers and interest', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -1858,8 +1859,8 @@ test('excessive loan', async t => { // prices drop. Bob will be charged interest (twice), which will trigger // liquidation. Alice's withdrawal is precisely gauged so the difference between // a floorDivideBy and a ceilingDivideBy will leave her unliquidated. -// FIXME -test.skip('mutable liquidity triggers and interest sensitivity', async t => { +// FIXME legit bug (Cannot finish after termination.) +test('mutable liquidity triggers and interest sensitivity', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index aae32d10de0..6dbbb8a90bc 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -2,7 +2,7 @@ import '@agoric/zoe/src/types.js'; -import { makeIssuerKit, AssetKind } from '@agoric/ertp'; +import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; import { assert } from '@agoric/assert'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; @@ -74,7 +74,10 @@ export async function start(zcf, privateArgs) { reallocateReward, applyDebtDelta() {}, getCollateralQuote() { - return Promise.resolve({ quoteAmount: null, quotePayment: null }); + return Promise.resolve({ + quoteAmount: AmountMath.make(runBrand, 0n), + quotePayment: null, + }); }, getCompoundedInterest: () => makeRatio(1n, runBrand), updateVaultPriority: () => { From c81bfdbc9bdc7cba4c4f7dd0b21e835720765d3d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 16:42:38 -0800 Subject: [PATCH 24/47] make test-orderedVaultStore deterministic --- .../test/vaultFactory/test-orderedVaultStore.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js index 19a2df9ca41..1f427255943 100644 --- a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -35,9 +35,16 @@ const mockVault = (runCount, collateralCount) => { }; /** + * Records to be inserted in this order. Jumbled to verify insertion order invariance. + * * @type {Array<[string, bigint, bigint]>} */ const fixture = [ + ['vault-E', 40n, 100n], + ['vault-F', 50n, 100n], + ['vault-M', 1n, 1000n], + ['vault-Y', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], + ['vault-Z-withoutdebt', 0n, 100n], ['vault-A-underwater', 1000n, 100n], ['vault-B', 101n, 1000n], // because the C vaults all have same ratio, order among them is not defined @@ -45,21 +52,15 @@ const fixture = [ ['vault-C2', 200n, 2000n], ['vault-C3', 300n, 3000n], ['vault-D', 30n, 100n], - ['vault-E', 40n, 100n], - ['vault-F', 50n, 100n], - ['vault-M', 1n, 1000n], - ['vault-Y', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], - ['vault-Z-withoutdebt', 0n, 100n], ]; -test.skip('ordering', t => { +test('ordering', t => { const vaults = makeOrderedVaultStore(); // TODO keep a seed so we can debug when it does fail // randomize because the add order should not matter // Maybe use https://dubzzz.github.io/fast-check.github.com/ - const params = fixture.sort(Math.random); - for (const [vaultId, runCount, collateralCount] of params) { + for (const [vaultId, runCount, collateralCount] of fixture) { const mockVaultKit = harden({ vault: mockVault(runCount, collateralCount), }); From 4259d64b8e06dc6aad4522edede7ace722de4f9d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 10 Feb 2022 21:15:17 -0800 Subject: [PATCH 25/47] forEachRatioGTE cb --> entriesPrioritizedGTE generator --- .../src/vaultFactory/prioritizedVaults.js | 26 ++--- .../src/vaultFactory/vaultManager.js | 22 ++--- .../vaultFactory/test-prioritizedVaults.js | 94 +++++++++---------- 3 files changed, 66 insertions(+), 76 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 53c1785192d..bd89c3efa37 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -104,6 +104,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const [[_, vaultKit]] = vaults.entries(); const { vault } = vaultKit; + const collateralAmount = vault.getCollateralAmount(); + if (AmountMath.isEmpty(collateralAmount)) { + // Would be an infinite ratio + return undefined; + } const actualDebtAmount = vault.getDebtAmount(); return makeRatioFromAmounts(actualDebtAmount, vault.getCollateralAmount()); }; @@ -152,28 +157,27 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * Invoke a function for vaults with debt to collateral at or above the ratio. * - * Callbacks are called in order of priority. Vaults that are under water - * (more debt than collateral) are all tied for first. + * Results are returned in order of priority, with highest debt to collateral first. + * + * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 * * @param {Ratio} ratio - * @param {(key: string, vk: VaultKit) => void} cb + * @yields {[string, VaultKit]>} + * @returns {IterableIterator<[string, VaultKit]>} */ - // TODO switch to generator - const forEachRatioGTE = (ratio, cb) => { + // eslint-disable-next-line func-names + function* entriesPrioritizedGTE(ratio) { // TODO use a Pattern to limit the query for (const [key, vk] of vaults.entries()) { const debtToCollateral = currentDebtToCollateral(vk.vault); - - console.log('forEachRatioGTE', { ratio, debtToCollateral }); - if (ratioGTE(debtToCollateral, ratio)) { - cb(key, vk); + yield [key, vk]; } else { // stop once we are below the target ratio break; } } - }; + } /** * @param {Amount} oldDebt @@ -188,7 +192,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { return harden({ addVaultKit, entries: vaults.entries, - forEachRatioGTE, + entriesPrioritizedGTE, highestRatio: () => oracleQueryThreshold, refreshVaultPriority, removeVault, diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 4cc65918872..9eb3ab0631d 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -124,10 +124,9 @@ export const makeVaultManager = ( /** * - * @param {string} key - * @param {VaultKit} vaultKit + * @param {[key: string, vaultKit: VaultKit]} record */ - const liquidateAndRemove = async (key, vaultKit) => { + const liquidateAndRemove = async ([key, vaultKit]) => { assert(prioritizedVaults); trace('liquidating', vaultKit.vaultSeat.getProposal()); @@ -207,13 +206,9 @@ export const makeVaultManager = ( ); /** @type {Array>} */ - const toLiquidate = []; - - // TODO try pattern matching to achieve GTE - // TODO replace forEachRatioGTE with a generator pattern like liquidateAll below - prioritizedVaults.forEachRatioGTE(quoteRatioPlusMargin, (key, vaultKit) => { - toLiquidate.push(liquidateAndRemove(key, vaultKit)); - }); + const toLiquidate = Array.from( + prioritizedVaults.entriesPrioritizedGTE(quoteRatioPlusMargin), + ).map(liquidateAndRemove); outstandingQuote = undefined; // Ensure all vaults complete @@ -226,9 +221,10 @@ export const makeVaultManager = ( // In extreme situations, system health may require liquidating all vaults. const liquidateAll = async () => { assert(prioritizedVaults); - for await (const [key, vaultKit] of prioritizedVaults.entries()) { - await liquidateAndRemove(key, vaultKit); - } + const toLiquidate = Array.from(prioritizedVaults.entries()).map( + liquidateAndRemove, + ); + await Promise.all(toLiquidate); }; /** diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 1a963fd46dd..f98ea28eac7 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -33,10 +33,9 @@ function makeCollector() { /** * - * @param {VaultId} _vaultId - * @param {VaultKit} vaultKit + * @param {[string, VaultKit]} record */ - function lookForRatio(_vaultId, vaultKit) { + function lookForRatio([_, vaultKit]) { ratios.push(currentDebtToCollateral(vaultKit.vault)); } @@ -67,45 +66,47 @@ const percent = n => makeRatio(BigInt(n), brand); test('add to vault', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVaultKit = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVaultKit', - AmountMath.make(brand, 130n), + makeFakeVaultKit('id-fakeVaultKit', AmountMath.make(brand, 130n)), ); - vaults.addVaultKit('id-fakeVaultKit', fakeVaultKit); const collector = makeCollector(); - vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); + // ??? why doesn't this work? + // mapIterable( + // vaults.entriesPrioritizedGTE(makeRatio(1n, brand, 10n)), + // collector.lookForRatio, + // ); + Array.from(vaults.entriesPrioritizedGTE(makeRatio(1n, brand, 10n))).map( + collector.lookForRatio, + ); t.deepEqual(collector.getPercentages(), [130], 'expected vault'); t.truthy(rescheduler.called(), 'should call reschedule()'); - // FIXME is it material that this be undefined? - // t.deepEqual(vaults.highestRatio(), undefined); }); test('updates', async t => { const rescheduler = makeRescheduler(); const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); - const fakeVault1 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 20n), + makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 20n)), ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault2', - AmountMath.make(brand, 80n), + makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 80n)), ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); await waitForPromisesToSettle(); const collector = makeCollector(); rescheduler.resetCalled(); - vaults.forEachRatioGTE(makeRatio(1n, brand, 10n), collector.lookForRatio); + Array.from(vaults.entriesPrioritizedGTE(makeRatio(1n, brand, 10n))).map( + collector.lookForRatio, + ); t.deepEqual(collector.getPercentages(), [80, 20]); t.falsy(rescheduler.called(), 'second vault did not call reschedule()'); - // FIXME is it material that this be undefined? - // t.deepEqual(vaults.highestRatio(), undefined); }); test('update changes ratio', async t => { @@ -118,11 +119,10 @@ test('update changes ratio', async t => { ); vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault2', - AmountMath.make(brand, 80n), + makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 80n)), ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); await waitForPromisesToSettle(); @@ -146,7 +146,9 @@ test('update changes ratio', async t => { const newCollector = makeCollector(); rescheduler.resetCalled(); - vaults.forEachRatioGTE(percent(90), newCollector.lookForRatio); + Array.from(vaults.entriesPrioritizedGTE(percent(90))).map( + newCollector.lookForRatio, + ); t.deepEqual( newCollector.getPercentages(), [95], @@ -161,22 +163,18 @@ test('removals', async t => { const vaults = makePrioritizedVaults(rescheduler.fakeReschedule); // Add fakes 1,2,3 - const fakeVault1 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 150n), + makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 150n)), ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); - const fakeVault2 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault2', - AmountMath.make(brand, 130n), + makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 130n)), ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); - - const fakeVault3 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault3', - AmountMath.make(brand, 140n), + makeFakeVaultKit('id-fakeVault3', AmountMath.make(brand, 140n)), ); - vaults.addVaultKit('id-fakeVault3', fakeVault3); // remove fake 3 rescheduler.resetCalled(); @@ -211,11 +209,10 @@ test('highestRatio', async t => { const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 30n), + makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 30n)), ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); const cr1 = percent(30); t.deepEqual(vaults.highestRatio(), cr1); @@ -227,11 +224,10 @@ test('highestRatio', async t => { const cr6 = percent(50); t.deepEqual(vaults.highestRatio(), cr6); - const fakeVault3 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault3', - AmountMath.make(brand, 40n), + makeFakeVaultKit('id-fakeVault3', AmountMath.make(brand, 40n)), ); - vaults.addVaultKit('id-fakeVault3', fakeVault3); // sanity check ordering t.deepEqual( @@ -244,7 +240,7 @@ test('highestRatio', async t => { ); const debtsOverThreshold = []; - vaults.forEachRatioGTE(percent(45), (key, vk) => + Array.from(vaults.entriesPrioritizedGTE(percent(45))).map(([_key, vk]) => debtsOverThreshold.push([ vk.vault.getDebtAmount(), vk.vault.getCollateralAmount(), @@ -261,19 +257,17 @@ test.skip('removal by notification', async t => { const reschedulePriceCheck = makeRescheduler(); const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - const fakeVault1 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault1', - AmountMath.make(brand, 150n), + makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 150n)), ); - vaults.addVaultKit('id-fakeVault1', fakeVault1); const cr1 = percent(150); t.deepEqual(vaults.highestRatio(), cr1); - const fakeVault2 = makeFakeVaultKit( + vaults.addVaultKit( 'id-fakeVault2', - AmountMath.make(brand, 130n), + makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 130n)), ); - vaults.addVaultKit('id-fakeVault2', fakeVault2); t.deepEqual(vaults.highestRatio(), cr1, 'should be new highest'); const fakeVault3 = makeFakeVaultKit( @@ -289,13 +283,9 @@ test.skip('removal by notification', async t => { t.deepEqual(vaults.highestRatio(), cr3, 'should have removed 150'); const touchedVaults = []; - vaults.forEachRatioGTE(makeRatio(135n, brand), vaultPair => - touchedVaults.push(vaultPair), + Array.from(vaults.entriesPrioritizedGTE(makeRatio(135n, brand))).map( + ([_key, vaultKit]) => touchedVaults.push(vaultKit), ); - t.deepEqual( - touchedVaults, - [{ vaultKit: fakeVault3, debtToCollateral: cr3 }], - 'should be only one', - ); + t.deepEqual(touchedVaults, [fakeVault3], 'should be only one'); }); From ab670eb05e54a8615027487bedd9cc08e64e2043 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 11 Feb 2022 04:38:24 -0800 Subject: [PATCH 26/47] work around mixing the notifier streams --- .../run-protocol/src/vaultFactory/types.js | 1 + .../run-protocol/src/vaultFactory/vault.js | 11 ++- .../test/vaultFactory/test-vaultFactory.js | 73 +++++++++---------- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index abce73071a9..baee9e72f8e 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -70,6 +70,7 @@ * @property {Ratio} interestRate Annual interest rate charge * @property {Ratio} liquidationRatio * @property {boolean} liquidated boolean showing whether liquidation occurred + * @property {'active' | 'liquidating' | 'closed'} vaultState */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index a646dc393f6..1301254429c 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -270,7 +270,16 @@ export const makeVaultKit = ( }; // ??? better to provide this notifier downstream to partition broadcasts? // Propagate notifications from the manager to observers of this vault - observeNotifier(managerNotifier, { updateState: updateUiState }); + observeNotifier(managerNotifier, { + updateState: () => { + // XXX managerNotifier updates can keep coming after close (uiUpdater.finish() called) + // Is there a way to stop observing? + // If not, what alternatives to changing the client to separate the observers? + if (vaultState !== VaultState.CLOSED) { + updateUiState(); + } + }, + }); /** * Call must check for and remember shortfall diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index 0deeaae7ad1..d6943f383a1 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -298,7 +298,6 @@ async function setupServices( }; } -// FIXME test('first', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -430,7 +429,6 @@ test('first', async t => { 'vault is cleared', ); - // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); const liquidations = await E( E(vault).getLiquidationSeat(), ).getCurrentAllocation(); @@ -442,8 +440,7 @@ test('first', async t => { }); }); -// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test -test.skip('price drop', async t => { +test('price drop', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -508,10 +505,12 @@ test.skip('price drop', async t => { 'borrower RUN amount does not match', ); - const notification1 = await E(uiNotifier).getUpdateSince(); - t.falsy(notification1.value.liquidated); + /** @type {UpdateRecord} */ + let notification = await E(uiNotifier).getUpdateSince(); + + t.falsy(notification.value.liquidated); t.deepEqual( - await notification1.value.collateralizationRatio, + await notification.value.collateralizationRatio, makeRatio(444n, runBrand, 284n), ); const { RUN: lentAmount } = await E(loanSeat).getCurrentAllocation(); @@ -523,25 +522,18 @@ test.skip('price drop', async t => { ); await manualTimer.tick(); - const notification2 = await E(uiNotifier).getUpdateSince(); - t.is(notification2.updateCount, 2); - t.falsy(notification2.value.liquidated); - - await manualTimer.tick(); - const notification3 = await E(uiNotifier).getUpdateSince(); - t.is(notification3.updateCount, 2); - t.falsy(notification3.value.liquidated); + notification = await E(uiNotifier).getUpdateSince(); + t.falsy(notification.value.liquidated); await manualTimer.tick(); - const notification4 = await E(uiNotifier).getUpdateSince(2); - t.falsy(notification4.value.liquidated); - t.is(notification4.value.vaultState, VaultState.LIQUIDATING); + notification = await E(uiNotifier).getUpdateSince(notification.updateCount); + t.falsy(notification.value.liquidated); + t.is(notification.value.vaultState, VaultState.LIQUIDATING); await manualTimer.tick(); - const notification5 = await E(uiNotifier).getUpdateSince(3); - - t.falsy(notification5.updateCount); - t.truthy(notification5.value.liquidated); + notification = await E(uiNotifier).getUpdateSince(notification.updateCount); + t.falsy(notification.updateCount); + t.truthy(notification.value.liquidated); const debtAmountAfter = await E(vault).getDebtAmount(); const finalNotification = await E(uiNotifier).getUpdateSince(); @@ -556,7 +548,6 @@ test.skip('price drop', async t => { RUN: AmountMath.make(runBrand, 14n), }); - // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); const liquidations = await E( E(vault).getLiquidationSeat(), ).getCurrentAllocation(); @@ -564,8 +555,7 @@ test.skip('price drop', async t => { t.deepEqual(liquidations.RUN, AmountMath.makeEmpty(runBrand)); }); -// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test -test.skip('price falls precipitously', async t => { +test('price falls precipitously', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, } = setupAssets(); @@ -624,6 +614,7 @@ test.skip('price falls precipitously', async t => { }), ); + /** @type {{vault: Vault}} */ const { vault } = await E(loanSeat).getOfferResult(); const debtAmount = await E(vault).getDebtAmount(); const fee = ceilMultiplyBy(AmountMath.make(runBrand, 370n), rates.loanFee); @@ -658,20 +649,32 @@ test.skip('price falls precipitously', async t => { }), ); + async function assertDebtIs(value) { + const debt = await E(vault).getDebtAmount(); + t.is( + debt.value, + BigInt(value), + `Expected debt ${debt.value} to be ${value}`, + ); + } + await manualTimer.tick(); - t.falsy(AmountMath.isEmpty(await E(vault).getDebtAmount())); - await manualTimer.tick(); - t.falsy(AmountMath.isEmpty(await E(vault).getDebtAmount())); - await manualTimer.tick(); - t.falsy(AmountMath.isEmpty(await E(vault).getDebtAmount())); + await assertDebtIs(debtAmount.value); + await manualTimer.tick(); + await assertDebtIs(debtAmount.value); - // t.is(await E(vault).getLiquidationPromise(), 'Liquidated'); + await manualTimer.tick(); + await assertDebtIs(debtAmount.value); + await manualTimer.tick(); + await waitForPromisesToSettle(); // An emergency liquidation got less than full value const newDebtAmount = await E(vault).getDebtAmount(); - - t.truthy(AmountMath.isGTE(AmountMath.make(runBrand, 70n), newDebtAmount)); + t.truthy( + AmountMath.isGTE(AmountMath.make(runBrand, 70n), newDebtAmount), + `Expected ${newDebtAmount.value} to be less than 70`, + ); t.deepEqual(await E(vaultFactory).getRewardAllocation(), { RUN: AmountMath.make(runBrand, 19n), @@ -738,7 +741,6 @@ test('vaultFactory display collateral', async t => { }); // charging period is 1 week. Clock ticks by days -// FIXME don't know yet whether failure is bug, legitimate change in new design, or merely brittle test test('interest on multiple vaults', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -1336,7 +1338,6 @@ test('overdeposit', async t => { // Both loans will initially be over collateralized 100%. Alice will withdraw // enough of the overage that she'll get caught when prices drop. Bob will be // charged interest (twice), which will trigger liquidation. -// FIXME legit bug (Cannot finish after termination.) test('mutable liquidity triggers and interest', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, @@ -1790,7 +1791,6 @@ test('close loan', async t => { AmountMath.makeEmpty(aethBrand), ); - // t.is(await E(aliceVault).getLiquidationPromise(), 'Closed'); t.deepEqual( await E(E(aliceVault).getLiquidationSeat()).getCurrentAllocation(), {}, @@ -1859,7 +1859,6 @@ test('excessive loan', async t => { // prices drop. Bob will be charged interest (twice), which will trigger // liquidation. Alice's withdrawal is precisely gauged so the difference between // a floorDivideBy and a ceilingDivideBy will leave her unliquidated. -// FIXME legit bug (Cannot finish after termination.) test('mutable liquidity triggers and interest sensitivity', async t => { const { aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand }, From 2146ec09a917bdbda9f2228b5b09574c38589fc3 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 11 Feb 2022 04:53:08 -0800 Subject: [PATCH 27/47] remove vault-interest unit test b/c interest requires a real vaultManager --- .../test/vaultFactory/test-vault-interest.js | 149 ------------------ 1 file changed, 149 deletions(-) delete mode 100644 packages/run-protocol/test/vaultFactory/test-vault-interest.js diff --git a/packages/run-protocol/test/vaultFactory/test-vault-interest.js b/packages/run-protocol/test/vaultFactory/test-vault-interest.js deleted file mode 100644 index 3568ca659c1..00000000000 --- a/packages/run-protocol/test/vaultFactory/test-vault-interest.js +++ /dev/null @@ -1,149 +0,0 @@ -// @ts-check -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import '@agoric/zoe/exported.js'; - -import { E } from '@agoric/eventual-send'; -import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; -import { makeLoopback } from '@endo/captp'; -import { makeZoeKit } from '@agoric/zoe'; -import bundleSource from '@endo/bundle-source'; -import { resolve as importMetaResolve } from 'import-meta-resolve'; - -import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; -import { AmountMath } from '@agoric/ertp'; -import { assert } from '@agoric/assert'; -import { makeTracer } from '../../src/makeTracer.js'; - -const vaultRoot = './vault-contract-wrapper.js'; -const trace = makeTracer('TestVault'); - -/** - * The properties will be asssigned by `setTestJig` in the contract. - * - * @typedef {Object} TestContext - * @property {ContractFacet} zcf - * @property {ZCFMint} runMint - * @property {IssuerKit} collateralKit - * @property {Vault} vault - * @property {TimerService} timer - */ - -// There's one copy of Vault shared across each test file, so this test runs in -// a separate context from test-vault.js - -/* @type {TestContext} */ -let testJig; -const setJig = jig => { - testJig = jig; -}; - -const { makeFar, makeNear: makeRemote } = makeLoopback('zoeTest'); - -const { zoeService, feeMintAccess: nonFarFeeMintAccess } = makeZoeKit( - makeFakeVatAdmin(setJig, makeRemote).admin, -); -/** @type {ERef} */ -const zoe = makeFar(zoeService); -trace('makeZoe'); -const feeMintAccessP = makeFar(nonFarFeeMintAccess); - -/** - * @param {ERef} zoeP - * @param {string} sourceRoot - */ -async function launch(zoeP, sourceRoot) { - const contractUrl = await importMetaResolve(sourceRoot, import.meta.url); - const contractPath = new URL(contractUrl).pathname; - const contractBundle = await bundleSource(contractPath); - const installation = await E(zoeP).install(contractBundle); - const feeMintAccess = await feeMintAccessP; - const { creatorInvitation, creatorFacet, instance } = await E( - zoeP, - ).startInstance( - installation, - undefined, - undefined, - harden({ feeMintAccess }), - ); - const { - runMint, - collateralKit: { mint: collateralMint, brand: collaterlBrand }, - } = testJig; - const { brand: runBrand } = runMint.getIssuerRecord(); - - const collateral50 = AmountMath.make(collaterlBrand, 50000n); - const proposal = harden({ - give: { Collateral: collateral50 }, - want: { RUN: AmountMath.make(runBrand, 70000n) }, - }); - const payments = harden({ - Collateral: collateralMint.mintPayment(collateral50), - }); - assert(creatorInvitation); - return { - creatorSeat: E(zoeP).offer(creatorInvitation, proposal, payments), - creatorFacet, - instance, - }; -} - -const helperContract = launch(zoe, vaultRoot); - -// FIXME test fails waiting for updates -test.skip('interest', async t => { - const { creatorSeat } = await helperContract; - - // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 - // RUN (charging 3 RUN fee), which uses an automatic market maker that - // presents a fixed price of 4 RUN per Collateral. - const { notifier } = await E(creatorSeat).getOfferResult(); - const { - runMint, - collateralKit: { brand: collateralBrand }, - vault, - timer, - } = testJig; - const { brand: runBrand } = runMint.getIssuerRecord(); - - const { value: v1, updateCount: c1 } = await E(notifier).getUpdateSince(); - t.deepEqual(v1.debt, AmountMath.make(runBrand, 73500n)); - t.deepEqual(v1.locked, AmountMath.make(collateralBrand, 50000n)); - t.is(c1, 2); - - t.deepEqual( - vault.getDebtAmount(), - AmountMath.make(runBrand, 73_500n), - 'borrower owes 73,500 RUN', - ); - t.deepEqual( - vault.getCollateralAmount(), - AmountMath.make(collateralBrand, 50_000n), - 'vault holds 50,000 Collateral', - ); - - timer.tick(); - // const noInterest = actions.accrueInterestAndAddToPool(1n); - // t.truthy(AmountMath.isEqual(noInterest, AmountMath.makeEmpty(runBrand))); - - // { chargingPeriod: 3, recordingPeriod: 9 } charge 2% 3 times - for (let i = 0; i < 12; i += 1) { - timer.tick(); - } - - // const nextInterest = actions.accrueInterestAndAddToPool( - // timer.getCurrentTimestamp(), - // ); - // t.truthy( - // AmountMath.isEqual(nextInterest, AmountMath.make(runBrand, 70n)), - // `interest should be 70, was ${nextInterest.value}`, - // ); - const { value: v2, updateCount: c2 } = await E(notifier).getUpdateSince(c1); - t.deepEqual(v2.debt, AmountMath.make(runBrand, 73500n + 70n)); - t.deepEqual(v2.interestRate, makeRatio(5n, runBrand, 100n)); - t.deepEqual(v2.liquidationRatio, makeRatio(105n, runBrand)); - const collateralization = v2.collateralizationRatio; - t.truthy( - collateralization.numerator.value > collateralization.denominator.value, - ); - t.is(c2, 3); -}); From ff101671249d02a335a196a33f676ae218f209d3 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 11 Feb 2022 10:31:20 -0800 Subject: [PATCH 28/47] stop testing removal by notification that no longer happens --- .../vaultFactory/test-prioritizedVaults.js | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index f98ea28eac7..4cb0478587e 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -252,40 +252,3 @@ test('highestRatio', async t => { ]); t.deepEqual(vaults.highestRatio(), percent(50), 'expected 50% to be highest'); }); - -test.skip('removal by notification', async t => { - const reschedulePriceCheck = makeRescheduler(); - const vaults = makePrioritizedVaults(reschedulePriceCheck.fakeReschedule); - - vaults.addVaultKit( - 'id-fakeVault1', - makeFakeVaultKit('id-fakeVault1', AmountMath.make(brand, 150n)), - ); - const cr1 = percent(150); - t.deepEqual(vaults.highestRatio(), cr1); - - vaults.addVaultKit( - 'id-fakeVault2', - makeFakeVaultKit('id-fakeVault2', AmountMath.make(brand, 130n)), - ); - t.deepEqual(vaults.highestRatio(), cr1, 'should be new highest'); - - const fakeVault3 = makeFakeVaultKit( - 'id-fakeVault3', - AmountMath.make(brand, 140n), - ); - vaults.addVaultKit('id-fakeVault2', fakeVault3); - const cr3 = makeRatio(140n, brand); - t.deepEqual(vaults.highestRatio(), cr1, '130 expected'); - - await waitForPromisesToSettle(); - - t.deepEqual(vaults.highestRatio(), cr3, 'should have removed 150'); - - const touchedVaults = []; - Array.from(vaults.entriesPrioritizedGTE(makeRatio(135n, brand))).map( - ([_key, vaultKit]) => touchedVaults.push(vaultKit), - ); - - t.deepEqual(touchedVaults, [fakeVault3], 'should be only one'); -}); From 21d74d9ee0122f906d4e77e8db18d5e208fc6e7e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 11 Feb 2022 18:57:03 -0800 Subject: [PATCH 29/47] docs --- packages/run-protocol/README.md | 3 +-- .../run-protocol/src/vaultFactory/storeUtils.js | 14 +++++++------- packages/run-protocol/src/vaultFactory/vault.js | 11 ++++------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/run-protocol/README.md b/packages/run-protocol/README.md index 6f9c75d1fe8..c9581621445 100644 --- a/packages/run-protocol/README.md +++ b/packages/run-protocol/README.md @@ -8,9 +8,8 @@ By convention there is one well-known **VaultFactory**. By governance it creates Anyone can open make a **Vault** by putting up collateral the appropriate VaultManager. Then they can request RUN that is backed by that collateral. -When any vat the ratio of the debt to the collateral exceeds a governed threshold, the collateral is sold until the ratio reaches the set point. This is called liquidation and managed by the VaultManager. +When any vat the ratio of the debt to the collateral exceeds a governed threshold, it is deemed undercollateralized. If the result of a price check shows that a vault is undercollateralized. the VaultManager liquidates it. ## Persistence The above states are robust to system restarts and upgrades. This is accomplished using the Agoric (Endo?) Collections API. - diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index d11bb497ee8..1340f0779d4 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -7,7 +7,7 @@ // XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. /* global BigUint64Array */ -/** @typedef {[normalizedDebtRatio: number, vaultId: VaultId]} CompositeKey */ +/** @typedef {[normalizedCollateralization: number, vaultId: VaultId]} CompositeKey */ const asNumber = new Float64Array(1); const asBits = new BigUint64Array(asNumber.buffer); @@ -71,12 +71,12 @@ const dbEntryKeyToNumber = k => { * @param {Amount} collateral * @returns {number} */ -const inverseDebtQuotient = (normalizedDebt, collateral) => { - const a = Number(collateral.value); - const b = normalizedDebt.value +const collateralizationRatio = (normalizedDebt, collateral) => { + const c = Number(collateral.value); + const d = normalizedDebt.value ? Number(normalizedDebt.value) : Number.EPSILON; - return a / b; + return c / d; }; /** @@ -93,14 +93,14 @@ const toVaultKey = (normalizedDebt, collateral, vaultId) => { assert(vaultId); // until DB supports composite keys, copy its method for turning numbers to DB entry keys const numberPart = numberToDBEntryKey( - inverseDebtQuotient(normalizedDebt, collateral), + collateralizationRatio(normalizedDebt, collateral), ); return `${numberPart}:${vaultId}`; }; /** * @param {string} key - * @returns {[normalizedDebtRatio: number, vaultId: VaultId]} + * @returns {[normalizedCollateralization: number, vaultId: VaultId]} */ const fromVaultKey = key => { const [numberPart, vaultIdPart] = key.split(':'); diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 1301254429c..3e9497599f3 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -1,6 +1,4 @@ // @ts-check -// XXX we do this a lot, okay to drop it to warning in eslint config? -/* eslint-disable no-use-before-define */ import '@agoric/zoe/exported.js'; import { E } from '@agoric/eventual-send'; @@ -243,6 +241,7 @@ export const makeVaultKit = ( const collateralizationRatio = await getCollateralizationRatio(); /** @type {VaultUIState} */ const uiState = harden({ + // TODO move manager state to a separate notifer https://github.com/Agoric/agoric-sdk/issues/4540 interestRate: manager.getInterestRate(), liquidationRatio: manager.getLiquidationMargin(), runDebtSnapshot, @@ -250,6 +249,7 @@ export const makeVaultKit = ( locked: getCollateralAmount(), debt: getDebtAmount(), collateralizationRatio, + // TODO state distinct from CLOSED https://github.com/Agoric/agoric-sdk/issues/4539 liquidated: vaultState === VaultState.CLOSED, vaultState, }); @@ -268,13 +268,10 @@ export const makeVaultKit = ( throw Error(`unreachable vaultState: ${vaultState}`); } }; - // ??? better to provide this notifier downstream to partition broadcasts? - // Propagate notifications from the manager to observers of this vault + // XXX Echo notifications from the manager though all vaults + // TODO move manager state to a separate notifer https://github.com/Agoric/agoric-sdk/issues/4540 observeNotifier(managerNotifier, { updateState: () => { - // XXX managerNotifier updates can keep coming after close (uiUpdater.finish() called) - // Is there a way to stop observing? - // If not, what alternatives to changing the client to separate the observers? if (vaultState !== VaultState.CLOSED) { updateUiState(); } From 0b1d5d2c810020e751c742490631f6096a8869a5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 11 Feb 2022 19:09:55 -0800 Subject: [PATCH 30/47] object for debtSnapshot --- .../run-protocol/src/vaultFactory/vault.js | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 3e9497599f3..6a4dc7fdc94 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -88,28 +88,24 @@ export const makeVaultKit = ( const { brand: runBrand } = runMint.getIssuerRecord(); - // ??? is there a good way to encapsulate these snapshot values so they can only be touched together? - // perhaps a hardened DebtSnapshot {debt: Amount, interest: Ratio} - let runDebtSnapshot = AmountMath.makeEmpty(runBrand); /** - * compounded interest at the time the debt was snapshotted + * Snapshot of the debt and cmpouneded interest when the principal was last changed * - * @type {Ratio} + * @type {{run: Amount, interest: Ratio}} */ - let interestSnapshot = manager.getCompoundedInterest(); + let debtSnapshot = { + run: AmountMath.makeEmpty(runBrand), + interest: manager.getCompoundedInterest(), + }; /** * @param {Amount} newDebt - principal and all accrued interest */ const updateDebtSnapshot = newDebt => { // update local state - runDebtSnapshot = newDebt; - interestSnapshot = manager.getCompoundedInterest(); + debtSnapshot = { run: newDebt, interest: manager.getCompoundedInterest() }; - trace(`${idInManager} updateDebtSnapshot`, newDebt.value, { - interestSnapshot, - runDebtSnapshot, - }); + trace(`${idInManager} updateDebtSnapshot`, newDebt.value, debtSnapshot); }; /** @@ -154,10 +150,10 @@ export const makeVaultKit = ( // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( manager.getCompoundedInterest(), - invertRatio(interestSnapshot), + invertRatio(debtSnapshot.interest), ); - return floorMultiplyBy(runDebtSnapshot, interestSinceSnapshot); + return floorMultiplyBy(debtSnapshot.run, interestSinceSnapshot); }; /** @@ -170,8 +166,11 @@ export const makeVaultKit = ( * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued */ const getNormalizedDebt = () => { - assert(interestSnapshot); - return floorMultiplyBy(runDebtSnapshot, invertRatio(interestSnapshot)); + assert(debtSnapshot); + return floorMultiplyBy( + debtSnapshot.run, + invertRatio(debtSnapshot.interest), + ); }; const getCollateralAllocated = seat => @@ -225,11 +224,11 @@ export const makeVaultKit = ( ); // TODO: allow Ratios to represent X/0. - if (AmountMath.isEmpty(runDebtSnapshot)) { + if (AmountMath.isEmpty(debtSnapshot.run)) { return makeRatio(collateralAmount.value, runBrand, 1n); } const collateralValueInRun = getAmountOut(quoteAmount); - return makeRatioFromAmounts(collateralValueInRun, runDebtSnapshot); + return makeRatioFromAmounts(collateralValueInRun, debtSnapshot.run); }; // call this whenever anything changes! @@ -244,8 +243,7 @@ export const makeVaultKit = ( // TODO move manager state to a separate notifer https://github.com/Agoric/agoric-sdk/issues/4540 interestRate: manager.getInterestRate(), liquidationRatio: manager.getLiquidationMargin(), - runDebtSnapshot, - interestSnapshot, + debtSnapshot, locked: getCollateralAmount(), debt: getDebtAmount(), collateralizationRatio, @@ -583,7 +581,7 @@ export const makeVaultKit = ( /** @type {OfferHandler} */ const openLoan = async seat => { assert( - AmountMath.isEmpty(runDebtSnapshot), + AmountMath.isEmpty(debtSnapshot.run), X`vault must be empty initially`, ); const oldDebt = getDebtAmount(); From ce035f9baf493cfc720a5b68f4368b4f3d6ef71f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 14 Feb 2022 09:57:15 -0800 Subject: [PATCH 31/47] vault test for compound interest --- .../run-protocol/src/vaultFactory/interest.js | 32 ++-- .../run-protocol/src/vaultFactory/vault.js | 1 + .../src/vaultFactory/vaultManager.js | 2 +- .../test/vaultFactory/test-vault-interest.js | 163 ++++++++++++++++++ .../test/vaultFactory/test-vault.js | 5 +- .../vaultFactory/vault-contract-wrapper.js | 45 ++++- 6 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 packages/run-protocol/test/vaultFactory/test-vault-interest.js diff --git a/packages/run-protocol/src/vaultFactory/interest.js b/packages/run-protocol/src/vaultFactory/interest.js index 02039391729..d4d20e4f3f8 100644 --- a/packages/run-protocol/src/vaultFactory/interest.js +++ b/packages/run-protocol/src/vaultFactory/interest.js @@ -9,12 +9,6 @@ import { } from '@agoric/zoe/src/contractSupport/ratio.js'; import { AmountMath } from '@agoric/ertp'; -const makeResult = (latestInterestUpdate, interest, newDebt) => ({ - latestInterestUpdate, - interest, - newDebt, -}); - export const SECONDS_PER_YEAR = 60n * 60n * 24n * 365n; const BASIS_POINTS = 10000; // single digit APR is less than a basis point per day. @@ -47,8 +41,11 @@ export const makeInterestCalculator = ( BigInt(LARGE_DENOMINATOR), ); - // Calculate new debt for charging periods up to the present. - /** @type {Calculate} */ + /** + * Calculate new debt for charging periods up to the present. + * + * @type {Calculate} + */ const calculate = (debtStatus, currentTime) => { const { newDebt, latestInterestUpdate } = debtStatus; let newRecent = latestInterestUpdate; @@ -60,14 +57,21 @@ export const makeInterestCalculator = ( growingInterest = AmountMath.add(growingInterest, newInterest); growingDebt = AmountMath.add(growingDebt, newInterest, brand); } - return makeResult(newRecent, growingInterest, growingDebt); + return { + latestInterestUpdate: newRecent, + interest: growingInterest, + newDebt: growingDebt, + }; }; - // Calculate new debt for reporting periods up to the present. If some - // charging periods have elapsed that don't constitute whole reporting - // periods, the time is not updated past them and interest is not accumulated - // for them. - /** @type {Calculate} */ + /** + * Calculate new debt for reporting periods up to the present. If some + * charging periods have elapsed that don't constitute whole reporting + * periods, the time is not updated past them and interest is not accumulated + * for them. + * + * @type {Calculate} + */ const calculateReportingPeriod = (debtStatus, currentTime) => { const { latestInterestUpdate } = debtStatus; const overshoot = (currentTime - latestInterestUpdate) % recordingPeriod; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 6a4dc7fdc94..3f2135077dc 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -165,6 +165,7 @@ export const makeVaultKit = ( * @see getActualDebAmount * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued */ + // Not in use until https://github.com/Agoric/agoric-sdk/issues/4540 const getNormalizedDebt = () => { assert(debtSnapshot); return floorMultiplyBy( diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 9eb3ab0631d..bb5054599e4 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -246,7 +246,7 @@ export const makeVaultManager = ( { latestInterestUpdate, newDebt: totalDebt, - interest: AmountMath.makeEmpty(runBrand), + interest: AmountMath.makeEmpty(runBrand), // ??FIXME }, updateTime, ); diff --git a/packages/run-protocol/test/vaultFactory/test-vault-interest.js b/packages/run-protocol/test/vaultFactory/test-vault-interest.js new file mode 100644 index 00000000000..5a1536191ef --- /dev/null +++ b/packages/run-protocol/test/vaultFactory/test-vault-interest.js @@ -0,0 +1,163 @@ +// @ts-check + +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import '@agoric/zoe/exported.js'; + +import { E } from '@agoric/eventual-send'; +import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js'; +import { makeLoopback } from '@endo/captp'; +import { makeZoeKit } from '@agoric/zoe'; +import bundleSource from '@endo/bundle-source'; +import { resolve as importMetaResolve } from 'import-meta-resolve'; + +import { AmountMath } from '@agoric/ertp'; + +import { assert } from '@agoric/assert'; +import { makeTracer } from '../../src/makeTracer.js'; + +const vaultRoot = './vault-contract-wrapper.js'; +const trace = makeTracer('TestVault'); + +/** + * The properties will be asssigned by `setTestJig` in the contract. + * + * @typedef {Object} TestContext + * @property {ContractFacet} zcf + * @property {ZCFMint} runMint + * @property {IssuerKit} collateralKit + * @property {Vault} vault + * @property {Function} advanceRecordingPeriod + * @property {Function} setInterestRate + */ +let testJig; +const setJig = jig => { + testJig = jig; +}; + +const { makeFar, makeNear: makeRemote } = makeLoopback('zoeTest'); + +const { zoeService, feeMintAccess: nonFarFeeMintAccess } = makeZoeKit( + makeFakeVatAdmin(setJig, makeRemote).admin, +); +/** @type {ERef} */ +const zoe = makeFar(zoeService); +trace('makeZoe'); +const feeMintAccessP = makeFar(nonFarFeeMintAccess); + +/** + * @param {ERef} zoeP + * @param {string} sourceRoot + */ +async function launch(zoeP, sourceRoot) { + const contractUrl = await importMetaResolve(sourceRoot, import.meta.url); + const contractPath = new URL(contractUrl).pathname; + const contractBundle = await bundleSource(contractPath); + const installation = await E(zoeP).install(contractBundle); + const feeMintAccess = await feeMintAccessP; + const { creatorInvitation, creatorFacet, instance } = await E( + zoeP, + ).startInstance( + installation, + undefined, + undefined, + harden({ feeMintAccess }), + ); + const { + runMint, + collateralKit: { mint: collateralMint, brand: collaterlBrand }, + } = testJig; + const { brand: runBrand } = runMint.getIssuerRecord(); + + const collateral50 = AmountMath.make(collaterlBrand, 50n); + const proposal = harden({ + give: { Collateral: collateral50 }, + want: { RUN: AmountMath.make(runBrand, 70n) }, + }); + const payments = harden({ + Collateral: collateralMint.mintPayment(collateral50), + }); + assert(creatorInvitation); + return { + creatorSeat: E(zoeP).offer(creatorInvitation, proposal, payments), + creatorFacet, + instance, + }; +} + +test('charges', async t => { + const { creatorSeat, creatorFacet } = await launch(zoe, vaultRoot); + + // Our wrapper gives us a Vault which holds 50 Collateral, has lent out 70 + // RUN (charging 3 RUN fee), which uses an automatic market maker that + // presents a fixed price of 4 RUN per Collateral. + await E(creatorSeat).getOfferResult(); + const { runMint, collateralKit, vault } = testJig; + const { brand: runBrand } = runMint.getIssuerRecord(); + + const { brand: cBrand } = collateralKit; + + const startingDebt = 74n; + t.deepEqual( + vault.getDebtAmount(), + AmountMath.make(runBrand, startingDebt), + 'borrower owes 74 RUN', + ); + t.deepEqual( + vault.getCollateralAmount(), + AmountMath.make(cBrand, 50n), + 'vault holds 50 Collateral', + ); + t.deepEqual(vault.getNormalizedDebt().value, startingDebt); + + let interest = 0n; + for (const [i, charge] of [3n, 4n, 4n, 4n].entries()) { + testJig.advanceRecordingPeriod(); + interest += charge; + t.is( + vault.getDebtAmount().value, + startingDebt + interest, + `interest charge ${i} should have been ${charge}`, + ); + t.is(vault.getNormalizedDebt().value, startingDebt); + } + + // partially payback + const paybackValue = 3n; + const collateralWanted = AmountMath.make(cBrand, 1n); + const paybackAmount = AmountMath.make(runBrand, paybackValue); + const payback = await E(creatorFacet).mintRun(paybackAmount); + const paybackSeat = E(zoe).offer( + vault.makeAdjustBalancesInvitation(), + harden({ + give: { RUN: paybackAmount }, + want: { Collateral: collateralWanted }, + }), + harden({ RUN: payback }), + ); + await E(paybackSeat).getOfferResult(); + t.deepEqual( + vault.getDebtAmount(), + AmountMath.make(runBrand, startingDebt + interest - paybackValue), + ); + const normalizedPaybackValue = paybackValue + 1n; + t.deepEqual( + vault.getNormalizedDebt(), + AmountMath.make(runBrand, startingDebt - normalizedPaybackValue), + ); + + testJig.setInterestRate(25n); + + for (const [i, charge] of [21n, 27n, 33n].entries()) { + testJig.advanceRecordingPeriod(); + interest += charge; + t.is( + vault.getDebtAmount().value, + startingDebt + interest - paybackValue, + `interest charge ${i} should have been ${charge}`, + ); + t.is( + vault.getNormalizedDebt().value, + startingDebt - normalizedPaybackValue, + ); + } +}); diff --git a/packages/run-protocol/test/vaultFactory/test-vault.js b/packages/run-protocol/test/vaultFactory/test-vault.js index 004a38829f6..46ebe8385d8 100644 --- a/packages/run-protocol/test/vaultFactory/test-vault.js +++ b/packages/run-protocol/test/vaultFactory/test-vault.js @@ -27,10 +27,9 @@ const trace = makeTracer('TestVault'); * @property {ZCFMint} runMint * @property {IssuerKit} collateralKit * @property {Vault} vault - * @property {TimerService} timer + * @property {Function} advanceRecordingPeriod + * @property {Function} setInterestRate */ - -/** @type {TestContext} */ let testJig; const setJig = jig => { testJig = jig; diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index 6dbbb8a90bc..6e24fc90720 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -7,7 +7,10 @@ import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; import { assert } from '@agoric/assert'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority.js'; -import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { + makeRatio, + multiplyRatios, +} from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@endo/marshal'; import { makeNotifierKit } from '@agoric/notifier'; @@ -16,6 +19,7 @@ import { paymentFromZCFMint } from '../../src/vaultFactory/burn.js'; const BASIS_POINTS = 10000n; const SECONDS_PER_HOUR = 60n * 60n; +const DAY = SECONDS_PER_HOUR * 24n; /** @type {ContractStartFn} */ export async function start(zcf, privateArgs) { @@ -33,6 +37,9 @@ export async function start(zcf, privateArgs) { let vaultCounter = 0; + let currentInterest = makeRatio(5n, runBrand); // 5% + let compoundedInterest = makeRatio(100n, runBrand); // starts at 1.0, no interest + function reallocateReward(amount, fromSeat, otherSeat) { vaultFactorySeat.incrementBy( fromSeat.decrementBy( @@ -60,16 +67,16 @@ export async function start(zcf, privateArgs) { return makeRatio(500n, runBrand, BASIS_POINTS); }, getInterestRate() { - return makeRatio(5n, runBrand); + return currentInterest; }, getCollateralBrand() { return collateralBrand; }, getChargingPeriod() { - return SECONDS_PER_HOUR * 24n; + return DAY; }, getRecordingPeriod() { - return SECONDS_PER_HOUR * 24n * 7n; + return DAY; }, reallocateReward, applyDebtDelta() {}, @@ -79,13 +86,13 @@ export async function start(zcf, privateArgs) { quotePayment: null, }); }, - getCompoundedInterest: () => makeRatio(1n, runBrand), + getCompoundedInterest: () => compoundedInterest, updateVaultPriority: () => { // noop }, }); - const timer = buildManualTimer(console.log, 0n, SECONDS_PER_HOUR * 24n); + const timer = buildManualTimer(console.log, 0n, DAY); const options = { actualBrandIn: collateralBrand, actualBrandOut: runBrand, @@ -111,7 +118,31 @@ export async function start(zcf, privateArgs) { priceAuthority, ); - zcf.setTestJig(() => ({ collateralKit, runMint, vault, timer })); + const advanceRecordingPeriod = () => { + timer.tick(); + + // skip the debt calculation for this mock manager + const currentInterestAsMultiplicand = makeRatio( + 100n + currentInterest.numerator.value, + currentInterest.numerator.brand, + ); + compoundedInterest = multiplyRatios( + compoundedInterest, + currentInterestAsMultiplicand, + ); + }; + + const setInterestRate = percent => { + currentInterest = makeRatio(percent, runBrand); + }; + + zcf.setTestJig(() => ({ + advanceRecordingPeriod, + collateralKit, + runMint, + setInterestRate, + vault, + })); async function makeHook(seat) { const { notifier } = await openLoan(seat); From 9b36c94b40312fc50d391c4f21cf0919ab59e0e5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 14 Feb 2022 10:46:03 -0800 Subject: [PATCH 32/47] clean up TODOs (remove or include ticket) --- packages/run-protocol/src/vaultFactory/orderedVaultStore.js | 2 +- packages/run-protocol/src/vaultFactory/prioritizedVaults.js | 3 +-- packages/run-protocol/src/vaultFactory/storeUtils.js | 3 +-- packages/run-protocol/src/vaultFactory/vault.js | 2 +- .../run-protocol/test/vaultFactory/test-orderedVaultStore.js | 3 --- .../run-protocol/test/vaultFactory/test-prioritizedVaults.js | 2 +- 6 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index 1ab14908b91..fbb78dc1fae 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -17,7 +17,7 @@ import { fromVaultKey, toVaultKey } from './storeUtils.js'; /** @typedef {import('./storeUtils').CompositeKey} CompositeKey */ export const makeOrderedVaultStore = () => { - // TODO make it work durably + // TODO make it work durably https://github.com/Agoric/agoric-sdk/issues/4550 /** @type {MapStore} */ const store = makeScalarBigMapStore('orderedVaultStore', { durable: false }); diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index bd89c3efa37..7bb124f8c8f 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -122,7 +122,6 @@ export const makePrioritizedVaults = reschedulePriceCheck => { const debtToCollateral = currentDebtToCollateral(vk.vault); if ( !oracleQueryThreshold || - // TODO check for equality is sufficient and faster ratioGTE(debtToCollateral, oracleQueryThreshold) ) { // don't call reschedulePriceCheck, but do reset the highest. @@ -167,7 +166,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { */ // eslint-disable-next-line func-names function* entriesPrioritizedGTE(ratio) { - // TODO use a Pattern to limit the query + // TODO use a Pattern to limit the query https://github.com/Agoric/agoric-sdk/issues/4550 for (const [key, vk] of vaults.entries()) { const debtToCollateral = currentDebtToCollateral(vk.vault); if (ratioGTE(debtToCollateral, ratio)) { diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 1340f0779d4..892996b24f8 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -2,9 +2,8 @@ /** * Module to improvise composite keys for orderedVaultStore until Collections API supports them. * - * TODO BEFORE MERGE: assess maximum key length limits with Collections API */ -// XXX declaration shouldn't be necessary. Add exception to eslint or make a real import. +// XXX declaration shouldn't be necessary. Fixed by https://github.com/endojs/endo/pull/1071 /* global BigUint64Array */ /** @typedef {[normalizedCollateralization: number, vaultId: VaultId]} CompositeKey */ diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 3f2135077dc..70ae9fe48c0 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -145,7 +145,7 @@ export const makeVaultKit = ( * @see getNormalizedDebt * @returns {Amount} */ - // TODO rename to getActualDebtAmount throughout codebase + // TODO rename to getActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 const getDebtAmount = () => { // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js index 1f427255943..024ca77fb00 100644 --- a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -57,9 +57,6 @@ const fixture = [ test('ordering', t => { const vaults = makeOrderedVaultStore(); - // TODO keep a seed so we can debug when it does fail - // randomize because the add order should not matter - // Maybe use https://dubzzz.github.io/fast-check.github.com/ for (const [vaultId, runCount, collateralCount] of fixture) { const mockVaultKit = harden({ vault: mockVault(runCount, collateralCount), diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 4cb0478587e..93e92617166 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -20,7 +20,7 @@ import { makeFakeVaultKit } from '../supports.js'; // This invocation (thanks to Warner) waits for all promises that can fire to // have all their callbacks run async function waitForPromisesToSettle() { - // TODO can't we do simply: + // ??? can't we do simply: // return new Promise(resolve => setImmediate(resolve)); const pk = makePromiseKit(); setImmediate(pk.resolve); From c66af6242a14d8dece000345fd4e4483b54308e7 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 14 Feb 2022 14:24:57 -0800 Subject: [PATCH 33/47] vaultFactory test for minimum debt --- .../src/vaultFactory/vaultManager.js | 2 +- .../test/vaultFactory/test-vaultFactory.js | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index bb5054599e4..9eb3ab0631d 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -246,7 +246,7 @@ export const makeVaultManager = ( { latestInterestUpdate, newDebt: totalDebt, - interest: AmountMath.makeEmpty(runBrand), // ??FIXME + interest: AmountMath.makeEmpty(runBrand), }, updateTime, ); diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index d6943f383a1..d19b35695e7 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -892,6 +892,11 @@ test('interest on multiple vaults', async t => { AmountMath.make(runBrand, 4700n + aliceAddedDebt), `should have collected ${aliceAddedDebt}`, ); + // but no change to the snapshot + t.deepEqual(aliceUpdate.value.debtSnapshot, { + run: AmountMath.make(runBrand, 4935n), + interest: makeRatio(100n, runBrand, 100n), + }); t.deepEqual(aliceUpdate.value.interestRate, interestRate); t.deepEqual(aliceUpdate.value.liquidationRatio, makeRatio(105n, runBrand)); const aliceCollateralization = aliceUpdate.value.collateralizationRatio; @@ -910,6 +915,36 @@ test('interest on multiple vaults', async t => { // reward includes 5% fees on two loans plus 1% interest three times on each `Should be ${rewardRunCount}, was ${rewardAllocation.RUN.value}`, ); + + // try opening a vault that can't cover fees + const caroleLoanSeat = await E(zoe).offer( + E(lender).makeLoanInvitation(), + harden({ + give: { Collateral: AmountMath.make(aethBrand, 200n) }, + want: { RUN: AmountMath.make(runBrand, 0n) }, // no debt + }), + harden({ + Collateral: aethMint.mintPayment(AmountMath.make(aethBrand, 200n)), + }), + ); + await t.throwsAsync(E(caroleLoanSeat).getOfferResult()); + + // open a vault with floor debt (1n RUN) + const danLoanSeat = await E(zoe).offer( + E(lender).makeLoanInvitation(), + harden({ + give: { Collateral: AmountMath.make(aethBrand, 200n) }, + want: { RUN: AmountMath.make(runBrand, 1n) }, // smallest debt + }), + harden({ + Collateral: aethMint.mintPayment(AmountMath.make(aethBrand, 200n)), + }), + ); + /** @type {{vault: Vault}} */ + const { vault: danVault } = await E(danLoanSeat).getOfferResult(); + console.log('DEBUG', danVault); + t.is((await E(danVault).getDebtAmount()).value, 2n); + t.is((await E(danVault).getNormalizedDebt()).value, 1n); }); test('adjust balances', async t => { @@ -991,6 +1026,10 @@ test('adjust balances', async t => { const aliceCollateralization1 = aliceUpdate.value.collateralizationRatio; t.deepEqual(aliceCollateralization1.numerator.value, 15000n); t.deepEqual(aliceCollateralization1.denominator.value, runDebtLevel.value); + t.deepEqual(aliceUpdate.value.debtSnapshot, { + run: AmountMath.make(runBrand, 5250n), + interest: makeRatio(100n, runBrand), + }); // increase collateral 1 ///////////////////////////////////// (give both) @@ -1095,6 +1134,10 @@ test('adjust balances', async t => { ceilMultiplyBy(collateralLevel, priceConversion), ); t.deepEqual(aliceCollateralization3.denominator, runDebtLevel); + t.deepEqual(aliceUpdate.value.debtSnapshot, { + run: AmountMath.make(runBrand, 5253n), + interest: makeRatio(100n, runBrand), + }); // reduce collateral ///////////////////////////////////// (want both) From ed3523640537b46797260e57f28d92ecda792819 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 14 Feb 2022 14:28:32 -0800 Subject: [PATCH 34/47] reduce debug/trace output --- .../run-protocol/src/vaultFactory/vault.js | 31 ++----------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 70ae9fe48c0..adad2c65764 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -112,20 +112,8 @@ export const makeVaultKit = ( * @param {Amount} oldDebt - prior principal and all accrued interest * @param {Amount} oldCollateral - actual collateral * @param {Amount} newDebt - actual principal and all accrued interest - * @param {Amount} newCollateral - actual collateral */ - const refreshLoanTracking = ( - oldDebt, - oldCollateral, - newDebt, - newCollateral, - ) => { - trace(idInManager, 'refreshLoanTracking', { - oldDebt, - oldCollateral, - newDebt, - newCollateral, - }); + const refreshLoanTracking = (oldDebt, oldCollateral, newDebt) => { updateDebtSnapshot(newDebt); // update vault manager which tracks total debt manager.applyDebtDelta(oldDebt, newDebt); @@ -253,8 +241,6 @@ export const makeVaultKit = ( vaultState, }); - trace('updateUiState', uiState); - switch (vaultState) { case VaultState.ACTIVE: case VaultState.LIQUIDATING: @@ -283,7 +269,6 @@ export const makeVaultKit = ( * @param {Amount} newDebt */ const liquidated = newDebt => { - trace(idInManager, 'liquidated', newDebt); updateDebtSnapshot(newDebt); vaultState = VaultState.CLOSED; @@ -291,7 +276,6 @@ export const makeVaultKit = ( }; const liquidating = () => { - trace(idInManager, 'liquidating'); vaultState = VaultState.LIQUIDATING; updateUiState(); }; @@ -489,7 +473,6 @@ export const makeVaultKit = ( * @param {ZCFSeat} clientSeat */ const adjustBalancesHook = async clientSeat => { - trace('adjustBalancesHook start'); assertVaultIsOpen(); const proposal = clientSeat.getProposal(); const oldDebt = getDebtAmount(); @@ -559,10 +542,8 @@ export const makeVaultKit = ( transferRun(clientSeat); manager.reallocateReward(fee, vaultSeat, clientSeat); - trace('adjustBalancesHook', { oldCollateral, newDebt }); - // parent needs to know about the change in debt - refreshLoanTracking(oldDebt, oldCollateral, newDebt, getCollateralAmount()); + refreshLoanTracking(oldDebt, oldCollateral, newDebt); runMint.burnLosses(harden({ RUN: runAfter.vault }), vaultSeat); @@ -587,7 +568,6 @@ export const makeVaultKit = ( ); const oldDebt = getDebtAmount(); const oldCollateral = getCollateralAmount(); - trace('openLoan start: collateral', { oldDebt, oldCollateral }); // get the payout to provide access to the collateral if the // contract abandons @@ -604,7 +584,6 @@ export const makeVaultKit = ( Error('loan requested is too small; cannot accrue interest'), ); } - trace(idInManager, 'openLoan', { wantedRun, fee }, getCollateralAmount()); const runDebt = AmountMath.add(wantedRun, fee); await assertSufficientCollateral(collateralAmount, runDebt); @@ -617,11 +596,7 @@ export const makeVaultKit = ( ); manager.reallocateReward(fee, vaultSeat, seat); - trace( - 'openLoan about to refreshLoanTracking: collateral', - getCollateralAmount(), - ); - refreshLoanTracking(oldDebt, oldCollateral, runDebt, collateralAmount); + refreshLoanTracking(oldDebt, oldCollateral, runDebt); updateUiState(); From 21bc9a169387c11ead525a2083bf121404d5603c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 14 Feb 2022 16:46:11 -0800 Subject: [PATCH 35/47] docs --- packages/run-protocol/README.md | 10 ++++++++++ packages/run-protocol/src/vaultFactory/interest.js | 1 + .../test/vaultFactory/vault-contract-wrapper.js | 1 + 3 files changed, 12 insertions(+) diff --git a/packages/run-protocol/README.md b/packages/run-protocol/README.md index c9581621445..a3b9ffd92db 100644 --- a/packages/run-protocol/README.md +++ b/packages/run-protocol/README.md @@ -13,3 +13,13 @@ When any vat the ratio of the debt to the collateral exceeds a governed threshol ## Persistence The above states are robust to system restarts and upgrades. This is accomplished using the Agoric (Endo?) Collections API. + +## Debts + +Debts are denominated in µRUN. (1 million µRUN = 1 RUN) + +Each interest charging period (say daily) the actual debts in all vaults are affected. Materializing that across all vaults would be O(n) writes. Instead, to make charging interest O(1) we virtualize the debt that a vault owes to be a function of stable vault attributes and values that change in the vault manager when it charges interest. Specifically, +- a compoundedInterest value on the manager that keeps track of interest accrual since its launch +- a debtSnapshot on the vault by which one can calculate the actual debt + +To maintain that the keys of vaults to liquidate are stable requires that its keys are also time-independent so they're recorded as a "normalized collateralization ratio", with the actual collateral divided by the normalized debt. diff --git a/packages/run-protocol/src/vaultFactory/interest.js b/packages/run-protocol/src/vaultFactory/interest.js index d4d20e4f3f8..5921289ce3c 100644 --- a/packages/run-protocol/src/vaultFactory/interest.js +++ b/packages/run-protocol/src/vaultFactory/interest.js @@ -53,6 +53,7 @@ export const makeInterestCalculator = ( let growingDebt = newDebt; while (newRecent + chargingPeriod <= currentTime) { newRecent += chargingPeriod; + // The `ceil` implies that a vault with any debt will accrue at least one µRUN. const newInterest = ceilMultiplyBy(growingDebt, ratePerChargingPeriod); growingInterest = AmountMath.add(growingInterest, newInterest); growingDebt = AmountMath.add(growingDebt, newInterest, brand); diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index 6e24fc90720..afb83cae12c 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -123,6 +123,7 @@ export async function start(zcf, privateArgs) { // skip the debt calculation for this mock manager const currentInterestAsMultiplicand = makeRatio( + // @ts-ignore XXX can be cleaned up with https://github.com/Agoric/agoric-sdk/pull/4551 100n + currentInterest.numerator.value, currentInterest.numerator.brand, ); From 3e0d68584b0c8ac1122df354b47ab97c66dbf011 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 15 Feb 2022 09:58:40 -0800 Subject: [PATCH 36/47] prevent liquidating a vault that is already liquidating --- .../src/vaultFactory/liquidation.js | 2 -- .../src/vaultFactory/orderedVaultStore.js | 1 + .../src/vaultFactory/prioritizedVaults.js | 3 +- .../run-protocol/src/vaultFactory/vault.js | 3 ++ .../src/vaultFactory/vaultManager.js | 34 +++++++++++-------- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/liquidation.js b/packages/run-protocol/src/vaultFactory/liquidation.js index 2dbd47ae2ec..90e9972d963 100644 --- a/packages/run-protocol/src/vaultFactory/liquidation.js +++ b/packages/run-protocol/src/vaultFactory/liquidation.js @@ -31,8 +31,6 @@ const liquidate = async ( strategy, collateralBrand, ) => { - // ??? should we bail if it's already liquidating? - // if so should that be done here or throw here and managed at the caller vaultKit.actions.liquidating(); const runDebt = vaultKit.vault.getDebtAmount(); const { brand: runBrand } = runDebt; diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index fbb78dc1fae..ff6ed2b4cde 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -37,6 +37,7 @@ export const makeOrderedVaultStore = () => { key, }); store.init(key, vaultKit); + return key; }; /** diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 7bb124f8c8f..4c24bd9f2a4 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -147,10 +147,11 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * @param {VaultKit} vaultKit */ const addVaultKit = (vaultId, vaultKit) => { - vaults.addVaultKit(vaultId, vaultKit); + const key = vaults.addVaultKit(vaultId, vaultKit); const debtToCollateral = currentDebtToCollateral(vaultKit.vault); rescheduleIfHighest(debtToCollateral); + return key; }; /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index adad2c65764..1f732cbc6ef 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -276,6 +276,9 @@ export const makeVaultKit = ( }; const liquidating = () => { + if (vaultState === VaultState.LIQUIDATING) { + throw new Error('Vault already liquidating'); + } vaultState = VaultState.LIQUIDATING; updateUiState(); }; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 9eb3ab0631d..1e02a3181ef 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -291,7 +291,6 @@ export const makeVaultManager = ( const debtDelta = (oldDebt, newDebt) => { // Since newDebt includes accrued interest we need to use getDebtAmount() // to get a baseline that also includes accrued interest. - // eslint-disable-next-line no-use-before-define const priorDebtValue = oldDebt.value; const newDebtValue = newDebt.value; // We can't used AmountMath because the delta can be negative. @@ -410,20 +409,27 @@ export const makeVaultManager = ( actions: { openLoan }, } = vaultKit; assert(prioritizedVaults); - prioritizedVaults.addVaultKit(vaultId, vaultKit); + const addedVaultKey = prioritizedVaults.addVaultKit(vaultId, vaultKit); - const vaultResult = await openLoan(seat); - - seat.exit(); - - return harden({ - uiNotifier: vaultResult.notifier, - invitationMakers: Far('invitation makers', { - AdjustBalances: vault.makeAdjustBalancesInvitation, - CloseVault: vault.makeCloseInvitation, - }), - vault, - }); + try { + const vaultResult = await openLoan(seat); + + seat.exit(); + + return harden({ + uiNotifier: vaultResult.notifier, + invitationMakers: Far('invitation makers', { + AdjustBalances: vault.makeAdjustBalancesInvitation, + CloseVault: vault.makeCloseInvitation, + }), + vault, + }); + } catch (err) { + // remove it from prioritizedVaults + // XXX openLoan shouldn't assume it's already in the prioritizedVaults + prioritizedVaults.removeVault(addedVaultKey); + throw err; + } }; /** @type {VaultManager} */ From d139de3c161bd45b1d3d4c34a541170f08864267 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 15 Feb 2022 10:14:55 -0800 Subject: [PATCH 37/47] fix bug in getCollateralizationRatio --- packages/run-protocol/src/vaultFactory/vault.js | 9 +++++++-- .../run-protocol/test/vaultFactory/test-vaultFactory.js | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 1f732cbc6ef..17decd86c9f 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -99,6 +99,8 @@ export const makeVaultKit = ( }; /** + * Called whenever principal changes. + * * @param {Amount} newDebt - principal and all accrued interest */ const updateDebtSnapshot = newDebt => { @@ -133,7 +135,7 @@ export const makeVaultKit = ( * @see getNormalizedDebt * @returns {Amount} */ - // TODO rename to getActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 + // TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 const getDebtAmount = () => { // divide compounded interest by the the snapshot const interestSinceSnapshot = multiplyRatios( @@ -204,6 +206,9 @@ export const makeVaultKit = ( : getCollateralAllocated(vaultSeat); }; + /** + * @returns {Promise} Collateral over actual debt + */ const getCollateralizationRatio = async () => { const collateralAmount = getCollateralAmount(); @@ -217,7 +222,7 @@ export const makeVaultKit = ( return makeRatio(collateralAmount.value, runBrand, 1n); } const collateralValueInRun = getAmountOut(quoteAmount); - return makeRatioFromAmounts(collateralValueInRun, debtSnapshot.run); + return makeRatioFromAmounts(collateralValueInRun, getDebtAmount()); }; // call this whenever anything changes! diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index d19b35695e7..dc2b47c1fe2 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -904,6 +904,11 @@ test('interest on multiple vaults', async t => { aliceCollateralization.numerator.value > aliceCollateralization.denominator.value, ); + t.is( + aliceCollateralization.denominator.value, + aliceUpdate.value.debt.value, + `Debt in collateralizationRatio should match actual debt`, + ); const rewardAllocation = await E(vaultFactory).getRewardAllocation(); const rewardRunCount = aliceAddedDebt + bobAddedDebt + 1n; // +1 due to rounding From b9371871e3588c987d773f65805c0d416fd5ff3e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 15 Feb 2022 13:44:19 -0800 Subject: [PATCH 38/47] more specific test for snapshot state when opening vault after launch --- .../run-protocol/src/vaultFactory/vault.js | 2 +- .../test/vaultFactory/test-vaultFactory.js | 37 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 17decd86c9f..d08cc254ec7 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -89,7 +89,7 @@ export const makeVaultKit = ( const { brand: runBrand } = runMint.getIssuerRecord(); /** - * Snapshot of the debt and cmpouneded interest when the principal was last changed + * Snapshot of the debt and compouneded interest when the principal was last changed * * @type {{run: Amount, interest: Ratio}} */ diff --git a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js index dc2b47c1fe2..0702005bad1 100644 --- a/packages/run-protocol/test/vaultFactory/test-vaultFactory.js +++ b/packages/run-protocol/test/vaultFactory/test-vaultFactory.js @@ -861,6 +861,7 @@ test('interest on multiple vaults', async t => { ); // { chargingPeriod: weekly, recordingPeriod: weekly } + // Advance 8 days, past one charging and recording period for (let i = 0; i < 8; i += 1) { manualTimer.tick(); } @@ -934,22 +935,40 @@ test('interest on multiple vaults', async t => { ); await t.throwsAsync(E(caroleLoanSeat).getOfferResult()); - // open a vault with floor debt (1n RUN) + // Advance another 7 days, past one charging and recording period + for (let i = 0; i < 8; i += 1) { + manualTimer.tick(); + } + await waitForPromisesToSettle(); + + // open a vault when manager's interest already compounded + const wantedRun = 1_000n; const danLoanSeat = await E(zoe).offer( E(lender).makeLoanInvitation(), harden({ - give: { Collateral: AmountMath.make(aethBrand, 200n) }, - want: { RUN: AmountMath.make(runBrand, 1n) }, // smallest debt + give: { Collateral: AmountMath.make(aethBrand, 2_000n) }, + want: { RUN: AmountMath.make(runBrand, wantedRun) }, }), harden({ - Collateral: aethMint.mintPayment(AmountMath.make(aethBrand, 200n)), + Collateral: aethMint.mintPayment(AmountMath.make(aethBrand, 2_000n)), }), ); - /** @type {{vault: Vault}} */ - const { vault: danVault } = await E(danLoanSeat).getOfferResult(); - console.log('DEBUG', danVault); - t.is((await E(danVault).getDebtAmount()).value, 2n); - t.is((await E(danVault).getNormalizedDebt()).value, 1n); + /** @type {{vault: Vault, uiNotifier: Notifier<*>}} */ + const { vault: danVault, uiNotifier: danNotifier } = await E( + danLoanSeat, + ).getOfferResult(); + const danActualDebt = wantedRun + 50n; // includes fees + t.is((await E(danVault).getDebtAmount()).value, danActualDebt); + const normalizedDebt = (await E(danVault).getNormalizedDebt()).value; + t.true( + normalizedDebt < danActualDebt, + `Normalized debt ${normalizedDebt} must be less than actual ${danActualDebt} (after any time elapsed)`, + ); + t.is((await E(danVault).getNormalizedDebt()).value, 1_047n); + const danUpdate = await E(danNotifier).getUpdateSince(); + // snapshot should equal actual since no additional time has elapsed + const { debtSnapshot: danSnap } = danUpdate.value; + t.is(danSnap.run.value, danActualDebt); }); test('adjust balances', async t => { From 3410eb5fd21c6eacefbdeb942e89ba34da0a1cbd Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 16 Feb 2022 07:45:50 -0800 Subject: [PATCH 39/47] track debts as NatValue instead of Amount --- .../run-protocol/src/vaultFactory/interest.js | 18 +- .../src/vaultFactory/prioritizedVaults.js | 14 +- .../src/vaultFactory/storeUtils.js | 8 +- .../run-protocol/src/vaultFactory/types.js | 14 +- .../run-protocol/src/vaultFactory/vault.js | 11 +- .../src/vaultFactory/vaultManager.js | 74 ++--- .../test/vaultFactory/test-interest.js | 263 +++++++----------- packages/zoe/src/contractSupport/ratio.js | 4 + packages/zoe/src/contractSupport/types.js | 8 +- 9 files changed, 163 insertions(+), 251 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/interest.js b/packages/run-protocol/src/vaultFactory/interest.js index 5921289ce3c..c3d2459a6c2 100644 --- a/packages/run-protocol/src/vaultFactory/interest.js +++ b/packages/run-protocol/src/vaultFactory/interest.js @@ -2,12 +2,9 @@ import '@agoric/zoe/exported.js'; import '@agoric/zoe/src/contracts/callSpread/types.js'; +import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js'; +import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import './types.js'; -import { - ceilMultiplyBy, - makeRatio, -} from '@agoric/zoe/src/contractSupport/ratio.js'; -import { AmountMath } from '@agoric/ertp'; export const SECONDS_PER_YEAR = 60n * 60n * 24n * 365n; const BASIS_POINTS = 10000; @@ -15,14 +12,12 @@ const BASIS_POINTS = 10000; const LARGE_DENOMINATOR = BASIS_POINTS * BASIS_POINTS; /** - * @param {Brand} brand * @param {Ratio} annualRate * @param {RelativeTime} chargingPeriod * @param {RelativeTime} recordingPeriod * @returns {CalculatorKit} */ export const makeInterestCalculator = ( - brand, annualRate, chargingPeriod, recordingPeriod, @@ -54,9 +49,12 @@ export const makeInterestCalculator = ( while (newRecent + chargingPeriod <= currentTime) { newRecent += chargingPeriod; // The `ceil` implies that a vault with any debt will accrue at least one µRUN. - const newInterest = ceilMultiplyBy(growingDebt, ratePerChargingPeriod); - growingInterest = AmountMath.add(growingInterest, newInterest); - growingDebt = AmountMath.add(growingDebt, newInterest, brand); + const newInterest = natSafeMath.ceilDivide( + growingDebt * ratePerChargingPeriod.numerator.value, + ratePerChargingPeriod.denominator.value, + ); + growingInterest += newInterest; + growingDebt += newInterest; } return { latestInterestUpdate: newRecent, diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 4c24bd9f2a4..07e574d3abd 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -34,8 +34,8 @@ const ratioGTE = (left, right) => { /** * - * @param {Amount} debtAmount - * @param {Amount} collateralAmount + * @param {Amount} debtAmount + * @param {Amount} collateralAmount * @returns {Ratio} */ const calculateDebtToCollateral = (debtAmount, collateralAmount) => { @@ -132,8 +132,8 @@ export const makePrioritizedVaults = reschedulePriceCheck => { /** * - * @param {Amount} oldDebt - * @param {Amount} oldCollateral + * @param {Amount} oldDebt + * @param {Amount} oldCollateral * @param {string} vaultId */ const removeVaultByAttributes = (oldDebt, oldCollateral, vaultId) => { @@ -162,7 +162,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { * Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857 * * @param {Ratio} ratio - * @yields {[string, VaultKit]>} + * @yields {[string, VaultKit]} * @returns {IterableIterator<[string, VaultKit]>} */ // eslint-disable-next-line func-names @@ -180,8 +180,8 @@ export const makePrioritizedVaults = reschedulePriceCheck => { } /** - * @param {Amount} oldDebt - * @param {Amount} oldCollateral + * @param {Amount} oldDebt + * @param {Amount} oldCollateral * @param {string} vaultId */ const refreshVaultPriority = (oldDebt, oldCollateral, vaultId) => { diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index 892996b24f8..a52cfa56bb7 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -66,8 +66,8 @@ const dbEntryKeyToNumber = k => { * Overcollateralized are greater than one. * The more undercollaterized the smaller in [0-1]. * - * @param {Amount} normalizedDebt normalized (not actual) total debt - * @param {Amount} collateral + * @param {Amount} normalizedDebt normalized (not actual) total debt + * @param {Amount} collateral * @returns {number} */ const collateralizationRatio = (normalizedDebt, collateral) => { @@ -81,8 +81,8 @@ const collateralizationRatio = (normalizedDebt, collateral) => { /** * Sorts by ratio in descending debt. Ordering of vault id is undefined. * - * @param {Amount} normalizedDebt normalized (not actual) total debt - * @param {Amount} collateral + * @param {Amount} normalizedDebt normalized (not actual) total debt + * @param {Amount} collateral * @param {VaultId} vaultId * @returns {string} lexically sortable string in which highest debt-to-collateral is earliest */ diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index baee9e72f8e..99e7a4cd0c1 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -59,8 +59,8 @@ /** * @typedef {Object} BaseUIState - * @property {Amount} locked Amount of Collateral locked - * @property {Amount} debt Amount of Loan (including accrued interest) + * @property {Amount} locked Amount of Collateral locked + * @property {Amount} debt Amount of Loan (including accrued interest) * @property {Ratio} collateralizationRatio */ @@ -121,9 +121,9 @@ /** * @typedef {Object} BaseVault - * @property {() => Amount} getCollateralAmount - * @property {() => Amount} getDebtAmount - * @property {() => Amount} getNormalizedDebt + * @property {() => Amount} getCollateralAmount + * @property {() => Amount} getDebtAmount + * @property {() => Amount} getNormalizedDebt * * @typedef {BaseVault & VaultMixin} Vault * @typedef {Object} VaultMixin @@ -181,8 +181,8 @@ /** * @typedef {Object} DebtStatus * @property {Timestamp} latestInterestUpdate - * @property {Amount} interest - * @property {Amount} newDebt + * @property {NatValue} interest + * @property {NatValue} newDebt */ /** diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index d08cc254ec7..d20c0e7d413 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -94,7 +94,7 @@ export const makeVaultKit = ( * @type {{run: Amount, interest: Ratio}} */ let debtSnapshot = { - run: AmountMath.makeEmpty(runBrand), + run: AmountMath.makeEmpty(runBrand, 'nat'), interest: manager.getCompoundedInterest(), }; @@ -133,7 +133,7 @@ export const makeVaultKit = ( * what interest has compounded since this vault record was written. * * @see getNormalizedDebt - * @returns {Amount} + * @returns {Amount} */ // TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 const getDebtAmount = () => { @@ -153,7 +153,7 @@ export const makeVaultKit = ( * the interest accrues. * * @see getActualDebAmount - * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued + * @returns {Amount} as if the vault was open at the launch of this manager, before any interest accrued */ // Not in use until https://github.com/Agoric/agoric-sdk/issues/4540 const getNormalizedDebt = () => { @@ -199,6 +199,10 @@ export const makeVaultKit = ( ); }; + /** + * + * @returns {Amount} + */ const getCollateralAmount = () => { // getCollateralAllocated would return final allocations return vaultSeat.hasExited() @@ -584,6 +588,7 @@ export const makeVaultKit = ( want: { RUN: wantedRun }, } = seat.getProposal(); + if (typeof wantedRun.value !== 'bigint') throw new Error(); // todo trigger process() check right away, in case the price dropped while we ran const fee = ceilMultiplyBy(wantedRun, manager.getLoanFee()); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 1e02a3181ef..20d0759c746 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -75,6 +75,7 @@ export const makeVaultManager = ( harden({ compoundedInterest: makeRatio(1n, runBrand, 1n, runBrand), latestInterestUpdate: 0n, + // XXX since debt will always be in RUN, no need to wrap in an Amount totalDebt: AmountMath.makeEmpty(runBrand), }), ); @@ -113,8 +114,8 @@ export const makeVaultManager = ( let prioritizedVaults; /** @type {MutableQuote=} */ let outstandingQuote; - /** @type {Amount} */ - let totalDebt = AmountMath.makeEmpty(runBrand); + /** @type {NatValue} */ + let totalDebt = 0n; /** @type {Ratio}} */ let compoundedInterest = makeRatio(100n, runBrand); // starts at 1.0, no interest @@ -235,7 +236,6 @@ export const makeVaultManager = ( const chargeAllVaults = async (updateTime, poolIncrementSeat) => { trace('chargeAllVault', { updateTime }); const interestCalculator = makeInterestCalculator( - runBrand, shared.getInterestRate(), shared.getChargingPeriod(), shared.getRecordingPeriod(), @@ -246,27 +246,28 @@ export const makeVaultManager = ( { latestInterestUpdate, newDebt: totalDebt, - interest: AmountMath.makeEmpty(runBrand), + interest: 0n, }, updateTime, ); const interestAccrued = debtStatus.interest; // done if none - if (AmountMath.isEmpty(interestAccrued)) { + if (interestAccrued === 0n) { return; } // compoundedInterest *= debtStatus.newDebt / totalDebt; compoundedInterest = multiplyRatios( compoundedInterest, - makeRatioFromAmounts(debtStatus.newDebt, totalDebt), + makeRatio(debtStatus.newDebt, runBrand, totalDebt, runBrand), ); - totalDebt = AmountMath.add(totalDebt, interestAccrued); + totalDebt += interestAccrued; // mint that much RUN for the reward pool - runMint.mintGains(harden({ RUN: interestAccrued }), poolIncrementSeat); - reallocateReward(interestAccrued, poolIncrementSeat); + const rewarded = AmountMath.make(runBrand, interestAccrued); + runMint.mintGains(harden({ RUN: rewarded }), poolIncrementSeat); + reallocateReward(rewarded, poolIncrementSeat); // update running tally of total debt against this collateral ({ latestInterestUpdate } = debtStatus); @@ -274,7 +275,7 @@ export const makeVaultManager = ( const payload = harden({ compoundedInterest, latestInterestUpdate, - totalDebt, + totalDebt: AmountMath.make(runBrand, totalDebt), }); updater.updateState(payload); @@ -284,38 +285,12 @@ export const makeVaultManager = ( }; /** - * @param {Amount} oldDebt - principal and all accrued interest - * @param {Amount} newDebt - principal and all accrued interest - * @returns {bigint} in brand of the manager's debt - */ - const debtDelta = (oldDebt, newDebt) => { - // Since newDebt includes accrued interest we need to use getDebtAmount() - // to get a baseline that also includes accrued interest. - const priorDebtValue = oldDebt.value; - const newDebtValue = newDebt.value; - // We can't used AmountMath because the delta can be negative. - assert.typeof( - priorDebtValue, - 'bigint', - 'vault debt supports only bigint amounts', - ); - assert.typeof( - newDebtValue, - 'bigint', - 'vault debt supports only bigint amounts', - ); - return newDebtValue - priorDebtValue; - }; - - /** - * @param {Amount} oldDebtOnVault - * @param {Amount} newDebtOnVault + * @param {Amount} oldDebtOnVault + * @param {Amount} newDebtOnVault */ const applyDebtDelta = (oldDebtOnVault, newDebtOnVault) => { - const delta = debtDelta(oldDebtOnVault, newDebtOnVault); - trace( - `updating total debt of ${totalDebt.value} ${totalDebt.brand} by ${delta}`, - ); + const delta = newDebtOnVault.value - oldDebtOnVault.value; + trace(`updating total debt ${totalDebt} by ${delta}`); if (delta === 0n) { // nothing to do return; @@ -323,28 +298,19 @@ export const makeVaultManager = ( if (delta > 0n) { // add the amount - totalDebt = AmountMath.add( - totalDebt, - AmountMath.make(totalDebt.brand, delta), - ); + totalDebt += delta; } else { // negate the amount so that it's a natural number, then subtract const absDelta = -delta; - assert( - !(absDelta > totalDebt.value), - 'Negative delta greater than total debt', - ); - totalDebt = AmountMath.subtract( - totalDebt, - AmountMath.make(totalDebt.brand, absDelta), - ); + assert(!(absDelta > totalDebt), 'Negative delta greater than total debt'); + totalDebt -= absDelta; } trace('applyDebtDelta complete', { totalDebt }); }; /** - * @param {Amount} oldDebt - * @param {Amount} oldCollateral + * @param {Amount} oldDebt + * @param {Amount} oldCollateral * @param {VaultId} vaultId */ const updateVaultPriority = (oldDebt, oldCollateral, vaultId) => { diff --git a/packages/run-protocol/test/vaultFactory/test-interest.js b/packages/run-protocol/test/vaultFactory/test-interest.js index 5fd1a75eba2..7c8183c69da 100644 --- a/packages/run-protocol/test/vaultFactory/test-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-interest.js @@ -2,7 +2,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import '@agoric/zoe/exported.js'; -import { makeIssuerKit, AmountMath } from '@agoric/ertp'; +import { makeIssuerKit } from '@agoric/ertp'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; import { @@ -20,119 +20,98 @@ const TEN_MILLION = 10000000n; test('too soon', async t => { const { brand } = makeIssuerKit('ducats'); const calculator = makeInterestCalculator( - brand, makeRatio(1n * SECONDS_PER_YEAR, brand), 3n, 6n, ); const debtStatus = { - newDebt: AmountMath.make(brand, 1000n), + newDebt: 1000n, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // no interest because the charging period hasn't elapsed t.deepEqual(calculator.calculate(debtStatus, 12n), { latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, 1000n), + interest: 0n, + newDebt: 1000n, }); }); test('basic charge 1 period', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 0n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // 7n is daily interest of 2.5% APR on 100k. Compounding is in the noise. t.deepEqual(calculator.calculate(debtStatus, ONE_DAY), { latestInterestUpdate: ONE_DAY, - interest: AmountMath.make(brand, 7n), - newDebt: AmountMath.make(brand, 100007n), + interest: 7n, + newDebt: 100007n, }); }); test('basic 2 charge periods', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: ONE_DAY, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // 14n is 2x daily (from day 1 to day 3) interest of 2.5% APR on 100k. // Compounding is in the noise. t.deepEqual(calculator.calculate(debtStatus, ONE_DAY * 3n), { latestInterestUpdate: ONE_DAY * 3n, - interest: AmountMath.make(brand, 14n), - newDebt: AmountMath.make(brand, 100014n), + interest: 14n, + newDebt: 100014n, }); }); test('partial periods', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // just less than three days gets two days of interest (7n/day) t.deepEqual(calculator.calculate(debtStatus, ONE_DAY * 3n - 1n), { latestInterestUpdate: 10n + ONE_DAY * 2n, - interest: AmountMath.make(brand, 14n), - newDebt: AmountMath.make(brand, 100014n), + interest: 14n, + newDebt: 100014n, }); }); test('reportingPeriod: partial', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // charge at reporting period intervals t.deepEqual(calculator.calculateReportingPeriod(debtStatus, ONE_MONTH), { latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + interest: 0n, + newDebt: HUNDRED_THOUSAND, }); // charge daily, record monthly. After a month, charge 30 * 7n t.deepEqual( calculator.calculateReportingPeriod(debtStatus, ONE_DAY + ONE_MONTH), { latestInterestUpdate: 10n + ONE_MONTH, - interest: AmountMath.make(brand, 210n), - newDebt: AmountMath.make(brand, 100210n), + interest: 210n, + newDebt: 100210n, }, ); }); @@ -140,16 +119,11 @@ test('reportingPeriod: partial', async t => { test('reportingPeriod: longer', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_MONTH, - ONE_DAY, - ); + const calculator = makeInterestCalculator(annualRate, ONE_MONTH, ONE_DAY); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // charge monthly, record daily. 2.5% APR compounded monthly rate is 204 BP. // charge at reporting period intervals @@ -157,8 +131,8 @@ test('reportingPeriod: longer', async t => { calculator.calculateReportingPeriod(debtStatus, ONE_MONTH + ONE_DAY), { latestInterestUpdate: ONE_MONTH + 10n, - interest: AmountMath.make(brand, 204n), - newDebt: AmountMath.make(brand, 100204n), + interest: 204n, + newDebt: 100204n, }, ); }); @@ -166,44 +140,34 @@ test('reportingPeriod: longer', async t => { test('start charging later', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 16n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // from a baseline of 16n, we don't charge interest until the timer gets to // ONE_DAY plus 16n. t.deepEqual(calculator.calculate(debtStatus, ONE_DAY), { latestInterestUpdate: 16n, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + interest: 0n, + newDebt: HUNDRED_THOUSAND, }); t.deepEqual(calculator.calculate(debtStatus, ONE_DAY + 16n), { latestInterestUpdate: ONE_DAY + 16n, - interest: AmountMath.make(brand, 7n), - newDebt: AmountMath.make(brand, 100007n), + interest: 7n, + newDebt: 100007n, }); }); test('simple compounding', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); const debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; // 30 days of 7n interest per day. Compounding is in the noise. // charge at reporting period intervals @@ -211,8 +175,8 @@ test('simple compounding', async t => { calculator.calculateReportingPeriod(debtStatus, ONE_MONTH + ONE_DAY), { latestInterestUpdate: ONE_MONTH + 10n, - interest: AmountMath.make(brand, 210n), - newDebt: AmountMath.make(brand, 100210n), + interest: 210n, + newDebt: 100210n, }, ); }); @@ -220,21 +184,16 @@ test('simple compounding', async t => { test('reportingPeriod shorter than charging', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_MONTH, - ONE_DAY, - ); + const calculator = makeInterestCalculator(annualRate, ONE_MONTH, ONE_DAY); let debtStatus = { - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + newDebt: HUNDRED_THOUSAND, latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), + interest: 0n, }; const afterOneMonth = { latestInterestUpdate: 10n, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + interest: 0n, + newDebt: HUNDRED_THOUSAND, }; // charging period is 30 days. interest isn't charged until then. t.deepEqual(calculator.calculate(debtStatus, ONE_DAY), afterOneMonth); @@ -244,19 +203,19 @@ test('reportingPeriod shorter than charging', async t => { t.deepEqual(calculator.calculate(debtStatus, 29n * ONE_DAY), afterOneMonth); t.deepEqual(calculator.calculate(debtStatus, ONE_MONTH + 10n), { latestInterestUpdate: ONE_MONTH + 10n, - interest: AmountMath.make(brand, 204n), - newDebt: AmountMath.make(brand, 100204n), + interest: 204n, + newDebt: 100204n, }); debtStatus = { - newDebt: AmountMath.make(brand, 100204n), - interest: AmountMath.make(brand, 204n), + newDebt: 100204n, + interest: 204n, latestInterestUpdate: ONE_MONTH, }; const afterTwoMonths = { latestInterestUpdate: ONE_MONTH, - interest: AmountMath.make(brand, 204n), - newDebt: AmountMath.make(brand, 100204n), + interest: 204n, + newDebt: 100204n, }; // charging period is 30 days. 2nd interest isn't charged until 60 days. t.deepEqual(calculator.calculate(debtStatus, 32n * ONE_DAY), afterTwoMonths); @@ -265,29 +224,24 @@ test('reportingPeriod shorter than charging', async t => { t.deepEqual(calculator.calculate(debtStatus, 59n * ONE_DAY), afterTwoMonths); t.deepEqual(calculator.calculate(debtStatus, 60n * ONE_DAY), { latestInterestUpdate: 2n * ONE_MONTH, - interest: AmountMath.make(brand, 408n), - newDebt: AmountMath.make(brand, 100408n), + interest: 408n, + newDebt: 100408n, }); }); test('reportingPeriod shorter than charging; start day boundary', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_MONTH, - ONE_DAY, - ); + const calculator = makeInterestCalculator(annualRate, ONE_MONTH, ONE_DAY); const startOneDay = { latestInterestUpdate: ONE_DAY, - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), - interest: AmountMath.makeEmpty(brand), + newDebt: HUNDRED_THOUSAND, + interest: 0n, }; const afterOneDay = { latestInterestUpdate: ONE_DAY, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, HUNDRED_THOUSAND), + interest: 0n, + newDebt: HUNDRED_THOUSAND, }; // no interest charged before a month elapses t.deepEqual(calculator.calculate(startOneDay, 4n * ONE_DAY), afterOneDay); @@ -298,8 +252,8 @@ test('reportingPeriod shorter than charging; start day boundary', async t => { const afterAMonth = { latestInterestUpdate: ONE_MONTH + ONE_DAY, - interest: AmountMath.make(brand, 204n), - newDebt: AmountMath.make(brand, 100204n), + interest: 204n, + newDebt: 100204n, }; // 204n is 2.5% APR charged monthly t.deepEqual( @@ -311,21 +265,16 @@ test('reportingPeriod shorter than charging; start day boundary', async t => { test('reportingPeriod shorter than charging; start not even days', async t => { const { brand } = makeIssuerKit('ducats'); const annualRate = makeRatio(250n, brand, BASIS_POINTS); - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_MONTH, - ONE_DAY, - ); + const calculator = makeInterestCalculator(annualRate, ONE_MONTH, ONE_DAY); const startPartialDay = { latestInterestUpdate: 20n, - newDebt: AmountMath.make(brand, 101000n), - interest: AmountMath.makeEmpty(brand), + newDebt: 101000n, + interest: 0n, }; const afterOneMonth = { latestInterestUpdate: 20n, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, 101000n), + interest: 0n, + newDebt: 101000n, }; t.deepEqual(calculator.calculate(startPartialDay, ONE_MONTH), afterOneMonth); t.deepEqual( @@ -335,8 +284,8 @@ test('reportingPeriod shorter than charging; start not even days', async t => { // interest not charged until ONE_MONTH + 20n t.deepEqual(calculator.calculate(startPartialDay, ONE_MONTH + 20n), { latestInterestUpdate: 20n + ONE_MONTH, - interest: AmountMath.make(brand, 206n), - newDebt: AmountMath.make(brand, 101206n), + interest: 206n, + newDebt: 101206n, }); }); @@ -346,48 +295,43 @@ test('basic charge large numbers, compounding', async t => { const annualRate = makeRatio(250n, brand, BASIS_POINTS); // Unix epoch time: Tuesday April 6th 2021 at 11:45am PT const START_TIME = 1617734746n; - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); // TEN_MILLION is enough to observe compounding const debtStatus = { - newDebt: AmountMath.make(brand, TEN_MILLION), - interest: AmountMath.makeEmpty(brand), + newDebt: TEN_MILLION, + interest: 0n, latestInterestUpdate: START_TIME, }; t.deepEqual(calculator.calculate(debtStatus, START_TIME), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }); t.deepEqual(calculator.calculate(debtStatus, START_TIME + 1n), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }); // 677n is one day's interest on TEN_MILLION at 2.5% APR, rounded up. t.deepEqual(calculator.calculate(debtStatus, START_TIME + ONE_DAY), { latestInterestUpdate: START_TIME + ONE_DAY, - interest: AmountMath.make(brand, 677n), - newDebt: AmountMath.make(brand, 10000677n), + interest: 677n, + newDebt: 10000677n, }); // two days interest. compounding not visible t.deepEqual( calculator.calculate(debtStatus, START_TIME + ONE_DAY + ONE_DAY), { latestInterestUpdate: START_TIME + ONE_DAY + ONE_DAY, - interest: AmountMath.make(brand, 1354n), - newDebt: AmountMath.make(brand, 10001354n), + interest: 1354n, + newDebt: 10001354n, }, ); // Notice that interest compounds 30 days * 677 = 20310 < 20329 t.deepEqual(calculator.calculate(debtStatus, START_TIME + ONE_MONTH), { latestInterestUpdate: START_TIME + ONE_MONTH, - interest: AmountMath.make(brand, 20329n), - newDebt: AmountMath.make(brand, 10020329n), + interest: 20329n, + newDebt: 10020329n, }); }); @@ -398,38 +342,33 @@ test('basic charge reasonable numbers monthly', async t => { const annualRate = makeRatio(250n, brand, BASIS_POINTS); // Unix epoch time: Tuesday April 6th 2021 at 11:45am PT const START_TIME = 1617734746n; - const calculator = makeInterestCalculator( - brand, - annualRate, - ONE_DAY, - ONE_MONTH, - ); + const calculator = makeInterestCalculator(annualRate, ONE_DAY, ONE_MONTH); // TEN_MILLION is enough to observe compounding const debtStatus = { - newDebt: AmountMath.make(brand, TEN_MILLION), - interest: AmountMath.makeEmpty(brand), + newDebt: TEN_MILLION, + interest: 0n, latestInterestUpdate: START_TIME, }; // don't charge, since a month hasn't elapsed t.deepEqual(calculator.calculateReportingPeriod(debtStatus, START_TIME), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }); t.deepEqual( calculator.calculateReportingPeriod(debtStatus, START_TIME + 1n), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }, ); t.deepEqual( calculator.calculateReportingPeriod(debtStatus, START_TIME + ONE_DAY), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }, ); t.deepEqual( @@ -439,8 +378,8 @@ test('basic charge reasonable numbers monthly', async t => { ), { latestInterestUpdate: START_TIME, - interest: AmountMath.makeEmpty(brand), - newDebt: AmountMath.make(brand, TEN_MILLION), + interest: 0n, + newDebt: TEN_MILLION, }, ); @@ -449,8 +388,8 @@ test('basic charge reasonable numbers monthly', async t => { calculator.calculateReportingPeriod(debtStatus, START_TIME + ONE_MONTH), { latestInterestUpdate: START_TIME + ONE_MONTH, - interest: AmountMath.make(brand, 20329n), - newDebt: AmountMath.make(brand, 10020329n), + interest: 20329n, + newDebt: 10020329n, }, ); const HALF_YEAR = 6n * ONE_MONTH; @@ -460,8 +399,8 @@ test('basic charge reasonable numbers monthly', async t => { calculator.calculateReportingPeriod(debtStatus, START_TIME + HALF_YEAR), { latestInterestUpdate: START_TIME + HALF_YEAR, - interest: AmountMath.make(brand, 122601n), - newDebt: AmountMath.make(brand, 10122601n), + interest: 122601n, + newDebt: 10122601n, }, ); // compounding: 360 days * 677 = 243720 < 246705 @@ -469,8 +408,8 @@ test('basic charge reasonable numbers monthly', async t => { calculator.calculateReportingPeriod(debtStatus, START_TIME + ONE_YEAR), { latestInterestUpdate: START_TIME + ONE_YEAR, - interest: AmountMath.make(brand, 246705n), - newDebt: AmountMath.make(brand, 10246705n), + interest: 246705n, + newDebt: 10246705n, }, ); }); diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 6d7fa7f4539..a42a3c4518e 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -121,11 +121,13 @@ const multiplyHelper = (amount, ratio, divideOp) => { /** @type {FloorMultiplyBy} */ export const floorMultiplyBy = (amount, ratio) => { + // @ts-ignore cast return multiplyHelper(amount, ratio, floorDivide); }; /** @type {CeilMultiplyBy} */ export const ceilMultiplyBy = (amount, ratio) => { + // @ts-ignore cast return multiplyHelper(amount, ratio, ceilDivide); }; @@ -150,11 +152,13 @@ const divideHelper = (amount, ratio, divideOp) => { /** @type {FloorDivideBy} */ export const floorDivideBy = (amount, ratio) => { + // @ts-ignore cast return divideHelper(amount, ratio, floorDivide); }; /** @type {CeilDivideBy} */ export const ceilDivideBy = (amount, ratio) => { + // @ts-ignore cast return divideHelper(amount, ratio, ceilDivide); }; diff --git a/packages/zoe/src/contractSupport/types.js b/packages/zoe/src/contractSupport/types.js index 64c4ddf5d3a..01a4197aac6 100644 --- a/packages/zoe/src/contractSupport/types.js +++ b/packages/zoe/src/contractSupport/types.js @@ -123,22 +123,22 @@ /** * @typedef {Object} Ratio - * @property {Amount} numerator - * @property {Amount} denominator + * @property {Amount} numerator + * @property {Amount} denominator */ /** * @callback MultiplyBy * @param {Amount} amount * @param {Ratio} ratio - * @returns {Amount} + * @returns {Amount} */ /** * @callback DivideBy * @param {Amount} amount * @param {Ratio} ratio - * @returns {Amount} + * @returns {Amount} */ /** From 754c0855825b07d12ffe0f3062ad136af7c58a25 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 16 Feb 2022 10:49:27 -0800 Subject: [PATCH 40/47] factor out and test calculateCompoundedInterest() --- .../run-protocol/src/vaultFactory/interest.js | 27 +++++++++++++- .../src/vaultFactory/vaultManager.js | 12 ++++--- .../test/vaultFactory/test-interest.js | 36 ++++++++++++++++++- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/interest.js b/packages/run-protocol/src/vaultFactory/interest.js index c3d2459a6c2..8414d649159 100644 --- a/packages/run-protocol/src/vaultFactory/interest.js +++ b/packages/run-protocol/src/vaultFactory/interest.js @@ -3,7 +3,10 @@ import '@agoric/zoe/exported.js'; import '@agoric/zoe/src/contracts/callSpread/types.js'; import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js'; -import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { + makeRatio, + multiplyRatios, +} from '@agoric/zoe/src/contractSupport/ratio.js'; import './types.js'; export const SECONDS_PER_YEAR = 60n * 60n * 24n * 365n; @@ -82,3 +85,25 @@ export const makeInterestCalculator = ( calculateReportingPeriod, }); }; + +/** + * compoundedInterest *= (new debt) / (prior total debt) + * + * @param {Ratio} priorCompoundedInterest + * @param {NatValue} priorDebt + * @param {NatValue} newDebt + */ +export const calculateCompoundedInterest = ( + priorCompoundedInterest, + priorDebt, + newDebt, +) => { + const brand = priorCompoundedInterest.numerator.brand; + if (priorDebt === 0n) { + throw new Error('No interest on zero debt'); + } + return multiplyRatios( + priorCompoundedInterest, + makeRatio(newDebt, brand, priorDebt, brand), + ); +}; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 20d0759c746..228da7c0839 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -11,7 +11,6 @@ import { ceilMultiplyBy, ceilDivideBy, makeRatio, - multiplyRatios, } from '@agoric/zoe/src/contractSupport/index.js'; import { makeNotifierKit, observeNotifier } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp'; @@ -29,7 +28,10 @@ import { INTEREST_RATE_KEY, CHARGING_PERIOD_KEY, } from './params.js'; -import { makeInterestCalculator } from './interest.js'; +import { + calculateCompoundedInterest, + makeInterestCalculator, +} from './interest.js'; const { details: X } = assert; @@ -257,10 +259,10 @@ export const makeVaultManager = ( return; } - // compoundedInterest *= debtStatus.newDebt / totalDebt; - compoundedInterest = multiplyRatios( + compoundedInterest = calculateCompoundedInterest( compoundedInterest, - makeRatio(debtStatus.newDebt, runBrand, totalDebt, runBrand), + totalDebt, + debtStatus.newDebt, ); totalDebt += interestAccrued; diff --git a/packages/run-protocol/test/vaultFactory/test-interest.js b/packages/run-protocol/test/vaultFactory/test-interest.js index 7c8183c69da..86681c8ca8c 100644 --- a/packages/run-protocol/test/vaultFactory/test-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-interest.js @@ -4,8 +4,9 @@ import '@agoric/zoe/exported.js'; import { makeIssuerKit } from '@agoric/ertp'; import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; - +import { Far } from '@endo/marshal'; import { + calculateCompoundedInterest, makeInterestCalculator, SECONDS_PER_YEAR, } from '../../src/vaultFactory/interest.js'; @@ -413,3 +414,36 @@ test('basic charge reasonable numbers monthly', async t => { }, ); }); + +test('calculateCompoundedInterest on zero debt', t => { + const brand = Far('brand'); + t.throws(() => + calculateCompoundedInterest(makeRatio(0n, brand, 1n, brand), 0n, 100n), + ); +}); + +// -illions +const B = 1_000_000_000n; +const Q = B * B; + +test('calculateCompoundedInterest', t => { + const brand = Far('brand'); + const cases = [ + [1n, 1n, 1n, 1n, 1n, 1n], // no charge + [1n, 1n, 1n, 2n, 2n, 1n], // doubled + [11n, 10n, 1n, 2n * Q, 22n * Q, 10n], // >1 previous interest + [1n, 1n, 1n, 2n * Q, 2n * Q, 1n], // ludicrous rate greater than Number.MAX_SAFE_INTEGER + [2n, 1n, 1n * Q, 2n * Q, 4n * Q, 1n * Q], // 2x on huge debt + [4n * Q, 1n * Q, 4n * Q, 8n * Q, 32n * Q * Q, 4n * Q * Q], // 2x again + ]; + for (const [priorNum, priorDen, oldDebt, newDebt, newNum, newDen] of cases) { + const oldInterest = makeRatio(priorNum, brand, priorDen, brand); + const expectedInterest = makeRatio(newNum, brand, newDen, brand); + const compounded = calculateCompoundedInterest( + oldInterest, + oldDebt, + newDebt, + ); + t.deepEqual(compounded, expectedInterest); + } +}); From 5d43e74de66ce7922243a1b58b6095e1eff0f4d3 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 16 Feb 2022 11:25:22 -0800 Subject: [PATCH 41/47] arbitrary precision in compound interest state --- .../run-protocol/src/vaultFactory/interest.js | 12 ++-- .../run-protocol/src/vaultFactory/types.js | 4 +- .../src/vaultFactory/vaultManager.js | 7 +- .../test/vaultFactory/test-interest.js | 72 ++++++++++++++----- packages/zoe/src/contractSupport/ratio.js | 29 ++++++-- packages/zoe/src/contractSupport/types.js | 16 +---- .../unitTests/contractSupport/test-ratio.js | 37 +++++++++- 7 files changed, 132 insertions(+), 45 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/interest.js b/packages/run-protocol/src/vaultFactory/interest.js index 8414d649159..9866976dc65 100644 --- a/packages/run-protocol/src/vaultFactory/interest.js +++ b/packages/run-protocol/src/vaultFactory/interest.js @@ -6,6 +6,7 @@ import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js'; import { makeRatio, multiplyRatios, + quantize, } from '@agoric/zoe/src/contractSupport/ratio.js'; import './types.js'; @@ -14,6 +15,11 @@ const BASIS_POINTS = 10000; // single digit APR is less than a basis point per day. const LARGE_DENOMINATOR = BASIS_POINTS * BASIS_POINTS; +/** + * Number chosen from 6 digits for a basis point, doubled for multiplication. + */ +const COMPOUNDED_INTEREST_DENOMINATOR = 10n ** 20n; + /** * @param {Ratio} annualRate * @param {RelativeTime} chargingPeriod @@ -99,11 +105,9 @@ export const calculateCompoundedInterest = ( newDebt, ) => { const brand = priorCompoundedInterest.numerator.brand; - if (priorDebt === 0n) { - throw new Error('No interest on zero debt'); - } - return multiplyRatios( + const compounded = multiplyRatios( priorCompoundedInterest, makeRatio(newDebt, brand, priorDebt, brand), ); + return quantize(compounded, COMPOUNDED_INTEREST_DENOMINATOR); }; diff --git a/packages/run-protocol/src/vaultFactory/types.js b/packages/run-protocol/src/vaultFactory/types.js index 99e7a4cd0c1..a6acc74c8e3 100644 --- a/packages/run-protocol/src/vaultFactory/types.js +++ b/packages/run-protocol/src/vaultFactory/types.js @@ -181,8 +181,8 @@ /** * @typedef {Object} DebtStatus * @property {Timestamp} latestInterestUpdate - * @property {NatValue} interest - * @property {NatValue} newDebt + * @property {NatValue} interest interest accrued since latestInterestUpdate + * @property {NatValue} newDebt total including principal and interest */ /** diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 228da7c0839..a1a0a57b0f5 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -248,7 +248,7 @@ export const makeVaultManager = ( { latestInterestUpdate, newDebt: totalDebt, - interest: 0n, + interest: 0n, // XXX this is always zero, doesn't need to be an option }, updateTime, ); @@ -259,6 +259,11 @@ export const makeVaultManager = ( return; } + // NB: This method of inferring the compounded rate from the ratio of debts + // acrrued suffers slightly from the integer nature of debts. However in + // testing with small numbers there's 5 digits of precision, and with large + // numbers the ratios tend towards ample precision. Because this calculation + // is over all debts of the vault the numbers will be reliably large. compoundedInterest = calculateCompoundedInterest( compoundedInterest, totalDebt, diff --git a/packages/run-protocol/test/vaultFactory/test-interest.js b/packages/run-protocol/test/vaultFactory/test-interest.js index 86681c8ca8c..b7d859f7d17 100644 --- a/packages/run-protocol/test/vaultFactory/test-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-interest.js @@ -2,8 +2,11 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import '@agoric/zoe/exported.js'; -import { makeIssuerKit } from '@agoric/ertp'; -import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { AmountMath, makeIssuerKit } from '@agoric/ertp'; +import { + ceilMultiplyBy, + makeRatio, +} from '@agoric/zoe/src/contractSupport/ratio.js'; import { Far } from '@endo/marshal'; import { calculateCompoundedInterest, @@ -423,27 +426,60 @@ test('calculateCompoundedInterest on zero debt', t => { }); // -illions -const B = 1_000_000_000n; -const Q = B * B; +const M = 1_000_000n; test('calculateCompoundedInterest', t => { const brand = Far('brand'); + /** @type {Array<[bigint, bigint, bigint, number, bigint, number]>} */ const cases = [ - [1n, 1n, 1n, 1n, 1n, 1n], // no charge - [1n, 1n, 1n, 2n, 2n, 1n], // doubled - [11n, 10n, 1n, 2n * Q, 22n * Q, 10n], // >1 previous interest - [1n, 1n, 1n, 2n * Q, 2n * Q, 1n], // ludicrous rate greater than Number.MAX_SAFE_INTEGER - [2n, 1n, 1n * Q, 2n * Q, 4n * Q, 1n * Q], // 2x on huge debt - [4n * Q, 1n * Q, 4n * Q, 8n * Q, 32n * Q * Q, 4n * Q * Q], // 2x again + [250n, BASIS_POINTS, M, 1, 1025000n, 10], // 2.5% APR over 1 year yields 2.5% + [250n, BASIS_POINTS, M, 10, 1280090n, 5], // 2.5% APR over 10 year yields 28% + // XXX resolution was 12 with banker's rounding https://github.com/Agoric/agoric-sdk/issues/4573 + [250n, BASIS_POINTS, M * M, 10, 1280084544199n, 8], // 2.5% APR over 10 year yields 28% + [250n, BASIS_POINTS, M, 100, 11813903n, 5], // 2.5% APR over 10 year yields 1181% ]; - for (const [priorNum, priorDen, oldDebt, newDebt, newNum, newDen] of cases) { - const oldInterest = makeRatio(priorNum, brand, priorDen, brand); - const expectedInterest = makeRatio(newNum, brand, newDen, brand); - const compounded = calculateCompoundedInterest( - oldInterest, - oldDebt, - newDebt, + for (const [ + rateNum, + rateDen, + startingDebt, + charges, + expected, + floatMatch, + ] of cases) { + const apr = makeRatio(rateNum, brand, rateDen, brand); + const aprf = Number(rateNum) / Number(rateDen); + + let compoundedInterest = makeRatio(1n, brand, 1n, brand); + let compoundedFloat = 1.0; + let totalDebt = startingDebt; + + for (let i = 0; i < charges; i += 1) { + compoundedFloat *= 1 + aprf; + const delta = ceilMultiplyBy(AmountMath.make(brand, totalDebt), apr); + compoundedInterest = calculateCompoundedInterest( + compoundedInterest, + totalDebt, + totalDebt + delta.value, + ); + totalDebt += delta.value; + } + t.is( + compoundedFloat.toPrecision(floatMatch), + ( + Number(compoundedInterest.numerator.value) / + Number(compoundedInterest.denominator.value) + ).toPrecision(floatMatch), + `For ${startingDebt} at (${rateNum}/${rateDen})^${charges}, expected compounded ratio to match ${compoundedFloat}`, + ); + t.is( + (compoundedFloat * Number(startingDebt)).toPrecision(floatMatch), + Number(totalDebt).toPrecision(floatMatch), + `For ${startingDebt} at (${rateNum}/${rateDen})^${charges}, expected compounded float ${compoundedFloat} to match debt`, + ); + t.is( + totalDebt, + expected, + `For ${startingDebt} at (${rateNum}/${rateDen})^${charges}, expected ${expected}`, ); - t.deepEqual(compounded, expectedInterest); } }); diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index a42a3c4518e..10e84c3375c 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -119,13 +119,13 @@ const multiplyHelper = (amount, ratio, divideOp) => { ); }; -/** @type {FloorMultiplyBy} */ +/** @type {ScaleAmount} */ export const floorMultiplyBy = (amount, ratio) => { // @ts-ignore cast return multiplyHelper(amount, ratio, floorDivide); }; -/** @type {CeilMultiplyBy} */ +/** @type {ScaleAmount} */ export const ceilMultiplyBy = (amount, ratio) => { // @ts-ignore cast return multiplyHelper(amount, ratio, ceilDivide); @@ -150,13 +150,13 @@ const divideHelper = (amount, ratio, divideOp) => { ); }; -/** @type {FloorDivideBy} */ +/** @type {ScaleAmount} */ export const floorDivideBy = (amount, ratio) => { // @ts-ignore cast return divideHelper(amount, ratio, floorDivide); }; -/** @type {CeilDivideBy} */ +/** @type {ScaleAmount} */ export const ceilDivideBy = (amount, ratio) => { // @ts-ignore cast return divideHelper(amount, ratio, ceilDivide); @@ -249,3 +249,24 @@ export const oneMinus = ratio => { ratio.numerator.brand, ); }; + +/** + * Make an equivalant ratio with a new denominator + * + * @param {Ratio} ratio + * @param {bigint} newDen + * @returns {Ratio} + */ +export const quantize = (ratio, newDen) => { + const oldDen = ratio.denominator.value; + const oldNum = ratio.numerator.value; + const newNum = + // TODO adopt banker's rounding https://github.com/Agoric/agoric-sdk/issues/4573 + newDen === oldDen ? oldNum : ceilDivide(oldNum * newDen, oldDen); + return makeRatio( + newNum, + ratio.numerator.brand, + newDen, + ratio.denominator.brand, + ); +}; diff --git a/packages/zoe/src/contractSupport/types.js b/packages/zoe/src/contractSupport/types.js index 01a4197aac6..0bc5d839928 100644 --- a/packages/zoe/src/contractSupport/types.js +++ b/packages/zoe/src/contractSupport/types.js @@ -128,22 +128,8 @@ */ /** - * @callback MultiplyBy + * @callback ScaleAmount * @param {Amount} amount * @param {Ratio} ratio * @returns {Amount} */ - -/** - * @callback DivideBy - * @param {Amount} amount - * @param {Ratio} ratio - * @returns {Amount} - */ - -/** - * @typedef {MultiplyBy} CeilMultiplyBy - * @typedef {MultiplyBy} FloorMultiplyBy - * @typedef {DivideBy} FloorDivideBy - * @typedef {DivideBy} CeilDivideBy - */ diff --git a/packages/zoe/test/unitTests/contractSupport/test-ratio.js b/packages/zoe/test/unitTests/contractSupport/test-ratio.js index 59fdf625755..65fdba5d638 100644 --- a/packages/zoe/test/unitTests/contractSupport/test-ratio.js +++ b/packages/zoe/test/unitTests/contractSupport/test-ratio.js @@ -16,7 +16,8 @@ import { oneMinus, multiplyRatios, addRatios, -} from '../../../src/contractSupport/index.js'; + quantize, +} from '../../../src/contractSupport/ratio.js'; function amountsEqual(t, a1, a2, brand) { const brandEqual = a1.brand === a2.brand; @@ -334,3 +335,37 @@ test('ratio - complement', t => { message: /Parameter must be less than or equal to 1: .*/, }); }); + +// Rounding +const { brand } = makeIssuerKit('moe'); + +test('ratio - quantize', t => { + /** @type {Array<[numBefore: bigint, denBefore: bigint, numAfter: bigint, denAfter: bigint]>} */ + const cases = /** @type {const} */ [ + [1n, 1n, 1n, 1n], + [10n, 10n, 10n, 10n], + [2n * 10n ** 9n, 1n * 10n ** 9n, 20n, 10n], + + [12345n, 12345n, 100n, 100n], + [12345n, 12345n, 100000n, 100000n], + [12345n, 12345n, 10n ** 15n, 10n ** 15n], + + [12345n, 123n, 100365854n, 10n ** 6n], + [12345n, 123n, 10036586n, 10n ** 5n], + [12345n, 123n, 1003659n, 10n ** 4n], + [12345n, 123n, 100366n, 10n ** 3n], + [12345n, 123n, 10037n, 10n ** 2n], + [12345n, 123n, 1004n, 10n ** 1n], + [12345n, 123n, 101n, 10n ** 0n], + ]; + + for (const [numBefore, denBefore, numAfter, denAfter] of cases) { + const before = makeRatio(numBefore, brand, denBefore, brand); + const after = makeRatio(numAfter, brand, denAfter, brand); + t.deepEqual( + quantize(before, denAfter), + after, + `${numBefore}/${denBefore} quantized to ${denAfter} should be ${numAfter}/${denAfter}`, + ); + } +}); From 5e4cc5f7f18f71c5a1f09731f9011ee5615ae525 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 16 Feb 2022 14:09:21 -0800 Subject: [PATCH 42/47] refactor(zoe): move ratioGTE --- .../src/vaultFactory/prioritizedVaults.js | 26 ++----------------- packages/zoe/src/contractSupport/ratio.js | 18 +++++++++++++ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index 07e574d3abd..e01332ca405 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -1,36 +1,14 @@ // @ts-check -import { - natSafeMath, - makeRatioFromAmounts, -} from '@agoric/zoe/src/contractSupport/index.js'; -import { assert } from '@agoric/assert'; +import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/index.js'; import { AmountMath } from '@agoric/ertp'; +import { ratioGTE } from '@agoric/zoe/src/contractSupport/ratio.js'; import { makeOrderedVaultStore } from './orderedVaultStore.js'; import { toVaultKey } from './storeUtils.js'; -const { multiply, isGTE } = natSafeMath; - /** @typedef {import('./vault').VaultKit} VaultKit */ // TODO put this with other ratio math -/** - * - * @param {Ratio} left - * @param {Ratio} right - * @returns {boolean} - */ -const ratioGTE = (left, right) => { - assert( - left.numerator.brand === right.numerator.brand && - left.denominator.brand === right.denominator.brand, - `brands must match`, - ); - return isGTE( - multiply(left.numerator.value, right.denominator.value), - multiply(right.numerator.value, left.denominator.value), - ); -}; /** * diff --git a/packages/zoe/src/contractSupport/ratio.js b/packages/zoe/src/contractSupport/ratio.js index 10e84c3375c..d8f70180221 100644 --- a/packages/zoe/src/contractSupport/ratio.js +++ b/packages/zoe/src/contractSupport/ratio.js @@ -250,6 +250,24 @@ export const oneMinus = ratio => { ); }; +/** + * + * @param {Ratio} left + * @param {Ratio} right + * @returns {boolean} + */ +export const ratioGTE = (left, right) => { + assert( + left.numerator.brand === right.numerator.brand && + left.denominator.brand === right.denominator.brand, + `brands must match`, + ); + return natSafeMath.isGTE( + multiply(left.numerator.value, right.denominator.value), + multiply(right.numerator.value, left.denominator.value), + ); +}; + /** * Make an equivalant ratio with a new denominator * From d3a746f9730afc24dc7a69f9878c69364a3832cd Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 16 Feb 2022 11:51:51 -0800 Subject: [PATCH 43/47] cleanup --- packages/run-protocol/README.md | 8 +++--- .../src/vaultFactory/orderedVaultStore.js | 5 ---- .../src/vaultFactory/storeUtils.js | 3 +-- .../run-protocol/src/vaultFactory/vault.js | 2 +- .../src/vaultFactory/vaultManager.js | 20 ++++++-------- .../vaultFactory/test-orderedVaultStore.js | 26 +++++-------------- .../vaultFactory/test-prioritizedVaults.js | 2 -- .../test/vaultFactory/test-storeUtils.js | 19 ++++---------- .../vaultFactory/vault-contract-wrapper.js | 1 - 9 files changed, 26 insertions(+), 60 deletions(-) diff --git a/packages/run-protocol/README.md b/packages/run-protocol/README.md index a3b9ffd92db..2d5da67ff8d 100644 --- a/packages/run-protocol/README.md +++ b/packages/run-protocol/README.md @@ -6,10 +6,12 @@ RUN is a stable token that enables the core of the Agoric economy. By convention there is one well-known **VaultFactory**. By governance it creates a **VaultManager** for each type of asset that can serve as collateral to mint RUN. -Anyone can open make a **Vault** by putting up collateral the appropriate VaultManager. Then they can request RUN that is backed by that collateral. - -When any vat the ratio of the debt to the collateral exceeds a governed threshold, it is deemed undercollateralized. If the result of a price check shows that a vault is undercollateralized. the VaultManager liquidates it. +Anyone can make a **Vault** by putting up collateral with the appropriate VaultManager. Then +they can request RUN that is backed by that collateral. +In any vat, when the ratio of the debt to the collateral exceeds a governed threshold, it is +deemed undercollateralized. If the result of a price check shows that a vault is +undercollateralized, the VaultManager liquidates it. ## Persistence The above states are robust to system restarts and upgrades. This is accomplished using the Agoric (Endo?) Collections API. diff --git a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js index ff6ed2b4cde..edd2aed0dfa 100644 --- a/packages/run-protocol/src/vaultFactory/orderedVaultStore.js +++ b/packages/run-protocol/src/vaultFactory/orderedVaultStore.js @@ -31,11 +31,6 @@ export const makeOrderedVaultStore = () => { const debt = vault.getDebtAmount(); const collateral = vault.getCollateralAmount(); const key = toVaultKey(debt, collateral, vaultId); - console.log('addVaultKit', { - debt: debt.value, - collateral: collateral.value, - key, - }); store.init(key, vaultKit); return key; }; diff --git a/packages/run-protocol/src/vaultFactory/storeUtils.js b/packages/run-protocol/src/vaultFactory/storeUtils.js index a52cfa56bb7..4782ac52c2e 100644 --- a/packages/run-protocol/src/vaultFactory/storeUtils.js +++ b/packages/run-protocol/src/vaultFactory/storeUtils.js @@ -3,7 +3,7 @@ * Module to improvise composite keys for orderedVaultStore until Collections API supports them. * */ -// XXX declaration shouldn't be necessary. Fixed by https://github.com/endojs/endo/pull/1071 +// TODO remove after release of https://github.com/endojs/endo/pull/1071 /* global BigUint64Array */ /** @typedef {[normalizedCollateralization: number, vaultId: VaultId]} CompositeKey */ @@ -32,7 +32,6 @@ const numberToDBEntryKey = n => { asNumber[0] = n; let bits = asBits[0]; if (n < 0) { - // XXX Why is the no-bitwise lint rule even a thing?? // eslint-disable-next-line no-bitwise bits ^= 0xffffffffffffffffn; } else { diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index d20c0e7d413..9057065b815 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -137,7 +137,7 @@ export const makeVaultKit = ( */ // TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 const getDebtAmount = () => { - // divide compounded interest by the the snapshot + // divide compounded interest by the snapshot const interestSinceSnapshot = multiplyRatios( manager.getCompoundedInterest(), invertRatio(debtSnapshot.interest), diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index a1a0a57b0f5..ca3b8491c04 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -38,10 +38,9 @@ const { details: X } = assert; const trace = makeTracer('VM'); /** - * Each VaultManager manages a single collateralType. + * Each VaultManager manages a single collateral type. * - * It owns an autoswap instance which trades this collateralType against RUN. It - * also manages some number of outstanding loans, each called a Vault, for which + * It manages some number of outstanding loans, each called a Vault, for which * the collateral is provided in exchange for borrowed RUN. * * @param {ContractFacet} zcf @@ -76,8 +75,7 @@ export const makeVaultManager = ( const { updater, notifier } = makeNotifierKit( harden({ compoundedInterest: makeRatio(1n, runBrand, 1n, runBrand), - latestInterestUpdate: 0n, - // XXX since debt will always be in RUN, no need to wrap in an Amount + latestInterestUpdate: 0n, // no previous update totalDebt: AmountMath.makeEmpty(runBrand), }), ); @@ -105,13 +103,11 @@ export const makeVaultManager = ( let vaultCounter = 0; - // A Map from vaultKits to their most recent ratio of debt to - // collateralization. (This representation won't be optimized; when we need - // better performance, use virtual objects.) + // A store for of vaultKits prioritized by their collaterization ratio. // - // sortedVaultKits should only be set once, but can't be set until after the + // It should be set only once but it's a `let` because it can't be set until after the // definition of reschedulePriceCheck, which refers to sortedVaultKits - // XXX mutability and flow control + // XXX mutability and flow control, could be refactored with a listener /** @type {ReturnType=} */ let prioritizedVaults; /** @type {MutableQuote=} */ @@ -366,8 +362,8 @@ export const makeVaultManager = ( want: { RUN: null }, }); - // eslint-disable-next-line no-plusplus - const vaultId = String(vaultCounter++); + vaultCounter += 1; + const vaultId = String(vaultCounter); const vaultKit = makeVaultKit( zcf, diff --git a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js index 024ca77fb00..6feee609456 100644 --- a/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js +++ b/packages/run-protocol/test/vaultFactory/test-orderedVaultStore.js @@ -2,31 +2,16 @@ // Must be first to set up globals import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { AmountMath, AssetKind } from '@agoric/ertp'; +import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import { makeOrderedVaultStore } from '../../src/vaultFactory/orderedVaultStore.js'; import { fromVaultKey } from '../../src/vaultFactory/storeUtils.js'; -// XXX shouldn't we have a shared test utils for this kind of thing? -const runBrand = Far('brand', { - isMyIssuer: async _allegedIssuer => false, - getAllegedName: () => 'mockRUN', - getDisplayInfo: () => ({ - assetKind: AssetKind.NAT, - }), -}); - -const collateralBrand = Far('brand', { - isMyIssuer: async _allegedIssuer => false, - getAllegedName: () => 'mockCollateral', - getDisplayInfo: () => ({ - assetKind: AssetKind.NAT, - }), -}); +const brand = Far('brand'); const mockVault = (runCount, collateralCount) => { - const debtAmount = AmountMath.make(runBrand, runCount); - const collateralAmount = AmountMath.make(collateralBrand, collateralCount); + const debtAmount = AmountMath.make(brand, runCount); + const collateralAmount = AmountMath.make(brand, collateralCount); return Far('vault', { getDebtAmount: () => debtAmount, @@ -34,6 +19,7 @@ const mockVault = (runCount, collateralCount) => { }); }; +const BIGGER_INT = BigInt(Number.MAX_VALUE) + 1n; /** * Records to be inserted in this order. Jumbled to verify insertion order invariance. * @@ -43,7 +29,7 @@ const fixture = [ ['vault-E', 40n, 100n], ['vault-F', 50n, 100n], ['vault-M', 1n, 1000n], - ['vault-Y', BigInt(Number.MAX_VALUE), BigInt(Number.MAX_VALUE)], + ['vault-Y', BIGGER_INT, BIGGER_INT], ['vault-Z-withoutdebt', 0n, 100n], ['vault-A-underwater', 1000n, 100n], ['vault-B', 101n, 1000n], diff --git a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js index 93e92617166..14f68169e91 100644 --- a/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js +++ b/packages/run-protocol/test/vaultFactory/test-prioritizedVaults.js @@ -20,8 +20,6 @@ import { makeFakeVaultKit } from '../supports.js'; // This invocation (thanks to Warner) waits for all promises that can fire to // have all their callbacks run async function waitForPromisesToSettle() { - // ??? can't we do simply: - // return new Promise(resolve => setImmediate(resolve)); const pk = makePromiseKit(); setImmediate(pk.resolve); return pk.promise; diff --git a/packages/run-protocol/test/vaultFactory/test-storeUtils.js b/packages/run-protocol/test/vaultFactory/test-storeUtils.js index 8a68c3cdd17..a9003f422df 100644 --- a/packages/run-protocol/test/vaultFactory/test-storeUtils.js +++ b/packages/run-protocol/test/vaultFactory/test-storeUtils.js @@ -1,18 +1,12 @@ +// @ts-check // Must be first to set up globals import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { AmountMath, AssetKind } from '@agoric/ertp'; +import { AmountMath } from '@agoric/ertp'; import { Far } from '@endo/marshal'; import * as StoreUtils from '../../src/vaultFactory/storeUtils.js'; -// XXX shouldn't we have a shared test utils for this kind of thing? -export const mockBrand = Far('brand', { - isMyIssuer: async _allegedIssuer => false, - getAllegedName: () => 'mock', - getDisplayInfo: () => ({ - assetKind: AssetKind.NAT, - }), -}); +export const mockBrand = Far('brand'); for (const [before, after] of [ // matches @@ -28,10 +22,7 @@ for (const [before, after] of [ ]) { test(`cycle number from DB entry key function: ${before} => ${after}`, t => { t.is( - StoreUtils.dbEntryKeyToNumber( - StoreUtils.numberToDBEntryKey(before), - after, - ), + StoreUtils.dbEntryKeyToNumber(StoreUtils.numberToDBEntryKey(before)), after, ); }); @@ -69,7 +60,7 @@ for (const [debt, collat, vaultId, expectedKey, numberOut] of [ const key = StoreUtils.toVaultKey( AmountMath.make(mockBrand, BigInt(debt)), AmountMath.make(mockBrand, BigInt(collat)), - vaultId, + String(vaultId), ); t.is(key, expectedKey); t.deepEqual(StoreUtils.fromVaultKey(key), [numberOut, vaultId]); diff --git a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js index afb83cae12c..6e24fc90720 100644 --- a/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js +++ b/packages/run-protocol/test/vaultFactory/vault-contract-wrapper.js @@ -123,7 +123,6 @@ export async function start(zcf, privateArgs) { // skip the debt calculation for this mock manager const currentInterestAsMultiplicand = makeRatio( - // @ts-ignore XXX can be cleaned up with https://github.com/Agoric/agoric-sdk/pull/4551 100n + currentInterest.numerator.value, currentInterest.numerator.brand, ); From 9b3d4c245db803e24f96aff5b5032b515f46beb9 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 17 Feb 2022 11:06:52 -0800 Subject: [PATCH 44/47] docs: code review feedback --- .../src/vaultFactory/prioritizedVaults.js | 2 -- packages/run-protocol/src/vaultFactory/vault.js | 16 ++++++++-------- .../src/vaultFactory/vaultManager.js | 8 ++++---- .../test/vaultFactory/test-interest.js | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index e01332ca405..f64e32e107d 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -8,8 +8,6 @@ import { toVaultKey } from './storeUtils.js'; /** @typedef {import('./vault').VaultKit} VaultKit */ -// TODO put this with other ratio math - /** * * @param {Amount} debtAmount diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index 9057065b815..dbd1a122557 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -89,7 +89,7 @@ export const makeVaultKit = ( const { brand: runBrand } = runMint.getIssuerRecord(); /** - * Snapshot of the debt and compouneded interest when the principal was last changed + * Snapshot of the debt and compounded interest when the principal was last changed * * @type {{run: Amount, interest: Ratio}} */ @@ -99,7 +99,8 @@ export const makeVaultKit = ( }; /** - * Called whenever principal changes. + * Called whenever the debt is paid or created through a transaction, + * but not for interest accrual. * * @param {Amount} newDebt - principal and all accrued interest */ @@ -262,7 +263,7 @@ export const makeVaultKit = ( throw Error(`unreachable vaultState: ${vaultState}`); } }; - // XXX Echo notifications from the manager though all vaults + // XXX Echo notifications from the manager through all vaults // TODO move manager state to a separate notifer https://github.com/Agoric/agoric-sdk/issues/4540 observeNotifier(managerNotifier, { updateState: () => { @@ -588,7 +589,6 @@ export const makeVaultKit = ( want: { RUN: wantedRun }, } = seat.getProposal(); - if (typeof wantedRun.value !== 'bigint') throw new Error(); // todo trigger process() check right away, in case the price dropped while we ran const fee = ceilMultiplyBy(wantedRun, manager.getLoanFee()); @@ -598,10 +598,10 @@ export const makeVaultKit = ( ); } - const runDebt = AmountMath.add(wantedRun, fee); - await assertSufficientCollateral(collateralAmount, runDebt); + const stagedDebt = AmountMath.add(wantedRun, fee); + await assertSufficientCollateral(collateralAmount, stagedDebt); - runMint.mintGains(harden({ RUN: runDebt }), vaultSeat); + runMint.mintGains(harden({ RUN: stagedDebt }), vaultSeat); seat.incrementBy(vaultSeat.decrementBy(harden({ RUN: wantedRun }))); vaultSeat.incrementBy( @@ -609,7 +609,7 @@ export const makeVaultKit = ( ); manager.reallocateReward(fee, vaultSeat, seat); - refreshLoanTracking(oldDebt, oldCollateral, runDebt); + refreshLoanTracking(oldDebt, oldCollateral, stagedDebt); updateUiState(); diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index ca3b8491c04..c655ac2472c 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -103,11 +103,11 @@ export const makeVaultManager = ( let vaultCounter = 0; - // A store for of vaultKits prioritized by their collaterization ratio. + // A store for vaultKits prioritized by their collaterization ratio. // // It should be set only once but it's a `let` because it can't be set until after the // definition of reschedulePriceCheck, which refers to sortedVaultKits - // XXX mutability and flow control, could be refactored with a listener + // XXX misleading mutability and confusing flow control; could be refactored with a listener /** @type {ReturnType=} */ let prioritizedVaults; /** @type {MutableQuote=} */ @@ -346,7 +346,7 @@ export const makeVaultManager = ( observeNotifier(periodNotifier, timeObserver); /** @type {Parameters[1]} */ - const managerFacade = harden({ + const managerFacet = harden({ ...shared, applyDebtDelta, reallocateReward, @@ -367,7 +367,7 @@ export const makeVaultManager = ( const vaultKit = makeVaultKit( zcf, - managerFacade, + managerFacet, notifier, vaultId, runMint, diff --git a/packages/run-protocol/test/vaultFactory/test-interest.js b/packages/run-protocol/test/vaultFactory/test-interest.js index b7d859f7d17..f18918c87d8 100644 --- a/packages/run-protocol/test/vaultFactory/test-interest.js +++ b/packages/run-protocol/test/vaultFactory/test-interest.js @@ -436,7 +436,7 @@ test('calculateCompoundedInterest', t => { [250n, BASIS_POINTS, M, 10, 1280090n, 5], // 2.5% APR over 10 year yields 28% // XXX resolution was 12 with banker's rounding https://github.com/Agoric/agoric-sdk/issues/4573 [250n, BASIS_POINTS, M * M, 10, 1280084544199n, 8], // 2.5% APR over 10 year yields 28% - [250n, BASIS_POINTS, M, 100, 11813903n, 5], // 2.5% APR over 10 year yields 1181% + [250n, BASIS_POINTS, M, 100, 11813903n, 5], // 2.5% APR over 100 year yields 1181% ]; for (const [ rateNum, From ceb95a23a3e16dd5150225827c63241f290af1e5 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 18 Feb 2022 10:22:33 -0800 Subject: [PATCH 45/47] doc: code review improvements --- packages/run-protocol/README.md | 2 +- .../src/vaultFactory/prioritizedVaults.js | 1 + packages/run-protocol/src/vaultFactory/vault.js | 11 ++++++----- .../run-protocol/src/vaultFactory/vaultManager.js | 15 ++++++--------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/run-protocol/README.md b/packages/run-protocol/README.md index 2d5da67ff8d..c30e1321801 100644 --- a/packages/run-protocol/README.md +++ b/packages/run-protocol/README.md @@ -9,7 +9,7 @@ By convention there is one well-known **VaultFactory**. By governance it creates Anyone can make a **Vault** by putting up collateral with the appropriate VaultManager. Then they can request RUN that is backed by that collateral. -In any vat, when the ratio of the debt to the collateral exceeds a governed threshold, it is +In any vault, when the ratio of the debt to the collateral exceeds a governed threshold, it is deemed undercollateralized. If the result of a price check shows that a vault is undercollateralized, the VaultManager liquidates it. ## Persistence diff --git a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js index f64e32e107d..3e0cf0cb22c 100644 --- a/packages/run-protocol/src/vaultFactory/prioritizedVaults.js +++ b/packages/run-protocol/src/vaultFactory/prioritizedVaults.js @@ -101,6 +101,7 @@ export const makePrioritizedVaults = reschedulePriceCheck => { ratioGTE(debtToCollateral, oracleQueryThreshold) ) { // don't call reschedulePriceCheck, but do reset the highest. + // This could be expensive if we delete individual entries in order. Will know once we have perf data. oracleQueryThreshold = firstDebtRatio(); } return vk; diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index dbd1a122557..ebc5e3b9181 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -136,7 +136,7 @@ export const makeVaultKit = ( * @see getNormalizedDebt * @returns {Amount} */ - // TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 + // TODO rename to getActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 const getDebtAmount = () => { // divide compounded interest by the snapshot const interestSinceSnapshot = multiplyRatios( @@ -311,22 +311,23 @@ export const makeVaultKit = ( // you must pay off the entire remainder but if you offer too much, we won't // take more than you owe + const runDebt = getDebtAmount(); assert( - AmountMath.isGTE(runReturned, getDebtAmount()), - X`You must pay off the entire debt ${runReturned} > ${getDebtAmount()}`, + AmountMath.isGTE(runReturned, runDebt), + X`You must pay off the entire debt ${runReturned} > ${runDebt}`, ); // Return any overpayment const { zcfSeat: burnSeat } = zcf.makeEmptySeatKit(); - burnSeat.incrementBy(seat.decrementBy(harden({ RUN: getDebtAmount() }))); + burnSeat.incrementBy(seat.decrementBy(harden({ RUN: runDebt }))); seat.incrementBy( vaultSeat.decrementBy( harden({ Collateral: getCollateralAllocated(vaultSeat) }), ), ); zcf.reallocate(seat, vaultSeat, burnSeat); - runMint.burnLosses(harden({ RUN: getDebtAmount() }), burnSeat); + runMint.burnLosses(harden({ RUN: runDebt }), burnSeat); seat.exit(); burnSeat.exit(); vaultState = VaultState.CLOSED; diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 5c2b5ad248d..83069648a9c 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -286,9 +286,12 @@ export const makeVaultManager = ( }; /** + * Update total debt of this manager given the change in debt on a vault + * * @param {Amount} oldDebtOnVault * @param {Amount} newDebtOnVault */ + // TODO https://github.com/Agoric/agoric-sdk/issues/4599 const applyDebtDelta = (oldDebtOnVault, newDebtOnVault) => { const delta = newDebtOnVault.value - oldDebtOnVault.value; trace(`updating total debt ${totalDebt} by ${delta}`); @@ -297,15 +300,9 @@ export const makeVaultManager = ( return; } - if (delta > 0n) { - // add the amount - totalDebt += delta; - } else { - // negate the amount so that it's a natural number, then subtract - const absDelta = -delta; - assert(!(absDelta > totalDebt), 'Negative delta greater than total debt'); - totalDebt -= absDelta; - } + totalDebt += delta; + assert(totalDebt >= 0n, 'Negative delta greater than total debt'); + trace('applyDebtDelta complete', { totalDebt }); }; From d2560baf790b76891a039d132ece224f52e88824 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 18 Feb 2022 12:07:08 -0800 Subject: [PATCH 46/47] correct initial latestInterestUpdate --- .../src/vaultFactory/vaultManager.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 83069648a9c..66195cd0e83 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -71,14 +71,6 @@ export const makeVaultManager = ( ) => { const { brand: runBrand } = runMint.getIssuerRecord(); - const { updater, notifier } = makeNotifierKit( - harden({ - compoundedInterest: makeRatio(1n, runBrand, 1n, runBrand), - latestInterestUpdate: 0n, // no previous update - totalDebt: AmountMath.makeEmpty(runBrand), - }), - ); - /** @type {GetVaultParams} */ const shared = { // loans below this margin may be liquidated @@ -119,6 +111,14 @@ export const makeVaultManager = ( /** @type {bigint} */ let latestInterestUpdate = startTimeStamp; + const { updater, notifier } = makeNotifierKit( + harden({ + compoundedInterest, + latestInterestUpdate, + totalDebt: AmountMath.make(runBrand, totalDebt), + }), + ); + /** * * @param {[key: string, vaultKit: VaultKit]} record From e71db04c294b0908b16d1752b71a5f641f55a4cd Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 18 Feb 2022 12:15:01 -0800 Subject: [PATCH 47/47] store totalDebt as Amount --- .../src/vaultFactory/vaultManager.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/run-protocol/src/vaultFactory/vaultManager.js b/packages/run-protocol/src/vaultFactory/vaultManager.js index 66195cd0e83..e3716265871 100644 --- a/packages/run-protocol/src/vaultFactory/vaultManager.js +++ b/packages/run-protocol/src/vaultFactory/vaultManager.js @@ -102,8 +102,8 @@ export const makeVaultManager = ( let prioritizedVaults; /** @type {MutableQuote=} */ let outstandingQuote; - /** @type {NatValue} */ - let totalDebt = 0n; + /** @type {Amount} */ + let totalDebt = AmountMath.make(runBrand, 0n); /** @type {Ratio}} */ let compoundedInterest = makeRatio(100n, runBrand); // starts at 1.0, no interest @@ -115,7 +115,7 @@ export const makeVaultManager = ( harden({ compoundedInterest, latestInterestUpdate, - totalDebt: AmountMath.make(runBrand, totalDebt), + totalDebt, }), ); @@ -241,7 +241,7 @@ export const makeVaultManager = ( const debtStatus = interestCalculator.calculateReportingPeriod( { latestInterestUpdate, - newDebt: totalDebt, + newDebt: totalDebt.value, interest: 0n, // XXX this is always zero, doesn't need to be an option }, updateTime, @@ -260,10 +260,14 @@ export const makeVaultManager = ( // is over all debts of the vault the numbers will be reliably large. compoundedInterest = calculateCompoundedInterest( compoundedInterest, - totalDebt, + totalDebt.value, debtStatus.newDebt, ); - totalDebt += interestAccrued; + // totalDebt += interestAccrued + totalDebt = AmountMath.add( + totalDebt, + AmountMath.make(runBrand, interestAccrued), + ); // mint that much RUN for the reward pool const rewarded = AmountMath.make(runBrand, interestAccrued); @@ -276,7 +280,7 @@ export const makeVaultManager = ( const payload = harden({ compoundedInterest, latestInterestUpdate, - totalDebt: AmountMath.make(runBrand, totalDebt), + totalDebt, }); updater.updateState(payload); @@ -300,10 +304,8 @@ export const makeVaultManager = ( return; } - totalDebt += delta; - assert(totalDebt >= 0n, 'Negative delta greater than total debt'); - - trace('applyDebtDelta complete', { totalDebt }); + // totalDebt += delta (Amount type ensures natural value) + totalDebt = AmountMath.make(runBrand, totalDebt.value + delta); }; /**