Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(run-protocol): interest charging O(1) for all vaults in a manager #4527

Merged
merged 49 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bdc0c19
types
turadg Feb 1, 2022
5cae71a
runtime changes for type clarity (safe?)
turadg Feb 1, 2022
5101d54
types
turadg Feb 1, 2022
9d1b818
make accrued debt a function of prior debt and current compounded int…
turadg Feb 3, 2022
68c894f
stub for orderedVaultStore
turadg Feb 3, 2022
3757dc8
progress to hurdle: collectionManager cannot serialize Remotables wit…
turadg Feb 4, 2022
07cd9f1
green test-OrderedVaultStore
turadg Feb 4, 2022
bf301e7
WIP
turadg Feb 4, 2022
fe1241d
style: remove extraneous key quotes
turadg Feb 7, 2022
30f4089
comment
turadg Feb 7, 2022
ab1d602
getNormalizedDebt, toVaultKey, fromVaultKey
turadg Feb 8, 2022
cb26689
tests for orderedVaultStore
turadg Feb 8, 2022
e3b0be4
more isolation between modules
turadg Feb 8, 2022
9164c53
cleanup
turadg Feb 9, 2022
c63819c
percent() helper for tests
turadg Feb 9, 2022
bac833f
done for now with test-prioritizedVaults (it's doing considerably les…
turadg Feb 9, 2022
c6eb3a7
test-vault passing
turadg Feb 9, 2022
89a384b
test vaultFactory overdeposit passing
turadg Feb 10, 2022
a502eed
integration test passing (with an additional notification)
turadg Feb 10, 2022
89a2af8
cleanup
turadg Feb 10, 2022
0af7d8d
fix bug in key gen
turadg Feb 10, 2022
2167398
more robust handling of uncollaterialized vaults
turadg Feb 10, 2022
b97ba88
cleanup
turadg Feb 10, 2022
c81bfdb
make test-orderedVaultStore deterministic
turadg Feb 11, 2022
4259d64
forEachRatioGTE cb --> entriesPrioritizedGTE generator
turadg Feb 11, 2022
ab670eb
work around mixing the notifier streams
turadg Feb 11, 2022
2146ec0
remove vault-interest unit test b/c interest requires a real vaultMan…
turadg Feb 11, 2022
ff10167
stop testing removal by notification that no longer happens
turadg Feb 11, 2022
21d74d9
docs
turadg Feb 12, 2022
0b1d5d2
object for debtSnapshot
turadg Feb 12, 2022
ce035f9
vault test for compound interest
turadg Feb 14, 2022
9b36c94
clean up TODOs (remove or include ticket)
turadg Feb 14, 2022
c66af62
vaultFactory test for minimum debt
turadg Feb 14, 2022
ed35236
reduce debug/trace output
turadg Feb 14, 2022
21bc9a1
docs
turadg Feb 15, 2022
3e0d685
prevent liquidating a vault that is already liquidating
turadg Feb 15, 2022
d139de3
fix bug in getCollateralizationRatio
turadg Feb 15, 2022
b937187
more specific test for snapshot state when opening vault after launch
turadg Feb 15, 2022
3410eb5
track debts as NatValue instead of Amount
turadg Feb 16, 2022
754c085
factor out and test calculateCompoundedInterest()
turadg Feb 16, 2022
5d43e74
arbitrary precision in compound interest state
turadg Feb 16, 2022
5e4cc5f
refactor(zoe): move ratioGTE
turadg Feb 16, 2022
d3a746f
cleanup
turadg Feb 16, 2022
9b3d4c2
docs: code review feedback
turadg Feb 17, 2022
9c56712
Merge branch 'master' into ta/4341-durable-prioritizedVaults
turadg Feb 18, 2022
ceb95a2
doc: code review improvements
turadg Feb 18, 2022
d2560ba
correct initial latestInterestUpdate
turadg Feb 18, 2022
e71db04
store totalDebt as Amount
turadg Feb 18, 2022
d10e0e2
Merge branch 'master' into ta/4341-durable-prioritizedVaults
mergify[bot] Feb 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/threat_models/vaultFactory/vaultFactory.puml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions packages/run-protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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 make a **Vault** by putting up collateral with the appropriate VaultManager. Then
they can request RUN that is backed by that collateral.

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

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.
76 changes: 54 additions & 22 deletions packages/run-protocol/src/vaultFactory/interest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@

import '@agoric/zoe/exported.js';
import '@agoric/zoe/src/contracts/callSpread/types.js';
import './types.js';
import { natSafeMath } from '@agoric/zoe/src/contractSupport/index.js';
import {
ceilMultiplyBy,
makeRatio,
multiplyRatios,
quantize,
} from '@agoric/zoe/src/contractSupport/ratio.js';
import { AmountMath } from '@agoric/ertp';

const makeResult = (latestInterestUpdate, interest, newDebt) => ({
latestInterestUpdate,
interest,
newDebt,
});
import './types.js';

export const SECONDS_PER_YEAR = 60n * 60n * 24n * 365n;
const BASIS_POINTS = 10000;
// single digit APR is less than a basis point per day.
const LARGE_DENOMINATOR = BASIS_POINTS * BASIS_POINTS;

/**
* @param {Brand} brand
* Number chosen from 6 digits for a basis point, doubled for multiplication.
*/
const COMPOUNDED_INTEREST_DENOMINATOR = 10n ** 20n;

/**
* @param {Ratio} annualRate
* @param {RelativeTime} chargingPeriod
* @param {RelativeTime} recordingPeriod
* @returns {CalculatorKit}
*/
export const makeInterestCalculator = (
brand,
annualRate,
chargingPeriod,
recordingPeriod,
Expand All @@ -47,27 +45,41 @@ 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;
let growingInterest = debtStatus.interest;
let growingDebt = newDebt;
while (newRecent + chargingPeriod <= currentTime) {
newRecent += chargingPeriod;
const newInterest = ceilMultiplyBy(growingDebt, ratePerChargingPeriod);
growingInterest = AmountMath.add(growingInterest, newInterest);
growingDebt = AmountMath.add(growingDebt, newInterest, brand);
// The `ceil` implies that a vault with any debt will accrue at least one µRUN.
const newInterest = natSafeMath.ceilDivide(
growingDebt * ratePerChargingPeriod.numerator.value,
ratePerChargingPeriod.denominator.value,
);
growingInterest += newInterest;
growingDebt += newInterest;
}
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;
Expand All @@ -79,3 +91,23 @@ 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;
const compounded = multiplyRatios(
priorCompoundedInterest,
makeRatio(newDebt, brand, priorDebt, brand),
);
return quantize(compounded, COMPOUNDED_INTEREST_DENOMINATOR);
};
16 changes: 12 additions & 4 deletions packages/run-protocol/src/vaultFactory/liquidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {Promise<Vault>}
*/
const liquidate = async (
zcf,
Expand All @@ -24,7 +31,7 @@ const liquidate = async (
strategy,
collateralBrand,
) => {
vaultKit.liquidating();
vaultKit.actions.liquidating();
const runDebt = vaultKit.vault.getDebtAmount();
const { brand: runBrand } = runDebt;
const { vaultSeat, liquidationZcfSeat: liquidationSeat } = vaultKit;
Expand Down Expand Up @@ -57,12 +64,13 @@ 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));
vaultKit.actions.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');

return vaultKit.vault;
};

/**
Expand Down
71 changes: 71 additions & 0 deletions packages/run-protocol/src/vaultFactory/orderedVaultStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// @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 { 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 */
/** @typedef {import('./storeUtils').CompositeKey} CompositeKey */

export const makeOrderedVaultStore = () => {
// TODO make it work durably https://github.com/Agoric/agoric-sdk/issues/4550
/** @type {MapStore<string, VaultKit>} */
const store = makeScalarBigMapStore('orderedVaultStore', { durable: false });

/**
*
* @param {string} vaultId
* @param {VaultKit} vaultKit
*/
const addVaultKit = (vaultId, vaultKit) => {
const { vault } = vaultKit;
const debt = vault.getDebtAmount();
const collateral = vault.getCollateralAmount();
const key = toVaultKey(debt, collateral, vaultId);
store.init(key, vaultKit);
return key;
};

/**
*
* @param {string} key
* @returns {VaultKit}
*/
const removeByKey = key => {
Copy link
Contributor

Choose a reason for hiding this comment

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

is orderedVaultStore going to support removing a range of keys as a bulk operation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the underlying Collections API supports that but there's no need to use it yet since vaults are removed whenever their liquidation completes, not a single bulk operation.

try {
const vaultKit = store.get(key);
assert(vaultKit);
store.delete(key);
return vaultKit;
} catch (e) {
const keys = Array.from(store.keys());
console.error(
'removeByKey failed to remove',
key,
'parts:',
fromVaultKey(key),
);
console.error(' key literals:', keys);
console.error(' key parts:', keys.map(fromVaultKey));
throw e;
}
};

return harden({
addVaultKit,
removeByKey,
keys: store.keys,
entries: store.entries,
getSize: store.getSize,
values: store.values,
});
};
Loading