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

fix: Treasury burn debt repayment before zeroing the amount owed #3604

Merged
merged 1 commit into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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