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

Conversation

turadg
Copy link
Member

@turadg turadg commented Feb 10, 2022

closes: #4341
refs: #4345

Description

Vault need to be able to persist to disk (#4511). On the path to that is making them store as virtual objects in the new Collections API. While doing this refactor though we design for the durable case and take as a requirement that when interest is charged each day that it does not require disk IO for each vault (O(n)).

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.

Before merging I plan to,

  • fix or remove each skipped test
  • clean up the tracing (either remove or figure out a way to configure what gets output, e.g. like https://www.npmjs.com/package/debug )
  • turn each new TODO line into a ticket or do it
  • remove each ??? (hopefully by replacing with an answer)
  • convert the forEach method with callback to a generator
  • assess maximum key length limits with Collections API
  • eliminate Eslint-disable lines by updating eslint config (style: loosen some eslint rules #4530)

Security Considerations

Now the actual debts of all vaults are manipulated by a single value in the vault manager, making it higher stakes. Of course if the vault manager is compromised there are bigger problems and, on the other hand, this moves that out of the vaults which are more distributed.

An implication of this change is that we'll have to #4540 for which there may be some misleading UI could deceive users. Let's consider that when we come to that design.

Documentation Considerations

Reviewers, how much of the design should I document in the README.md?

Anything to do in https://github.com/Agoric/documentation ?

Testing Considerations

There are new tests for the functionality. I think it maintained the APi that the UI was depending on. @samsiegart anything extra to do?

@turadg turadg changed the title refactor vault storage and liquidation for durable collections redesign vault storage and interest charging for durable collections Feb 11, 2022
@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch from 7c2b647 to 421a2d9 Compare February 11, 2022 18:48
Copy link
Contributor

@Chris-Hibbert Chris-Hibbert left a comment

Choose a reason for hiding this comment

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

I'd like to see tests showing the interest rate and debt calculation when some of the vaults were opened significantly after the vaultManager.

I'm worried that the fact that the debt has to be represented as a back-dated value in order to agree with the compoundedInterest may impose a higher floor on the minimum debt. Currently you can take out a loan as long as the interest each period would be non-negligible. This may make that minimum larger in order to be able to back-date the debt.

packages/run-protocol/README.md Outdated Show resolved Hide resolved
*
* @typedef {BaseVault & VaultMixin} Vault
* @typedef {Object} VaultMixin
* @property {() => Promise<Invitation>} makeAdjustBalancesInvitation
* @property {() => Promise<Invitation>} makeCloseInvitation
* @property {() => ERef<UserSeat>} getLiquidationSeat
* @property {() => Promise<string>} getLiquidationPromise
Copy link
Contributor

Choose a reason for hiding this comment

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

why drop this? The Vault's owner has no other way to get the promise, which they can use to be notified when the vault is liquidated.

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 should have explained: #4539

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}
Copy link
Contributor

Choose a reason for hiding this comment

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

perhaps a hardened DebtSnapshot {debt: Amount, interest: Ratio}

That's what I'd do, together with a single method for updating the bundle, which you already have.

@@ -1,4 +1,6 @@
// @ts-check
// XXX we do this a lot, okay to drop it to warning in eslint config?
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm in favor. I've treated this restriction as a matter of JavaScript style and followed the rule most of the time. I think I've only disabled the check when circular dependencies made it too painful to comply with.

I'll continue as long as it requires the explicit eslint-disable in each file. If we drop the rule, I'll happily go along.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will come after a release of endojs/endo#1071

@@ -78,22 +92,61 @@ 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL about optional chaining. Why is this necessary here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not necessary just more clear intent.

Copy link
Member

@dckc dckc Feb 14, 2022

Choose a reason for hiding this comment

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

Is optional chaining in standard JS yet? Is it in test262? (that would let me check whether it's supported by XS.)

A quick check suggests that it is not supported by XS:

packages/xsnap$ ./src/xsrepl
xs> {bob: {size: 2}}?.fred
(Error#1)
Error#1: Uncaught exception in <unnamed xsnap worker>: SyntaxError: invalid token ?.

false alarm: 76/76 optional-chaining says XS conformance

$ ./src/xsrepl
xs> ({a: 1}?.b) === undefined
true

@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch from 421a2d9 to 73fc082 Compare February 12, 2022 03:10
@turadg turadg changed the title redesign vault storage and interest charging for durable collections redesign vault storage and interest charging for virtual collections Feb 14, 2022
@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch from 241907d to 5af883b Compare February 14, 2022 22:29
@turadg turadg marked this pull request as ready for review February 15, 2022 00:46
@turadg
Copy link
Member Author

turadg commented Feb 15, 2022

I'd like to see tests showing the interest rate and debt calculation when some of the vaults were opened significantly after the vaultManager.

I added some in b85cc96 and 162fba6.

I'm worried that the fact that the debt has to be represented as a back-dated value in order to agree with the compoundedInterest may impose a higher floor on the minimum debt. Currently you can take out a loan as long as the interest each period would be non-negligible. This may make that minimum larger in order to be able to back-date the debt.

We discussed offline how the check is currently only against loan fees. We also uncovered that the "ceil" implies at least one of the RUN brand amount will be charged at each interest period. However it's denominated in µRUN, not an usurious full RUN. I've documented that in the README.

@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch from 1159228 to 3e84c99 Compare February 15, 2022 18:04
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?
Copy link
Contributor

Choose a reason for hiding this comment

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

issuers are light weight enough that we usually just create a new issuer whenever we need a brand in a test. What's the advantage of a mock brand over that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Discovery, I suppose. I know now can simply do const mockBrand = Far('brand') because it's any but I first looked for fakeBrand.

Copy link
Contributor

Choose a reason for hiding this comment

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

If they don't serve any useful purpose, please replace them with either actual issuers or Far('brand'). If the test doesn't rely on these methods, let's not clutter the test with them. Being consistent with other tests when there's no need to diverge makes reviewing them easier.

* @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.

});
};

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

orderedVaultStore has methods that aren't tested here: removeByKey, keys, getSize, values. I'd like to see a test of at least removeByKey.

I expected a bulk removal operation. If that's coming, I'd like a test of that as well.

OIC. Those are now in test-prioritizedVaults.js. That's fine.

Comment on lines +71 to +74
const collateral50 = AmountMath.make(collaterlBrand, 50n);
const proposal = harden({
give: { Collateral: collateral50 },
want: { RUN: AmountMath.make(runBrand, 70000n) },
want: { RUN: AmountMath.make(runBrand, 70n) },
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused by this change. Is it now charging as much interest on 50n as it used to on 50_000n?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry about that. I blew away that file and made it anew, so ignore the diff on it.

}

// compoundedInterest *= debtStatus.newDebt / totalDebt;
compoundedInterest = multiplyRatios(
Copy link
Contributor

Choose a reason for hiding this comment

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

We never simplify ratios. This ratio is going to contain bigIntegers that grow by a few digits (!) every compounding period. I think we need to choose a resolution and reduce regularly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great catch! 🙏

7c3bff8 to address

Comment on lines 333 to 339
assert(
!(absDelta > totalDebt.value),
'Negative delta greater than total debt',
);
totalDebt = AmountMath.subtract(
totalDebt,
AmountMath.make(totalDebt.brand, absDelta),
Copy link
Contributor

Choose a reason for hiding this comment

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

switching back and forth between Amounts and bigInts seems wrong. Can everything here be done with Amounts? If we're already branching based on whether the result of a subtraction is positive or negative, we could branch on which operand is larger, and have nats on both branches, right?

Copy link
Member Author

@turadg turadg Feb 16, 2022

Choose a reason for hiding this comment

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

switching back and forth between Amounts and bigInts seems wrong.

Good call. For anyone following along, we chatted and agreed to use bigint more consistently. I think I mostly had them around to be able to make Ratios, but I should be able to simplify those too.

The utility of Amount is as an abstraction when brand or amount type could be different. In the case of interest calculation the brand is always RUN and the amount type is always bigint.

packages/run-protocol/src/vaultFactory/vaultManager.js Outdated Show resolved Hide resolved
*/
// TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540
const getDebtAmount = () => {
// divide compounded interest by the the snapshot
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// divide compounded interest by the the snapshot
// divide compounded interest by the snapshot

* Redundant tags until https://github.com/Microsoft/TypeScript/issues/23857
*
* @param {Ratio} ratio
* @yields {[string, VaultKit]>}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @yields {[string, VaultKit]>}
* @yields {[string, VaultKit]}

@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch 3 times, most recently from 59d86db to 7c3bff8 Compare February 16, 2022 19:27
Copy link
Contributor

@Chris-Hibbert Chris-Hibbert left a comment

Choose a reason for hiding this comment

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

The approach looks sound. There are a few things to clean up.

Isn't your expectation that all the '???' should be removed before merging?

Comment on lines 9 to 11
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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.

Comment on lines 108 to 114
while (
numerator > COMPOUNDED_INTEREST_PRECISION &&
denominator > COMPOUNDED_INTEREST_PRECISION
) {
numerator /= COMPOUNDED_INTEREST_PRECISION;
denominator /= COMPOUNDED_INTEREST_PRECISION;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like it'll do the wrong thing. If as a result of multiplication, the new ratio is 25.929489523*COMPOUNDED_INTEREST_PRECISION / 20.239482873*COMPOUNDED_INTEREST_PRECISION, this will reduce it to 25/20. I think Dean's suggestion to reduce the numerator to numerator/denominator * COMPOUNDED_INTEREST_PRECISION, and the denominator to COMPOUNDED_INTEREST_PRECISION would be a better approach.

and 10**30 is more than we need/want. How about 10**21?

Copy link
Member Author

Choose a reason for hiding this comment

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

This seems like it'll do the wrong thing

Yep, this was a silly thing whipped up for the design discussion.

and 1030 is more than we need/want. How about 1021?
👍 5d43e74

I think we can cut it down more after #4573

const debt = vault.getDebtAmount();
const collateral = vault.getCollateralAmount();
const key = toVaultKey(debt, collateral, vaultId);
console.log('addVaultKit', {
Copy link
Contributor

Choose a reason for hiding this comment

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

trace() would probably be better.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. I ended up removing it.

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?
Copy link
Contributor

Choose a reason for hiding this comment

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

If they don't serve any useful purpose, please replace them with either actual issuers or Far('brand'). If the test doesn't rely on these methods, let's not clutter the test with them. Being consistent with other tests when there's no need to diverge makes reviewing them easier.

Comment on lines 41 to 45
* 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.
*
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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 collateral type.
*
* It manages some number of outstanding loans, each called a Vault, for which
* the collateral is provided in exchange for borrowed RUN.
*

const { updater, notifier } = makeNotifierKit(
harden({
compoundedInterest: makeRatio(1n, runBrand, 1n, runBrand),
latestInterestUpdate: 0n,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the identity for interest 1n instead of 0n?

Copy link
Member Author

Choose a reason for hiding this comment

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

latestInterestUpdate is a timestamp, 0n here because there is no previous update. I'll add a comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't it start out as the current time? Won't it charge interest from the unix epoch to the current time the first time it reaches calculateReportingPeriod.
The tests all run with times around zero

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! d2560ba

Comment on lines 364 to 365
// eslint-disable-next-line no-plusplus
const vaultId = String(vaultCounter++);
Copy link
Contributor

Choose a reason for hiding this comment

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

If the style guide asks us to avoid ++, there isn't a good reason to violate that rule here. Doing what the style guide wants is at least as concise and understandable as the work to disable it.

Suggested change
// eslint-disable-next-line no-plusplus
const vaultId = String(vaultCounter++);
vaultCounter += 1;
const vaultId = String(vaultCounter);

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree completely about not violating style rules. I had a PR to remove this as a rule but it's closed and this is the right way.

test('calculateCompoundedInterest', t => {
const brand = Far('brand');
const cases = [
[1n, 1n, 1n, 1n, 1n, 1n], // no charge
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this a bug? Shouldn't something complain if the debt doesn't grow at all?

Copy link
Member Author

@turadg turadg Feb 17, 2022

Choose a reason for hiding this comment

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

I lost the context for this but the underlying buggy draft code is gone anyway.

@turadg turadg force-pushed the ta/4341-durable-prioritizedVaults branch from 7c3bff8 to d3a746f Compare February 17, 2022 05:52
Copy link
Contributor

@Chris-Hibbert Chris-Hibbert left a comment

Choose a reason for hiding this comment

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

I'm not done with this pass, but these are what I've found so far. I'll be away for a bit, so I thought I should update with interim results.

// better performance, use virtual objects.)
let vaultCounter = 0;

// A store for of vaultKits prioritized by their collaterization ratio.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// A store for of vaultKits prioritized by their collaterization ratio.
// A store for vaultKits prioritized by their collaterization ratio.

// definition of reschedulePriceCheck, which refers to sortedVaultKits
let sortedVaultKits;
// XXX mutability and flow control, could be refactored with a listener
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a TODO? I don't understand the point that it's making. Can you clarify?

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 chose XXX to say "here's a known issue" but I don't think we necessarily have to schedule addressing it. I've updated the comment to clarify:

// XXX misleading mutability and confusing flow control; could be refactored with a listener

Copy link
Contributor

Choose a reason for hiding this comment

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

That's clearer, though I guess I'm not familiar with listeners. Could you say a bit more? [in this thread; I doubt it needs to be added to the comment.]

And I don't see the update here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, didn't implement it yet. I've got some more refactoring tasks coming and it might fit into one of those.

Gist is that right now we're delaying construction of A until the callback B is available that need A in scope. What we can do instead is wire up the listening after they're both created.

const A = makeA();
const B = makeB();
A.onEventBCaresAbout(B);

export const floorMultiplyBy = (amount, ratio) => {
// @ts-ignore cast
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we were converting to ts-expect-error. Should this use that convention?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, though unfortunately we can't until #4560

@@ -213,8 +244,47 @@ export const oneMinus = ratio => {
return makeRatio(
subtract(ratio.denominator.value, ratio.numerator.value),
ratio.numerator.brand,
// @ts-ignore asserts ensure values are Nats
// @ts-ignore value can be any AmountValue but makeRatio() supports only bigint
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this addressed by declaring Ratio to use only Nat Amounts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, when the value is known to be Amount<NatValue>. However this function declaration is only, {Amount} which can be any kind of amount. Really this function should take only Amount<NatValue> but making that change reduces what this function takes and necessitates updates to every upstream declaration. Out of scope.

Copy link
Contributor

@Chris-Hibbert Chris-Hibbert left a comment

Choose a reason for hiding this comment

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

A few more things to clean up. Mostly comments and renamings.

[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%
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
[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%

};
/** @typedef {import('./vault').VaultKit} VaultKit */

// TODO put this with other ratio math
Copy link
Contributor

Choose a reason for hiding this comment

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

obsolete comment

let runDebt = AmountMath.makeEmpty(runBrand);

/**
* Snapshot of the debt and compouneded interest when the principal was last changed
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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

};

/**
* Called whenever principal changes.
Copy link
Contributor

Choose a reason for hiding this comment

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

whenever principal or debt changes? i.e. in adjustBalancesHook.

@@ -162,17 +262,32 @@ export const makeVaultKit = (
throw Error(`unreachable vaultState: ${vaultState}`);
}
};
// XXX Echo notifications from the manager though all vaults
Copy link
Contributor

Choose a reason for hiding this comment

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

"though" isn't right. Should it be "to" or "through"?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah typo for "through". corrected.

// get the payout to provide access to the collateral if the
// contract abandons
const {
give: { Collateral: collateralAmount },
want: { RUN: wantedRun },
} = seat.getProposal();

if (typeof wantedRun.value !== 'bigint') throw new Error();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm surprised eslint allows this. All if bodies should have braces.

All Errors should have a message.

Is this necessary? I expected Zoe to ensure the want amount in a proposal should be value for the issuer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not necessary. Maybe leftover from a type safety excursion. Removed now.

@@ -450,7 +598,7 @@ export const makeVaultKit = (
);
}

runDebt = AmountMath.add(wantedRun, fee);
const runDebt = AmountMath.add(wantedRun, fee);
Copy link
Contributor

Choose a reason for hiding this comment

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

runDebt was the old name for the amount that's now in debtSnapshot. Here it should be newDebt or stagedDebt to reflect that it won't be updated in debtSnapshot until we perform a couple of checks.

// definition of reschedulePriceCheck, which refers to sortedVaultKits
let sortedVaultKits;
// XXX mutability and flow control, could be refactored with a listener
Copy link
Contributor

Choose a reason for hiding this comment

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

That's clearer, though I guess I'm not familiar with listeners. Could you say a bit more? [in this thread; I doubt it needs to be added to the comment.]

And I don't see the update here.

/** @type {InnerVaultManager} */
const innerFacet = harden({
/** @type {Parameters<typeof makeVaultKit>[1]} */
const managerFacade = harden({
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this something other than what we call a facet?

Suggested change
const managerFacade = harden({
const managerFacet = harden({

@turadg turadg changed the title redesign vault storage and interest charging for virtual collections refactor(run-protocol): redesign vault storage and interest charging for virtual collections Feb 18, 2022
@turadg turadg changed the title refactor(run-protocol): redesign vault storage and interest charging for virtual collections feat(run-protocol): interest charging O(1) for all vaults in a manager Feb 18, 2022
@turadg turadg added the automerge:squash Automatically squash merge label Feb 18, 2022
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
Copy link
Member

Choose a reason for hiding this comment

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

"in any vault"

ratioGTE(debtToCollateral, oracleQueryThreshold)
) {
// don't call reschedulePriceCheck, but do reset the highest.
oracleQueryThreshold = firstDebtRatio();
Copy link
Member

Choose a reason for hiding this comment

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

This could be expensive if we delete individual entries in order. It would be too much to make it computed lazily I expect until we see the actual perf though. So just a note for the future

}
highestDebtToCollateral = highestRatio();
/**
* @param {Amount<NatValue>} oldDebt
Copy link
Member

Choose a reason for hiding this comment

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

could use a comment about the method :)

const assertSufficientCollateral = async (collateralAmount, wantedRun) => {
const assertSufficientCollateral = async (
collateralAmount,
proposedRunDebt,
Copy link
Member

Choose a reason for hiding this comment

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

the old name seems better since it aligns with the offer "give/want" terminology.

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 agree give/want are terms to use reliably, which is why I think the use of "want" here is an error. "Want" is for a particular transaction and this proposedRunDebt is cumulative. I.e. the total debt if wanted RUN were to be provided.

@@ -196,31 +311,30 @@ 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()),
Copy link
Member

Choose a reason for hiding this comment

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

this should define a local constant for the runDebt. It's both less churn and properly makes it clear that the value is expected to be the same.


if (delta > 0n) {
// add the amount
totalDebt += delta;
Copy link
Member

Choose a reason for hiding this comment

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

Why isn't totalDebt an Amount? There's no reason not to have it be a checked value where our units math is doing the right thing.

Copy link
Member

Choose a reason for hiding this comment

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

(I don't think that's blocking but something we should discuss)

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's do discuss. This is where @Chris-Hibbert and I arrived. My position for why to have it a bigint here is this makes the arithmetic easier to follow and the Amount wrapper didn't provide any extra assurances within this module because debt brand is invariant.

Totally open to changing back, just want to save it for the virtual objects refactor.

Copy link
Member Author

@turadg turadg Feb 18, 2022

Choose a reason for hiding this comment

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

The bug fixed in in d2560ba I think wouldn't have happened if not for wrapping amount values for totalDebt. So I'm convinced you're right that it's better to keep it as an amount: e71db04

return;
}

if (delta > 0n) {
Copy link
Member

Choose a reason for hiding this comment

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

why the funky absolute value?

const newTotal = totalDebt += delta;
assert(newTotal > 0n), 'Negative delta greater than total debt');
// TODO here we will call the factory machine to apply the delta to it
totalDebt = newTotal;

Copy link
Member

Choose a reason for hiding this comment

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

Should be fixed?

Copy link
Member Author

@turadg turadg Feb 18, 2022

Choose a reason for hiding this comment

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

I agree with the simplification. I don't recall why it's like this and have simplified to,

   totalDebt += delta;
    assert(totalDebt >= 0n, 'Negative delta greater than total debt');

I think the "call the factory machine" element is out of scope.

@@ -218,11 +345,14 @@ export const makeVaultManager = (

observeNotifier(periodNotifier, timeObserver);

/** @type {InnerVaultManager} */
const innerFacet = harden({
Copy link
Member

Choose a reason for hiding this comment

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

I was trying to be clear that it's closely held (inner), but YMMV

Copy link
Member Author

Choose a reason for hiding this comment

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

Punting on this topic as it's going to get refactored more by the vault transfer and virtual objects work.

* @see getNormalizedDebt
* @returns {Amount<NatValue>}
*/
// TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540
Copy link
Member

Choose a reason for hiding this comment

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

It's perfectly reasonable for this to remain getDebtAmount. The caller doesn't care that it's calculated. the only reason to rename is if it was doing side-effects, which it's not.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree that caller doesn't (and shouldn't) care. But I do think that since there is getNormalizedDebt exists it would help the caller to distinguish this as actual debt.

@turadg turadg removed the automerge:squash Automatically squash merge label Feb 18, 2022
Copy link
Member

@dtribble dtribble left a comment

Choose a reason for hiding this comment

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

LGTM except the one change

totalDebt -= absDelta;
}
totalDebt += delta;
assert(totalDebt >= 0n, 'Negative delta greater than total debt');
Copy link
Member

Choose a reason for hiding this comment

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

This assert needs to be before the totalDebt gets updated, I think. Hence compute a temporary, assert, then assign.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree the lines commented have the bug that if the assert fails totalDebt will have been updated. I thought the thrown error would break more things but that's not a valid assumption; something could catch it and believe the totalDebt that was left.

However, the lines highlighted were removed in e71db04 . That amountMath call will throw if the negative delta was greater than total debt (because the result wouldn't be a natural number, which is really what this is enforcing). totalDebt won't update.

/** @type {NatValue} */
let totalDebt = 0n;
/** @type {Amount<NatValue>} */
let totalDebt = AmountMath.make(runBrand, 0n);
Copy link
Contributor

Choose a reason for hiding this comment

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

makeEmpty is preferred, even when we know it's a Nat.

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'll get that into #4581 when I rebase it onto master.

@turadg turadg added the automerge:squash Automatically squash merge label Feb 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:squash Automatically squash merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

make run-protocol vault prioritization use the new Collections API
5 participants