Skip to content

Commit f0bc4cb

Browse files
fix: Treasury burn debt repayment before zeroing the amount owed (#3604)
fixes #3495 fixed getCollateralBrand in the innerFacet, which wasn't a method. Moved a check for empty denominators to be after an await. Added assertVaultHoldsNoRun() in close. Added a test for closing a loan.
1 parent 6d2a8f2 commit f0bc4cb

File tree

5 files changed

+194
-14
lines changed

5 files changed

+194
-14
lines changed

packages/treasury/src/types.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959

6060
/**
6161
* @typedef {Object} InnerVaultManager
62-
* @property {Brand} collateralBrand
62+
* @property {() => Brand} getCollateralBrand
6363
* @property {() => Ratio} getLiquidationMargin
6464
* @property {() => Ratio} getLoanFee
6565
* @property {() => Promise<PriceQuote>} getCollateralQuote

packages/treasury/src/vault.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function makeVaultKit(
4040
assert(active, 'vault must still be active');
4141
}
4242

43-
const collateralBrand = manager.collateralBrand;
43+
const collateralBrand = manager.getCollateralBrand();
4444
// timestamp of most recent update to interest
4545
let latestInterestUpdate = startTimeStamp;
4646

@@ -102,15 +102,16 @@ export function makeVaultKit(
102102

103103
async function getCollateralizationRatio() {
104104
const collateralAmount = getCollateralAmount();
105-
// TODO: allow Ratios to represent X/0.
106-
if (AmountMath.isEmpty(runDebt)) {
107-
return makeRatio(collateralAmount.value, runBrand, 1n);
108-
}
109105

110106
const quoteAmount = await E(priceAuthority).quoteGiven(
111107
collateralAmount,
112108
runBrand,
113109
);
110+
111+
// TODO: allow Ratios to represent X/0.
112+
if (AmountMath.isEmpty(runDebt)) {
113+
return makeRatio(collateralAmount.value, runBrand, 1n);
114+
}
114115
const collateralValueInRun = getAmountOut(quoteAmount);
115116
return makeRatioFromAmounts(collateralValueInRun, runDebt);
116117
}
@@ -173,11 +174,12 @@ export function makeVaultKit(
173174
zcf.reallocate(seat, vaultSeat);
174175

175176
seat.exit();
176-
runDebt = AmountMath.makeEmpty(runBrand);
177177
active = false;
178178
updateUiState();
179179

180180
runMint.burnLosses({ RUN: runDebt }, vaultSeat);
181+
runDebt = AmountMath.makeEmpty(runBrand);
182+
assertVaultHoldsNoRun();
181183
vaultSeat.exit();
182184

183185
return 'your loan is closed, thank you for your business';

packages/treasury/src/vaultManager.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export function makeVaultManager(
202202
/** @type {InnerVaultManager} */
203203
const innerFacet = harden({
204204
...shared,
205-
collateralBrand,
205+
getCollateralBrand: () => collateralBrand,
206206
});
207207

208208
/** @param {ZCFSeat} seat */

packages/treasury/test/test-stablecoin.js

+181-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @ts-check
22
/* global require, setImmediate */
3+
34
import { test } from '@agoric/zoe/tools/prepare-test-env-ava';
45
import '@agoric/zoe/exported';
56
import '../src/types';
@@ -273,12 +274,8 @@ test('first', async t => {
273274
'withdrew 100 collateral',
274275
);
275276

276-
console.log('preDEBT ', vault.getDebtAmount());
277-
278277
await E(aethVaultManager).liquidateAll();
279-
console.log('DEBT ', vault.getDebtAmount());
280278
t.truthy(AmountMath.isEmpty(vault.getDebtAmount()), 'debt is paid off');
281-
console.log('COLLATERAL ', vault.getCollateralAmount());
282279
t.truthy(AmountMath.isEmpty(vault.getCollateralAmount()), 'vault is cleared');
283280

284281
t.deepEqual(stablecoinMachine.getRewardAllocation(), {
@@ -1618,7 +1615,6 @@ test('mutable liquidity triggers and interest', async t => {
16181615
test('bad chargingPeriod', async t => {
16191616
/* @type {TestContext} */
16201617
const setJig = () => {};
1621-
/* @type {TestContext} */
16221618
const zoe = setUpZoeForTest(setJig);
16231619

16241620
const autoswapInstall = await makeInstall(autoswapRoot, zoe);
@@ -1788,3 +1784,183 @@ test('coll fees from loan and AMM', async t => {
17881784
.RUN;
17891785
t.truthy(AmountMath.isGTE(feePayoutAmount, feePoolBalance.RUN));
17901786
});
1787+
1788+
test('close loan', async t => {
1789+
/* @type {TestContext} */
1790+
let testJig;
1791+
const setJig = jig => {
1792+
testJig = jig;
1793+
};
1794+
const zoe = setUpZoeForTest(setJig);
1795+
1796+
const autoswapInstall = await makeInstall(autoswapRoot, zoe);
1797+
const stablecoinInstall = await makeInstall(stablecoinRoot, zoe);
1798+
const liquidationInstall = await makeInstall(liquidationRoot, zoe);
1799+
1800+
const {
1801+
aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand },
1802+
} = setupAssets();
1803+
1804+
const priceAuthorityPromiseKit = makePromiseKit();
1805+
const priceAuthorityPromise = priceAuthorityPromiseKit.promise;
1806+
const loanParams = {
1807+
chargingPeriod: 2n,
1808+
recordingPeriod: 6n,
1809+
poolFee: 24n,
1810+
protocolFee: 6n,
1811+
};
1812+
const manualTimer = buildManualTimer(console.log);
1813+
const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E(
1814+
zoe,
1815+
).startInstance(
1816+
stablecoinInstall,
1817+
{},
1818+
{
1819+
autoswapInstall,
1820+
priceAuthority: priceAuthorityPromise,
1821+
loanParams,
1822+
timerService: manualTimer,
1823+
liquidationInstall,
1824+
},
1825+
);
1826+
const { runIssuerRecord, govIssuerRecord } = testJig;
1827+
const { issuer: runIssuer, brand: runBrand } = runIssuerRecord;
1828+
const { brand: govBrand } = govIssuerRecord;
1829+
const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint;
1830+
1831+
const priceAuthority = makePriceAuthority(
1832+
aethBrand,
1833+
runBrand,
1834+
[15n],
1835+
null,
1836+
manualTimer,
1837+
quoteMint,
1838+
AmountMath.make(1n, aethBrand),
1839+
);
1840+
priceAuthorityPromiseKit.resolve(priceAuthority);
1841+
1842+
// Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate
1843+
const capitalAmount = AmountMath.make(900n, aethBrand);
1844+
const rates = makeRates(runBrand, aethBrand);
1845+
const aethVaultManagerSeat = await E(zoe).offer(
1846+
E(stablecoinMachine).makeAddTypeInvitation(aethIssuer, 'AEth', rates),
1847+
harden({
1848+
give: { Collateral: capitalAmount },
1849+
want: { Governance: AmountMath.makeEmpty(govBrand) },
1850+
}),
1851+
harden({
1852+
Collateral: aethMint.mintPayment(capitalAmount),
1853+
}),
1854+
);
1855+
1856+
await E(aethVaultManagerSeat).getOfferResult();
1857+
1858+
// initial loan /////////////////////////////////////
1859+
1860+
// Create a loan for Alice for 5000 RUN with 1000 aeth collateral
1861+
const collateralAmount = AmountMath.make(1000n, aethBrand);
1862+
const aliceLoanAmount = AmountMath.make(5000n, runBrand);
1863+
const aliceLoanSeat = await E(zoe).offer(
1864+
E(lender).makeLoanInvitation(),
1865+
harden({
1866+
give: { Collateral: collateralAmount },
1867+
want: { RUN: aliceLoanAmount },
1868+
}),
1869+
harden({
1870+
Collateral: aethMint.mintPayment(collateralAmount),
1871+
}),
1872+
);
1873+
const {
1874+
vault: aliceVault,
1875+
uiNotifier: aliceNotifier,
1876+
liquidationPayout,
1877+
} = await E(aliceLoanSeat).getOfferResult();
1878+
1879+
const debtAmount = await E(aliceVault).getDebtAmount();
1880+
const fee = multiplyBy(aliceLoanAmount, rates.loanFee);
1881+
const runDebtLevel = AmountMath.add(aliceLoanAmount, fee);
1882+
1883+
t.deepEqual(debtAmount, runDebtLevel, 'vault lent 5000 RUN + fees');
1884+
const { RUN: lentAmount } = await E(aliceLoanSeat).getCurrentAllocation();
1885+
const loanProceeds = await E(aliceLoanSeat).getPayouts();
1886+
t.deepEqual(lentAmount, aliceLoanAmount, 'received 5000 RUN');
1887+
1888+
const runLent = await loanProceeds.RUN;
1889+
t.truthy(
1890+
AmountMath.isEqual(
1891+
await E(runIssuer).getAmountOf(runLent),
1892+
AmountMath.make(5000n, runBrand),
1893+
),
1894+
);
1895+
1896+
const aliceUpdate = await aliceNotifier.getUpdateSince();
1897+
t.deepEqual(aliceUpdate.value.debt, runDebtLevel);
1898+
const aliceCollateralization1 = aliceUpdate.value.collateralizationRatio;
1899+
t.deepEqual(aliceCollateralization1.numerator.value, 15000n);
1900+
t.deepEqual(aliceCollateralization1.denominator.value, runDebtLevel.value);
1901+
1902+
// Create a loan for Bob for 1000 RUN with 200 aeth collateral
1903+
const bobCollateralAmount = AmountMath.make(200n, aethBrand);
1904+
const bobLoanAmount = AmountMath.make(1000n, runBrand);
1905+
const bobLoanSeat = await E(zoe).offer(
1906+
E(lender).makeLoanInvitation(),
1907+
harden({
1908+
give: { Collateral: bobCollateralAmount },
1909+
want: { RUN: bobLoanAmount },
1910+
}),
1911+
harden({
1912+
Collateral: aethMint.mintPayment(bobCollateralAmount),
1913+
}),
1914+
);
1915+
const bobProceeds = await E(bobLoanSeat).getPayouts();
1916+
await E(bobLoanSeat).getOfferResult();
1917+
const bobRun = await bobProceeds.RUN;
1918+
t.truthy(
1919+
AmountMath.isEqual(
1920+
await E(runIssuer).getAmountOf(bobRun),
1921+
AmountMath.make(1000n, runBrand),
1922+
),
1923+
);
1924+
1925+
// close loan, using Bob's RUN /////////////////////////////////////
1926+
1927+
const runRepayment = await E(runIssuer).combine([bobRun, runLent]);
1928+
1929+
const aliceCloseSeat = await E(zoe).offer(
1930+
E(aliceVault).makeCloseInvitation(),
1931+
harden({
1932+
give: { RUN: AmountMath.make(6000n, runBrand) },
1933+
want: { Collateral: AmountMath.makeEmpty(aethBrand) },
1934+
}),
1935+
harden({ RUN: runRepayment }),
1936+
);
1937+
1938+
const closeOfferResult = await E(aliceCloseSeat).getOfferResult();
1939+
t.is(closeOfferResult, 'your loan is closed, thank you for your business');
1940+
1941+
const closeAlloc = await E(aliceCloseSeat).getCurrentAllocation();
1942+
t.deepEqual(closeAlloc, {
1943+
RUN: AmountMath.make(750n, runBrand),
1944+
Collateral: AmountMath.make(1000n, aethBrand),
1945+
});
1946+
const closeProceeds = await E(aliceCloseSeat).getPayouts();
1947+
const collProceeds = await aethIssuer.getAmountOf(closeProceeds.Collateral);
1948+
const runProceeds = await E(runIssuer).getAmountOf(closeProceeds.RUN);
1949+
1950+
t.deepEqual(runProceeds, AmountMath.make(750n, runBrand));
1951+
t.deepEqual(collProceeds, AmountMath.make(1000n, aethBrand));
1952+
t.deepEqual(
1953+
await E(aliceVault).getCollateralAmount(),
1954+
AmountMath.makeEmpty(aethBrand),
1955+
);
1956+
1957+
const liquidation = await liquidationPayout;
1958+
t.deepEqual(
1959+
await E(runIssuer).getAmountOf(liquidation.RUN),
1960+
AmountMath.makeEmpty(runBrand),
1961+
);
1962+
t.deepEqual(
1963+
await aethIssuer.getAmountOf(liquidation.Collateral),
1964+
AmountMath.makeEmpty(aethBrand),
1965+
);
1966+
});

packages/treasury/test/vault-contract-wrapper.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export async function start(zcf) {
6666
getInterestRate() {
6767
return makeRatio(5n, runBrand);
6868
},
69-
// collateralBrand, // TODO not a method. How did this ever work?
69+
getCollateralBrand() {
70+
return collateralBrand;
71+
},
7072
reallocateReward,
7173
});
7274

0 commit comments

Comments
 (0)