diff --git a/package.json b/package.json index 0611ddd..81d7d15 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "authorize": "graph auth https://api.thegraph.com/deploy/", "build": "graph build", "deploy": "graph deploy perspectivefi/spectra-mainnet --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", - "deploy:local": "graph deploy perspectivefi/spectra-mainnet --debug --ipfs http://localhost:5001 --node http://127.0.0.1:8020", + "deploy:local": "graph deploy perspectivefi/spectra-mainnet --ipfs http://localhost:5001 --node http://127.0.0.1:8020", "deploy:goerli": "graph deploy perspectivefi/spectra-goerli subgraph.goerli.yaml --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", "deploy:sepolia": "graph deploy --studio spectra-subgraph-sepolia subgraph.sepolia.yaml", "deploy:test": "graph deploy matstyler/spectra-subgraph subgraph.goerli.yaml --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", @@ -20,7 +20,8 @@ "generate-config": "node ./src/scripts/subgraphConfigGenerator.js", "generate-config:goerli": "node ./src/scripts/subgraphConfigGenerator.js goerli", "generate-config:sepolia": "node ./src/scripts/subgraphConfigGenerator.js sepolia", - "test": "graph test" + "test": "graph test", + "prettify": "prettier --write \"./**/*.{js,jsx,json,ts,tsx,css}\"" }, "devDependencies": { "@chainlink/contracts": "^0.4.1", diff --git a/schema.graphql b/schema.graphql index af17bf4..6a6d720 100644 --- a/schema.graphql +++ b/schema.graphql @@ -268,7 +268,9 @@ type FutureDailyStats @entity { dailyAddLiquidity: BigInt! # total add liquidity in the day in the AMM (the unit is a count of transactions) dailyRemoveLiquidity: BigInt! # total remove liquidity in the day in the AMM (the unit is a count of transactions) dailyUpdates: BigInt! # total updates of the FutureDailyStats. This is used to compute the IBT rate as an incremental average (the unit is a count of transactions) - ibtRate: BigInt! # The average rate of the IBT token for the day + ibtRateMA: BigInt! # The average rate of the IBT token for the day + lastIBTRate: BigInt! # The last rate of the IBT token for the day + lastPTRate: BigInt! # The last rate of the PT token for the day realizedAPR7D: BigDecimal! # The realized APR (in % i.e: realizedAPR7D = 1 <==> APR = 100%) of the IBT token over the last 7 days realizedAPR30D: BigDecimal! # The realized APR of the IBT token over the last 30 days realizedAPR90D: BigDecimal! # The realized APR of the IBT token over the last 90 days @@ -330,19 +332,16 @@ type Transaction @entity { # AMM #################### -type APRInTime @entity { +type APYInTime @entity { id: ID! createdAtTimestamp: BigInt! block: BigInt! - spotPrice: BigInt! + ptRate: BigInt! ibtRate: BigInt! - ibtSharesRate: BigInt! - underlyingToPT: BigInt! - "in %" - apr: BigDecimal! - pool: Pool + baseAPY: BigDecimal! + exponentAPY: BigDecimal! lpVault: LPVault } @@ -375,7 +374,7 @@ type Pool @entity { liquidityPositions: [AccountAsset!]! @derivedFrom(field: "pool") spotPrice: BigInt! - apr: [APRInTime!]! @derivedFrom(field: "pool") + apy: [APYInTime!]! @derivedFrom(field: "pool") } type Transfer @entity { @@ -432,5 +431,5 @@ type LPVault @entity { transactions: [Transaction!]! @derivedFrom(field: "lpVaultInTransaction") # interestInTime: [LPVaultInterest!]! @derivedFrom(field: "lpVault") - apr: [APRInTime!]! @derivedFrom(field: "lpVault") + apy: [APYInTime!]! @derivedFrom(field: "lpVault") } diff --git a/src/entities/APRInTime.ts b/src/entities/APRInTime.ts deleted file mode 100644 index 4859955..0000000 --- a/src/entities/APRInTime.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Address, BigInt } from "@graphprotocol/graph-ts" - -import { APRInTime } from "../../generated/schema" -import { UNIT_BI, ZERO_BD } from "../constants" - -export function createAPRInTimeForPool( - poolAddress: Address, - timestamp: BigInt, - blockNumber: BigInt -): APRInTime { - let aprInTime = new APRInTime( - `${poolAddress.toHex()}-${timestamp.toString()}` - ) - - aprInTime.createdAtTimestamp = timestamp - aprInTime.block = blockNumber - aprInTime.pool = poolAddress.toHex() - - aprInTime.spotPrice = UNIT_BI - aprInTime.ibtRate = UNIT_BI - aprInTime.ibtSharesRate = UNIT_BI - aprInTime.underlyingToPT = UNIT_BI - aprInTime.apr = ZERO_BD - - aprInTime.save() - - return aprInTime -} - -export function createAPRInTimeForLPVault( - lpVaultAddress: Address, - timestamp: BigInt -): APRInTime { - let aprInTime = new APRInTime( - `${lpVaultAddress.toHex()}-${timestamp.toString()}` - ) - - aprInTime.createdAtTimestamp = timestamp - aprInTime.lpVault = lpVaultAddress.toHex() - - aprInTime.spotPrice = UNIT_BI - aprInTime.ibtRate = UNIT_BI - aprInTime.ibtSharesRate = UNIT_BI - aprInTime.underlyingToPT = UNIT_BI - aprInTime.apr = ZERO_BD - - aprInTime.save() - - return aprInTime -} diff --git a/src/entities/APYInTime.ts b/src/entities/APYInTime.ts new file mode 100644 index 0000000..ef75ea6 --- /dev/null +++ b/src/entities/APYInTime.ts @@ -0,0 +1,49 @@ +import { Address, BigInt } from "@graphprotocol/graph-ts" + +import { APYInTime } from "../../generated/schema" +import { UNIT_BI, ZERO_BD, ZERO_BI } from "../constants" + +export function createAPYInTimeForPool( + poolAddress: Address, + timestamp: BigInt, + blockNumber: BigInt +): APYInTime { + let apyInTime = new APYInTime( + `${poolAddress.toHex()}-${timestamp.toString()}` + ) + + apyInTime.createdAtTimestamp = timestamp + apyInTime.block = blockNumber + apyInTime.pool = poolAddress.toHex() + + apyInTime.spotPrice = UNIT_BI + apyInTime.ptRate = ZERO_BI + apyInTime.ibtRate = ZERO_BI + apyInTime.baseAPY = ZERO_BD + apyInTime.exponentAPY = ZERO_BD + + apyInTime.save() + + return apyInTime +} + +export function createAPYInTimeForLPVault( + lpVaultAddress: Address, + timestamp: BigInt +): APYInTime { + let apyInTime = new APYInTime( + `${lpVaultAddress.toHex()}-${timestamp.toString()}` + ) + + apyInTime.createdAtTimestamp = timestamp + apyInTime.lpVault = lpVaultAddress.toHex() + + apyInTime.spotPrice = UNIT_BI + apyInTime.ptRate = ZERO_BI + apyInTime.ibtRate = ZERO_BI + apyInTime.baseAPY = ZERO_BD + apyInTime.exponentAPY = ZERO_BD + apyInTime.save() + + return apyInTime +} diff --git a/src/entities/FutureDailyStats.ts b/src/entities/FutureDailyStats.ts index d2793de..c00d5d2 100644 --- a/src/entities/FutureDailyStats.ts +++ b/src/entities/FutureDailyStats.ts @@ -4,7 +4,8 @@ import { Future, FutureDailyStats } from "../../generated/schema" import { DAYS_PER_YEAR_BD, ZERO_BD, ZERO_BI } from "../constants" import { generateFutureDailyStatsId } from "../utils" import { getDayIdFromTimestamp, getPastDayId } from "../utils/dayId" -import { getIBTRate } from "./ERC4626" +import { getERC20Decimals } from "./ERC20" +import { getIBTRate, getPTRate, getUnderlying } from "./FutureVault" /** * Update the daily data for a future. This function is called every time a @@ -19,6 +20,8 @@ export function updateFutureDailyStats( futureAddress: Address ): FutureDailyStats { let dayId = getDayIdFromTimestamp(event.block.timestamp) + const asset = getUnderlying(futureAddress) + const underlyingDecimals = getERC20Decimals(asset) const futureDailyStatsID = generateFutureDailyStatsId( futureAddress.toHex(), dayId.toString() @@ -27,17 +30,20 @@ export function updateFutureDailyStats( if (futureDailyStats === null) { futureDailyStats = createFutureDailyStats(futureAddress, dayId) } - let future = Future.load(futureAddress.toHex()) - let ibt = future!.ibtAsset - const currentRate = getIBTRate(Address.fromString(ibt)) - - let currentIBTRate = futureDailyStats.ibtRate + const currentRate = getIBTRate(futureAddress) + const currentPTRate = getPTRate(futureAddress) + let currentIBTRateMA = futureDailyStats.ibtRateMA let dailyUpdates = futureDailyStats.dailyUpdates.plus(BigInt.fromI32(1)) + const precision = BigInt.fromI32(underlyingDecimals) // Compute the ibt rate new average - futureDailyStats.ibtRate = currentIBTRate.plus( - currentRate.minus(currentIBTRate).div(dailyUpdates) + let scaledDifference = currentRate.minus(currentIBTRateMA).times(precision) + let scaledDivision = scaledDifference.div(dailyUpdates) + futureDailyStats.ibtRateMA = currentIBTRateMA.plus( + scaledDivision.div(precision) ) futureDailyStats.dailyUpdates = dailyUpdates + futureDailyStats.lastIBTRate = currentRate + futureDailyStats.lastPTRate = currentPTRate futureDailyStats.realizedAPR7D = getAPR( futureAddress, @@ -82,7 +88,7 @@ export function getAPR( if (previousFutureDailyStats === null) { return ZERO_BD } - const previousRate = previousFutureDailyStats.ibtRate + const previousRate = previousFutureDailyStats.ibtRateMA if (previousRate.equals(ZERO_BI)) { return ZERO_BD } @@ -143,7 +149,9 @@ export function createFutureDailyStats( futureDailyStats.dailyAddLiquidity = ZERO_BI futureDailyStats.dailyRemoveLiquidity = ZERO_BI futureDailyStats.dailyUpdates = ZERO_BI - futureDailyStats.ibtRate = ZERO_BI + futureDailyStats.ibtRateMA = ZERO_BI + futureDailyStats.lastIBTRate = ZERO_BI + futureDailyStats.lastPTRate = ZERO_BI futureDailyStats.realizedAPR7D = ZERO_BD futureDailyStats.realizedAPR30D = ZERO_BD futureDailyStats.realizedAPR90D = ZERO_BD diff --git a/src/entities/FutureVault.ts b/src/entities/FutureVault.ts index 6e2c14c..a70d477 100644 --- a/src/entities/FutureVault.ts +++ b/src/entities/FutureVault.ts @@ -105,6 +105,21 @@ export function getIBTRate(address: Address): BigInt { return UNIT_BI } +// With 27 decimals precision +export function getPTRate(address: Address): BigInt { + const principalTokenContract = PrincipalToken.bind(address) + + let ptRateCall = principalTokenContract.try_getPTRate() + + if (!ptRateCall.reverted) { + return ptRateCall.value + } + + log.warning("getPTRate() call reverted for {}", [address.toHex()]) + + return UNIT_BI +} + export function getYT(address: Address): Address { const principalTokenContract = PrincipalToken.bind(address) diff --git a/src/entities/Pool.ts b/src/entities/Pool.ts index 604b2f4..e84b016 100644 --- a/src/entities/Pool.ts +++ b/src/entities/Pool.ts @@ -3,7 +3,7 @@ import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts" import { Future, Factory, Pool } from "../../generated/schema" import { ZERO_BI } from "../constants" import { AssetType } from "../utils" -import { createAPRInTimeForPool } from "./APRInTime" +import { createAPYInTimeForPool } from "./APYInTime" import { getAsset } from "./Asset" import { getAssetAmount } from "./AssetAmount" import { @@ -103,13 +103,13 @@ export function createPool(params: PoolDetails): Pool { let spotPrice = getPoolPriceScale(params.poolAddress) if (pool.futureVault) { - let poolAPR = createAPRInTimeForPool( + let poolAPY = createAPYInTimeForPool( params.poolAddress, params.timestamp, params.blockNumber ) - poolAPR.save() + poolAPY.save() } pool.spotPrice = spotPrice diff --git a/src/mappings/amm.ts b/src/mappings/amm.ts index 75f2020..c84b248 100644 --- a/src/mappings/amm.ts +++ b/src/mappings/amm.ts @@ -20,7 +20,7 @@ import { getERC20Decimals, getERC20TotalSupply } from "../entities/ERC20" import { updateFutureDailyStats } from "../entities/FutureDailyStats" import { createTransaction } from "../entities/Transaction" import { AssetType, generateFeeClaimId } from "../utils" -import { updatePoolAPR } from "../utils/calculateAPR" +import { updatePoolAPY } from "../utils/calculateAPY" import { generateTransactionId } from "../utils/idGenerators" import { toPrecision } from "../utils/toPrecision" @@ -163,7 +163,7 @@ export function handleAddLiquidity(event: AddLiquidity): void { } if (pool.futureVault) { - updatePoolAPR( + updatePoolAPY( event.address, Address.fromString(pool.futureVault!), event.block.timestamp, @@ -447,7 +447,7 @@ export function handleTokenExchange(event: TokenExchange): void { } if (pool.futureVault) { - updatePoolAPR( + updatePoolAPY( event.address, Address.fromString(pool.futureVault!), event.block.timestamp, @@ -606,7 +606,7 @@ export function handleRemoveLiquidityOne(event: RemoveLiquidityOne): void { } if (pool.futureVault) { - updatePoolAPR( + updatePoolAPY( event.address, Address.fromString(pool.futureVault!), event.block.timestamp, diff --git a/src/mappings/futures.ts b/src/mappings/futures.ts index 8df1ec9..2ff5173 100644 --- a/src/mappings/futures.ts +++ b/src/mappings/futures.ts @@ -27,7 +27,7 @@ import { Redeem, } from "../../generated/templates/PrincipalToken/PrincipalToken" import { ZERO_ADDRESS, UNIT_BI, ZERO_BI } from "../constants" -// import { createAPRInTimeForLPVault } from "../entities/APRInTime" +// import { createAPYInTimeForLPVault } from "../entities/APYInTime" import { getAccount } from "../entities/Account" import { updateAccountAssetBalance, @@ -57,7 +57,7 @@ import { import { AssetType, generateFeeClaimId } from "../utils" // import FutureState from "../utils/FutureState" import transactionType from "../utils/TransactionType" -// import { calculateLpVaultAPR } from "../utils/calculateAPR" +// import { calculateLpVaultAPY } from "../utils/calculateAPY" import { generateTransactionId } from "../utils/idGenerators" export function handleRegistryChange(event: RegistryChange): void { @@ -442,6 +442,7 @@ export function handleYieldClaimed(event: YieldClaimed): void { event.params.yieldInIBT, event.block.timestamp ) + updateFutureDailyStats(event, event.address) } else { log.warning("YieldClaimed event call for not existing Future {}", [ event.address.toHex(), @@ -454,6 +455,7 @@ export function handlePTTransfer(event: PTTransfer): void { if (future) { updateYieldForAll(event.address, event.block.timestamp) + updateFutureDailyStats(event, event.address) } else { log.warning("PTTransfer event call for not existing Future {}", [ event.address.toHex(), @@ -525,10 +527,10 @@ export function handlePTTransfer(event: PTTransfer): void { // // Create dynamic data source for LPVault events // LPVaultTemplate.create(Address.fromBytes(lpVault.address)) // -// let lpVaultAPR = createAPRInTimeForLPVault( +// let lpVaultAPY = createAPYInTimeForLPVault( // event.params.lpv, // event.block.timestamp // ) -// lpVaultAPR.apr = calculateLpVaultAPR(event.params.lpv) -// lpVaultAPR.save() +// lpVaultAPY.apy = calculateLpVaultAPY(event.params.lpv) +// lpVaultAPY.save() // } diff --git a/src/tests/amm.test.ts b/src/tests/amm.test.ts index 711e976..c5f3ef9 100644 --- a/src/tests/amm.test.ts +++ b/src/tests/amm.test.ts @@ -53,6 +53,7 @@ import { POOL_SECOND_EXCHANGE_TRANSACTION_HASH, FIRST_POOL_ADDRESS_MOCK, SECOND_POOL_ADDRESS_MOCK, + POOL_PRICE_SCALE_MOCK, } from "./mocks/CurvePool" import { STANDARD_DECIMALS_MOCK, @@ -80,6 +81,7 @@ import { FIRST_USER_MOCK, IBT_ADDRESS_MOCK, mockFutureVaultFunctions, + mockFutureVaultIBTRate, } from "./mocks/FutureVault" import { ASSET_AMOUNT_ENTITY, @@ -89,7 +91,7 @@ import { ACCOUNT_ASSET_ENTITY, ACCOUNT_ENTITY, FUTURE_DAILY_STATS_ENTITY, - APR_IN_TIME_ENTITY, + APY_IN_TIME_ENTITY, } from "./utils/entities" const ADD_LIQUIDITY_LOG_INDEX = BigInt.fromI32(1) @@ -145,7 +147,10 @@ describe("handleAddLiquidity()", () => { mockFutureVaultFunctions() mockFeedRegistryInterfaceFunctions() mockCurvePoolFunctions() - createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + mockFutureVaultIBTRate( + FIRST_FUTURE_VAULT_ADDRESS_MOCK, + BigInt.fromI32(1) + ) createConvertToSharesCallMock( IBT_ADDRESS_MOCK, toPrecision(BigInt.fromI32(10), 1, 18) @@ -403,7 +408,7 @@ describe("handleAddLiquidity()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", + "ibtRateMA", "1" ) }) @@ -655,7 +660,7 @@ describe("handleRemoveLiquidity()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", + "ibtRateMA", "1" ) }) @@ -926,17 +931,17 @@ describe("handleTokenExchange()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", + "ibtRateMA", "1" ) }) - // TODO: Fix pool APR - // test("Recalculate pool APR", () => { + // TODO: Fix pool APY + // test("Recalculate pool APY", () => { // assert.fieldEquals( - // APR_IN_TIME_ENTITY, + // APY_IN_TIME_ENTITY, // `${FIRST_POOL_ADDRESS_MOCK.toHex()}-0`, - // "apr", + // "apy", // "63113852000" // ) // }) @@ -1213,42 +1218,20 @@ describe("handleRemoveLiquidityOne()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", + "ibtRateMA", "1" ) }) test("Recalculate pool APR", () => { - const aprInTimeId = `${FIRST_POOL_ADDRESS_MOCK.toHex()}-1` + const apyInTimeId = `${FIRST_POOL_ADDRESS_MOCK.toHex()}-1` assert.fieldEquals( - APR_IN_TIME_ENTITY, - aprInTimeId, + APY_IN_TIME_ENTITY, + apyInTimeId, "spotPrice", - "90000000000" - ) - - assert.fieldEquals( - APR_IN_TIME_ENTITY, - aprInTimeId, - "ibtRate", - "200000000000000000" + POOL_PRICE_SCALE_MOCK.toString() ) - - assert.fieldEquals( - APR_IN_TIME_ENTITY, - aprInTimeId, - "underlyingToPT", - "90000000000" - ) - - // TODO: Fix pool APR - // assert.fieldEquals( - // APR_IN_TIME_ENTITY, - // aprInTimeId, - // "apr", - // "126227704000" - // ) }) }) diff --git a/src/tests/futureDayData.test.ts b/src/tests/futureDayData.test.ts index cc7579f..4387de9 100644 --- a/src/tests/futureDayData.test.ts +++ b/src/tests/futureDayData.test.ts @@ -1,4 +1,4 @@ -import { BigDecimal } from "@graphprotocol/graph-ts" +import { BigDecimal, BigInt } from "@graphprotocol/graph-ts" import { assert, beforeAll, clearStore, describe, test } from "matchstick-as" import { FutureDailyStats } from "../../generated/schema" @@ -23,6 +23,7 @@ import { FIRST_FUTURE_VAULT_ADDRESS_MOCK, IBT_ADDRESS_MOCK, mockFutureVaultFunctions, + mockFutureVaultIBTRate, } from "./mocks/FutureVault" import { assertAlmostEquals } from "./utils/asserts" import { FUTURE_DAILY_STATS_ENTITY } from "./utils/entities" @@ -47,14 +48,12 @@ describe("APY Computations on futureDailyStats", () => { test("Should create 2 FutureDailyStats entities with a 1 week interval", () => { // Mock the rate of the interest bearing token (1 IBT = 1 underlying) - let rate0D = "1000000000000000000" - const expectedRate0D = BigDecimal.fromString(rate0D) - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate0D) + let rate0D = BigInt.fromU64(1000000000000000000) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate0D) emitMint() // make a first deposit at timestamp 0 - let rate7D = "1009651000000000000" - const expectedRate7D = BigDecimal.fromString(rate7D) + let rate7D = BigInt.fromU64(1009651000000000000) // Mock a change of rate of the interest bearing token (1 IBT = 1.00961 underlying <=> 50% anualized APR) - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate7D) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate7D) emitMint(7 * SECONDS_PER_DAY) // make a second deposit at timestamp 7 days assert.entityCount(FUTURE_DAILY_STATS_ENTITY, 2) @@ -64,8 +63,8 @@ describe("APY Computations on futureDailyStats", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), "0" ), - "ibtRate", - expectedRate0D.toString() + "ibtRateMA", + rate0D.toString() ) assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, @@ -73,8 +72,8 @@ describe("APY Computations on futureDailyStats", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), "7" ), - "ibtRate", - expectedRate7D.toString() + "ibtRateMA", + rate7D.toString() ) }) @@ -92,23 +91,22 @@ describe("APY Computations on futureDailyStats", () => { }) test("Should compute correctly the 30D APR", () => { - let rate30D = "1041666666666666752" - const expectedRate30D = BigDecimal.fromString(rate30D) + let rate30D = BigInt.fromU64(1041666666666666752) const futureDailyStats30Id = generateFutureDailyStatsId( FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), "30" ) // Mock a change of rate of the interest bearing token (1 IBT = 1.04167 underlying <=> 50% anualized APR) - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate30D) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate30D) emitMint(30 * SECONDS_PER_DAY) // make a third deposit at timestamp 30 days assert.entityCount(FUTURE_DAILY_STATS_ENTITY, 3) assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, futureDailyStats30Id, - "ibtRate", - expectedRate30D.toString() + "ibtRateMA", + rate30D.toString() ) let loadFutureDailyStats = FutureDailyStats.load(futureDailyStats30Id) @@ -119,22 +117,21 @@ describe("APY Computations on futureDailyStats", () => { }) test("Should compute correctly the 90D APR", () => { - let rate90D = "1125000000000000000" - const expectedRate90D = BigDecimal.fromString(rate90D) + let rate90D = BigInt.fromU64(1125000000000000000) const futureDailyStats90Id = generateFutureDailyStatsId( FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), "90" ) // Mock a change of rate of the interest bearing token (1 IBT = 1.125 underlying <=> 50% anualized APR) - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate90D) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate90D) emitMint(90 * SECONDS_PER_DAY) // make a third deposit at timestamp 90 days assert.entityCount(FUTURE_DAILY_STATS_ENTITY, 4) assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, futureDailyStats90Id, - "ibtRate", - expectedRate90D.toString() + "ibtRateMA", + rate90D.toString() ) let loadFutureDailyStats = FutureDailyStats.load(futureDailyStats90Id) @@ -147,14 +144,14 @@ describe("APY Computations on futureDailyStats", () => { describe("IBT Rate Average computation in FutureDailyStats", () => { beforeAll(() => { - let rate120D = "1000000000000000000" - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate120D) + let rate120D = BigInt.fromU64(1000000000000000000) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate120D) emitMint(120 * SECONDS_PER_DAY) // make a deposit on day 120 - rate120D = "2000000000000000000" - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate120D) + rate120D = BigInt.fromU64(2000000000000000000) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate120D) emitMint(120 * SECONDS_PER_DAY) // make a deposit on day 120 - rate120D = "3000000000000000000" - createConvertToAssetsCallMockFromString(IBT_ADDRESS_MOCK, rate120D) + rate120D = BigInt.fromU64(3000000000000000000) + mockFutureVaultIBTRate(FIRST_FUTURE_VAULT_ADDRESS_MOCK, rate120D) emitMint(120 * SECONDS_PER_DAY) // make a deposit on day 120 }) @@ -175,7 +172,7 @@ describe("IBT Rate Average computation in FutureDailyStats", () => { ) }) - test("The FutureDayDaya updated corretly the ibtRate using the incremental average formula", () => { + test("The FutureDayDaya updated corretly the ibtRateMA using the incremental average formula", () => { const futureDailyStats120Id = generateFutureDailyStatsId( FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), "120" @@ -183,7 +180,7 @@ describe("IBT Rate Average computation in FutureDailyStats", () => { assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, futureDailyStats120Id, - "ibtRate", + "ibtRateMA", "2000000000000000000" ) }) diff --git a/src/tests/futures.test.ts b/src/tests/futures.test.ts index b11fba0..f5542b2 100644 --- a/src/tests/futures.test.ts +++ b/src/tests/futures.test.ts @@ -91,7 +91,7 @@ import { POOL_ENTITY, FACTORY_ENTITY, TRANSACTION_ENTITY, - APR_IN_TIME_ENTITY, + APY_IN_TIME_ENTITY, } from "./utils/entities" const COLLECTED_FEE = 50 @@ -525,7 +525,7 @@ describe("handleMint()", () => { assert.i32Equals(portfolio.length, 3) }) - test("Should create FutureDailyStats with the correct details", () => { + test("Should create FutureDailyStats with the correct details at Mint", () => { assert.entityCount(FUTURE_DAILY_STATS_ENTITY, 1) assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, @@ -562,8 +562,28 @@ describe("handleMint()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", - "1" + "lastIBTRate", + "2000000000000000000000000000" + ) + + assert.fieldEquals( + FUTURE_DAILY_STATS_ENTITY, + generateFutureDailyStatsId( + FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), + DAY_ID_0 + ), + "lastPTRate", + "1000000000000000000000000000" + ) + + assert.fieldEquals( + FUTURE_DAILY_STATS_ENTITY, + generateFutureDailyStatsId( + FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), + DAY_ID_0 + ), + "ibtRateMA", + "2000000000000000000000000000" ) }) }) @@ -654,7 +674,7 @@ describe("handleRedeem()", () => { ) }) - test("Should create FutureDailyStats with the correct details", () => { + test("Should create FutureDailyStats with the correct details at Redeem", () => { assert.entityCount(FUTURE_DAILY_STATS_ENTITY, 1) assert.fieldEquals( FUTURE_DAILY_STATS_ENTITY, @@ -701,8 +721,28 @@ describe("handleRedeem()", () => { FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), DAY_ID_0 ), - "ibtRate", - "1" + "lastIBTRate", + "2000000000000000000000000000" + ) + + assert.fieldEquals( + FUTURE_DAILY_STATS_ENTITY, + generateFutureDailyStatsId( + FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), + DAY_ID_0 + ), + "lastPTRate", + "1000000000000000000000000000" + ) + + assert.fieldEquals( + FUTURE_DAILY_STATS_ENTITY, + generateFutureDailyStatsId( + FIRST_FUTURE_VAULT_ADDRESS_MOCK.toHex(), + DAY_ID_0 + ), + "ibtRateMA", + "2000000000000000000000000000" ) }) }) @@ -852,13 +892,13 @@ describe("handleCurvePoolDeployed()", () => { ) }) - test("Should create new APR entity assigned to the right pool", () => { + test("Should create new APY entity assigned to the right pool", () => { test("Should create new pool entity", () => { assert.entityCount(POOL_ENTITY, 1) }) assert.fieldEquals( - APR_IN_TIME_ENTITY, + APY_IN_TIME_ENTITY, `${FIRST_POOL_ADDRESS_MOCK.toHex()}-1`, "pool", FIRST_POOL_ADDRESS_MOCK.toHex() diff --git a/src/tests/mocks/FutureVault.ts b/src/tests/mocks/FutureVault.ts index 530780d..8558eba 100644 --- a/src/tests/mocks/FutureVault.ts +++ b/src/tests/mocks/FutureVault.ts @@ -50,7 +50,8 @@ export const YIELD_USER_YIELD_IN_IBT_MOCK = BigInt.fromString("110") export const SENDER_YIELD_IN_IBT_MOCK = BigInt.fromString("220") export const RECEIVER_YIELD_IN_IBT_MOCK = BigInt.fromString("330") -const IBT_RATE_MOCK = toPrecision(BigInt.fromI32(2), 1, RAYS_PRECISION) +const IBT_RATE_MOCK = toPrecision(BigInt.fromI32(2), 0, RAYS_PRECISION) +const PT_RATE_MOCK = toPrecision(BigInt.fromI32(1), 0, RAYS_PRECISION) export function mockFutureVaultFunctions(): void { ;[ @@ -102,6 +103,12 @@ export function mockFutureVaultFunctions(): void { "getIBTRate():(uint256)" ).returns([ethereum.Value.fromUnsignedBigInt(IBT_RATE_MOCK)]) + createMockedFunction( + addressMock, + "getPTRate", + "getPTRate():(uint256)" + ).returns([ethereum.Value.fromUnsignedBigInt(PT_RATE_MOCK)]) + createMockedFunction(addressMock, "getYT", "getYT():(address)").returns( [ethereum.Value.fromAddress(YT_ADDRESS_MOCK)] ) @@ -169,3 +176,14 @@ export function mockFutureVaultFunctions(): void { ]) }) } + +export function mockFutureVaultIBTRate( + futureVaultAddress: Address, + rate: BigInt +): void { + createMockedFunction( + futureVaultAddress, + "getIBTRate", + "getIBTRate():(uint256)" + ).returns([ethereum.Value.fromUnsignedBigInt(rate)]) +} diff --git a/src/tests/utils/entities.ts b/src/tests/utils/entities.ts index 2b69fd2..69b4572 100644 --- a/src/tests/utils/entities.ts +++ b/src/tests/utils/entities.ts @@ -11,7 +11,7 @@ export let ASSET_AMOUNT_ENTITY = "AssetAmount" export let ACCOUNT_ASSET_ENTITY = "AccountAsset" export let POOL_ENTITY = "Pool" -export let APR_IN_TIME_ENTITY = "APRInTime" +export let APY_IN_TIME_ENTITY = "APYInTime" export let TRANSFER_ENTITY = "Transfer" diff --git a/src/utils/calculateAPR.ts b/src/utils/calculateAPR.ts deleted file mode 100644 index 0e1bc51..0000000 --- a/src/utils/calculateAPR.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Address, BigDecimal, BigInt, log } from "@graphprotocol/graph-ts" - -import { LPVault } from "../../generated/schema" -import { SECONDS_PER_YEAR, UNIT_BI, ZERO_BD, ZERO_BI } from "../constants" -import { createAPRInTimeForPool } from "../entities/APRInTime" -import { getIBTtoPTRate } from "../entities/CurvePool" -import { getERC20Decimals } from "../entities/ERC20" -import { getSharesRate } from "../entities/ERC4626" -import { - getExpirationTimestamp, - getIBT, - getIBTRate, - getUnderlying, -} from "../entities/FutureVault" -import { RAYS_PRECISION, toPrecision } from "./toPrecision" - -export function updatePoolAPR( - poolAddress: Address, - principalToken: Address, - currentTimestamp: BigInt, - blockNumber: BigInt -): void { - let poolAPR = createAPRInTimeForPool( - poolAddress, - currentTimestamp, - blockNumber - ) - - let underlyingAddress = getUnderlying(principalToken) - let ibtAddress = getIBT(principalToken) - - let underlyingDecimals = getERC20Decimals(underlyingAddress) - let ibtDecimals = getERC20Decimals(ibtAddress) - let smallInput = BigInt.fromI32(10).pow((ibtDecimals as u8) - 1) // small input to ensure correct rate for small amount - - const ibtToPT = getIBTtoPTRate(poolAddress, smallInput).times( - BigInt.fromI32(10) - ) - - const principalTokenExpiration = getExpirationTimestamp(principalToken) - - const ibtRate = toPrecision( - getIBTRate(principalToken), - RAYS_PRECISION, - underlyingDecimals - ) - - const spotPrice = ibtToPT - - const underlyingUnit = BigInt.fromI32(10).pow(underlyingDecimals as u8) - const ibtUnit = BigInt.fromI32(10).pow(ibtDecimals as u8) - - const ibtSharesRate = getSharesRate(ibtAddress, underlyingUnit) - - poolAPR.spotPrice = spotPrice - poolAPR.ibtRate = ibtRate - poolAPR.ibtSharesRate = ibtSharesRate - - if ( - principalTokenExpiration.gt(currentTimestamp) && - ibtRate.notEqual(ZERO_BI) && - ibtSharesRate.notEqual(ZERO_BI) - ) { - const underlyingToPTRate = ibtSharesRate // Reflect IBT/Underlying rate - .times(spotPrice) // IBT/PT rate - .div(ibtUnit) - - poolAPR.underlyingToPT = underlyingToPTRate - - const underlyingToPTRatePerSecond = principalTokenExpiration - .minus(currentTimestamp) - .toBigDecimal() - - let apr = ZERO_BD - if (underlyingToPTRatePerSecond.gt(ZERO_BD)) { - apr = underlyingToPTRate - .minus(ibtUnit) - .toBigDecimal() - .div(underlyingToPTRatePerSecond) // Get rate per second - .times(SECONDS_PER_YEAR) // Convert to rate per year - .times(BigDecimal.fromString("100")) // to percentage - .div(ibtRate.toBigDecimal()) - } - - poolAPR.apr = apr - } else { - poolAPR.apr = ZERO_BD - poolAPR.underlyingToPT = UNIT_BI - } - - poolAPR.save() -} - -const LP_VAULT_APR_MOCK: BigDecimal[] = [ - BigDecimal.fromString("3"), - BigDecimal.fromString("6"), -] - -export function calculateLpVaultAPR(lpVaultAddress: Address): BigDecimal { - let lpVault = LPVault.load(lpVaultAddress.toHex()) - - if (lpVault) { - log.warning(lpVault.underlying, []) - if ( - lpVault.underlying == "0x792f2d31b2aadac705d57735855b299f84b999b9" - ) { - return LP_VAULT_APR_MOCK[0] - } else if ( - lpVault.underlying == "0x8494a4761a5d969d3f80f7110fbaa29e4072cdcd" - ) { - return LP_VAULT_APR_MOCK[1] - } - } - - return ZERO_BD -} diff --git a/src/utils/calculateAPY.ts b/src/utils/calculateAPY.ts new file mode 100644 index 0000000..baf0b23 --- /dev/null +++ b/src/utils/calculateAPY.ts @@ -0,0 +1,74 @@ +import { Address, BigDecimal, BigInt, log } from "@graphprotocol/graph-ts" + +import { LPVault } from "../../generated/schema" +import { SECONDS_PER_YEAR, ZERO_BD } from "../constants" +import { createAPYInTimeForPool } from "../entities/APYInTime" +import { getPoolPriceScale } from "../entities/CurvePool" +import { + getIBTRate, + getPTRate, + getExpirationTimestamp, +} from "../entities/FutureVault" + +export function updatePoolAPY( + poolAddress: Address, + principalToken: Address, + currentTimestamp: BigInt, + blockNumber: BigInt +): void { + let poolAPY = createAPYInTimeForPool( + poolAddress, + currentTimestamp, + blockNumber + ) + + const curveUnit = BigInt.fromI32(10).pow(18) + const rayUnit = BigInt.fromI32(10).pow(27) + + const expirationTimestamp = getExpirationTimestamp(principalToken) + const timeLeft = expirationTimestamp.minus(currentTimestamp) + const spotPrice = getPoolPriceScale(poolAddress) + + poolAPY.spotPrice = spotPrice + const ibtRate = getIBTRate(principalToken) + const ptRate = getPTRate(principalToken) + poolAPY.ptRate = ptRate + poolAPY.ibtRate = ibtRate + + const baseAPY = ptRate + .times(curveUnit) + .times(rayUnit) + .div(spotPrice.times(ibtRate)) + const expAPY = SECONDS_PER_YEAR.div( + BigDecimal.fromString(timeLeft.toString()) + ) + poolAPY.baseAPY = BigDecimal.fromString(baseAPY.toString()).div( + BigDecimal.fromString(rayUnit.toString()) + ) + poolAPY.exponentAPY = expAPY + poolAPY.save() +} + +const LP_VAULT_APY_MOCK: BigDecimal[] = [ + BigDecimal.fromString("3"), + BigDecimal.fromString("6"), +] + +export function calculateLpVaultAPY(lpVaultAddress: Address): BigDecimal { + let lpVault = LPVault.load(lpVaultAddress.toHex()) + + if (lpVault) { + log.warning(lpVault.underlying, []) + if ( + lpVault.underlying == "0x792f2d31b2aadac705d57735855b299f84b999b9" + ) { + return LP_VAULT_APY_MOCK[0] + } else if ( + lpVault.underlying == "0x8494a4761a5d969d3f80f7110fbaa29e4072cdcd" + ) { + return LP_VAULT_APY_MOCK[1] + } + } + + return ZERO_BD +}