-
Notifications
You must be signed in to change notification settings - Fork 207
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
Conversation
7c2b647
to
421a2d9
Compare
There was a problem hiding this 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.
* | ||
* @typedef {BaseVault & VaultMixin} Vault | ||
* @typedef {Object} VaultMixin | ||
* @property {() => Promise<Invitation>} makeAdjustBalancesInvitation | ||
* @property {() => Promise<Invitation>} makeCloseInvitation | ||
* @property {() => ERef<UserSeat>} getLiquidationSeat | ||
* @property {() => Promise<string>} getLiquidationPromise |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
421a2d9
to
73fc082
Compare
241907d
to
5af883b
Compare
I added some in b85cc96 and 162fba6.
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. |
1159228
to
3e84c99
Compare
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? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
}); | ||
}; | ||
|
||
/** |
There was a problem hiding this comment.
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.
const collateral50 = AmountMath.make(collaterlBrand, 50n); | ||
const proposal = harden({ | ||
give: { Collateral: collateral50 }, | ||
want: { RUN: AmountMath.make(runBrand, 70000n) }, | ||
want: { RUN: AmountMath.make(runBrand, 70n) }, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
assert( | ||
!(absDelta > totalDebt.value), | ||
'Negative delta greater than total debt', | ||
); | ||
totalDebt = AmountMath.subtract( | ||
totalDebt, | ||
AmountMath.make(totalDebt.brand, absDelta), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
*/ | ||
// TODO rename to calculateActualDebtAmount throughout codebase https://github.com/Agoric/agoric-sdk/issues/4540 | ||
const getDebtAmount = () => { | ||
// divide compounded interest by the the snapshot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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]>} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @yields {[string, VaultKit]>} | |
* @yields {[string, VaultKit]} |
59d86db
to
7c3bff8
Compare
There was a problem hiding this 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?
packages/run-protocol/README.md
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
while ( | ||
numerator > COMPOUNDED_INTEREST_PRECISION && | ||
denominator > COMPOUNDED_INTEREST_PRECISION | ||
) { | ||
numerator /= COMPOUNDED_INTEREST_PRECISION; | ||
denominator /= COMPOUNDED_INTEREST_PRECISION; | ||
} |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const debt = vault.getDebtAmount(); | ||
const collateral = vault.getCollateralAmount(); | ||
const key = toVaultKey(debt, collateral, vaultId); | ||
console.log('addVaultKit', { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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.
* 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. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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, |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! d2560ba
// eslint-disable-next-line no-plusplus | ||
const vaultId = String(vaultCounter++); |
There was a problem hiding this comment.
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.
// eslint-disable-next-line no-plusplus | |
const vaultId = String(vaultCounter++); | |
vaultCounter += 1; | |
const vaultId = String(vaultCounter); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…h non-methods like vault in VaultKit
7c3bff8
to
d3a746f
Compare
There was a problem hiding this 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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% |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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?
const managerFacade = harden({ | |
const managerFacet = harden({ |
packages/run-protocol/README.md
Outdated
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()), |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return; | ||
} | ||
|
||
if (delta > 0n) { |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be fixed?
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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,
compoundedInterest
value on the manager that keeps track of interest accrual since its launchdebtSnapshot
on the vault by which one can calculate the actual debtTo 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,
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?