diff --git a/abi/balancer-meta-pool-strategy.json b/abi/balancer-meta-pool-strategy.json index f36f56e8..2a9f9ab5 100644 --- a/abi/balancer-meta-pool-strategy.json +++ b/abi/balancer-meta-pool-strategy.json @@ -847,4 +847,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/balancer-vault.json b/abi/balancer-vault.json index ccaba518..2c6506f8 100644 --- a/abi/balancer-vault.json +++ b/abi/balancer-vault.json @@ -1176,4 +1176,4 @@ "stateMutability": "payable", "type": "receive" } -] \ No newline at end of file +] diff --git a/abi/base-reward-pool-4626.json b/abi/base-reward-pool-4626.json index 26c842c8..28152ae8 100644 --- a/abi/base-reward-pool-4626.json +++ b/abi/base-reward-pool-4626.json @@ -1233,4 +1233,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/base-reward-pool.json b/abi/base-reward-pool.json index 6e6afb94..371155a7 100644 --- a/abi/base-reward-pool.json +++ b/abi/base-reward-pool.json @@ -660,4 +660,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/chainlink-feed-registry.json b/abi/chainlink-feed-registry.json index 6aa18416..9803aff5 100644 --- a/abi/chainlink-feed-registry.json +++ b/abi/chainlink-feed-registry.json @@ -929,4 +929,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/curve-lp-token.json b/abi/curve-lp-token.json index 33c1da44..2617f2cc 100644 --- a/abi/curve-lp-token.json +++ b/abi/curve-lp-token.json @@ -1183,4 +1183,4 @@ } ] } -] \ No newline at end of file +] diff --git a/abi/eac-aggregator-proxy.json b/abi/eac-aggregator-proxy.json index f89fce1a..bca8a841 100644 --- a/abi/eac-aggregator-proxy.json +++ b/abi/eac-aggregator-proxy.json @@ -506,4 +506,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/initializable-abstract-strategy.json b/abi/initializable-abstract-strategy.json index ce372861..07d50149 100644 --- a/abi/initializable-abstract-strategy.json +++ b/abi/initializable-abstract-strategy.json @@ -529,4 +529,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/lido.json b/abi/lido.json index eaea0200..c5a3f659 100644 --- a/abi/lido.json +++ b/abi/lido.json @@ -1597,4 +1597,4 @@ "name": "ContractVersionSet", "type": "event" } -] \ No newline at end of file +] diff --git a/abi/meta-stable-pool.json b/abi/meta-stable-pool.json index 1ae16a10..9bcefb71 100644 --- a/abi/meta-stable-pool.json +++ b/abi/meta-stable-pool.json @@ -1434,4 +1434,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/oeth-oracle-router.json b/abi/oeth-oracle-router.json index 89b0f4fb..191e8d3f 100644 --- a/abi/oeth-oracle-router.json +++ b/abi/oeth-oracle-router.json @@ -37,4 +37,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/origin-lens.json b/abi/origin-lens.json index 4e360855..590ceaaa 100644 --- a/abi/origin-lens.json +++ b/abi/origin-lens.json @@ -407,4 +407,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/otoken-vault.json b/abi/otoken-vault.json index 5c7d0550..9fe5cf0d 100644 --- a/abi/otoken-vault.json +++ b/abi/otoken-vault.json @@ -985,4 +985,4 @@ "stateMutability": "view", "type": "function" } -] \ No newline at end of file +] diff --git a/abi/sfrx-eth.json b/abi/sfrx-eth.json index 1b48b716..983d80f5 100644 --- a/abi/sfrx-eth.json +++ b/abi/sfrx-eth.json @@ -847,4 +847,4 @@ "stateMutability": "nonpayable", "type": "function" } -] \ No newline at end of file +] diff --git a/src/abi/balancer-rate-provider.abi.ts b/src/abi/balancer-rate-provider.abi.ts new file mode 100644 index 00000000..5b7138ac --- /dev/null +++ b/src/abi/balancer-rate-provider.abi.ts @@ -0,0 +1,41 @@ +export const ABI_JSON = [ + { + "type": "constructor", + "stateMutability": "undefined", + "payable": false, + "inputs": [ + { + "type": "address", + "name": "_rocketTokenRETH" + } + ] + }, + { + "type": "function", + "name": "getRate", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "uint256", + "name": "" + } + ] + }, + { + "type": "function", + "name": "rocketTokenRETH", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "address", + "name": "" + } + ] + } +] diff --git a/src/abi/balancer-rate-provider.ts b/src/abi/balancer-rate-provider.ts new file mode 100644 index 00000000..7b8ba9c4 --- /dev/null +++ b/src/abi/balancer-rate-provider.ts @@ -0,0 +1,25 @@ +import * as ethers from 'ethers' +import {LogEvent, Func, ContractBase} from './abi.support' +import {ABI_JSON} from './balancer-rate-provider.abi' + +export const abi = new ethers.Interface(ABI_JSON); + +export const functions = { + getRate: new Func<[], {}, bigint>( + abi, '0x679aefce' + ), + rocketTokenRETH: new Func<[], {}, string>( + abi, '0xdb5dacc9' + ), +} + +export class Contract extends ContractBase { + + getRate(): Promise { + return this.eth_call(functions.getRate, []) + } + + rocketTokenRETH(): Promise { + return this.eth_call(functions.rocketTokenRETH, []) + } +} diff --git a/src/post-processors/validate-oeth/validate-oeth.ts b/src/post-processors/validate-oeth/validate-oeth.ts index e1b1e976..91f59fe9 100644 --- a/src/post-processors/validate-oeth/validate-oeth.ts +++ b/src/post-processors/validate-oeth/validate-oeth.ts @@ -2,7 +2,7 @@ import { Entity, EntityClass } from '@subsquid/typeorm-store' import assert from 'assert' import { sortBy } from 'lodash' -import { OETHMorphoAave, OETHVault } from '../../model' +import { OETHMorphoAave, OETHVault, StrategyBalance } from '../../model' import { Block, Context } from '../../processor' import { jsonify } from '../../utils/jsonify' @@ -19,6 +19,12 @@ export const process = async (ctx: Context) => { OETHMorphoAave, expectations.oethMorphoAave, ) + await validateExpectations( + ctx, + block, + StrategyBalance, + expectations.strategyBalances, + ) firstBlock = false } } @@ -57,11 +63,12 @@ const validateExpectation = async < const actual = await ctx.store.findOne(Class, { where: { id: expectation.id }, }) + assert(actual, 'Expected entity does not exist.') expectation.timestamp = new Date(expectation.timestamp).toJSON() assert.deepEqual(JSON.parse(jsonify(actual)), expectation) } -const expectations: Record = { +const expectations = { oethVaults: sortBy( [ { @@ -100,7 +107,7 @@ const expectations: Record = { weth: '368830581327791252482', frxETH: '0', }, - ], + ] as any[], (v) => v.blockNumber, ), oethMorphoAave: sortBy( @@ -123,7 +130,13 @@ const expectations: Record = { blockNumber: 17479788, weth: '103288680000000000000', }, - ], + ] as any[], (v) => v.blockNumber, ), -} + strategyBalances: sortBy( + [ + // Place verified strategy balances in here. + ] as any[], + (v) => v.blockNumber, + ), +} as const diff --git a/src/processor-templates/strategy/strategy.ts b/src/processor-templates/strategy/strategy.ts index 8547d92d..2b48f580 100644 --- a/src/processor-templates/strategy/strategy.ts +++ b/src/processor-templates/strategy/strategy.ts @@ -1,9 +1,38 @@ import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { memoize } from 'lodash' + +import * as abstractStrategyAbi from '../../abi/initializable-abstract-strategy' +import * as curvePool from '../../abi/curve-lp-token' +import * as erc20 from '../../abi/erc20' +import * as balancerVaultAbi from '../../abi/balancer-vault' +import * as balancerMetaStablePoolAbi from '../../abi/meta-stable-pool' +import * as balancerRateProvider from '../../abi/balancer-rate-provider' +import * as balancerMetaStablePoolStrategyAbi from '../../abi/balancer-meta-pool-strategy' -import * as initializableAbstractStrategy from '../../abi/initializable-abstract-strategy' import { StrategyBalance } from '../../model' -import { Context } from '../../processor' +import { Context, Block } from '../../processor' import { blockFrequencyUpdater } from '../../utils/blockFrequencyUpdater' +import { ADDRESS_ZERO, BALANCER_VAULT, ETH_ADDRESS, OETH_ADDRESS, WETH_ADDRESS } from '../../utils/addresses' + +export type IBalancerPoolInfo = { + poolId: string, + poolAddress: string, +} + +export type ICurveAMOInfo = { + poolAddress: string, + rewardsPoolAddress: string +} + +export type IStrategyData = { + from: number, + name: string, + address: string, + kind: 'Generic' | 'CurveAMO' | 'BalancerMetaStablePool' | 'BalancerComposableStablePool', + assets: readonly string[], + balancerPoolInfo?: IBalancerPoolInfo, + curvePoolInfo?: ICurveAMOInfo, +} export const createStrategySetup = (from: number) => (processor: EvmBatchProcessor) => { @@ -11,23 +40,155 @@ export const createStrategySetup = } // Used by `src/processors/strategies/strategies.ts` -export const createStrategyProcessor = ({ - from, - address, - kind, -}: { - from: number - address: string - kind: 'CurveAMO' | 'BalancerMetaStablePool' | 'BalancerComposableStablePool' -}) => { +export const createStrategyProcessor = (strategyData: IStrategyData) => { + const { from, kind } = strategyData const update = blockFrequencyUpdater({ from }) return async (ctx: Context) => { const results = { strategyBalances: [] as StrategyBalance[], } await update(ctx, async (ctx, block) => { - // TODO: Process + if (kind == 'Generic') { + results.strategyBalances.push( + ...(await _getStrategyHoldings(ctx, block, strategyData)) + ) + } else if (kind == 'CurveAMO') { + results.strategyBalances.push( + ...(await _getCurveAMOStrategyHoldings(ctx, block, strategyData)) + ) + } else if (kind == 'BalancerMetaStablePool') { + results.strategyBalances.push( + ...(await _getBalancerStrategyHoldings(ctx, block, strategyData)) + ) + } }) await ctx.store.insert(results.strategyBalances) } } + +const _getStrategyHoldings = async (ctx: Context, block: Block, strategyData: IStrategyData): Promise => { + const { assets, address } = strategyData + const strategy = new abstractStrategyAbi.Contract(ctx, block.header, address) + const promises = assets.map(async asset => { + return new StrategyBalance({ + id: `${address}:${asset}:${block.header.height}`, + strategy: address, + asset: asset, + balance: await strategy.checkBalance(asset), + blockNumber: block.header.height, + timestamp: new Date(block.header.timestamp) + }) + }) + + return await Promise.all(promises) +} + +const _getCurveAMOStrategyHoldings = async (ctx: Context, block: Block, strategyData: IStrategyData): Promise => { + const { assets, address, curvePoolInfo } = strategyData + const { poolAddress, rewardsPoolAddress } = curvePoolInfo! + + const pool = new curvePool.Contract(ctx, block.header, poolAddress) + const rewardsPool = new erc20.Contract(ctx, block.header, rewardsPoolAddress) + const strategy = new abstractStrategyAbi.Contract(ctx, block.header, address) + + const lpPrice = await pool.get_virtual_price() + const stakedLPBalance = await rewardsPool.balanceOf(address) + let unstakedBalance = BigInt(0) + + const poolAssets: string[] = [] + const assetBalances: bigint[] = [] + let totalPoolValue = BigInt(0) + for (let i = 0; i < assets.length; i++) { + const balance = await pool.balances(BigInt(i)) + assetBalances.push(balance) + totalPoolValue += balance + + let coin = (await pool.coins(BigInt(i))).toLowerCase() + if (coin == ETH_ADDRESS) { + // Vault only deals in WETH not ETH + coin = WETH_ADDRESS + } + + if (coin != OETH_ADDRESS) { + const pTokenAddr = await strategy.assetToPToken(assets[i]) + const pToken = new erc20.Contract(ctx, block.header, pTokenAddr) + unstakedBalance += await pToken.balanceOf(address) + } + + poolAssets.push(coin) + } + + const eth1 = BigInt("1000000000000000000") + const totalStrategyLPBalance = (stakedLPBalance + unstakedBalance) * lpPrice / eth1 + + return poolAssets.map((asset, i) => { + const poolAssetSplit = BigInt(10000) * assetBalances[i] / totalPoolValue + const balance = totalStrategyLPBalance * poolAssetSplit / BigInt(10000) + + return new StrategyBalance({ + id: `${address}:${asset}:${block.header.height}`, + strategy: address, + asset, + balance, + blockNumber: block.header.height, + timestamp: new Date(block.header.timestamp) + }) + }) +} + +const _getBalancerStrategyHoldings = async (ctx: Context, block: Block, strategyData: IStrategyData) => { + const { address, balancerPoolInfo } = strategyData + const { poolAddress, poolId } = balancerPoolInfo! + + const rateProviders = await _getBalancePoolRateProviders(ctx, block, poolAddress); + + const strategy = new balancerMetaStablePoolStrategyAbi.Contract(ctx, block.header, address) + const balancerVault = new balancerVaultAbi.Contract(ctx, block.header, BALANCER_VAULT) + let [poolAssets, balances] = await balancerVault.getPoolTokens(poolId) + + const totalStrategyBalance = await strategy['checkBalance()']() // in WETH + const eth1 = BigInt("1000000000000000000") + + let totalPoolValue = BigInt(0) + const assetBalances: bigint[] = [] + const assetRates: bigint[] = [] + for (let i = 0; i < poolAssets.length; i++) { + let tokenBalance = balances[i] // Balance of asset + + if ([ADDRESS_ZERO, WETH_ADDRESS, ETH_ADDRESS].includes(poolAssets[i])) { + poolAssets[i] = WETH_ADDRESS + } + + if (ADDRESS_ZERO == rateProviders[i]) { + assetRates.push(eth1) + } else { + const provider = new balancerRateProvider.Contract(ctx, block.header, rateProviders[i]) + const rate = await provider.getRate() + assetRates.push(rate) + tokenBalance = tokenBalance * rate / eth1 + } + + assetBalances.push(tokenBalance) + totalPoolValue += tokenBalance // Balance of asset in WETH + } + + return poolAssets.map((asset, i) => { + const poolAssetSplit = BigInt(10000) * assetBalances[i] / totalPoolValue + const balance = eth1 * totalStrategyBalance * poolAssetSplit / assetRates[i] / BigInt(10000) + + return new StrategyBalance({ + id: `${address}:${asset}:${block.header.height}`, + strategy: address, + asset, + balance, + blockNumber: block.header.height, + timestamp: new Date(block.header.timestamp) + }) + }) +} + +const _getBalancePoolRateProviders = memoize(async (ctx: Context, block: Block, address: string) => { + const pool = new balancerMetaStablePoolAbi.Contract(ctx, block.header, address) + const rateProviders = await pool.getRateProviders(); + return rateProviders +}, (_ctx, _block, address) => address.toLowerCase()) diff --git a/src/processors/strategies/strategies.ts b/src/processors/strategies/strategies.ts index 57d383e0..32ba41cb 100644 --- a/src/processors/strategies/strategies.ts +++ b/src/processors/strategies/strategies.ts @@ -4,7 +4,9 @@ import { Context } from '../../processor' import { createStrategyProcessor, createStrategySetup, + IStrategyData, } from '../../processor-templates/strategy' +import { FRXETH_ADDRESS, OETH_ADDRESS, RETH_ADDRESS, WETH_ADDRESS } from '../../utils/addresses' // const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'.toLowerCase() // const USDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'.toLowerCase() @@ -65,30 +67,42 @@ import { // }, // ] as const -const oethStrategies = [ +const oethStrategies: readonly IStrategyData[] = [ { from: 18083920, name: 'OETH Convex ETH+OETH (AMO)', address: '0x1827F9eA98E0bf96550b2FC20F7233277FcD7E63'.toLowerCase(), kind: 'CurveAMO', + curvePoolInfo: { + poolAddress: '0x94b17476a93b3262d87b9a326965d1e91f9c13e7', + rewardsPoolAddress: '0x24b65dc1cf053a8d96872c323d29e86ec43eb33a' + }, + assets: [WETH_ADDRESS, OETH_ADDRESS] }, { from: 17513633, name: 'OETH Frax Staking', address: '0x3fF8654D633D4Ea0faE24c52Aec73B4A20D0d0e5'.toLowerCase(), - kind: 'CurveAMO', + kind: 'Generic', + assets: [FRXETH_ADDRESS] }, { from: 17612333, name: 'OETH Morpho Aave V2', address: '0xc1fc9E5eC3058921eA5025D703CBE31764756319'.toLowerCase(), - kind: 'CurveAMO', + kind: 'Generic', + assets: [WETH_ADDRESS] }, { from: 18156225, name: 'OETH Aura rETH/WETH', address: '0x49109629aC1deB03F2e9b2fe2aC4a623E0e7dfDC'.toLowerCase(), kind: 'BalancerMetaStablePool', + assets: [WETH_ADDRESS, RETH_ADDRESS], + balancerPoolInfo: { + poolId: '0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112', + poolAddress: '0x1e19cf2d73a72ef1332c882f20534b6519be0276' + } }, ] as const diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index d455a26b..bd1ecebd 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -1,6 +1,7 @@ // Lowercase Addresses export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' +export const ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' export const OUSD_ADDRESS = '0x2a8e1e676ec238d8a992307b495b45b3feaa5e86' export const OUSD_VAULT_ADDRESS = '0xe75d77b1865ae93c7eaa3040b038d7aa7bc02f70' @@ -41,3 +42,6 @@ export const OETH_DRIPPER_ADDRESS = '0xc0f42f73b8f01849a2dd99753524d4ba14317eb3' export const OETH_STRATEGY_BALANCER_ADDRESS = '0x49109629ac1deb03f2e9b2fe2ac4a623e0e7dfdc' + + +export const BALANCER_VAULT = '0xba12222222228d8ba445958a75a0704d566bf2c8' \ No newline at end of file