Skip to content

Commit

Permalink
fix: Treasury burn debt repayment before zeroing the amount owed (#3604)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Chris-Hibbert authored Aug 9, 2021
1 parent 6d2a8f2 commit f0bc4cb
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/treasury/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

/**
* @typedef {Object} InnerVaultManager
* @property {Brand} collateralBrand
* @property {() => Brand} getCollateralBrand
* @property {() => Ratio} getLiquidationMargin
* @property {() => Ratio} getLoanFee
* @property {() => Promise<PriceQuote>} getCollateralQuote
Expand Down
14 changes: 8 additions & 6 deletions packages/treasury/src/vault.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function makeVaultKit(
assert(active, 'vault must still be active');
}

const collateralBrand = manager.collateralBrand;
const collateralBrand = manager.getCollateralBrand();
// timestamp of most recent update to interest
let latestInterestUpdate = startTimeStamp;

Expand Down Expand Up @@ -102,15 +102,16 @@ export function makeVaultKit(

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

const quoteAmount = await E(priceAuthority).quoteGiven(
collateralAmount,
runBrand,
);

// TODO: allow Ratios to represent X/0.
if (AmountMath.isEmpty(runDebt)) {
return makeRatio(collateralAmount.value, runBrand, 1n);
}
const collateralValueInRun = getAmountOut(quoteAmount);
return makeRatioFromAmounts(collateralValueInRun, runDebt);
}
Expand Down Expand Up @@ -173,11 +174,12 @@ export function makeVaultKit(
zcf.reallocate(seat, vaultSeat);

seat.exit();
runDebt = AmountMath.makeEmpty(runBrand);
active = false;
updateUiState();

runMint.burnLosses({ RUN: runDebt }, vaultSeat);
runDebt = AmountMath.makeEmpty(runBrand);
assertVaultHoldsNoRun();
vaultSeat.exit();

return 'your loan is closed, thank you for your business';
Expand Down
2 changes: 1 addition & 1 deletion packages/treasury/src/vaultManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export function makeVaultManager(
/** @type {InnerVaultManager} */
const innerFacet = harden({
...shared,
collateralBrand,
getCollateralBrand: () => collateralBrand,
});

/** @param {ZCFSeat} seat */
Expand Down
186 changes: 181 additions & 5 deletions packages/treasury/test/test-stablecoin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
/* global require, setImmediate */

import { test } from '@agoric/zoe/tools/prepare-test-env-ava';
import '@agoric/zoe/exported';
import '../src/types';
Expand Down Expand Up @@ -273,12 +274,8 @@ test('first', async t => {
'withdrew 100 collateral',
);

console.log('preDEBT ', vault.getDebtAmount());

await E(aethVaultManager).liquidateAll();
console.log('DEBT ', vault.getDebtAmount());
t.truthy(AmountMath.isEmpty(vault.getDebtAmount()), 'debt is paid off');
console.log('COLLATERAL ', vault.getCollateralAmount());
t.truthy(AmountMath.isEmpty(vault.getCollateralAmount()), 'vault is cleared');

t.deepEqual(stablecoinMachine.getRewardAllocation(), {
Expand Down Expand Up @@ -1618,7 +1615,6 @@ test('mutable liquidity triggers and interest', async t => {
test('bad chargingPeriod', async t => {
/* @type {TestContext} */
const setJig = () => {};
/* @type {TestContext} */
const zoe = setUpZoeForTest(setJig);

const autoswapInstall = await makeInstall(autoswapRoot, zoe);
Expand Down Expand Up @@ -1788,3 +1784,183 @@ test('coll fees from loan and AMM', async t => {
.RUN;
t.truthy(AmountMath.isGTE(feePayoutAmount, feePoolBalance.RUN));
});

test('close loan', async t => {
/* @type {TestContext} */
let testJig;
const setJig = jig => {
testJig = jig;
};
const zoe = setUpZoeForTest(setJig);

const autoswapInstall = await makeInstall(autoswapRoot, zoe);
const stablecoinInstall = await makeInstall(stablecoinRoot, zoe);
const liquidationInstall = await makeInstall(liquidationRoot, zoe);

const {
aethKit: { mint: aethMint, issuer: aethIssuer, brand: aethBrand },
} = setupAssets();

const priceAuthorityPromiseKit = makePromiseKit();
const priceAuthorityPromise = priceAuthorityPromiseKit.promise;
const loanParams = {
chargingPeriod: 2n,
recordingPeriod: 6n,
poolFee: 24n,
protocolFee: 6n,
};
const manualTimer = buildManualTimer(console.log);
const { creatorFacet: stablecoinMachine, publicFacet: lender } = await E(
zoe,
).startInstance(
stablecoinInstall,
{},
{
autoswapInstall,
priceAuthority: priceAuthorityPromise,
loanParams,
timerService: manualTimer,
liquidationInstall,
},
);
const { runIssuerRecord, govIssuerRecord } = testJig;
const { issuer: runIssuer, brand: runBrand } = runIssuerRecord;
const { brand: govBrand } = govIssuerRecord;
const quoteMint = makeIssuerKit('quote', AssetKind.SET).mint;

const priceAuthority = makePriceAuthority(
aethBrand,
runBrand,
[15n],
null,
manualTimer,
quoteMint,
AmountMath.make(1n, aethBrand),
);
priceAuthorityPromiseKit.resolve(priceAuthority);

// Add a vaultManager with 900 aeth collateral at a 201 aeth/RUN rate
const capitalAmount = AmountMath.make(900n, aethBrand);
const rates = makeRates(runBrand, aethBrand);
const aethVaultManagerSeat = await E(zoe).offer(
E(stablecoinMachine).makeAddTypeInvitation(aethIssuer, 'AEth', rates),
harden({
give: { Collateral: capitalAmount },
want: { Governance: AmountMath.makeEmpty(govBrand) },
}),
harden({
Collateral: aethMint.mintPayment(capitalAmount),
}),
);

await E(aethVaultManagerSeat).getOfferResult();

// initial loan /////////////////////////////////////

// Create a loan for Alice for 5000 RUN with 1000 aeth collateral
const collateralAmount = AmountMath.make(1000n, aethBrand);
const aliceLoanAmount = AmountMath.make(5000n, runBrand);
const aliceLoanSeat = await E(zoe).offer(
E(lender).makeLoanInvitation(),
harden({
give: { Collateral: collateralAmount },
want: { RUN: aliceLoanAmount },
}),
harden({
Collateral: aethMint.mintPayment(collateralAmount),
}),
);
const {
vault: aliceVault,
uiNotifier: aliceNotifier,
liquidationPayout,
} = await E(aliceLoanSeat).getOfferResult();

const debtAmount = await E(aliceVault).getDebtAmount();
const fee = multiplyBy(aliceLoanAmount, rates.loanFee);
const runDebtLevel = AmountMath.add(aliceLoanAmount, fee);

t.deepEqual(debtAmount, runDebtLevel, 'vault lent 5000 RUN + fees');
const { RUN: lentAmount } = await E(aliceLoanSeat).getCurrentAllocation();
const loanProceeds = await E(aliceLoanSeat).getPayouts();
t.deepEqual(lentAmount, aliceLoanAmount, 'received 5000 RUN');

const runLent = await loanProceeds.RUN;
t.truthy(
AmountMath.isEqual(
await E(runIssuer).getAmountOf(runLent),
AmountMath.make(5000n, runBrand),
),
);

const aliceUpdate = await aliceNotifier.getUpdateSince();
t.deepEqual(aliceUpdate.value.debt, runDebtLevel);
const aliceCollateralization1 = aliceUpdate.value.collateralizationRatio;
t.deepEqual(aliceCollateralization1.numerator.value, 15000n);
t.deepEqual(aliceCollateralization1.denominator.value, runDebtLevel.value);

// Create a loan for Bob for 1000 RUN with 200 aeth collateral
const bobCollateralAmount = AmountMath.make(200n, aethBrand);
const bobLoanAmount = AmountMath.make(1000n, runBrand);
const bobLoanSeat = await E(zoe).offer(
E(lender).makeLoanInvitation(),
harden({
give: { Collateral: bobCollateralAmount },
want: { RUN: bobLoanAmount },
}),
harden({
Collateral: aethMint.mintPayment(bobCollateralAmount),
}),
);
const bobProceeds = await E(bobLoanSeat).getPayouts();
await E(bobLoanSeat).getOfferResult();
const bobRun = await bobProceeds.RUN;
t.truthy(
AmountMath.isEqual(
await E(runIssuer).getAmountOf(bobRun),
AmountMath.make(1000n, runBrand),
),
);

// close loan, using Bob's RUN /////////////////////////////////////

const runRepayment = await E(runIssuer).combine([bobRun, runLent]);

const aliceCloseSeat = await E(zoe).offer(
E(aliceVault).makeCloseInvitation(),
harden({
give: { RUN: AmountMath.make(6000n, runBrand) },
want: { Collateral: AmountMath.makeEmpty(aethBrand) },
}),
harden({ RUN: runRepayment }),
);

const closeOfferResult = await E(aliceCloseSeat).getOfferResult();
t.is(closeOfferResult, 'your loan is closed, thank you for your business');

const closeAlloc = await E(aliceCloseSeat).getCurrentAllocation();
t.deepEqual(closeAlloc, {
RUN: AmountMath.make(750n, runBrand),
Collateral: AmountMath.make(1000n, aethBrand),
});
const closeProceeds = await E(aliceCloseSeat).getPayouts();
const collProceeds = await aethIssuer.getAmountOf(closeProceeds.Collateral);
const runProceeds = await E(runIssuer).getAmountOf(closeProceeds.RUN);

t.deepEqual(runProceeds, AmountMath.make(750n, runBrand));
t.deepEqual(collProceeds, AmountMath.make(1000n, aethBrand));
t.deepEqual(
await E(aliceVault).getCollateralAmount(),
AmountMath.makeEmpty(aethBrand),
);

const liquidation = await liquidationPayout;
t.deepEqual(
await E(runIssuer).getAmountOf(liquidation.RUN),
AmountMath.makeEmpty(runBrand),
);
t.deepEqual(
await aethIssuer.getAmountOf(liquidation.Collateral),
AmountMath.makeEmpty(aethBrand),
);
});
4 changes: 3 additions & 1 deletion packages/treasury/test/vault-contract-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export async function start(zcf) {
getInterestRate() {
return makeRatio(5n, runBrand);
},
// collateralBrand, // TODO not a method. How did this ever work?
getCollateralBrand() {
return collateralBrand;
},
reallocateReward,
});

Expand Down

0 comments on commit f0bc4cb

Please sign in to comment.