Skip to content

Commit a18dbbf

Browse files
authored
fix: giving collateral should not change totalDebt (#9873)
closes: #XXXX refs: #XXXX ## Description Updating `totalDebt` for 1 `vault` was doing `totalDebt += vault.getCurrentDebt() - old` where current debt includes compounded interest but old did not. ### Security Considerations This (in combination with another PR to reset totalDebt to the sum over all vaults) should address a confusion risk for metrics clients. ### Scaling Considerations The separate work to reset totalDebt to the sum over all vaults may have some, but I don't see any here. ### Testing Considerations A straightforward unit test is included. ### Upgrade Considerations This change takes effect with the next upgrade to the `vaultFactory`. ### Documentation Considerations As part of the governance decision to upgrade `vaultFactory`, metrics consumers should be notified of this fix.
2 parents aaa0def + e29a42c commit a18dbbf

7 files changed

Lines changed: 91 additions & 29 deletions

File tree

‎packages/inter-protocol/src/interest.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const chargeInterest = (powers, params, prior, accruedUntil) => {
197197
}
198198

199199
// NB: This method of inferring the compounded rate from the ratio of debts
200-
// acrrued suffers slightly from the integer nature of debts. However in
200+
// accrued suffers slightly from the integer nature of debts. However in
201201
// testing with small numbers there's 5 digits of precision, and with large
202202
// numbers the ratios tend towards ample precision.
203203
// TODO adopt banker's rounding https://github.com/Agoric/agoric-sdk/issues/4573

‎packages/inter-protocol/src/vaultFactory/vaultManager.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
TopicsRecordShape,
5353
} from '@agoric/zoe/src/contractSupport/index.js';
5454
import { PriceQuoteShape, SeatShape } from '@agoric/zoe/src/typeGuards.js';
55+
import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js';
5556
import {
5657
checkDebtLimit,
5758
makeNatAmountShape,
@@ -993,8 +994,9 @@ export const prepareVaultManagerKit = (
993994
);
994995
state.totalDebt = AmountMath.subtract(
995996
AmountMath.add(state.totalDebt, vault.getCurrentDebt()),
996-
oldDebtNormalized,
997+
multiplyBy(oldDebtNormalized, state.compoundedInterest),
997998
);
999+
9981000
void facets.helper.writeMetrics();
9991001
},
10001002
},

‎packages/inter-protocol/test/vaultFactory/driver.js‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ const getRunFromFaucet = async (t, amt) => {
213213
* @param {Amount} priceBase
214214
*/
215215
const setupServices = async (t, initialPrice, priceBase) => {
216-
const timer = buildZoeManualTimer(t.log);
216+
const timer = buildZoeManualTimer(t.log, 0n, { timeStep: 60n * 60n });
217217
const { zoe, run, aeth, interestTiming, minInitialDebt, rates } = t.context;
218218
t.context.timer = timer;
219219

@@ -336,6 +336,10 @@ export const makeManagerDriver = async (
336336
publicTopics.asset.subscriber,
337337
);
338338
let managerNotification = await E(managerNotifier).getUpdateSince();
339+
const metricsNotifier = await makeNotifierFromSubscriber(
340+
publicTopics.metrics.subscriber,
341+
);
342+
let metricsNotification = await E(metricsNotifier).getUpdateSince();
339343

340344
/** @type {UserSeat} */
341345
let currentSeat;
@@ -559,6 +563,21 @@ export const makeManagerDriver = async (
559563
}
560564
return managerNotification;
561565
},
566+
/**
567+
* @param {object} [likeExpected]
568+
* @param {AT_NEXT | number} [optSince] AT_NEXT is an alias for updateCount
569+
* of the last update, forcing to wait for another
570+
*/
571+
metricsNotified: async (likeExpected, optSince) => {
572+
metricsNotification = await E(metricsNotifier).getUpdateSince(
573+
optSince === AT_NEXT ? metricsNotification.updateCount : optSince,
574+
);
575+
trace(t, 'metrics notifier', metricsNotification);
576+
if (likeExpected) {
577+
t.like(metricsNotification.value, likeExpected);
578+
}
579+
return managerNotification;
580+
},
562581
checkReserveAllocation: async stableValue => {
563582
const { reserveCreatorFacet } = t.context;
564583
const reserveAllocations = await E(reserveCreatorFacet).getAllocations();

‎packages/inter-protocol/test/vaultFactory/snapshots/vaultFactory.test.js.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Generated by [AVA](https://avajs.dev).
204204
},
205205
totalDebt: {
206206
brand: Object @Alleged: IST brand {},
207-
value: 1634n,
207+
value: 1608n,
208208
},
209209
totalOverageReceived: {
210210
brand: Object @Alleged: IST brand {},
Binary file not shown.

‎packages/inter-protocol/test/vaultFactory/vault-collateralization.test.js‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
22

33
import { makeTracer } from '@agoric/internal';
44
import { E } from '@endo/eventual-send';
5-
import { makeDriverContext, makeManagerDriver } from './driver.js';
5+
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
6+
import { AT_NEXT, makeDriverContext, makeManagerDriver } from './driver.js';
67

78
/** @typedef {import('./driver.js').DriverContext & {}} Context */
89
/** @type {import('ava').TestFn<Context>} */
@@ -15,6 +16,46 @@ test.before(async t => {
1516
trace(t, 'CONTEXT');
1617
});
1718

19+
test('totalDebt calculation includes compoundedInterest', async t => {
20+
const { aeth, run } = t.context;
21+
const md = await makeManagerDriver(t);
22+
const timer = md.timer();
23+
24+
t.log('charge 2% per day to simulate lower rates over longer times');
25+
const aethKey = { collateralBrand: aeth.brand };
26+
const twoPctPerDay = run.makeRatio(2n * 360n, 100n);
27+
await md.setGovernedParam('InterestRate', twoPctPerDay, { key: aethKey });
28+
const plausibleDebtLimit = run.units(500000);
29+
await md.setGovernedParam('DebtLimit', plausibleDebtLimit, { key: aethKey });
30+
31+
t.log('mint 100 IST against 25 AETH');
32+
const vd = await md.makeVaultDriver(aeth.units(25), run.units(100));
33+
const v1debtAfterMint = await E(vd.vault()).getCurrentDebt();
34+
t.deepEqual(v1debtAfterMint, run.units(100 + 5));
35+
// totalDebt is debit of this 1 vault
36+
await md.metricsNotified({ totalDebt: v1debtAfterMint }, AT_NEXT);
37+
38+
await E(timer).tickN(40, 'interest accumulates over 40hrs');
39+
await eventLoopIteration(); // let all timer-induced promises settle
40+
const v1debtAfterDay = await E(vd.vault()).getCurrentDebt();
41+
t.deepEqual(v1debtAfterDay, run.make(106_008_000n));
42+
43+
// Checking that totalDebt here matches v1debtAfterDay would be handy,
44+
// but totalDebt isn't updated when calculating interest.
45+
const expectedCompoundedInterest = {
46+
denominator: { value: 100000000000000000000n },
47+
numerator: { value: 100960000000000000000n },
48+
};
49+
await md.managerNotified({ compoundedInterest: expectedCompoundedInterest });
50+
51+
t.log('give some collateral (adjustBalances); no change to debt');
52+
await E(vd).giveCollateral(50n, aeth);
53+
const v1debtAfterGive = await E(vd.vault()).getCurrentDebt();
54+
t.deepEqual(v1debtAfterGive, v1debtAfterDay, 'no debt change');
55+
56+
await md.metricsNotified({ totalDebt: v1debtAfterDay }, AT_NEXT);
57+
});
58+
1859
test('excessive loan', async t => {
1960
const { aeth, run } = t.context;
2061
const md = await makeManagerDriver(t);

‎packages/inter-protocol/test/vaultFactory/vaultFactory.test.js‎

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,13 @@ test('manager notifiers, with snapshot', async t => {
18381838
}),
18391839
);
18401840
({ vault } = await E(vaultSeat).getOfferResult());
1841+
totalCollateral += ENOUGH;
1842+
totalDebt += DEBT2;
1843+
await m.assertChange({
1844+
numActiveVaults: 4,
1845+
totalCollateral: { value: totalCollateral },
1846+
totalDebt: { value: totalDebt },
1847+
});
18411848

18421849
trace('6. Loan interest');
18431850
vaultSeat = await E(services.zoe).offer(
@@ -1851,18 +1858,21 @@ test('manager notifiers, with snapshot', async t => {
18511858
}),
18521859
);
18531860
({ vault } = await E(vaultSeat).getOfferResult());
1854-
totalCollateral += ENOUGH;
1855-
totalDebt += DEBT2;
1861+
totalCollateral += AMPLE;
1862+
totalDebt += DEBT1;
18561863
await m.assertChange({
1857-
numActiveVaults: 4,
1864+
numActiveVaults: 5,
18581865
totalCollateral: { value: totalCollateral },
18591866
totalDebt: { value: totalDebt },
18601867
});
1861-
m.addDebt(DEBT2);
1868+
18621869
await manualTimer.tickN(5);
1870+
// This is interest for a single vault.
18631871
const interestAccrued = (await E(vault).getCurrentDebt()).value - DEBT1;
18641872
m.addDebt(interestAccrued);
1865-
t.is(interestAccrued, 9n);
1873+
1874+
t.is(interestAccrued, 9n); // interest on OPEN1 for 5 periods
1875+
totalDebt += 30n; // interest on 270_000 for 5 periods
18661876

18671877
trace('7. make another loan to trigger a publish');
18681878
vaultSeat = await E(services.zoe).offer(
@@ -1876,10 +1886,10 @@ test('manager notifiers, with snapshot', async t => {
18761886
}),
18771887
);
18781888
({ vault } = await E(vaultSeat).getOfferResult());
1879-
totalCollateral += AMPLE;
1880-
totalDebt += DEBT1;
1889+
totalCollateral += ENOUGH;
1890+
totalDebt += DEBT2;
18811891
await m.assertChange({
1882-
numActiveVaults: 5,
1892+
numActiveVaults: 6,
18831893
totalCollateral: { value: totalCollateral },
18841894
totalDebt: { value: totalDebt },
18851895
});
@@ -1898,11 +1908,10 @@ test('manager notifiers, with snapshot', async t => {
18981908
}),
18991909
);
19001910
({ vault } = await E(vaultSeat).getOfferResult());
1901-
totalCollateral += ENOUGH;
1902-
totalDebt += DEBT2 + 30n; // XXX ??
1903-
m.addDebt(DEBT2);
1911+
totalCollateral += AMPLE;
1912+
totalDebt += DEBT1;
19041913
await m.assertChange({
1905-
numActiveVaults: 6,
1914+
numActiveVaults: 7,
19061915
totalCollateral: { value: totalCollateral },
19071916
totalDebt: { value: totalDebt },
19081917
});
@@ -1925,20 +1934,11 @@ test('manager notifiers, with snapshot', async t => {
19251934
}),
19261935
);
19271936
await E(vaultOpSeat).getOfferResult();
1928-
totalCollateral += AMPLE;
1929-
totalDebt += DEBT1;
1930-
await m.assertChange({
1931-
numActiveVaults: 7,
1932-
totalDebt: { value: totalDebt },
1933-
totalCollateral: { value: totalCollateral },
1934-
});
1935-
19361937
totalCollateral += given.value;
1937-
totalDebt += WANT_EXTRA + 29n; // magic number is fees
1938-
1938+
totalDebt += WANT_EXTRA + 20n;
19391939
await m.assertChange({
1940-
totalCollateral: { value: totalCollateral },
19411940
totalDebt: { value: totalDebt },
1941+
totalCollateral: { value: totalCollateral },
19421942
});
19431943

19441944
trace('10. Close vault');
@@ -1955,7 +1955,7 @@ test('manager notifiers, with snapshot', async t => {
19551955
await E(vaultOpSeat).getOfferResult();
19561956

19571957
totalCollateral -= AMPLE + given.value;
1958-
totalDebt -= DEBT1_EXTRA - 17n; // magic number is fees
1958+
totalDebt -= DEBT1_EXTRA;
19591959
await m.assertChange({
19601960
numActiveVaults: 6,
19611961
totalCollateral: { value: totalCollateral },

0 commit comments

Comments
 (0)