From ffb1616b3d5c4c5b2651b3d1662abcc2cfece7cd Mon Sep 17 00:00:00 2001 From: William Poulin Date: Wed, 29 Nov 2023 13:33:03 -0500 Subject: [PATCH] fix(notional-finance-v3): Remove fCash app token and move logic into supply and borrow positions (#3089) --- ...nce-v3.borrow.contract-position-fetcher.ts | 21 +- ...otional-finance-v3.f-cash.token-fetcher.ts | 201 ------------------ ...nce-v3.supply.contract-position-fetcher.ts | 24 ++- .../notional-finance-v3.module.ts | 2 - 4 files changed, 36 insertions(+), 212 deletions(-) delete mode 100644 src/apps/notional-finance-v3/arbitrum/notional-finance-v3.f-cash.token-fetcher.ts diff --git a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.borrow.contract-position-fetcher.ts b/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.borrow.contract-position-fetcher.ts index 37d237233..dcef6e59f 100644 --- a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.borrow.contract-position-fetcher.ts +++ b/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.borrow.contract-position-fetcher.ts @@ -31,9 +31,14 @@ export type NotionalBorrowingDataProps = { currencyId: number; tokenId: string; maturity: number; + fCashPV: number; + positionKey: string; type: string; }; +const NOTIONAL_CONSTANT = 10 ** 7; +const SECONDS_IN_YEAR = 31104000; // 360 days + @PositionTemplate() export class ArbitrumNotionalFinanceV3BorrowContractPositionFetcher extends ContractPositionTemplatePositionFetcher< NotionalView, @@ -79,11 +84,19 @@ export class ArbitrumNotionalFinanceV3BorrowContractPositionFetcher extends Cont .read.encodeToId([currencyId, maturity, 0]) .then(v => v.toString()); + const apy = Number(activeMarket.lastImpliedRate ?? 0) / NOTIONAL_CONSTANT; + + const dateNowEpoch = Date.now() / 1000; + const timeToMaturity = (maturity - dateNowEpoch) / SECONDS_IN_YEAR; + const lastImpliedRate = apy / 100; + const fCashPV = 1 / Math.exp(lastImpliedRate * timeToMaturity); + return { address: this.notionalViewContractAddress, currencyId, underlyingTokenAddress, maturity, + fCashPV, tokenId, type, }; @@ -107,7 +120,7 @@ export class ArbitrumNotionalFinanceV3BorrowContractPositionFetcher extends Cont params: GetDataPropsParams, ) { const defaultDataProps = await super.getDataProps(params); - const props = pick(params.definition, ['currencyId', 'tokenId', 'maturity', 'type']); + const props = pick(params.definition, ['currencyId', 'tokenId', 'maturity', 'fCashPV', 'type']); return { ...defaultDataProps, ...props, positionKey: Object.values(props).join(':') }; } @@ -120,7 +133,7 @@ export class ArbitrumNotionalFinanceV3BorrowContractPositionFetcher extends Cont contractPosition, contract, }: GetTokenBalancesParams) { - const { maturity, currencyId } = contractPosition.dataProps; + const { maturity, currencyId, fCashPV } = contractPosition.dataProps; const portfolio = await contract.read.getAccountPortfolio([address]); const debtPositions = portfolio.filter(v => v.notional < 0); const position = debtPositions.find(v => Number(v.maturity) === maturity && Number(v.currencyId) === currencyId); @@ -130,6 +143,8 @@ export class ArbitrumNotionalFinanceV3BorrowContractPositionFetcher extends Cont .times(10 ** contractPosition.tokens[0].decimals) .div(10 ** 8); - return [fcashAmountAtMaturity.abs().toString()]; + const presentValue = fcashAmountAtMaturity.times(new BigNumber(fCashPV)); + + return [presentValue.toString()]; } } diff --git a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.f-cash.token-fetcher.ts b/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.f-cash.token-fetcher.ts deleted file mode 100644 index d6db47c51..000000000 --- a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.f-cash.token-fetcher.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Inject } from '@nestjs/common'; -import { BigNumberish } from 'ethers'; -import { pick, range } from 'lodash'; - -import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; -import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; -import { ViemMulticallDataLoader } from '~multicall'; -import { AppTokenPosition } from '~position/position.interface'; -import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher'; -import { - DefaultAppTokenDataProps, - GetAddressesParams, - GetDataPropsParams, - GetDefinitionsParams, - GetDisplayPropsParams, - GetPricePerShareParams, - GetTokenPropsParams, - GetUnderlyingTokensParams, -} from '~position/template/app-token.template.types'; - -import { NotionalFinanceV3ViemContractFactory } from '../contracts'; -import { NotionalFCash } from '../contracts/viem'; - -export type NotionalFCashTokenDefinition = { - address: string; - underlyingTokenAddress: string; - currencyId: number; - tokenId: string; - maturity: number; -}; - -export type NotionalFCashTokenDataProps = DefaultAppTokenDataProps & { - currencyId: number; - tokenId: string; - maturity: number; - positionKey: string; -}; - -const NOTIONAL_CONSTANT = 10 ** 7; -const SECONDS_IN_YEAR = 31104000; // 360 days - -@PositionTemplate() -export class ArbitrumNotionalFinanceV3FCashTokenFetcher extends AppTokenTemplatePositionFetcher< - NotionalFCash, - NotionalFCashTokenDataProps, - NotionalFCashTokenDefinition -> { - groupLabel = 'fCash'; - - notionalViewContractAddress = '0x1344a36a1b56144c3bc62e7757377d288fde0369'; - - constructor( - @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, - @Inject(NotionalFinanceV3ViemContractFactory) - protected readonly contractFactory: NotionalFinanceV3ViemContractFactory, - ) { - super(appToolkit); - } - - getContract(address: string) { - return this.contractFactory.notionalFCash({ address, network: this.network }); - } - - async getDefinitions({ multicall }: GetDefinitionsParams): Promise { - const notionalViewContract = this.contractFactory.notionalView({ - address: this.notionalViewContractAddress, - network: this.network, - }); - - const currencyCount = await multicall.wrap(notionalViewContract).read.getMaxCurrencyId(); - const currencyRange = range(1, currencyCount + 1); - const definitions = await Promise.all( - currencyRange.map(async currencyId => { - const currency = await multicall.wrap(notionalViewContract).read.getCurrency([currencyId]); - const underlyingTokenAddress = currency[1].tokenAddress.toLowerCase(); - const activeMarkets = await multicall.wrap(notionalViewContract).read.getActiveMarkets([currencyId]); - - const markets = await Promise.all( - activeMarkets.map(async activeMarket => { - const maturity = Number(activeMarket.maturity); - const tokenId = await multicall - .wrap(notionalViewContract) - .read.encodeToId([currencyId, maturity, 0]) - .then(v => v.toString()); - - return { - address: this.notionalViewContractAddress, - currencyId, - underlyingTokenAddress, - maturity, - tokenId, - }; - }), - ); - - return markets; - }), - ); - - return definitions.flat(); - } - - async getAddresses({ definitions }: GetAddressesParams) { - return definitions.map(v => v.address); - } - - async getSymbol({ appToken }: GetTokenPropsParams) { - return `f${appToken.tokens[0].symbol}`; - } - - async getSupply() { - return 0; - } - - async getDecimals({ appToken }: GetTokenPropsParams) { - return appToken.tokens[0].decimals; - } - - async getUnderlyingTokenDefinitions({ - definition, - }: GetUnderlyingTokensParams) { - return [{ address: definition.underlyingTokenAddress, network: this.network }]; - } - - async getPricePerShare({ - definition, - multicall, - }: GetPricePerShareParams) { - const notionalViewContract = this.contractFactory.notionalView({ - address: this.notionalViewContractAddress, - network: this.network, - }); - - const activeMarkets = await multicall.wrap(notionalViewContract).read.getActiveMarkets([definition.currencyId]); - const market = activeMarkets.find(v => Number(v.maturity) === definition.maturity); - const apy = Number(market?.lastImpliedRate ?? 0) / NOTIONAL_CONSTANT; - - const dateNowEpoch = Date.now() / 1000; - const timeToMaturity = (definition.maturity - dateNowEpoch) / SECONDS_IN_YEAR; - const lastImpliedRate = apy / 100; - const fCashPV = 1 / Math.exp(lastImpliedRate * timeToMaturity); - - return [fCashPV]; - } - - async getLiquidity({ appToken }: GetDataPropsParams) { - return appToken.supply * appToken.price; - } - - async getReserves() { - return [0]; - } - - async getApy({ - definition, - multicall, - }: GetDataPropsParams) { - const notionalViewContract = this.contractFactory.notionalView({ - address: this.notionalViewContractAddress, - network: this.network, - }); - - const activeMarkets = await multicall.wrap(notionalViewContract).read.getActiveMarkets([definition.currencyId]); - const market = activeMarkets.find(v => Number(v.maturity) === definition.maturity); - return Number(market?.lastImpliedRate ?? 0) / NOTIONAL_CONSTANT; - } - - async getDataProps( - params: GetDataPropsParams, - ) { - const defaultDataProps = await super.getDataProps(params); - return { - ...defaultDataProps, - ...pick(params.definition, ['currencyId', 'tokenId', 'maturity']), - positionKey: params.definition.tokenId, - }; - } - - async getLabel({ appToken }: GetDisplayPropsParams) { - const maturityDate = new Date(Number(appToken.dataProps.maturity) * 1000); - const year = maturityDate.getFullYear(); - const month = maturityDate.getMonth() + 1; - const day = maturityDate.getDate(); - const displayMaturity = day + '/' + month + '/' + year; - return `${appToken.symbol} - ${displayMaturity}`; - } - - getBalancePerToken({ - address, - appToken, - multicall, - }: { - address: string; - appToken: AppTokenPosition; - multicall: ViemMulticallDataLoader; - }): Promise { - return multicall - .wrap(this.getContract(appToken.address)) - .read.balanceOf([address, BigInt(appToken.dataProps.tokenId)]); - } -} diff --git a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.supply.contract-position-fetcher.ts b/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.supply.contract-position-fetcher.ts index 1c5d0ac37..b27b68450 100644 --- a/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.supply.contract-position-fetcher.ts +++ b/src/apps/notional-finance-v3/arbitrum/notional-finance-v3.supply.contract-position-fetcher.ts @@ -32,10 +32,14 @@ export type NotionalFinanceLendingDataProps = { currencyId: number; tokenId: string; maturity: number; + fCashPV: number; positionKey: string; type: string; }; +const NOTIONAL_CONSTANT = 10 ** 7; +const SECONDS_IN_YEAR = 31104000; // 360 days + @PositionTemplate() export class ArbitrumNotionalFinanceV3SupplyContractPositionFetcher extends ContractPositionTemplatePositionFetcher< NotionalView, @@ -81,11 +85,19 @@ export class ArbitrumNotionalFinanceV3SupplyContractPositionFetcher extends Cont .read.encodeToId([currencyId, maturity, 0]) .then(v => v.toString()); + const apy = Number(activeMarket.lastImpliedRate ?? 0) / NOTIONAL_CONSTANT; + + const dateNowEpoch = Date.now() / 1000; + const timeToMaturity = (maturity - dateNowEpoch) / SECONDS_IN_YEAR; + const lastImpliedRate = apy / 100; + const fCashPV = 1 / Math.exp(lastImpliedRate * timeToMaturity); + return { address: this.notionalViewContractAddress, currencyId, underlyingTokenAddress, maturity, + fCashPV, tokenId, type, }; @@ -100,16 +112,14 @@ export class ArbitrumNotionalFinanceV3SupplyContractPositionFetcher extends Cont } async getTokenDefinitions({ definition }: GetTokenDefinitionsParams) { - return [ - { metaType: MetaType.SUPPLIED, address: definition.address, network: this.network, tokenId: definition.tokenId }, - ]; + return [{ metaType: MetaType.SUPPLIED, address: definition.underlyingTokenAddress, network: this.network }]; } async getDataProps( params: GetDataPropsParams, ) { const defaultDataProps = await super.getDataProps(params); - const props = pick(params.definition, ['currencyId', 'tokenId', 'maturity', 'type']); + const props = pick(params.definition, ['currencyId', 'tokenId', 'maturity', 'fCashPV', 'type']); return { ...defaultDataProps, ...props, positionKey: Object.values(props).join(':') }; } @@ -122,7 +132,7 @@ export class ArbitrumNotionalFinanceV3SupplyContractPositionFetcher extends Cont contractPosition, contract, }: GetTokenBalancesParams): Promise { - const { maturity, currencyId } = contractPosition.dataProps; + const { maturity, currencyId, fCashPV } = contractPosition.dataProps; const portfolio = await contract.read.getAccountPortfolio([address]); const supplyPositions = portfolio.filter(v => v.notional >= 0); const position = supplyPositions.find(v => Number(v.maturity) === maturity && Number(v.currencyId) === currencyId); @@ -132,6 +142,8 @@ export class ArbitrumNotionalFinanceV3SupplyContractPositionFetcher extends Cont .times(10 ** contractPosition.tokens[0].decimals) .div(10 ** 8); - return [fcashAmountAtMaturity.abs().toString()]; + const presentValue = fcashAmountAtMaturity.times(new BigNumber(fCashPV)); + + return [presentValue.toString()]; } } diff --git a/src/apps/notional-finance-v3/notional-finance-v3.module.ts b/src/apps/notional-finance-v3/notional-finance-v3.module.ts index 31ce8a787..9993b1279 100644 --- a/src/apps/notional-finance-v3/notional-finance-v3.module.ts +++ b/src/apps/notional-finance-v3/notional-finance-v3.module.ts @@ -3,7 +3,6 @@ import { Module } from '@nestjs/common'; import { AbstractApp } from '~app/app.dynamic-module'; import { ArbitrumNotionalFinanceV3BorrowContractPositionFetcher } from './arbitrum/notional-finance-v3.borrow.contract-position-fetcher'; -import { ArbitrumNotionalFinanceV3FCashTokenFetcher } from './arbitrum/notional-finance-v3.f-cash.token-fetcher'; import { ArbitrumNotionalFinanceV3NTokenTokenFetcher } from './arbitrum/notional-finance-v3.n-token.token-fetcher'; import { ArbitrumNotionalFinanceV3PCashTokenFetcher } from './arbitrum/notional-finance-v3.p-cash.token-fetcher'; import { ArbitrumNotionalFinanceV3PDebtTokenFetcher } from './arbitrum/notional-finance-v3.p-debt.token-fetcher'; @@ -17,7 +16,6 @@ import { NotionalFinanceV3ViemContractFactory } from './contracts'; ArbitrumNotionalFinanceV3PCashTokenFetcher, ArbitrumNotionalFinanceV3PDebtTokenFetcher, ArbitrumNotionalFinanceV3NTokenTokenFetcher, - ArbitrumNotionalFinanceV3FCashTokenFetcher, ArbitrumNotionalFinanceV3SupplyContractPositionFetcher, ArbitrumNotionalFinanceV3BorrowContractPositionFetcher, ],