From f4eff940f211ceca3723248fad69c6106caa0bf0 Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Fri, 15 Oct 2021 07:27:22 -0700 Subject: [PATCH] Fixes to nToken redeem and borrow requirement (#4) --- jest.config.js | 6 +- package.json | 2 +- src/account/AccountData.ts | 6 + src/account/AssetSummary.ts | 8 +- src/account/BalanceSummary.ts | 18 +-- src/libs/TypedBigNumber.ts | 10 ++ src/libs/types.ts | 4 +- src/system/FreeCollateral.ts | 102 +++------------ src/system/NTokenValue.ts | 16 ++- tests/unit/AssetSummary.test.ts | 1 + tests/unit/FreeCollateral.test.ts | 199 +++++++++++++++++++++--------- tests/unit/NTokenValue.test.ts | 9 +- 12 files changed, 216 insertions(+), 165 deletions(-) diff --git a/jest.config.js b/jest.config.js index deb5ab1..28ea2d3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,5 +14,9 @@ module.exports = { }, ], coverageReporters: ["text", "html"], - collectCoverage: true + collectCoverage: true, + collectCoverageFrom: [ + "src/**/*.ts", + "!src/typechain/**" + ] }; diff --git a/package.json b/package.json index cae8783..5496693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@notional-finance/sdk-v2", - "version": "0.0.4", + "version": "0.0.5", "description": "Notional Finance SDK V2", "author": "Jeff Wu ", "homepage": "https://notional.finance", diff --git a/src/account/AccountData.ts b/src/account/AccountData.ts index 9d72c6b..57dbf2a 100644 --- a/src/account/AccountData.ts +++ b/src/account/AccountData.ts @@ -68,6 +68,10 @@ export default class AccountData { ); } + public static emptyAccountData() { + return AccountData.copyAccountData(); + } + /** * Copies an account data object for simulation * @param account if undefined, will return an empty account data object @@ -192,6 +196,8 @@ export default class AccountData { */ public updateAsset(asset: Asset) { if (!this.isCopy) throw Error('Cannot update assets on non copy'); + if (asset.hasMatured) throw Error('Cannot add matured asset to account copy'); + // eslint-disable-next-line no-underscore-dangle this.portfolio = AccountData._updateAsset(this.portfolio, asset, this.bitmapCurrencyId); const {symbol} = System.getSystem().getCurrencyById(asset.currencyId); diff --git a/src/account/AssetSummary.ts b/src/account/AssetSummary.ts index eb01260..719b485 100644 --- a/src/account/AssetSummary.ts +++ b/src/account/AssetSummary.ts @@ -51,15 +51,17 @@ export default class AssetSummary { return this.currency.symbol; } - public rateOfReturnString(locale = 'en-US', precision = 3) { - // TODO: override this if there is no actual IRR - // TODO: change this to look at the last rate lent or borrowed + public internalRateOfReturnString(locale = 'en-US', precision = 3) { return `${this.irr.toLocaleString(locale, { maximumFractionDigits: precision, minimumFractionDigits: precision, })}%`; } + public mostRecentTradedRate() { + return this.history[this.history.length - 1].tradedInterestRate; + } + constructor( public assetKey: string, public underlyingInternalPV: TypedBigNumber, diff --git a/src/account/BalanceSummary.ts b/src/account/BalanceSummary.ts index e609926..2e49a76 100644 --- a/src/account/BalanceSummary.ts +++ b/src/account/BalanceSummary.ts @@ -124,15 +124,13 @@ export default class BalanceSummary { if (withdrawAmountInternalAsset.gt(this.maxWithdrawValueAssetCash)) { throw new Error('Cannot withdraw, over maximum value'); - } else if (!canWithdrawNToken) { + } else if (!canWithdrawNToken || !this.nTokenBalance || this.nTokenBalance.isZero()) { // If the nToken cannot be withdrawn, only return cash amounts - const cashWithdraw = withdrawAmountInternalAsset.lte(this.assetCashBalance) - ? withdrawAmountInternalAsset - : this.assetCashBalance; + const cashWithdraw = TypedBigNumber.min(withdrawAmountInternalAsset, this.assetCashBalance); const nTokenRedeem = this.nTokenBalance?.copy(0); return {cashWithdraw, nTokenRedeem}; - } else if (preferCash || !this.nTokenBalance || this.nTokenBalance.isZero()) { - // Cash preference supersedes nToken (or nToken balance does not exist) + } else if (preferCash) { + // Cash preference supersedes nToken if (withdrawAmountInternalAsset.lte(this.assetCashBalance)) { // Cash is sufficient to cover the withdraw return { @@ -143,7 +141,13 @@ export default class BalanceSummary { // Withdraw all cash and some part of the nToken balance const requiredNTokenRedeem = withdrawAmountInternalAsset.sub(this.assetCashBalance); const nTokenRedeem = NTokenValue.getNTokenRedeemFromAsset(this.currencyId, requiredNTokenRedeem); - return {cashWithdraw: this.assetCashBalance, nTokenRedeem}; + + return { + cashWithdraw: this.assetCashBalance, + // Cap the nTokenRedeem at the balance, it may go slightly over due to the reverse approximation inside + // getNTokenRedeemFromAsset + nTokenRedeem: TypedBigNumber.min(nTokenRedeem, this.nTokenBalance), + }; } else { // nToken supersedes cash and we know there is some nToken balance const nTokenRedeem = NTokenValue.getNTokenRedeemFromAsset(this.currencyId, withdrawAmountInternalAsset); diff --git a/src/libs/TypedBigNumber.ts b/src/libs/TypedBigNumber.ts index a4a3350..a3d6b23 100644 --- a/src/libs/TypedBigNumber.ts +++ b/src/libs/TypedBigNumber.ts @@ -53,6 +53,16 @@ class TypedBigNumber { return new TypedBigNumber(BigNumber.from(value), type, symbol); } + static max(a: TypedBigNumber, b: TypedBigNumber): TypedBigNumber { + a.checkMatch(b); + return a.gte(b) ? a : b; + } + + static min(a: TypedBigNumber, b: TypedBigNumber): TypedBigNumber { + a.checkMatch(b); + return a.lte(b) ? a : b; + } + checkType(type: BigNumberType) { if (this.type !== type) throw TypeError(`Invalid TypedBigNumber type ${this.type} != ${type}`); } diff --git a/src/libs/types.ts b/src/libs/types.ts index f26bdee..6e34264 100644 --- a/src/libs/types.ts +++ b/src/libs/types.ts @@ -136,17 +136,15 @@ export interface TradeHistory { transactionHash: string; blockTime: Date; currencyId: number; - tradeType: TradeType; settlementDate: BigNumber | null; maturityLength: number | null; maturity: BigNumber; - netAssetCash: TypedBigNumber; netUnderlyingCash: TypedBigNumber; netfCash: TypedBigNumber; - netLiquidityTokens: TypedBigNumber | null; + tradedInterestRate: number; } export interface BalanceHistory { diff --git a/src/system/FreeCollateral.ts b/src/system/FreeCollateral.ts index 9811561..3128126 100644 --- a/src/system/FreeCollateral.ts +++ b/src/system/FreeCollateral.ts @@ -190,12 +190,9 @@ export default class FreeCollateral { /** * Calculates borrow requirements for a given amount of fCash and a target collateral ratio * - * @param borrowfCashAmount fcash amount to borrow (must be negative) - * @param maturity maturity of the fcash to borrow - * @param borrowCurrencyId currency id of the fcash asset * @param collateralCurrencyId currency to collateralize this asset by * @param bufferedRatio the target post haircut / buffer collateral ratio - * @param accountData account data object, if it exists + * @param accountData account data object with borrow amounts applied * @param blockTime * @returns * - minCollateral: minimum amount of collateral required for the borrow @@ -204,12 +201,9 @@ export default class FreeCollateral { * - targetCollateralRatio: target buffered/haircut collateral ratio */ public static calculateBorrowRequirement( - borrowfCashAmount: TypedBigNumber, - maturity: number, - borrowCurrencyId: number, collateralCurrencyId: number, _bufferedRatio: number, - accountData?: AccountData, + accountData: AccountData, blockTime = getNowSeconds(), ): { minCollateral: TypedBigNumber; @@ -217,51 +211,24 @@ export default class FreeCollateral { minCollateralRatio: number | null; targetCollateralRatio: number | null; } { - const system = System.getSystem(); - if (!borrowfCashAmount.isNegative()) throw new Error('Borrow fCash amount must be negative'); - const cashGroup = system.getCashGroup(borrowCurrencyId); const bufferedRatio = Math.trunc(_bufferedRatio); - if (!cashGroup) throw new Error(`Cash group for ${borrowCurrencyId} not found`); if (bufferedRatio < 100) throw new RangeError('Buffered ratio must be more than 100'); - let netETHDebt = FreeCollateral.getZeroUnderlying(ETH); - let netETHDebtWithBuffer = FreeCollateral.getZeroUnderlying(ETH); - let netETHCollateralWithHaircut = FreeCollateral.getZeroUnderlying(ETH); - let borrowNetAvailable = FreeCollateral.getZeroUnderlying(borrowCurrencyId); - let collateralNetAvailable = FreeCollateral.getZeroUnderlying(collateralCurrencyId); - - if (accountData) { - let netUnderlyingAvailable: Map; - // prettier-ignore - ({ - netETHCollateralWithHaircut, - netETHDebt, - netETHDebtWithBuffer, - netUnderlyingAvailable, - } = FreeCollateral.getFreeCollateral( - accountData, - blockTime, - )); - borrowNetAvailable = netUnderlyingAvailable.get(borrowCurrencyId) || borrowNetAvailable; - collateralNetAvailable = netUnderlyingAvailable.get(collateralCurrencyId) || collateralNetAvailable; - } - - const borrowAmountHaircutPV = cashGroup.getfCashPresentValueUnderlyingInternal( - maturity, - borrowfCashAmount, - true, - blockTime, - ); - - // Updates the net ETH amounts to take into account the new debt, netting for - // local currency purposes only - ({netETHCollateralWithHaircut, netETHDebt, netETHDebtWithBuffer} = FreeCollateral.updateNetETHAmounts( - borrowAmountHaircutPV, + // prettier-ignore + const { netETHCollateralWithHaircut, netETHDebt, netETHDebtWithBuffer, - borrowNetAvailable, - )); + netUnderlyingAvailable, + } = FreeCollateral.getFreeCollateral( + accountData, + blockTime, + ); + + const collateralNetAvailable = ( + netUnderlyingAvailable.get(collateralCurrencyId) + || FreeCollateral.getZeroUnderlying(collateralCurrencyId) + ); return FreeCollateral.calculateTargetCollateral( netETHCollateralWithHaircut, @@ -273,47 +240,6 @@ export default class FreeCollateral { ); } - /* eslint-disable no-param-reassign */ - private static updateNetETHAmounts( - borrowAmountHaircutPV: TypedBigNumber, - netETHCollateralWithHaircut: TypedBigNumber, - netETHDebt: TypedBigNumber, - netETHDebtWithBuffer: TypedBigNumber, - borrowNetAvailable: TypedBigNumber, - ) { - if (borrowNetAvailable.isNegative() || borrowNetAvailable.isZero()) { - // If local net is already in debt then borrowAmount is added to netETHDebt - netETHDebtWithBuffer = netETHDebtWithBuffer.add(borrowAmountHaircutPV.toETH(useHaircut).abs()); - netETHDebt = netETHDebt.add(borrowAmountHaircutPV.toETH(noHaircut).abs()); - } else if (borrowNetAvailable.gte(borrowAmountHaircutPV.abs())) { - // If there's enough local to net off then this is the change to ETH collateral - // Formula here is: - // netETHBorrowBefore = convertToETH(borrowNetAvailable) * haircut - // netETHBorrowAfter = convertToETH(borrowNetAvailable - borrowAmountHaircutPV) * haircut - // netETHCollateralWithHaircutFinal = netETHCollateralWithHaircut - (netETHBorrowBefore - netETHBorrowAfter) - const netCollateralDifferenceWithHaircut = borrowNetAvailable - .toETH(useHaircut) - .sub(borrowNetAvailable.add(borrowAmountHaircutPV).toETH(useHaircut)); - netETHCollateralWithHaircut = netETHCollateralWithHaircut.sub(netCollateralDifferenceWithHaircut); - } else { - // In this case it's a partial thing so we add/subtract to both. - // First, we reduce the collateral by the borrowNetAvailable - netETHCollateralWithHaircut = netETHCollateralWithHaircut.sub(borrowNetAvailable.toETH(useHaircut)); - - // Second we add whatever remaining debt there is after accounting for the borrowNetAvailable - const netBorrowCurrencyDebt = borrowAmountHaircutPV.add(borrowNetAvailable); - netETHDebtWithBuffer = netETHDebtWithBuffer.add(netBorrowCurrencyDebt.toETH(useHaircut).abs()); - netETHDebt = netETHDebt.add(netBorrowCurrencyDebt.toETH(noHaircut).abs()); - } - - return { - netETHCollateralWithHaircut, - netETHDebtWithBuffer, - netETHDebt, - }; - } - /* eslint-enable no-param-reassign */ - /** * Returns the amount of target collateral required to achieve the given buffered ratio * diff --git a/src/system/NTokenValue.ts b/src/system/NTokenValue.ts index ca50a13..875378d 100644 --- a/src/system/NTokenValue.ts +++ b/src/system/NTokenValue.ts @@ -111,7 +111,7 @@ export default class NTokenValue { public static getNTokenRedeemFromAsset( currencyId: number, assetCashAmountInternal: TypedBigNumber, - precision = BigNumber.from(1e4), + precision = BigNumber.from(1e2), ) { const {totalSupply, nTokenPV} = NTokenValue.getNTokenFactors(currencyId); @@ -121,18 +121,24 @@ export default class NTokenValue { let redeemValue = NTokenValue.getAssetFromRedeemNToken(currencyId, nTokenRedeem); // We always want to redeem value slightly less than the specified amount, if we were to // redeem slightly more then it could result in a free collateral failure. We continue to - // loop while assetCash - redeemValue < 0 or assetCash - redeemValue > precision + // loop while assetCash - redeemValue < 0 or assetCash - redeemValue > precision. Note that + // we allow negative one as a diff due to rounding issues let diff = assetCashAmountInternal.sub(redeemValue); let totalLoops = 0; - while (diff.isNegative() || diff.n.gt(precision)) { + while (diff.n.lt(-1) || diff.n.gt(precision)) { // If the nToken redeem value is too high (diff < 0), we reduce the nTokenRedeem amount by // the proportion of the total supply. If the nToken redeem value is too low (diff > 0), increase // the nTokenRedeem amount by the proportion of the total supply - nTokenRedeem = nTokenRedeem.add(totalSupply.scale(diff.n, nTokenPV.n)); + const updateAmount = totalSupply.scale(diff.n, nTokenPV.n); + // If the diff is so small it rounds down to zero when we convert to an nToken balance then + // we can break at this point, the calculation will not converge after this + if (updateAmount.isZero()) break; + + nTokenRedeem = nTokenRedeem.add(updateAmount); redeemValue = NTokenValue.getAssetFromRedeemNToken(currencyId, nTokenRedeem); diff = assetCashAmountInternal.sub(redeemValue); totalLoops += 1; - if (totalLoops > 250) throw Error('Unable to converge on nTokenRedeem'); + if (totalLoops > 50) throw Error('Unable to converge on nTokenRedeem'); } return nTokenRedeem; diff --git a/tests/unit/AssetSummary.test.ts b/tests/unit/AssetSummary.test.ts index 43e8d2a..1cabb27 100644 --- a/tests/unit/AssetSummary.test.ts +++ b/tests/unit/AssetSummary.test.ts @@ -36,6 +36,7 @@ describe('Asset Summary', () => { netUnderlyingCash: TypedBigNumber.from(-95e8, BigNumberType.InternalUnderlying, 'DAI'), netfCash: TypedBigNumber.from(100e8, BigNumberType.InternalUnderlying, 'DAI'), netLiquidityTokens: null, + tradedInterestRate: 0, }; it('produces irr for a lend fcash asset', () => { diff --git a/tests/unit/FreeCollateral.test.ts b/tests/unit/FreeCollateral.test.ts index 6eff5ee..a27cc43 100644 --- a/tests/unit/FreeCollateral.test.ts +++ b/tests/unit/FreeCollateral.test.ts @@ -10,6 +10,7 @@ import {AssetType} from '../../src/libs/types'; import {getNowSeconds} from '../../src/libs/utils'; import MockAccountData from './AccountData.test'; import MockNotionalProxy from '../mocks/MockNotionalProxy'; +import {AccountData} from '../../src/account'; describe('calculates free collateral', () => { let system: System; @@ -245,18 +246,26 @@ describe('calculates free collateral', () => { }); it('calculates borrowing requirements for stable / stable with no account data', () => { + const accountData = AccountData.emptyAccountData(); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); + const { minCollateral, targetCollateral, minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, - undefined, + accountData, blockTime, ); @@ -269,18 +278,26 @@ describe('calculates free collateral', () => { }); it('calculates borrowing requirements for stable / crypto with no account data', () => { + const accountData = AccountData.emptyAccountData(); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); + const { minCollateral, targetCollateral, minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, 1, 200, - undefined, + accountData, blockTime, ); @@ -308,8 +325,17 @@ describe('calculates free collateral', () => { }, ], [], - false, + true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const { minCollateral, @@ -317,9 +343,6 @@ describe('calculates free collateral', () => { minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -348,8 +371,17 @@ describe('calculates free collateral', () => { }, ], [], - false, + true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const { minCollateral, @@ -357,9 +389,6 @@ describe('calculates free collateral', () => { minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -390,8 +419,17 @@ describe('calculates free collateral', () => { }, ], [], - false, + true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const { minCollateral, @@ -399,9 +437,6 @@ describe('calculates free collateral', () => { minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -432,8 +467,17 @@ describe('calculates free collateral', () => { }, ], [], - false, + true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const { minCollateral, @@ -441,9 +485,6 @@ describe('calculates free collateral', () => { minCollateralRatio, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -476,17 +517,23 @@ describe('calculates free collateral', () => { }, ], [], - false, + true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const { minCollateral, targetCollateral, targetCollateralRatio, } = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -523,11 +570,17 @@ describe('calculates free collateral', () => { [], true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const {minCollateral, targetCollateral, minCollateralRatio} = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -543,17 +596,6 @@ describe('calculates free collateral', () => { assetCashBalance, TypedBigNumber.from(0, BigNumberType.nToken, 'nUSDC'), ); - accountData.updateAsset( - { - currencyId: borrowCurrencyId, - assetType: AssetType.fCash, - notional: borrowfCashAmount, - maturity, - hasMatured: false, - settlementDate: maturity, - isIdiosyncratic: false, - }, - ); const {netETHCollateralWithHaircut, netETHDebtWithBuffer} = FreeCollateral.getFreeCollateral(accountData); // Expect that the buffered collateral ratio afterwards is about 200 expect(FreeCollateral.calculateCollateralRatio(netETHCollateralWithHaircut, netETHDebtWithBuffer)).toBeCloseTo( @@ -580,11 +622,17 @@ describe('calculates free collateral', () => { [], true, ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); const {targetCollateral} = FreeCollateral.calculateBorrowRequirement( - borrowfCashAmount, - maturity, - borrowCurrencyId, collateralCurrencyId, 200, accountData, @@ -597,17 +645,6 @@ describe('calculates free collateral', () => { targetAssetCashBalance, TypedBigNumber.from(0, BigNumberType.nToken, 'nUSDC'), ); - accountData.updateAsset( - { - currencyId: borrowCurrencyId, - assetType: AssetType.fCash, - notional: borrowfCashAmount, - maturity, - hasMatured: false, - settlementDate: maturity, - isIdiosyncratic: false, - }, - ); const {netETHCollateralWithHaircut, netETHDebtWithBuffer} = FreeCollateral.getFreeCollateral(accountData); // Expect that the buffered collateral ratio afterwards is about 200 expect(FreeCollateral.calculateCollateralRatio(netETHCollateralWithHaircut, netETHDebtWithBuffer)).toBeCloseTo( @@ -616,6 +653,56 @@ describe('calculates free collateral', () => { ); }); + it('it nets off fcash before calculating collateral requirement', () => { + const accountData = new MockAccountData( + 0, + false, + false, + undefined, + [ + { + currencyId: borrowCurrencyId, + cashBalance: TypedBigNumber.from(0, BigNumberType.InternalAsset, 'cDAI'), + nTokenBalance: TypedBigNumber.from(0, BigNumberType.nToken, 'nDAI'), + lastClaimTime: BigNumber.from(0), + lastClaimIntegralSupply: BigNumber.from(0), + }, + ], + [ + // This asset should be net off exactly + { + currencyId: borrowCurrencyId, + assetType: AssetType.fCash, + notional: borrowfCashAmount.neg(), + maturity, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }, + ], + true, + ); + accountData.updateAsset({ + currencyId: borrowCurrencyId, + maturity, + assetType: AssetType.fCash, + notional: borrowfCashAmount, + hasMatured: false, + settlementDate: maturity, + isIdiosyncratic: false, + }); + + const {targetCollateral, minCollateral} = FreeCollateral.calculateBorrowRequirement( + collateralCurrencyId, + 200, + accountData, + blockTime, + ); + + expect(minCollateral.isZero()).toBeTruthy(); + expect(targetCollateral.isZero()).toBeTruthy(); + }); + it('can override ETH rate', () => { const accountData = new MockAccountData( 0, diff --git a/tests/unit/NTokenValue.test.ts b/tests/unit/NTokenValue.test.ts index 37bc00e..44f9120 100644 --- a/tests/unit/NTokenValue.test.ts +++ b/tests/unit/NTokenValue.test.ts @@ -123,13 +123,20 @@ describe('nToken value', () => { expect(incentives.toString()).toBe(BigNumber.from(0.01e8).toString()); }); - it('gets redeem ntoken values', () => { + it('gets smaller redeem ntoken values', () => { const assetCash = TypedBigNumber.from(100e8, BigNumberType.InternalAsset, 'cDAI'); const nTokenRedeem = NTokenValue.getNTokenRedeemFromAsset(2, assetCash); const assetFromRedeem = NTokenValue.getAssetFromRedeemNToken(2, nTokenRedeem); expect(assetCash.n.toNumber()).toBeCloseTo(assetFromRedeem.n.toNumber(), -4); }); + it('gets larger redeem ntoken values', () => { + const assetCash = TypedBigNumber.from(1_000_000e8, BigNumberType.InternalAsset, 'cDAI'); + const nTokenRedeem = NTokenValue.getNTokenRedeemFromAsset(2, assetCash); + const assetFromRedeem = NTokenValue.getAssetFromRedeemNToken(2, nTokenRedeem); + expect(assetCash.n.toNumber()).toBeCloseTo(assetFromRedeem.n.toNumber(), -4); + }); + it('calculates the nToken blended yield', () => { const blendedYield = NTokenValue.getNTokenBlendedYield(2); // Supply rate is 5% and fCash is yielding 4%