diff --git a/schema.graphql b/schema.graphql index 6a6d720..e0c0336 100644 --- a/schema.graphql +++ b/schema.graphql @@ -143,6 +143,12 @@ type Asset @entity { ibt: Asset fytTokenDetails: FYTTokenDetails lpTokenDetails: LPTokenDetails + "in case the AssetType is IBT we give the IBT Rates" + lastIBTRate: BigDecimal + "IBT to underlying asset as returned by convertToAssets(UNIT). The number is in underlying asset decimals" + convertToAssetsUnit: BigInt + lastUpdateTimestamp: BigInt + } "AssetPrice entity to assign a price of a token to an Asset entity and its price source" diff --git a/src/entities/ERC20.ts b/src/entities/ERC20.ts index 057f40e..ed7d2bd 100644 --- a/src/entities/ERC20.ts +++ b/src/entities/ERC20.ts @@ -1,11 +1,17 @@ import { Address, BigInt, log } from "@graphprotocol/graph-ts" +import { Asset } from "../../generated/schema" import { ERC20 } from "../../generated/templates/ERC20/ERC20" import { ZERO_BI } from "../constants" const UNKNOWN = "Unknown" export function getERC20Name(address: Address): string { + let asset = Asset.load(address.toHex()) + if (asset) { + return asset.name + } + let erc20Contract = ERC20.bind(address) let nameCall = erc20Contract.try_name() @@ -21,6 +27,11 @@ export function getERC20Name(address: Address): string { } export function getERC20Symbol(address: Address): string { + let asset = Asset.load(address.toHex()) + if (asset) { + return asset.symbol + } + let erc20Contract = ERC20.bind(address) let symbolCall = erc20Contract.try_symbol() @@ -36,6 +47,11 @@ export function getERC20Symbol(address: Address): string { } export function getERC20Decimals(address: Address): i32 { + let asset = Asset.load(address.toHex()) + if (asset) { + return asset.decimals + } + let erc20Contract = ERC20.bind(address) let decimalsCall = erc20Contract.try_decimals() diff --git a/src/entities/ERC4626.ts b/src/entities/ERC4626.ts index bb708fa..dbb7f11 100644 --- a/src/entities/ERC4626.ts +++ b/src/entities/ERC4626.ts @@ -1,11 +1,15 @@ import { Address, BigInt, log } from "@graphprotocol/graph-ts" +import { Asset } from "../../generated/schema" import { ERC4626 } from "../../generated/templates/PrincipalToken/ERC4626" -import { UNIT_BI, ZERO_BI } from "../constants" +import { UNIT_BI, ZERO_ADDRESS, ZERO_BI } from "../constants" +import { getERC20Decimals } from "./ERC20" export function getIBTRate(address: Address): BigInt { let erc4626Contract = ERC4626.bind(address) - let rate = erc4626Contract.try_convertToAssets(UNIT_BI) + const ibtDecimals = getERC20Decimals(address) + const IBT_UNIT = BigInt.fromI32(10).pow(ibtDecimals as u8) + let rate = erc4626Contract.try_convertToAssets(IBT_UNIT) if (!rate.reverted) { return rate.value @@ -15,6 +19,36 @@ export function getIBTRate(address: Address): BigInt { return UNIT_BI } +export function getERC4626Asset(address: Address): Address { + let erc4626Contract = ERC4626.bind(address) + let asset = erc4626Contract.try_asset() + + if (!asset.reverted) { + return asset.value + } + + log.warning("asset() call reverted for {}", [address.toHex()]) + + return ZERO_ADDRESS +} + +export function getERC4626UnderlyingDecimals(address: Address): i32 { + let ibtAsset = Asset.load(address.toHex()) + if (ibtAsset) { + let underlyingAddress = ibtAsset.underlying + if (underlyingAddress) { + return getERC20Decimals(Address.fromString(underlyingAddress)) + } + } + let underlying = getERC4626Asset(address) + return getERC20Decimals(underlying) +} + +export function getUnderlyingUnit(address: Address): BigInt { + let underlyingDecimals = getERC4626UnderlyingDecimals(address) + return BigInt.fromI32(10).pow(underlyingDecimals as u8) +} + export function getERC4626Balance( tokenAddress: Address, account: Address diff --git a/src/entities/IBTAsset.ts b/src/entities/IBTAsset.ts new file mode 100644 index 0000000..2fd4aef --- /dev/null +++ b/src/entities/IBTAsset.ts @@ -0,0 +1,43 @@ +import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts" + +import { Asset } from "../../generated/schema" +import { AssetType } from "../utils" +import { getAsset } from "./Asset" +import { getERC20Decimals } from "./ERC20" +import { getERC4626Asset, getIBTRate, getUnderlyingUnit } from "./ERC4626" +import { getNetwork } from "./Network" + +export function getIBTAsset(ibtAddress: Bytes, timestamp: BigInt): Asset { + let ibt = Asset.load(ibtAddress.toString()) + if (ibt) { + return ibt + } + ibt = createIBTAsset(ibtAddress, timestamp) + return ibt +} + +export function updateIBTRates(ibtAddress: Bytes, timestamp: BigInt): void { + let ibt = getIBTAsset(ibtAddress, timestamp) + let convertToAssets = getIBTRate(Address.fromBytes(ibtAddress)) + ibt.convertToAssetsUnit = convertToAssets + const UNDERLYING_UNIT = getUnderlyingUnit(Address.fromBytes(ibtAddress)) + ibt.lastIBTRate = convertToAssets.divDecimal(UNDERLYING_UNIT.toBigDecimal()) + ibt.lastUpdateTimestamp = timestamp + ibt.save() +} + +export function createIBTAsset(ibtAddress: Bytes, timestamp: BigInt): Asset { + let ibt = getAsset(ibtAddress.toHex(), timestamp, AssetType.IBT) + ibt.chainId = getNetwork().chainId + ibt.address = ibtAddress + ibt.createdAtTimestamp = timestamp + let convertToAssets = getIBTRate(Address.fromBytes(ibtAddress)) + ibt.convertToAssetsUnit = convertToAssets + const underlying = getERC4626Asset(Address.fromBytes(ibtAddress)) + const underlying_decimals = getERC20Decimals(underlying) + const UNDERLYING_UNIT = BigInt.fromI32(10).pow(underlying_decimals as u8) + ibt.lastIBTRate = convertToAssets.divDecimal(UNDERLYING_UNIT.toBigDecimal()) + ibt.lastUpdateTimestamp = timestamp + ibt.save() + return ibt as Asset +} diff --git a/src/mappings/futures.ts b/src/mappings/futures.ts index 2ff5173..642a710 100644 --- a/src/mappings/futures.ts +++ b/src/mappings/futures.ts @@ -1,4 +1,4 @@ -import { Address, ethereum, log } from "@graphprotocol/graph-ts" +import { Address, Bytes, ethereum, log } from "@graphprotocol/graph-ts" import { CurveFactoryChange, // CurveFactoryChange, @@ -15,6 +15,7 @@ import { import { ERC20, // LPVault as LPVaultTemplate, PrincipalToken as PrincipalTokenTemplate, + IBT, } from "../../generated/templates" import { FeeClaimed, @@ -47,6 +48,7 @@ import { getTotalAssets, getYT, } from "../entities/FutureVault" +import { getIBTAsset } from "../entities/IBTAsset" import { getNetwork } from "../entities/Network" import { createPool } from "../entities/Pool" import { createTransaction } from "../entities/Transaction" @@ -104,17 +106,15 @@ export function handlePTDeployed(event: PTDeployed): void { underlyingAsset.save() let ibtAddress = getIBT(ptAddress) - let ibtAsset = getAsset( - ibtAddress.toHex(), - event.block.timestamp, - AssetType.IBT + let ibtAsset = getIBTAsset( + Bytes.fromHexString(ibtAddress.toHex()), + event.block.timestamp ) ibtAsset.underlying = underlyingAsset.id ibtAsset.save() newFuture.underlyingAsset = underlyingAddress.toHex() newFuture.ibtAsset = ibtAddress.toHex() - newFuture.yieldGenerators = [] newFuture.save() @@ -140,6 +140,9 @@ export function handlePTDeployed(event: PTDeployed): void { // Create dynamic data source for PT token events ERC20.create(event.params.pt) + // Create dynamic data source for IBT token events + IBT.create(ibtAddress) + // Create dynamic data source for YT token events ERC20.create(Address.fromBytes(ytToken.address)) diff --git a/src/mappings/transfers.ts b/src/mappings/transfers.ts index 5c3c369..4c8c16c 100644 --- a/src/mappings/transfers.ts +++ b/src/mappings/transfers.ts @@ -7,6 +7,7 @@ import { updateAccountAssetYTBalance, } from "../entities/AccountAsset" import { getAssetAmount } from "../entities/AssetAmount" +import { updateIBTRates } from "../entities/IBTAsset" import { updateYieldForAll } from "../entities/Yield" import { AssetType, logWarning } from "../utils" import { generateTransferId } from "../utils/idGenerators" @@ -89,3 +90,12 @@ export function handleTransfer(event: TransferEvent): void { ]) } } + +/** @dev Handles the Transfer event for IBT tokens. + * @param event The Transfer event. + * @notice We use a separate function for IBT tokens because to limit the number of entities stored we won't store all IBT transfer entities. We will simply update the IBT entity to update its IBTRate. + * @returns void + */ +export function handleIBTTransfer(event: TransferEvent): void { + updateIBTRates(event.address, event.block.timestamp) +} diff --git a/src/tests/amm.test.ts b/src/tests/amm.test.ts index c5f3ef9..97d8305 100644 --- a/src/tests/amm.test.ts +++ b/src/tests/amm.test.ts @@ -1,4 +1,4 @@ -import { BigInt, ethereum } from "@graphprotocol/graph-ts" +import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts" import { describe, test, @@ -63,8 +63,10 @@ import { POOL_PT_BALANCE_MOCK, POOL_LP_BALANCE_MOCK, LP_TOTAL_SUPPLY, + ETH_ADDRESS_MOCK, } from "./mocks/ERC20" import { + createAssetCallMock, createConvertToAssetsCallMock, createConvertToSharesCallMock, } from "./mocks/ERC4626" @@ -155,7 +157,11 @@ describe("handleAddLiquidity()", () => { IBT_ADDRESS_MOCK, toPrecision(BigInt.fromI32(10), 1, 18) ) - + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) emitFactoryUpdated() emitFutureVaultDeployed(FIRST_FUTURE_VAULT_ADDRESS_MOCK) emiCurveFactoryChange() @@ -448,6 +454,10 @@ describe("handleRemoveLiquidity()", () => { tokenSupplyParam, ] createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleRemoveLiquidity(removeLiquidityEvent) }) @@ -710,7 +720,11 @@ describe("handleTokenExchange()", () => { boughtIdParam, tokensBoughtParam, ] - + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleTokenExchange(tokenExchangeEvent) }) @@ -1039,7 +1053,11 @@ describe("handleRemoveLiquidityOne()", () => { coinIndexParam, coinAmountParam, ] - + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleRemoveLiquidityOne(removeLiquidityOneEvent) }) @@ -1319,6 +1337,11 @@ describe("handleClaimAdminFee", () => { claimAdminFeeEvent.parameters = [adminParam, tokensParam] + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleClaimAdminFee(claimAdminFeeEvent) }) diff --git a/src/tests/futureDayData.test.ts b/src/tests/futureDayData.test.ts index 4387de9..b435a24 100644 --- a/src/tests/futureDayData.test.ts +++ b/src/tests/futureDayData.test.ts @@ -1,4 +1,4 @@ -import { BigDecimal, BigInt } from "@graphprotocol/graph-ts" +import { Address, BigDecimal, BigInt } from "@graphprotocol/graph-ts" import { assert, beforeAll, clearStore, describe, test } from "matchstick-as" import { FutureDailyStats } from "../../generated/schema" @@ -15,8 +15,15 @@ import { FIRST_POOL_ADDRESS_MOCK, mockCurvePoolFunctions, } from "./mocks/CurvePool" -import { mockERC20Balances, mockERC20Functions } from "./mocks/ERC20" -import { createConvertToAssetsCallMockFromString } from "./mocks/ERC4626" +import { + ETH_ADDRESS_MOCK, + mockERC20Balances, + mockERC20Functions, +} from "./mocks/ERC20" +import { + createAssetCallMock, + createConvertToAssetsCallMock, +} from "./mocks/ERC4626" import { mockFactoryFunctions } from "./mocks/Factory" import { mockFeedRegistryInterfaceFunctions } from "./mocks/FeedRegistryInterface" import { @@ -39,6 +46,11 @@ describe("APY Computations on futureDailyStats", () => { mockFeedRegistryInterfaceFunctions() mockFactoryFunctions() mockCurvePoolFunctions() + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) emitFactoryUpdated() emitFutureVaultDeployed(FIRST_FUTURE_VAULT_ADDRESS_MOCK) diff --git a/src/tests/futures.test.ts b/src/tests/futures.test.ts index f5542b2..0770b59 100644 --- a/src/tests/futures.test.ts +++ b/src/tests/futures.test.ts @@ -1,4 +1,4 @@ -import { BigInt, ethereum } from "@graphprotocol/graph-ts" +import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts" import { assert, beforeAll, @@ -57,7 +57,10 @@ import { POOL_PT_BALANCE_MOCK, YT_BALANCE_MOCK, } from "./mocks/ERC20" -import { createConvertToAssetsCallMock } from "./mocks/ERC4626" +import { + createAssetCallMock, + createConvertToAssetsCallMock, +} from "./mocks/ERC4626" import { mockFactoryFunctions, FACTORY_ADDRESS_MOCK, @@ -121,6 +124,12 @@ describe("handleFutureVaultDeployed()", () => { mockFutureVaultFunctions() mockFeedRegistryInterfaceFunctions() + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) + emitFactoryUpdated() emitFutureVaultDeployed(FIRST_FUTURE_VAULT_ADDRESS_MOCK) emitFutureVaultDeployed(SECOND_FUTURE_VAULT_ADDRESS_MOCK) @@ -269,6 +278,10 @@ describe("handleUnpaused()", () => { describe("handleYieldUpdated()", () => { beforeAll(() => { createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) emitMint(0, FEE_COLLECTOR_ADDRESS_MOCK) @@ -356,6 +369,11 @@ describe("handleYieldUpdated()", () => { describe("handleFeeClaimed()", () => { test("Should create a new FeeClaim entity with properly assign future as well as fee collector entity", () => { + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) let feeClaimedEvent = changetype(newMockEvent()) feeClaimedEvent.address = FIRST_FUTURE_VAULT_ADDRESS_MOCK @@ -429,6 +447,10 @@ describe("handleFeeClaimed()", () => { describe("handleMint()", () => { beforeAll(() => { createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) emitMint() }) @@ -612,6 +634,10 @@ describe("handleRedeem()", () => { redeemEvent.parameters = [senderParam, receiverParam, sharesParam] createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleRedeem(redeemEvent) }) @@ -931,7 +957,11 @@ describe("handleYieldClaimed()", () => { receiverParam, yieldInIBTParam, ] - + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) handleYieldClaimed(yieldClaimedEvent) }) diff --git a/src/tests/mocks/ERC4626.ts b/src/tests/mocks/ERC4626.ts index 8c70923..7b290cf 100644 --- a/src/tests/mocks/ERC4626.ts +++ b/src/tests/mocks/ERC4626.ts @@ -18,26 +18,22 @@ export const createConvertToAssetsCallMock = ( ) .withArgs([ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1))]) .returns([ethereum.Value.fromI32(rate as u32)]) -} -/** - * Mock the convertToAsset function from the ERC4626 contract. Specify the mocked rate as a String - * The rate is computed for a unit of IBT - * @param addressMock The address of the ERC4626 asset - * @param rate The rate to return but the convertToAsset function - */ -export const createConvertToAssetsCallMockFromString = ( - addressMock: Address, - rate: string -): void => { - let rateBI = BigInt.fromString(rate) createMockedFunction( addressMock, "convertToAssets", "convertToAssets(uint256):(uint256)" ) - .withArgs([ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1))]) - .returns([ethereum.Value.fromSignedBigInt(rateBI)]) + .withArgs([ + ethereum.Value.fromUnsignedBigInt( + BigInt.fromI64(1000000000000000000) + ), + ]) + .returns([ + ethereum.Value.fromUnsignedBigInt( + BigInt.fromI64(1000000000000000000) + ), + ]) } export const createConvertToSharesCallMock = ( @@ -52,3 +48,12 @@ export const createConvertToSharesCallMock = ( .withArgs([ethereum.Value.fromUnsignedBigInt(rate)]) .returns([ethereum.Value.fromUnsignedBigInt(rate)]) } + +export const createAssetCallMock = ( + addressMock: Address, + asset: Address +): void => { + createMockedFunction(addressMock, "asset", "asset():(address)").returns([ + ethereum.Value.fromAddress(asset), + ]) +} diff --git a/src/tests/transfers.test.ts b/src/tests/transfers.test.ts index 32bd638..2bb4211 100644 --- a/src/tests/transfers.test.ts +++ b/src/tests/transfers.test.ts @@ -31,13 +31,17 @@ import { POOL_PT_ADDRESS_MOCK, } from "./mocks/CurvePool" import { + ETH_ADDRESS_MOCK, mockERC20Balances, mockERC20Functions, POOL_LP_BALANCE_MOCK, POOL_PT_BALANCE_MOCK, STANDARD_DECIMALS_MOCK, } from "./mocks/ERC20" -import { createConvertToAssetsCallMock } from "./mocks/ERC4626" +import { + createAssetCallMock, + createConvertToAssetsCallMock, +} from "./mocks/ERC4626" import { mockFactoryFunctions } from "./mocks/Factory" import { mockFeedRegistryInterfaceFunctions } from "./mocks/FeedRegistryInterface" import { @@ -87,6 +91,12 @@ describe("handleTransfer()", () => { mockFactoryFunctions() + createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) + createAssetCallMock( + IBT_ADDRESS_MOCK, + Address.fromString(ETH_ADDRESS_MOCK) + ) + mockFutureVaultFunctions() mockFeedRegistryInterfaceFunctions() mockCurvePoolFunctions() @@ -95,8 +105,6 @@ describe("handleTransfer()", () => { emitFutureVaultDeployed(FIRST_FUTURE_VAULT_ADDRESS_MOCK) emiCurveFactoryChange() emitCurvePoolDeployed(FIRST_POOL_ADDRESS_MOCK) - - createConvertToAssetsCallMock(IBT_ADDRESS_MOCK, 1) // Necessary to have YT entity to follow yield emitMint(0, SENDER_USER_MOCK) emitMint(0, RECEIVER_USER_MOCK) diff --git a/subgraph.template.yaml b/subgraph.template.yaml index 337d310..82f4609 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -1,4 +1,4 @@ -specVersion: 0.0.2 +specVersion: 0.0.4 description: Spectra Subgraph repository: https://github.com/perspectivefi/spectra-subgraph schema: @@ -16,7 +16,7 @@ dataSources: mapping: kind: ethereum/events language: wasm/assemblyscript - apiVersion: 0.0.5 + apiVersion: 0.0.6 entities: [ ] file: ./src/mappings/registry.ts abis: @@ -37,7 +37,7 @@ dataSources: mapping: kind: ethereum/events language: wasm/assemblyscript - apiVersion: 0.0.5 + apiVersion: 0.0.6 entities: [ ] file: ./src/mappings/futures.ts abis: @@ -74,7 +74,7 @@ dataSources: mapping: kind: ethereum/events language: wasm/assemblyscript - apiVersion: 0.0.5 + apiVersion: 0.0.6 entities: [ ] file: ./src/mappings/amm.ts abis: @@ -111,7 +111,7 @@ templates: abi: ERC20 mapping: kind: ethereum/events - apiVersion: 0.0.5 + apiVersion: 0.0.6 language: wasm/assemblyscript entities: [ ] file: ./src/mappings/transfers.ts @@ -125,6 +125,27 @@ templates: eventHandlers: - event: Transfer(indexed address,indexed address,uint256) handler: handleTransfer + - name: IBT + kind: ethereum/contract + network: {{ network }} + source: + abi: ERC4626 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: [ ] + file: ./src/mappings/transfers.ts + abis: + - name: ERC20 + file: ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json + - name: ERC4626 + file: ./node_modules/@openzeppelin/contracts/build/contracts/ERC4626.json + - name: PrincipalToken + file: ./abis/PrincipalToken.json + eventHandlers: + - event: Transfer(indexed address,indexed address,uint256) + handler: handleIBTTransfer - name: PrincipalToken kind: ethereum/contract network: {{ network }} @@ -133,7 +154,7 @@ templates: mapping: kind: ethereum/events language: wasm/assemblyscript - apiVersion: 0.0.5 + apiVersion: 0.0.6 entities: [ ] file: ./src/mappings/futures.ts abis: @@ -170,7 +191,7 @@ templates: # mapping: # kind: ethereum/events # language: wasm/assemblyscript -# apiVersion: 0.0.5 +# apiVersion: 0.0.6 # entities: [ ] # file: ./src/mappings/lpVaults.ts # abis: