UNPKG

@agoric/zoe

Version:

Zoe: the Smart Contract Framework for Offer Enforcement

154 lines (128 loc) 5.07 kB
import { assert, Fail } from '@endo/errors'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; import { AmountMath } from '@agoric/ertp'; import { assertProposalShape, getAmountOut, ceilMultiplyBy, getTimestamp, } from '../../contractSupport/index.js'; import { scheduleLiquidation } from './scheduleLiquidation.js'; import { calculateInterest, makeDebtCalculator } from './updateDebt.js'; import { makeCloseLoanInvitation } from './close.js'; import { makeAddCollateralInvitation } from './addCollateral.js'; /** @type {MakeBorrowInvitation} */ export const makeBorrowInvitation = (zcf, config) => { const { mmr, // Maintenance Margin Requirement, as a Ratio priceAuthority, periodNotifier, interestRate, interestPeriod, lenderSeat, } = config; // We can only lend what the lender has already escrowed. const maxLoan = lenderSeat.getAmountAllocated('Loan'); /** @type {OfferHandler} */ const borrow = async borrowerSeat => { assertProposalShape(borrowerSeat, { give: { Collateral: null }, want: { Loan: null }, }); const collateralGiven = borrowerSeat.getAmountAllocated( 'Collateral', /** @type {Brand<'nat'>} */ ( borrowerSeat.getProposal().give.Collateral.brand ), ); const loanWanted = borrowerSeat.getProposal().want.Loan; const loanBrand = zcf.getTerms().brands.Loan; // The value of the collateral in the Loan brand const quote = await E(priceAuthority).quoteGiven( collateralGiven, loanBrand, ); const collateralPriceInLoanBrand = getAmountOut(quote); // Assert the required collateral was escrowed. const requiredMargin = ceilMultiplyBy(loanWanted, mmr); AmountMath.isGTE(collateralPriceInLoanBrand, requiredMargin) || Fail`The required margin is ${requiredMargin.value}% but collateral only had value of ${collateralPriceInLoanBrand.value}`; const timestamp = getTimestamp(quote); // Assert that the collateralGiven has not changed after the AWAIT assert( AmountMath.isEqual( collateralGiven, borrowerSeat.getAmountAllocated('Collateral'), ), `The collateral allocated changed during the borrow step, which should not have been possible`, ); // Assert that loanWanted <= maxLoan AmountMath.isGTE(maxLoan, loanWanted) || Fail`The wanted loan ${loanWanted} must be below or equal to the maximum possible loan ${maxLoan}`; const { zcfSeat: collateralSeat } = zcf.makeEmptySeatKit(); zcf.atomicRearrange( harden([ // Transfer the wanted Loan amount to the borrower [lenderSeat, borrowerSeat, { Loan: loanWanted }], // Transfer *all* collateral to the collateral seat. [borrowerSeat, collateralSeat, { Collateral: collateralGiven }], ]), ); // We now exit the borrower seat so that the borrower gets their // loan. However, the borrower gets an object as their offerResult // that will let them continue to interact with the contract. borrowerSeat.exit(); const liquidationPromiseKit = makePromiseKit(); /** @type {DebtCalculatorConfig} */ const debtCalculatorConfig = { calcInterestFn: calculateInterest, originalDebt: loanWanted, periodNotifier, interestRate, interestPeriod, basetime: timestamp, zcf, configMinusGetDebt: { ...config, collateralSeat, liquidationPromiseKit, }, }; const { getDebt, getDebtNotifier, getLastCalculationTimestamp } = makeDebtCalculator(harden(debtCalculatorConfig)); /** @type {LoanConfigWithBorrower} */ const configWithBorrower = { ...config, collateralSeat, getDebt, liquidationPromiseKit, }; // Schedule the liquidation. If the liquidation cannot be scheduled // because of a problem with a misconfigured priceAuthority, an // error will be thrown and the borrower will be stuck with their // loan and the lender will receive the collateral. It is // important for the borrower to validate the priceAuthority for // this reason. scheduleLiquidation(zcf, configWithBorrower); // TODO: Add ability to liquidate partially // TODO: Add ability to withdraw excess collateral // TODO: Add ability to repay partially /** @type {BorrowFacet} */ const borrowFacet = Far('borrowFacet', { makeCloseLoanInvitation: () => makeCloseLoanInvitation(zcf, configWithBorrower), makeAddCollateralInvitation: () => makeAddCollateralInvitation(zcf, configWithBorrower), getLiquidationPromise: () => liquidationPromiseKit.promise, getDebtNotifier, getLastCalculationTimestamp, getRecentCollateralAmount: () => collateralSeat.getAmountAllocated('Collateral'), }); return borrowFacet; }; const customBorrowProps = harden({ maxLoan }); return zcf.makeInvitation(borrow, 'borrow', customBorrowProps); };