From 28593bac8db1ef4a581d8895e734b74d6a7c695c Mon Sep 17 00:00:00 2001 From: Dmitri Tsumak Date: Fri, 11 Oct 2024 15:23:02 +0300 Subject: [PATCH] User rewards (#80) * Fix osToken apy * Add allocator assets * Add allocator ltv and assets * Fix network entity * Remove checkpointcreated handler * Remove blockHandlersStartBlock config * WIP * Fix block number for block handlers * Revert block initialize to Keeper * Refactor apy calculation * Add aggregations, refactor keeper rewards sync * Update husky precommit * Add totalEarnedAssets * Implement reward splitter snapshots * Fix block handlers * Add ltvStatus to the allocator * Add exitRequests and rewardSplitter handlers to keeper * Fix isClaimable check * Add vault to reward splitter shares holder * Revert exit requests handler * Add exchange rates * Add DAI Price feed * Add usdToDaiRate to snapshot * Fix vault snapshot, osTokenHolder assets calc * Fix swap xdai to gno * Remove queued shares --- .husky/pre-commit | 3 - README.md | 11 +- package-lock.json | 4 +- package.json | 18 +- scripts/deploy.js | 58 +--- src/abis/PriceFeed.json | 509 ++++++++++++++++++++++++++++ src/config/chiado.json | 6 + src/config/gnosis.json | 10 +- src/config/holesky.json | 6 + src/config/mainnet.json | 6 + src/entities/allocator.ts | 188 +++++++++- src/entities/apySnapshots.ts | 193 ----------- src/entities/exitRequests.ts | 153 +++++++++ src/entities/network.ts | 72 +++- src/entities/osToken.ts | 118 ++++++- src/entities/rewardSplitter.ts | 130 ++++++- src/entities/tokenTransfer.ts | 37 +- src/entities/v2pool.ts | 128 ++++++- src/entities/vaults.ts | 176 +++++++--- src/helpers/utils.ts | 154 +-------- src/mappings/depositDataRegistry.ts | 8 - src/mappings/erc20Token.ts | 97 ++++++ src/mappings/erc20Vault.ts | 57 +++- src/mappings/exchangeRates.ts | 73 ++++ src/mappings/gnoVault.ts | 49 ++- src/mappings/keeper.ts | 184 ++++++++-- src/mappings/mevEscrow.ts | 19 -- src/mappings/osToken.ts | 56 +-- src/mappings/osTokenConfig.ts | 39 ++- src/mappings/rewardSplitter.ts | 45 ++- src/mappings/swiseToken.ts | 20 -- src/mappings/v2pool.ts | 127 +++++-- src/mappings/vault.ts | 410 ++++++++++++---------- src/schema.graphql | 425 +++++++++++++++-------- src/subgraph.template.yaml | 160 ++++++--- 35 files changed, 2698 insertions(+), 1051 deletions(-) create mode 100644 src/abis/PriceFeed.json delete mode 100644 src/entities/apySnapshots.ts create mode 100644 src/entities/exitRequests.ts create mode 100644 src/mappings/erc20Token.ts create mode 100644 src/mappings/exchangeRates.ts delete mode 100644 src/mappings/mevEscrow.ts delete mode 100644 src/mappings/swiseToken.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219..2312dc5 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/README.md b/README.md index e2c5dc4..74f17cb 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ and entities within the StakeWise ecosystem. ```shell script npm install ``` + 2. Build the subgraph to check compile errors before deploying: @@ -20,10 +21,16 @@ and entities within the StakeWise ecosystem. npm run build:mainnet ``` -3. Deploy subgraph to your graph node +3. Deploy subgraph to your stage environment: + + ```shell script + IPFS_URL= GRAPH_URL= npm run deploy-stage:mainnet + ``` + +4. Deploy subgraph to your prod environment: ```shell script - IPFS_URL= LOCAL_GRAPH_URL= npm run deploy-local:mainnet + IPFS_URL= GRAPH_URL= npm run deploy-prod:mainnet ``` ## Documentation diff --git a/package-lock.json b/package-lock.json index d70c1e7..4fb187d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "v3-subgraph", - "version": "2.0.6", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "v3-subgraph", - "version": "2.0.6", + "version": "3.0.0", "license": "AGPL-3.0-only", "devDependencies": { "@graphprotocol/graph-cli": "0.81.0", diff --git a/package.json b/package.json index 05d94fc..53430e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "v3-subgraph", - "version": "2.0.6", + "version": "3.0.0", "description": "Subgraph for the StakeWise Protocol", "repository": "https://github.com/stakewise/v3-subgraph", "license": "AGPL-3.0-only", @@ -19,8 +19,8 @@ "prepare:holesky": "npm run generate-const:holesky && npm run generate-yaml:holesky && npm run generate-types:holesky", "build:holesky": "npm run prepare:holesky && npm run generate-wasm:holesky", "test:holesky": "npm run prepare:holesky && npm run generate-tests-yaml:holesky && graph test", - "deploy-hosted:holesky": "npm run prepare:holesky && npm run deploy network:holesky node:hosted", - "deploy-local:holesky": "npm run prepare:holesky && npm run deploy network:holesky node:local", + "deploy-stage:holesky": "npm run prepare:holesky && npm run deploy network:holesky env:stage", + "deploy-prod:holesky": "npm run prepare:holesky && npm run deploy network:holesky env:prod", "---MAINNET---": "", "generate-const:mainnet": "node ./scripts/createConstants.js mainnet", "generate-yaml:mainnet": "mustache src/config/mainnet.json src/subgraph.template.yaml > src/subgraph-mainnet.yaml", @@ -30,8 +30,8 @@ "prepare:mainnet": "npm run generate-const:mainnet && npm run generate-yaml:mainnet && npm run generate-types:mainnet", "build:mainnet": "npm run prepare:mainnet && npm run generate-wasm:mainnet", "test:mainnet": "npm run prepare:mainnet && npm run generate-tests-yaml:mainnet && graph test", - "deploy-hosted:mainnet": "npm run prepare:mainnet && npm run deploy network:mainnet node:hosted", - "deploy-local:mainnet": "npm run prepare:mainnet && npm run deploy network:mainnet node:local", + "deploy-stage:mainnet": "npm run prepare:mainnet && npm run deploy network:mainnet env:stage", + "deploy-prod:mainnet": "npm run prepare:mainnet && npm run deploy network:mainnet env:prod", "---CHIADO---": "", "generate-const:chiado": "node ./scripts/createConstants.js chiado", "generate-yaml:chiado": "mustache src/config/chiado.json src/subgraph.template.yaml > src/subgraph-chiado.yaml", @@ -41,8 +41,8 @@ "prepare:chiado": "npm run generate-const:chiado && npm run generate-yaml:chiado && npm run generate-types:chiado", "build:chiado": "npm run prepare:chiado && npm run generate-wasm:chiado", "test:chiado": "npm run prepare:chiado && npm run generate-tests-yaml:chiado && graph test", - "deploy-hosted:chiado": "npm run prepare:chiado && npm run deploy network:chiado node:hosted", - "deploy-local:chiado": "npm run prepare:chiado && npm run deploy network:chiado node:local", + "deploy-stage:chiado": "npm run prepare:chiado && npm run deploy network:chiado env:stage", + "deploy-prod:chiado": "npm run prepare:chiado && npm run deploy network:chiado env:prod", "---GNOSIS---": "", "generate-const:gnosis": "node ./scripts/createConstants.js gnosis", "generate-yaml:gnosis": "mustache src/config/gnosis.json src/subgraph.template.yaml > src/subgraph-gnosis.yaml", @@ -52,8 +52,8 @@ "prepare:gnosis": "npm run generate-const:gnosis && npm run generate-yaml:gnosis && npm run generate-types:gnosis", "build:gnosis": "npm run prepare:gnosis && npm run generate-wasm:gnosis", "test:gnosis": "npm run prepare:gnosis && npm run generate-tests-yaml:gnosis && graph test", - "deploy-hosted:gnosis": "npm run prepare:gnosis && npm run deploy network:gnosis node:hosted", - "deploy-local:gnosis": "npm run prepare:gnosis && npm run deploy network:gnosis node:local" + "deploy-stage:gnosis": "npm run prepare:gnosis && npm run deploy network:gnosis env:stage", + "deploy-prod:gnosis": "npm run prepare:gnosis && npm run deploy network:gnosis env:prod" }, "devDependencies": { "@graphprotocol/graph-cli": "0.81.0", diff --git a/scripts/deploy.js b/scripts/deploy.js index 95ecacb..f3f79d3 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -3,13 +3,8 @@ const { execAsync } = require('./util') require('dotenv').config() -// LOCAL const IPFS_URL = process.env.IPFS_URL -const LOCAL_GRAPH_URL = process.env.LOCAL_GRAPH_URL - -// HOSTED -const HOSTED_GRAPH_TOKEN = process.env.HOSTED_GRAPH_TOKEN -const HOSTED_SUBGRAPH_URL = process.env.HOSTED_SUBGRAPH_URL +const GRAPH_URL = process.env.GRAPH_URL const args = process.argv.reduce((acc, arg) => { if (/:/.test(arg)) { @@ -22,66 +17,45 @@ const args = process.argv.reduce((acc, arg) => { }, {}) const validateEnv = () => { - if (args.node === 'hosted') { - if (!HOSTED_GRAPH_TOKEN) { - throw new Error('HOSTED_GRAPH_TOKEN is required env variable for "node:hosted" deployment') - } - if (!HOSTED_SUBGRAPH_URL) { - throw new Error('HOSTED_SUBGRAPH_URL is required env variable for "node:hosted" deployment') - } + if (!GRAPH_URL) { + throw new Error('GRAPH_URL is required env variable') } - if (args.node === 'local') { - if (!LOCAL_GRAPH_URL) { - throw new Error('LOCAL_GRAPH_URL is required env variable for "node:local" deployment') - } - if (!IPFS_URL) { - throw new Error('IPFS_URL is required env variable for "node:local" deployment') - } + if (!IPFS_URL) { + throw new Error('IPFS_URL is required env variable') } } const validateArgs = () => { - const { network, node } = args + const { network, env } = args const allowedNetworks = ['holesky', 'mainnet', 'chiado', 'gnosis'] - const allowedNodes = ['hosted', 'local'] + const allowedEnvs = ['prod', 'stage'] if (!network) { throw new Error('Argument "network" is required') } - if (!node) { - throw new Error('Argument "node" is required') + if (!env) { + throw new Error('Argument "env" is required') } if (!allowedNetworks.includes(network)) { throw new Error(`Argument "network" must include one of: ${allowedNetworks.join(', ')}`) } - if (!allowedNodes.includes(node)) { - throw new Error(`Argument "node" must include one of: ${allowedNodes.join(', ')}`) + if (!allowedEnvs.includes(env)) { + throw new Error(`Argument "env" must include one of: ${allowedEnvs.join(', ')}`) } } const deploy = async () => { - const { network, node } = args + const { network, env } = args const srcDirectory = path.resolve(__dirname, `../src`) - const buildDirectory = path.resolve(__dirname, `../build/${network}`) - - let authCommand = '' - let deployCommand = '' - if (node === 'hosted') { - authCommand = `graph auth --product hosted-service ${HOSTED_GRAPH_TOKEN}` - deployCommand = `graph deploy --product hosted-service ${HOSTED_SUBGRAPH_URL} --output-dir ${buildDirectory} --access-token ${HOSTED_GRAPH_TOKEN}` - } - if (node === 'local') { - const { version } = require('../package.json') - - authCommand = `graph create --node ${LOCAL_GRAPH_URL} stakewise/stakewise` - deployCommand = `graph deploy --version-label ${version} --node ${LOCAL_GRAPH_URL} --ipfs ${IPFS_URL} stakewise/stakewise` - } + const { version } = require('../package.json') + const createCommand = `graph create --node ${GRAPH_URL} stakewise/${env}` + const deployCommand = `graph deploy --version-label ${version} --node ${GRAPH_URL} --ipfs ${IPFS_URL} stakewise/${env}` const command = [ - authCommand, + createCommand, `cd ${srcDirectory}`, `cp subgraph-${network}.yaml subgraph.yaml`, deployCommand, diff --git a/src/abis/PriceFeed.json b/src/abis/PriceFeed.json new file mode 100644 index 0000000..62cc01b --- /dev/null +++ b/src/abis/PriceFeed.json @@ -0,0 +1,509 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_aggregator", + "type": "address" + }, + { + "internalType": "address", + "name": "_accessController", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "int256", + "name": "current", + "type": "int256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "roundId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + } + ], + "name": "AnswerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "roundId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "startedBy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + } + ], + "name": "NewRound", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessController", + "outputs": [ + { + "internalType": "contract AccessControllerInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "aggregator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_aggregator", + "type": "address" + } + ], + "name": "confirmAggregator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "description", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_roundId", + "type": "uint256" + } + ], + "name": "getAnswer", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint80", + "name": "_roundId", + "type": "uint80" + } + ], + "name": "getRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_roundId", + "type": "uint256" + } + ], + "name": "getTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestAnswer", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestRound", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "latestTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "name": "phaseAggregators", + "outputs": [ + { + "internalType": "contract AggregatorV2V3Interface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "phaseId", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_aggregator", + "type": "address" + } + ], + "name": "proposeAggregator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAggregator", + "outputs": [ + { + "internalType": "contract AggregatorV2V3Interface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint80", + "name": "_roundId", + "type": "uint80" + } + ], + "name": "proposedGetRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proposedLatestRoundData", + "outputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "int256", + "name": "answer", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "startedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_accessController", + "type": "address" + } + ], + "name": "setController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/src/config/chiado.json b/src/config/chiado.json index 7cae358..3926d46 100644 --- a/src/config/chiado.json +++ b/src/config/chiado.json @@ -1,5 +1,6 @@ { "network": "chiado", + "pollingBlocksInterval": "700", "wad": "1000000000000000000", "zeroAddress": "0x0000000000000000000000000000000000000000", "v2PoolFeePercent": "1500", @@ -23,6 +24,11 @@ "rewardSplitterFactoryV2": "0x63De511Ff504E70109Bb8312d1329f2C88c14f77", "foxVault1": "0x0000000000000000000000000000000000000000", "foxVault2": "0x0000000000000000000000000000000000000000", + "multicall": "0xcA11bde05977b3631167028862bE2a173976CA11", + "assetsUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "daiUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "eurUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "gbpUsdPriceFeed": "0x0000000000000000000000000000000000000000", "vestingEscrowFactory": { "address": "0x0000000000000000000000000000000000000000", "startBlock": "10627588" diff --git a/src/config/gnosis.json b/src/config/gnosis.json index 2503439..ca5ace1 100644 --- a/src/config/gnosis.json +++ b/src/config/gnosis.json @@ -1,5 +1,6 @@ { "network": "gnosis", + "pollingBlocksInterval": "700", "wad": "1000000000000000000", "zeroAddress": "0x0000000000000000000000000000000000000000", "v2PoolFeePercent": "1500", @@ -23,6 +24,11 @@ "rewardSplitterFactoryV2": "0x4c6306BA1821D88803e27A115433520F2d6276Fb", "foxVault1": "0x0000000000000000000000000000000000000000", "foxVault2": "0x0000000000000000000000000000000000000000", + "multicall": "0xcA11bde05977b3631167028862bE2a173976CA11", + "assetsUsdPriceFeed": "0x22441d81416430A54336aB28765abd31a792Ad37", + "daiUsdPriceFeed": "0x678df3415fc31947dA4324eC63212874be5a82f8", + "eurUsdPriceFeed": "0xab70BCB260073d036d1660201e9d5405F5829b7a", + "gbpUsdPriceFeed": "0x0000000000000000000000000000000000000000", "vestingEscrowFactory": { "address": "0x0000000000000000000000000000000000000000", "startBlock": "10627588" @@ -69,10 +75,10 @@ }, "eigenDelegationManager": { "address": "0x0000000000000000000000000000000000000000", - "startBlock": "10626869" + "startBlock": "34778574" }, "eigenPodManager": { "address": "0x0000000000000000000000000000000000000000", - "startBlock": "10626869" + "startBlock": "34778574" } } diff --git a/src/config/holesky.json b/src/config/holesky.json index 83a8524..a790cc8 100644 --- a/src/config/holesky.json +++ b/src/config/holesky.json @@ -1,5 +1,6 @@ { "network": "holesky", + "pollingBlocksInterval": "300", "wad": "1000000000000000000", "zeroAddress": "0x0000000000000000000000000000000000000000", "v2PoolFeePercent": "1000", @@ -21,8 +22,13 @@ "restakeErc20VaultFactoryV2": "0xdB79701D6a4d6476Bfe2d59Afb0d675F97A6f67D", "restakePrivErc20VaultFactoryV2": "0x470e61817bE4d064aCd9422aB6BfC23D5101F84E", "restakeBlocklistErc20VaultFactoryV2": "0xcCE89aB06221c533190E0001F6ad1BAD58888DC2", + "multicall": "0xcA11bde05977b3631167028862bE2a173976CA11", "foxVault1": "0x3c4ae629bf823475192124E02b9879D3C1fd4538", "foxVault2": "0x37Bf0883c27365CffCd0C4202918df930989891f", + "assetsUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "daiUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "eurUsdPriceFeed": "0x0000000000000000000000000000000000000000", + "gbpUsdPriceFeed": "0x0000000000000000000000000000000000000000", "vestingEscrowFactory": { "address": "0x0000000000000000000000000000000000000000", "startBlock": "215379" diff --git a/src/config/mainnet.json b/src/config/mainnet.json index c6333b9..f50e8d9 100644 --- a/src/config/mainnet.json +++ b/src/config/mainnet.json @@ -1,5 +1,6 @@ { "network": "mainnet", + "pollingBlocksInterval": "300", "wad": "1000000000000000000", "zeroAddress": "0x0000000000000000000000000000000000000000", "v2PoolFeePercent": "1000", @@ -23,6 +24,11 @@ "rewardSplitterFactoryV2": "0x256aF27ce81282A0491A5361172c1Db08f6cC5F8", "foxVault1": "0x4FEF9D741011476750A243aC70b9789a63dd47Df", "foxVault2": "0x0000000000000000000000000000000000000000", + "multicall": "0xcA11bde05977b3631167028862bE2a173976CA11", + "assetsUsdPriceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "daiUsdPriceFeed": "0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9", + "eurUsdPriceFeed": "0xb49f677943BC038e9857d61E7d053CaA2C1734C1", + "gbpUsdPriceFeed": "0x5c0Ab2d9b5a7ed9f470386e82BB36A3613cDd4b5", "vestingEscrowFactory": { "address": "0x7B910cc3D4B42FEFF056218bD56d7700E4ea7dD5", "startBlock": "12271842" diff --git a/src/entities/allocator.ts b/src/entities/allocator.ts index 723fd78..4f605f4 100644 --- a/src/entities/allocator.ts +++ b/src/entities/allocator.ts @@ -1,6 +1,51 @@ -import { Address, BigInt, ethereum, log } from '@graphprotocol/graph-ts' +import { Address, BigDecimal, BigInt, Bytes, ethereum, log } from '@graphprotocol/graph-ts' +import { Allocator, AllocatorAction, AllocatorSnapshot, OsToken, OsTokenConfig, Vault } from '../../generated/schema' +import { Vault as VaultContract } from '../../generated/Keeper/Vault' +import { WAD } from '../helpers/constants' +import { convertOsTokenSharesToAssets } from './osToken' +import { createOrLoadNetwork } from './network' +import { createOrLoadOsTokenConfig } from './osTokenConfig' -import { Allocator, AllocatorAction } from '../../generated/schema' +const osTokenPositionsSelector = '0x4ec96b22' + +export enum LtvStatus { + Healthy, + Moderate, + Risky, + Unhealthy, +} + +const LtvStatusStrings = ['Healthy', 'Moderate', 'Risky', 'Unhealthy'] + +export enum AllocatorActionType { + VaultCreated, + Deposited, + Migrated, + Redeemed, + TransferIn, + TransferOut, + ExitQueueEntered, + ExitedAssetsClaimed, + OsTokenMinted, + OsTokenBurned, + OsTokenLiquidated, + OsTokenRedeemed, +} + +const AllocatorActionTypeStrings = [ + 'VaultCreated', + 'Deposited', + 'Migrated', + 'Redeemed', + 'TransferIn', + 'TransferOut', + 'ExitQueueEntered', + 'ExitedAssetsClaimed', + 'OsTokenMinted', + 'OsTokenBurned', + 'OsTokenLiquidated', + 'OsTokenRedeemed', +] export function createOrLoadAllocator(allocatorAddress: Address, vaultAddress: Address): Allocator { const vaultAllocatorAddress = `${vaultAddress.toHex()}-${allocatorAddress.toHex()}` @@ -10,8 +55,13 @@ export function createOrLoadAllocator(allocatorAddress: Address, vaultAddress: A if (vaultAllocator === null) { vaultAllocator = new Allocator(vaultAllocatorAddress) vaultAllocator.shares = BigInt.zero() + vaultAllocator.assets = BigInt.zero() + vaultAllocator.mintedOsTokenShares = BigInt.zero() + vaultAllocator.ltv = BigDecimal.zero() + vaultAllocator.ltvStatus = LtvStatusStrings[LtvStatus.Healthy] vaultAllocator.address = allocatorAddress vaultAllocator.vault = vaultAddress.toHex() + vaultAllocator.osTokenMintApy = BigDecimal.zero() vaultAllocator.save() } @@ -21,22 +71,150 @@ export function createOrLoadAllocator(allocatorAddress: Address, vaultAddress: A export function createAllocatorAction( event: ethereum.Event, vaultAddress: Address, - actionType: string, + actionType: AllocatorActionType, owner: Address, assets: BigInt | null, shares: BigInt | null, ): void { + const allocatorActionString = AllocatorActionTypeStrings[actionType] if (assets === null && shares === null) { - log.error('[AllocatorAction] Both assets and shares cannot be null for action={}', [actionType]) + log.error('[AllocatorAction] Both assets and shares cannot be null for action={}', [allocatorActionString]) return } const txHash = event.transaction.hash.toHex() const allocatorAction = new AllocatorAction(`${txHash}-${event.transactionLogIndex.toString()}`) allocatorAction.vault = vaultAddress.toHex() allocatorAction.address = owner - allocatorAction.actionType = actionType + allocatorAction.actionType = allocatorActionString allocatorAction.assets = assets allocatorAction.shares = shares allocatorAction.createdAt = event.block.timestamp allocatorAction.save() } + +export function getAllocatorLtvStatus(allocator: Allocator, osTokenConfig: OsTokenConfig): string { + const disabledLiqThresholdPercent = BigInt.fromI32(2).pow(64).minus(BigInt.fromI32(1)) + if (osTokenConfig.liqThresholdPercent.equals(disabledLiqThresholdPercent)) { + return LtvStatusStrings[LtvStatus.Healthy] + } + const ltv = allocator.ltv + const step = new BigDecimal(osTokenConfig.liqThresholdPercent.minus(osTokenConfig.ltvPercent)) + .div(BigDecimal.fromString('3')) + .div(BigDecimal.fromString(WAD)) + const healthyLimit = new BigDecimal(osTokenConfig.ltvPercent).div(BigDecimal.fromString(WAD)).plus(step) + const moderateLimit = healthyLimit.plus(step) + const riskyLimit = moderateLimit.plus(step) + if (ltv.le(healthyLimit)) { + return LtvStatusStrings[LtvStatus.Healthy] + } else if (ltv.le(moderateLimit)) { + return LtvStatusStrings[LtvStatus.Moderate] + } else if (ltv.le(riskyLimit)) { + return LtvStatusStrings[LtvStatus.Risky] + } + return LtvStatusStrings[LtvStatus.Unhealthy] +} + +export function getAllocatorsMintedShares(vault: Vault, allocators: Allocator[]): Array { + if (!vault.isOsTokenEnabled) { + let response = new Array(allocators.length) + for (let i = 0; i < allocators.length; i++) { + response[i] = BigInt.zero() + } + return response + } + + const vaultAddress = Address.fromString(vault.id) + const vaultContract = VaultContract.bind(vaultAddress) + + let calls: Array = [] + for (let i = 0; i < allocators.length; i++) { + calls.push(_getOsTokenPositionsCall(allocators[i])) + } + + const result = vaultContract.multicall(calls) + const mintedShares: Array = [] + for (let i = 0; i < allocators.length; i++) { + mintedShares.push(ethereum.decode('uint256', result[i])!.toBigInt()) + } + return mintedShares +} + +export function getAllocatorLtv(allocator: Allocator, osToken: OsToken): BigDecimal { + if (allocator.assets.isZero()) { + return BigDecimal.zero() + } + const mintedOsTokenAssets = convertOsTokenSharesToAssets(osToken, allocator.mintedOsTokenShares) + return new BigDecimal(mintedOsTokenAssets).div(new BigDecimal(allocator.assets)) +} + +export function getAllocatorOsTokenMintApy( + allocator: Allocator, + osTokenApy: BigDecimal, + osToken: OsToken, + osTokenConfig: OsTokenConfig, +): BigDecimal { + if (allocator.assets.isZero() || osTokenConfig.ltvPercent.isZero()) { + return BigDecimal.zero() + } + const mintedOsTokenAssets = convertOsTokenSharesToAssets(osToken, allocator.mintedOsTokenShares) + if (mintedOsTokenAssets.isZero()) { + return BigDecimal.zero() + } + + const feePercent = new BigDecimal(BigInt.fromI32(osToken.feePercent)) + const maxPercent = new BigDecimal(BigInt.fromI32(10000)) + const maxOsTokenMintApy = osTokenApy + .times(feePercent) + .times(BigDecimal.fromString(WAD)) + .div(maxPercent.minus(feePercent)) + .div(new BigDecimal(osTokenConfig.ltvPercent)) + return maxOsTokenMintApy.times(new BigDecimal(mintedOsTokenAssets)).div(new BigDecimal(allocator.assets)) +} + +export function updateAllocatorsLtvStatus(): void { + const network = createOrLoadNetwork() + let vault: Vault + let osTokenConfig: OsTokenConfig + let allocators: Array + for (let i = 0; i < network.vaultIds.length; i++) { + vault = Vault.load(network.vaultIds[i]) as Vault + osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + allocators = vault.allocators.load() + for (let j = 0; j < allocators.length; j++) { + const allocator = allocators[j] + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.save() + } + } +} + +export function snapshotAllocator( + allocator: Allocator, + osToken: OsToken, + osTokenConfig: OsTokenConfig, + assetsDiff: BigInt, + osTokenMintedSharesDiff: BigInt, + rewardsTimestamp: BigInt, +): void { + let osTokenAssetsDiff: BigInt + if (osTokenConfig.ltvPercent.isZero()) { + osTokenAssetsDiff = BigInt.zero() + } else { + osTokenAssetsDiff = convertOsTokenSharesToAssets(osToken, osTokenMintedSharesDiff) + .times(BigInt.fromString(WAD)) + .div(osTokenConfig.ltvPercent) + } + + const allocatorSnapshot = new AllocatorSnapshot(rewardsTimestamp.toString()) + allocatorSnapshot.timestamp = rewardsTimestamp.toI64() + allocatorSnapshot.allocator = allocator.id + allocatorSnapshot.earnedAssets = assetsDiff.minus(osTokenAssetsDiff) + allocatorSnapshot.totalAssets = allocator.assets + allocatorSnapshot.ltv = allocator.ltv + allocatorSnapshot.save() +} + +function _getOsTokenPositionsCall(allocator: Allocator): Bytes { + const encodedArgs = ethereum.encode(ethereum.Value.fromAddress(Address.fromBytes(allocator.address))) + return Bytes.fromHexString(osTokenPositionsSelector).concat(encodedArgs as Bytes) +} diff --git a/src/entities/apySnapshots.ts b/src/entities/apySnapshots.ts deleted file mode 100644 index 140746b..0000000 --- a/src/entities/apySnapshots.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { BigDecimal, BigInt } from '@graphprotocol/graph-ts' -import { OsToken, OsTokenSnapshot, V2Pool, Vault, VaultApySnapshot } from '../../generated/schema' -import { WAD } from '../helpers/constants' - -const snapshotsPerWeek = 14 -const secondsInYear = '31536000' -const maxPercent = '100' - -function _calculateMedian(values: Array): BigDecimal { - if (values.length === 0) { - return BigDecimal.fromString('0') - } - - // Sort the values - const sortedValues = values.sort((a, b) => (a.lt(b) ? -1 : a.gt(b) ? 1 : 0)) - const mid = sortedValues.length / 2 - - if (sortedValues.length % 2 !== 0) { - // For odd number of elements, directly access the middle element - return sortedValues[(mid - 0.5) as i32] // Adjusting for 0-based index - } else { - // For even number of elements, calculate the average of the two middle elements - const lowerMidIndex = mid - 1 - return sortedValues[lowerMidIndex as i32].plus(sortedValues[mid as i32]).div(BigDecimal.fromString('2')) - } -} - -function _calculateAverage(values: Array): BigDecimal { - if (values.length === 0) { - return BigDecimal.fromString('0') - } - - // Start with a sum of zero. - let sum: BigDecimal = BigDecimal.fromString('0') - - // Iterate over all values to calculate the sum. - for (let i = 0; i < values.length; i++) { - sum = sum.plus(values[i]) - } - - // Divide the sum by the number of values to get the average. - // Note: BigDecimal division needs to handle scale/precision appropriately. - // Here, 'values.length' is converted to a BigDecimal for division. - return sum.div(BigDecimal.fromString(values.length.toString())) -} - -export function updateVaultApy( - vault: Vault, - fromTimestamp: BigInt | null, - toTimestamp: BigInt, - rateChange: BigInt, -): void { - if (fromTimestamp === null) { - // it's the first update, skip - return - } - const totalDuration = toTimestamp.minus(fromTimestamp) - const currentApy = BigDecimal.fromString(rateChange.toString()) - .times(BigDecimal.fromString(secondsInYear)) - .times(BigDecimal.fromString(maxPercent)) - .div(BigDecimal.fromString(WAD)) - .div(BigDecimal.fromString(totalDuration.toString())) - - // TODO: remove after deprecation period - const currentExecApy = currentApy.div(BigDecimal.fromString('2')) - const currentConsensusApy = currentApy.minus(currentExecApy) - - // calculate weekly apy - let executionApys: Array = [currentExecApy] - let consensusApys: Array = [currentConsensusApy] - const totalSnapshots = vault.apySnapshotsCount - for (let i = 1; i < snapshotsPerWeek; i++) { - const snapshotId = `${vault.id}-${totalSnapshots.minus(BigInt.fromI32(i))}` - const snapshot = VaultApySnapshot.load(snapshotId) - if (snapshot === null) { - break - } - executionApys.push(snapshot.executionApy) - consensusApys.push(snapshot.consensusApy) - } - - const snapshotId = `${vault.id}-${totalSnapshots}` - const vaultApySnapshot = new VaultApySnapshot(snapshotId) - vaultApySnapshot.apy = currentExecApy.plus(currentConsensusApy) - vaultApySnapshot.executionApy = currentExecApy - vaultApySnapshot.consensusApy = currentConsensusApy - vaultApySnapshot.fromEpochTimestamp = fromTimestamp - vaultApySnapshot.toEpochTimestamp = toTimestamp - vaultApySnapshot.vault = vault.id - vaultApySnapshot.save() - - vault.executionApy = _calculateAverage(executionApys) - vault.consensusApy = _calculateAverage(consensusApys) - vault.apy = vault.executionApy.plus(vault.consensusApy) - vault.weeklyApy = vault.apy - vault.medianExecutionApy = _calculateMedian(executionApys) - vault.medianConsensusApy = _calculateMedian(consensusApys) - vault.medianApy = vault.medianExecutionApy.plus(vault.medianConsensusApy) - vault.apySnapshotsCount = vault.apySnapshotsCount.plus(BigInt.fromI32(1)) -} - -export function updatePoolApy( - pool: V2Pool, - fromTimestamp: BigInt | null, - toTimestamp: BigInt, - rateChange: BigInt, -): void { - if (fromTimestamp === null) { - // it's the first update, skip - return - } - const totalDuration = toTimestamp.minus(fromTimestamp) - const currentApy = BigDecimal.fromString(rateChange.toString()) - .times(BigDecimal.fromString(secondsInYear)) - .times(BigDecimal.fromString(maxPercent)) - .div(BigDecimal.fromString(WAD)) - .div(BigDecimal.fromString(totalDuration.toString())) - - // TODO: remove after deprecation period - const currentExecApy = currentApy.div(BigDecimal.fromString('2')) - const currentConsensusApy = currentApy.minus(currentExecApy) - - // calculate weekly apy - let execApys: Array = [currentExecApy] - let consensusApys: Array = [currentConsensusApy] - const totalSnapshots = pool.apySnapshotsCount - for (let i = 1; i < snapshotsPerWeek; i++) { - const snapshotId = `${pool.id}-${totalSnapshots.minus(BigInt.fromI32(i))}` - const snapshot = VaultApySnapshot.load(snapshotId) - if (snapshot === null) { - break - } - execApys.push(snapshot.executionApy) - consensusApys.push(snapshot.consensusApy) - } - - const snapshotId = `${pool.id}-${totalSnapshots}` - const poolApySnapshot = new VaultApySnapshot(snapshotId) - poolApySnapshot.apy = currentExecApy.plus(currentConsensusApy) - poolApySnapshot.executionApy = currentExecApy - poolApySnapshot.consensusApy = currentConsensusApy - poolApySnapshot.fromEpochTimestamp = fromTimestamp - poolApySnapshot.toEpochTimestamp = toTimestamp - poolApySnapshot.save() - - pool.executionApy = _calculateAverage(execApys) - pool.consensusApy = _calculateAverage(consensusApys) - pool.apy = pool.executionApy.plus(pool.consensusApy) - pool.weeklyApy = pool.apy - pool.apySnapshotsCount = pool.apySnapshotsCount.plus(BigInt.fromI32(1)) -} - -export function updateOsTokenApy(osToken: OsToken, newAvgRewardPerSecond: BigInt, timestamp: BigInt): void { - // calculate borrow reward per second - const borrowRewardPerSecond = newAvgRewardPerSecond - .times(BigInt.fromI32(osToken.feePercent)) - .div(BigInt.fromString('10000')) - - // create new snapshot - const totalSnapshots = osToken.snapshotsCount - const snapshot = new OsTokenSnapshot(totalSnapshots.toString()) - snapshot.avgRewardPerSecond = newAvgRewardPerSecond - snapshot.borrowRewardPerSecond = borrowRewardPerSecond - snapshot.createdAt = timestamp - snapshot.save() - - let rewardPerSecondSum = newAvgRewardPerSecond - let borrowRewardPerSecondSum = borrowRewardPerSecond - let snapshotsCounter = 1 - - for (let i = 1; i < snapshotsPerWeek; i++) { - const snapshot = OsTokenSnapshot.load(osToken.snapshotsCount.minus(BigInt.fromI32(i)).toString()) - if (snapshot === null) { - break - } - - rewardPerSecondSum = rewardPerSecondSum.plus(snapshot.avgRewardPerSecond) - borrowRewardPerSecondSum = borrowRewardPerSecondSum.plus(snapshot.borrowRewardPerSecond) - snapshotsCounter += 1 - } - - osToken.snapshotsCount = osToken.snapshotsCount.plus(BigInt.fromI32(1)) - osToken.apy = BigDecimal.fromString(rewardPerSecondSum.toString()) - .times(BigDecimal.fromString(secondsInYear)) - .times(BigDecimal.fromString(maxPercent)) - .div(BigDecimal.fromString(snapshotsCounter.toString())) - .div(BigDecimal.fromString(WAD)) - osToken.borrowApy = BigDecimal.fromString(borrowRewardPerSecondSum.toString()) - .times(BigDecimal.fromString(secondsInYear)) - .times(BigDecimal.fromString(maxPercent)) - .div(BigDecimal.fromString(snapshotsCounter.toString())) - .div(BigDecimal.fromString(WAD)) -} diff --git a/src/entities/exitRequests.ts b/src/entities/exitRequests.ts new file mode 100644 index 0000000..7fac858 --- /dev/null +++ b/src/entities/exitRequests.ts @@ -0,0 +1,153 @@ +import { ExitRequestSnapshot, ExitRequest, Vault } from '../../generated/schema' +import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { Vault as VaultContract } from '../../generated/Keeper/Vault' +import { convertSharesToAssets, getUpdateStateCall } from './vaults' +import { GENESIS_VAULT } from '../helpers/constants' +import { createOrLoadV2Pool } from './v2pool' + +const secondsInDay = '86400' +const getExitQueueIndexSelector = '0x60d60e6e' +const calculateExitedAssetsSelector = '0x76b58b90' + +export function updateExitRequests(vault: Vault, block: ethereum.Block): void { + if (vault.rewardsTimestamp === null) { + return + } + if (Address.fromString(vault.id).equals(GENESIS_VAULT)) { + const v2Pool = createOrLoadV2Pool() + if (!v2Pool.migrated) { + // wait for the migration + return + } + } + + const vaultAddress = Address.fromString(vault.id) + const vaultContract = VaultContract.bind(vaultAddress) + const exitRequests: Array = vault.exitRequests.load() + let updateStateCall: Bytes | null = null + if ( + vault.rewardsRoot !== null && + vault.proofReward !== null && + vault.proofUnlockedMevReward !== null && + vault.proof !== null && + vault.proof!.length > 0 + ) { + updateStateCall = getUpdateStateCall( + vault.rewardsRoot as Bytes, + vault.proofReward as BigInt, + vault.proofUnlockedMevReward as BigInt, + (vault.proof as Array).map((p: string) => Bytes.fromHexString(p)), + ) + } + + let calls: Array = [] + if (updateStateCall !== null) { + calls.push(updateStateCall) + } + let exitRequest: ExitRequest + const pendingExitRequests: Array = [] + for (let i = 0; i < exitRequests.length; i++) { + exitRequest = exitRequests[i] + if (!exitRequest.isClaimed) { + pendingExitRequests.push(exitRequest) + calls.push(getExitQueueIndexCall(exitRequest.positionTicket)) + } + } + + let result = vaultContract.multicall(calls) + if (updateStateCall !== null) { + // remove first call result + result = result.slice(1) + } + + for (let i = 0; i < result.length; i++) { + const index = ethereum.decode('int256', result[i])!.toBigInt() + exitRequest = pendingExitRequests[i] + if (index.lt(BigInt.zero())) { + exitRequest.exitQueueIndex = null + } else { + exitRequest.exitQueueIndex = index + } + } + + calls = [] + if (updateStateCall !== null) { + calls.push(updateStateCall) + } + const maxUint255 = BigInt.fromI32(2).pow(255).minus(BigInt.fromI32(1)) + for (let i = 0; i < exitRequests.length; i++) { + exitRequest = exitRequests[i] + const exitQueueIndex = exitRequest.exitQueueIndex !== null ? (exitRequest.exitQueueIndex as BigInt) : maxUint255 + calls.push( + getCalculateExitedAssetsCall( + Address.fromBytes(exitRequest.receiver), + exitRequest.positionTicket, + exitRequest.timestamp, + exitQueueIndex, + ), + ) + } + + result = vaultContract.multicall(calls) + if (updateStateCall !== null) { + // remove first call result + result = result.slice(1) + } + + const one = BigInt.fromI32(1) + const vaultUpdateTimestamp = vault.rewardsTimestamp as BigInt + for (let i = 0; i < result.length; i++) { + exitRequest = exitRequests[i] + let decodedResult = ethereum.decode('(uint256,uint256,uint256)', result[i])!.toTuple() + const leftTickets = decodedResult[0].toBigInt() + const exitedAssets = decodedResult[2].toBigInt() + const totalAssetsBefore = exitRequest.totalAssets + if (leftTickets.gt(one)) { + exitRequest.totalAssets = exitRequest.isV2Position + ? leftTickets.times(vault.exitingAssets).div(vault.exitingTickets).plus(exitedAssets) + : convertSharesToAssets(vault, leftTickets).plus(exitedAssets) + } else { + exitRequest.totalAssets = exitedAssets + } + exitRequest.exitedAssets = exitedAssets + + if (!exitedAssets.isZero()) { + exitRequest.isClaimable = exitRequest.timestamp.plus(BigInt.fromString(secondsInDay)).lt(block.timestamp) + } else { + exitRequest.isClaimable = false + } + + if (exitRequest.lastSnapshotTimestamp.notEqual(vaultUpdateTimestamp)) { + exitRequest.lastSnapshotTimestamp = vaultUpdateTimestamp + snapshotExitRequest(exitRequest, exitRequest.totalAssets.minus(totalAssetsBefore), vaultUpdateTimestamp) + } + exitRequest.save() + } +} + +function getExitQueueIndexCall(positionTicket: BigInt): Bytes { + const encodedArgs = ethereum.encode(ethereum.Value.fromUnsignedBigInt(positionTicket)) + return Bytes.fromHexString(getExitQueueIndexSelector).concat(encodedArgs as Bytes) +} + +function getCalculateExitedAssetsCall( + receiver: Address, + positionTicket: BigInt, + timestamp: BigInt, + exitQueueIndex: BigInt, +): Bytes { + return Bytes.fromHexString(calculateExitedAssetsSelector) + .concat(ethereum.encode(ethereum.Value.fromAddress(receiver))!) + .concat(ethereum.encode(ethereum.Value.fromUnsignedBigInt(positionTicket))!) + .concat(ethereum.encode(ethereum.Value.fromUnsignedBigInt(timestamp))!) + .concat(ethereum.encode(ethereum.Value.fromUnsignedBigInt(exitQueueIndex))!) +} + +export function snapshotExitRequest(exitRequest: ExitRequest, earnedAssets: BigInt, rewardsTimestamp: BigInt): void { + const exitRequestSnapshot = new ExitRequestSnapshot(rewardsTimestamp.toString()) + exitRequestSnapshot.timestamp = rewardsTimestamp.toI64() + exitRequestSnapshot.exitRequest = exitRequest.id + exitRequestSnapshot.earnedAssets = earnedAssets + exitRequestSnapshot.totalAssets = exitRequest.totalAssets + exitRequestSnapshot.save() +} diff --git a/src/entities/network.ts b/src/entities/network.ts index 9190542..186ac56 100644 --- a/src/entities/network.ts +++ b/src/entities/network.ts @@ -1,4 +1,6 @@ -import { Network } from '../../generated/schema' +import { Address, BigDecimal, BigInt, Bytes, store } from '@graphprotocol/graph-ts' +import { Network, User, Vault } from '../../generated/schema' +import { NETWORK, V2_REWARD_TOKEN, V2_STAKED_TOKEN } from '../helpers/constants' export function createOrLoadNetwork(): Network { const id = '0' @@ -7,9 +9,75 @@ export function createOrLoadNetwork(): Network { if (network === null) { network = new Network(id) - network.vaultsTotal = 0 + network.vaultsCount = 0 + network.usersCount = 0 + network.totalAssets = BigInt.zero() + network.totalEarnedAssets = BigInt.zero() + network.vaultIds = [] + network.assetsUsdRate = BigDecimal.zero() + network.usdToDaiRate = BigDecimal.zero() + network.usdToEurRate = BigDecimal.zero() + network.usdToGbpRate = BigDecimal.zero() network.save() } return network } + +export function isGnosisNetwork(): boolean { + return NETWORK == 'chiado' || NETWORK == 'gnosis' +} + +export function createOrLoadUser(userAddress: Bytes): User { + const id = userAddress.toHexString() + + let user = User.load(id) + if (user === null) { + user = new User(id) + user.vaultsCount = 0 + user.isOsTokenHolder = false + user.save() + } + + return user +} + +export function decreaseUserVaultsCount(userAddress: Bytes): void { + if ( + Vault.load(userAddress.toHex()) !== null || + userAddress.equals(V2_REWARD_TOKEN) || + userAddress.equals(V2_STAKED_TOKEN) || + userAddress.equals(Address.zero()) + ) { + return + } + const user = createOrLoadUser(userAddress) + if (!user.isOsTokenHolder && user.vaultsCount === 1) { + const network = createOrLoadNetwork() + network.usersCount = network.usersCount - 1 + network.save() + store.remove('User', user.id) + } else { + user.vaultsCount = user.vaultsCount - 1 + user.save() + } +} + +export function increaseUserVaultsCount(userAddress: Bytes): void { + if ( + Vault.load(userAddress.toHex()) !== null || + userAddress.equals(V2_REWARD_TOKEN) || + userAddress.equals(V2_STAKED_TOKEN) || + userAddress.equals(Address.zero()) + ) { + return + } + const user = createOrLoadUser(userAddress) + if (!user.isOsTokenHolder && user.vaultsCount === 0) { + const network = createOrLoadNetwork() + network.usersCount = network.usersCount + 1 + network.save() + } + user.vaultsCount = user.vaultsCount + 1 + user.save() +} diff --git a/src/entities/osToken.ts b/src/entities/osToken.ts index 482a4a6..2f0fee4 100644 --- a/src/entities/osToken.ts +++ b/src/entities/osToken.ts @@ -1,7 +1,13 @@ -import { Address, BigDecimal, BigInt } from '@graphprotocol/graph-ts' -import { OsToken, OsTokenHolder } from '../../generated/schema' +import { Address, BigDecimal, BigInt, ethereum, log } from '@graphprotocol/graph-ts' +import { OsToken, OsTokenHolder, OsTokenHolderSnapshot, OsTokenSnapshot } from '../../generated/schema' +import { OsTokenVaultController as OsTokenVaultControllerContact } from '../../generated/Keeper/OsTokenVaultController' +import { OS_TOKEN_VAULT_CONTROLLER, WAD } from '../helpers/constants' +import { calculateAverage } from '../helpers/utils' const osTokenId = '1' +const snapshotsPerWeek = 14 +const secondsInYear = '31536000' +const maxPercent = '100' export function createOrLoadOsToken(): OsToken { let osToken = OsToken.load(osTokenId) @@ -9,28 +15,112 @@ export function createOrLoadOsToken(): OsToken { osToken = new OsToken(osTokenId) osToken.apy = BigDecimal.zero() - osToken.borrowApy = BigDecimal.zero() + osToken.apys = [] osToken.feePercent = 0 osToken.totalSupply = BigInt.zero() - osToken.snapshotsCount = BigInt.zero() + osToken.totalAssets = BigInt.zero() + osToken.lastUpdateTimestamp = BigInt.zero() osToken.save() } return osToken } -export function createOrLoadOsTokenHolder(holderAddress: Address): OsTokenHolder { - let holderId = holderAddress.toHexString() - let holder = OsTokenHolder.load(holderId) - if (holder == null) { - holder = new OsTokenHolder(holderId) - holder.shares = BigInt.zero() - holder.timestamp = BigInt.zero() +export function createOrLoadOsTokenHolder(osToken: OsToken, holderAddress: Address): OsTokenHolder { + const id = holderAddress.toHex() + let holder = OsTokenHolder.load(id) + + if (holder === null) { + holder = new OsTokenHolder(id) + holder.balance = BigInt.zero() + holder.assets = BigInt.zero() + holder.osToken = osToken.id + holder.transfersCount = BigInt.zero() holder.save() } - return holder as OsTokenHolder + + return holder +} + +export function convertOsTokenSharesToAssets(osToken: OsToken, shares: BigInt): BigInt { + if (osToken.totalAssets.isZero()) { + return shares + } else { + return shares.times(osToken.totalAssets).div(osToken.totalSupply) + } +} + +export function updateOsTokenApy(osToken: OsToken, newAvgRewardPerSecond: BigInt): void { + const netAvgRewardPerSecond = newAvgRewardPerSecond + .times(BigInt.fromI32(10000 - osToken.feePercent)) + .div(BigInt.fromI32(10000)) + + const currentApy = new BigDecimal(netAvgRewardPerSecond) + .times(BigDecimal.fromString(secondsInYear)) + .times(BigDecimal.fromString(maxPercent)) + .div(BigDecimal.fromString(WAD)) + + let apys = osToken.apys + apys.push(currentApy) + if (apys.length > snapshotsPerWeek) { + apys = apys.slice(apys.length - snapshotsPerWeek) + } + osToken.apys = apys + osToken.apy = calculateAverage(apys) +} + +export function updateOsTokenTotalAssets(osToken: OsToken, updateTimestamp: BigInt, block: ethereum.Block): BigInt { + if (osToken.lastUpdateTimestamp.isZero()) { + osToken.lastUpdateTimestamp = updateTimestamp + return BigInt.zero() + } + + const totalDuration = updateTimestamp.minus(osToken.lastUpdateTimestamp) + if (totalDuration.lt(BigInt.zero())) { + log.error('[OsToken] totalDuration cannot be negative={}', [totalDuration.toString()]) + return BigInt.zero() + } + + let updateSlippageSeconds = block.timestamp.minus(updateTimestamp) + if (updateSlippageSeconds.lt(BigInt.zero())) { + log.error('[OsToken] updateSlippageSeconds cannot be negative={}', [updateSlippageSeconds.toString()]) + updateSlippageSeconds = BigInt.zero() + } + + const osTokenVaultController = OsTokenVaultControllerContact.bind(OS_TOKEN_VAULT_CONTROLLER) + const newTotalAssets = osTokenVaultController.totalAssets() + if (newTotalAssets.lt(osToken.totalAssets)) { + log.error('[OsToken] newTotalAssets cannot be less than current current={} new={}', [ + osToken.totalAssets.toString(), + newTotalAssets.toString(), + ]) + return BigInt.zero() + } + + let totalAssetsDiff = newTotalAssets.minus(osToken.totalAssets) + if (!updateSlippageSeconds.isZero()) { + totalAssetsDiff = totalAssetsDiff.minus(totalAssetsDiff.times(updateSlippageSeconds).div(totalDuration)) + } + osToken.totalAssets = osToken.totalAssets.plus(totalAssetsDiff) + osToken.lastUpdateTimestamp = updateTimestamp + return totalAssetsDiff +} + +export function snapshotOsToken(osToken: OsToken, assetsDiff: BigInt, rewardsTimestamp: BigInt): void { + const osTokenSnapshot = new OsTokenSnapshot(rewardsTimestamp.toString()) + osTokenSnapshot.timestamp = rewardsTimestamp.toI64() + osTokenSnapshot.earnedAssets = assetsDiff.plus( + assetsDiff.times(BigInt.fromI32(osToken.feePercent)).div(BigInt.fromI32(10000 - osToken.feePercent)), + ) + osTokenSnapshot.totalAssets = osToken.totalAssets + osTokenSnapshot.save() } -export function isSupportedOsTokenHolder(holderAddress: Address): boolean { - return holderAddress != Address.zero() +export function snapshotOsTokenHolder(holder: OsTokenHolder, assetsDiff: BigInt, timestamp: BigInt): void { + const snapshot = new OsTokenHolderSnapshot(timestamp.toString()) + snapshot.timestamp = timestamp.toI64() + snapshot.osTokenHolder = holder.id + snapshot.earnedAssets = assetsDiff + snapshot.totalAssets = holder.assets + snapshot.save() } diff --git a/src/entities/rewardSplitter.ts b/src/entities/rewardSplitter.ts index de972a4..4fe1651 100644 --- a/src/entities/rewardSplitter.ts +++ b/src/entities/rewardSplitter.ts @@ -1,10 +1,23 @@ -import { Address, BigInt } from '@graphprotocol/graph-ts' +import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { + RewardSplitter, + RewardSplitterShareHolder, + RewardSplitterShareHolderSnapshot, + Vault, +} from '../../generated/schema' +import { RewardSplitter as RewardSplitterContract } from '../../generated/Keeper/RewardSplitter' +import { convertSharesToAssets } from './vaults' +import { GENESIS_VAULT } from '../helpers/constants' +import { createOrLoadV2Pool } from './v2pool' -import { RewardSplitterShareHolder } from '../../generated/schema' +const vaultUpdateStateSelector = '0x79c702ad' +const syncRewardsCallSelector = '0x72c0c211' +const rewardsOfSelector = '0x479ba7ae' export function createOrLoadRewardSplitterShareHolder( shareHolderAddress: Address, rewardSplitter: Address, + vault: string, ): RewardSplitterShareHolder { const rewardSplitterShareHolderId = `${rewardSplitter.toHex()}-${shareHolderAddress.toHex()}` @@ -15,8 +28,121 @@ export function createOrLoadRewardSplitterShareHolder( rewardSplitterShareHolder.shares = BigInt.zero() rewardSplitterShareHolder.address = shareHolderAddress rewardSplitterShareHolder.rewardSplitter = rewardSplitter.toHex() + rewardSplitterShareHolder.vault = vault + rewardSplitterShareHolder.earnedVaultShares = BigInt.zero() + rewardSplitterShareHolder.earnedVaultAssets = BigInt.zero() rewardSplitterShareHolder.save() } return rewardSplitterShareHolder } + +export function updateRewardSplitters(vault: Vault): void { + if (vault.rewardsTimestamp === null) { + return + } + if (Address.fromString(vault.id).equals(GENESIS_VAULT)) { + const v2Pool = createOrLoadV2Pool() + if (!v2Pool.migrated) { + // wait for the migration + return + } + } + + const lastRewardsTimestamp = vault.rewardsTimestamp as BigInt + const rewardSplitters: Array = vault.rewardSplitters.load() + let updateStateCall: Bytes | null = null + if ( + vault.rewardsRoot !== null && + vault.proofReward !== null && + vault.proofUnlockedMevReward !== null && + vault.proof !== null && + vault.proof!.length > 0 + ) { + updateStateCall = _getVaultUpdateStateCall( + vault.rewardsRoot as Bytes, + vault.proofReward as BigInt, + vault.proofUnlockedMevReward as BigInt, + (vault.proof as Array).map((p: string) => Bytes.fromHexString(p)), + ) + } + + let rewardSplitter: RewardSplitter + const outdatedRewardSplitters: Array = [] + for (let i = 0; i < rewardSplitters.length; i++) { + rewardSplitter = rewardSplitters[i] + if (rewardSplitter.lastSnapshotTimestamp.notEqual(lastRewardsTimestamp)) { + rewardSplitter.lastSnapshotTimestamp = lastRewardsTimestamp + rewardSplitter.save() + outdatedRewardSplitters.push(rewardSplitter) + } + } + + const syncRewardsCall = Bytes.fromHexString(syncRewardsCallSelector) + for (let i = 0; i < outdatedRewardSplitters.length; i++) { + rewardSplitter = outdatedRewardSplitters[i] + const shareHolders: Array = rewardSplitter.shareHolders.load() + let calls: Array = [] + if (updateStateCall !== null) { + calls.push(updateStateCall) + } + calls.push(syncRewardsCall) + for (let j = 0; j < shareHolders.length; j++) { + calls.push(_getRewardsOfCall(Address.fromBytes(shareHolders[j].address))) + } + + const rewardSplitterContract = RewardSplitterContract.bind(Address.fromString(rewardSplitter.id)) + let callResult: Array = rewardSplitterContract.multicall(calls) + callResult = callResult.slice(updateStateCall !== null ? 2 : 1) + + let shareHolder: RewardSplitterShareHolder + let earnedVaultAssetsBefore: BigInt + for (let j = 0; j < shareHolders.length; j++) { + shareHolder = shareHolders[j] + earnedVaultAssetsBefore = shareHolder.earnedVaultAssets + shareHolder.earnedVaultShares = ethereum.decode('uint256', callResult[j])!.toBigInt() + shareHolder.earnedVaultAssets = convertSharesToAssets(vault, shareHolder.earnedVaultShares) + shareHolder.save() + snapshotRewardSplitterShareHolder( + shareHolder, + shareHolder.earnedVaultAssets.minus(earnedVaultAssetsBefore), + lastRewardsTimestamp, + ) + } + } +} + +function _getVaultUpdateStateCall( + rewardsRoot: Bytes, + reward: BigInt, + unlockedMevReward: BigInt, + proof: Array, +): Bytes { + const updateStateArray: Array = [ + ethereum.Value.fromFixedBytes(rewardsRoot), + ethereum.Value.fromSignedBigInt(reward), + ethereum.Value.fromUnsignedBigInt(unlockedMevReward), + ethereum.Value.fromFixedBytesArray(proof), + ] + // Encode the tuple + const encodedUpdateStateArgs = ethereum.encode(ethereum.Value.fromTuple(changetype(updateStateArray))) + return Bytes.fromHexString(vaultUpdateStateSelector).concat(encodedUpdateStateArgs as Bytes) +} + +function _getRewardsOfCall(shareHolder: Address): Bytes { + const encodedRewardsOfArgs = ethereum.encode(ethereum.Value.fromAddress(shareHolder)) + return Bytes.fromHexString(rewardsOfSelector).concat(encodedRewardsOfArgs as Bytes) +} + +export function snapshotRewardSplitterShareHolder( + shareHolder: RewardSplitterShareHolder, + earnedAssets: BigInt, + rewardsTimestamp: BigInt, +): void { + const snapshot = new RewardSplitterShareHolderSnapshot(rewardsTimestamp.toString()) + snapshot.timestamp = rewardsTimestamp.toI64() + snapshot.rewardSpliterShareHolder = shareHolder.id + snapshot.earnedAssets = earnedAssets + snapshot.totalAssets = shareHolder.earnedVaultAssets + snapshot.save() +} diff --git a/src/entities/tokenTransfer.ts b/src/entities/tokenTransfer.ts index 0bf5fb1..05b45b1 100644 --- a/src/entities/tokenTransfer.ts +++ b/src/entities/tokenTransfer.ts @@ -1,21 +1,18 @@ import { Address, BigInt } from '@graphprotocol/graph-ts' -import { TokenHolder, TokenTransfer } from '../../generated/schema' +import { SwiseTokenHolder, TokenTransfer } from '../../generated/schema' -export function createOrLoadTokenHolder(tokenSymbol: string, tokenHolderAddress: Address): TokenHolder { - const id = `${tokenSymbol}-${tokenHolderAddress.toHex()}` +export function createOrLoadSwiseTokenHolder(holderAddress: Address): SwiseTokenHolder { + const id = holderAddress.toHex() + let holder = SwiseTokenHolder.load(id) - let token = TokenHolder.load(id) - - if (token === null) { - token = new TokenHolder(id) - - token.address = tokenHolderAddress - token.tokenSymbol = tokenSymbol - token.transfersCount = BigInt.zero() - token.save() + if (holder === null) { + holder = new SwiseTokenHolder(id) + holder.balance = BigInt.zero() + holder.transfersCount = BigInt.zero() + holder.save() } - return token + return holder } export function createTokenTransfer( @@ -27,24 +24,10 @@ export function createTokenTransfer( tokenSymbol: string, ): void { const transfer = new TokenTransfer(id) - transfer.to = to transfer.from = from transfer.amount = amount transfer.timestamp = timestamp transfer.tokenSymbol = tokenSymbol transfer.save() - - if (from) { - const tokenHolderFrom = createOrLoadTokenHolder(tokenSymbol, from) - - tokenHolderFrom.transfersCount = tokenHolderFrom.transfersCount.plus(BigInt.fromI32(1)) - tokenHolderFrom.save() - } - if (to) { - const tokenHolderTo = createOrLoadTokenHolder(tokenSymbol, to) - - tokenHolderTo.transfersCount = tokenHolderTo.transfersCount.plus(BigInt.fromI32(1)) - tokenHolderTo.save() - } } diff --git a/src/entities/v2pool.ts b/src/entities/v2pool.ts index 0eb904f..360c0b6 100644 --- a/src/entities/v2pool.ts +++ b/src/entities/v2pool.ts @@ -1,8 +1,27 @@ -import { BigDecimal, BigInt } from '@graphprotocol/graph-ts' -import { V2Pool } from '../../generated/schema' -import { V2_POOL_FEE_PERCENT, WAD } from '../helpers/constants' +import { Address, BigDecimal, BigInt, Bytes, ethereum, log } from '@graphprotocol/graph-ts' +import { V2Pool, V2PoolUser } from '../../generated/schema' +import { + GENESIS_VAULT, + V2_POOL_FEE_PERCENT, + V2_REWARD_TOKEN, + V2_STAKED_TOKEN, + MULTICALL, + WAD, +} from '../helpers/constants' +import { Multicall as MulticallContract, TryAggregateCallReturnDataStruct } from '../../generated/Keeper/Multicall' +import { isGnosisNetwork } from './network' +import { getUpdateStateCall } from './vaults' +import { calculateAverage, getAggregateCall } from '../helpers/utils' +const snapshotsPerWeek = 14 +const secondsInYear = '31536000' +const maxPercent = '100' const poolId = '1' +const swapXdaiToGnoSelector = '0xb0d11302' +const poolRewardAssetsSelector = '0x18160ddd' +const poolPrincipalAssetsSelector = '0x18160ddd' +const poolPenaltyAssetsSelector = '0xe6af61c8' +const rewardPerTokenSelector = '0xcd3daf9d' export function createOrLoadV2Pool(): V2Pool { let pool = V2Pool.load(poolId) @@ -16,13 +35,108 @@ export function createOrLoadV2Pool(): V2Pool { pool.feePercent = I32.parseInt(V2_POOL_FEE_PERCENT) pool.rate = BigInt.fromString(WAD) pool.migrated = false - pool.apySnapshotsCount = BigInt.zero() + pool.apys = [] pool.apy = BigDecimal.zero() - pool.weeklyApy = BigDecimal.zero() - pool.executionApy = BigDecimal.zero() - pool.consensusApy = BigDecimal.zero() pool.save() } return pool } + +export function createOrLoadV2PoolUser(userAddress: Bytes): V2PoolUser { + const id = userAddress.toHexString() + let v2PoolUser = V2PoolUser.load(id) + + if (v2PoolUser === null) { + v2PoolUser = new V2PoolUser(id) + v2PoolUser.balance = BigInt.zero() + v2PoolUser.save() + } + + return v2PoolUser +} + +export function updatePoolApy( + pool: V2Pool, + fromTimestamp: BigInt | null, + toTimestamp: BigInt, + rateChange: BigInt, +): void { + if (fromTimestamp === null) { + // it's the first update, skip + return + } + const totalDuration = toTimestamp.minus(fromTimestamp) + if (totalDuration.isZero()) { + log.error('[V2Pool] updatePoolApy totalDuration is zero fromTimestamp={} toTimestamp={}', [ + fromTimestamp.toString(), + toTimestamp.toString(), + ]) + return + } + const currentApy = new BigDecimal(rateChange) + .times(BigDecimal.fromString(secondsInYear)) + .times(BigDecimal.fromString(maxPercent)) + .div(BigDecimal.fromString(WAD)) + .div(new BigDecimal(totalDuration)) + + let apys = pool.apys + apys.push(currentApy) + if (apys.length > snapshotsPerWeek) { + apys = apys.slice(apys.length - snapshotsPerWeek) + } + pool.apys = apys + pool.apy = calculateAverage(apys) +} + +export function getPoolStateUpdate( + rewardsRoot: Bytes, + reward: BigInt, + unlockedMevReward: BigInt, + proof: Array, +): Array { + const isGnosis = isGnosisNetwork() + const rewardAssetsCall = Bytes.fromHexString(poolRewardAssetsSelector) + const principalAssetsCall = Bytes.fromHexString(poolPrincipalAssetsSelector) + const penaltyAssetsCall = Bytes.fromHexString(poolPenaltyAssetsSelector) + const rewardPerTokenCall = Bytes.fromHexString(rewardPerTokenSelector) + const updateStateCall = getUpdateStateCall(rewardsRoot, reward, unlockedMevReward, proof) + const swapXdaiToGnoCall = Bytes.fromHexString(swapXdaiToGnoSelector) + + const multicallContract = MulticallContract.bind(Address.fromString(MULTICALL)) + let calls: Array = [getAggregateCall(GENESIS_VAULT, updateStateCall)] + if (isGnosis) { + calls.push(getAggregateCall(GENESIS_VAULT, swapXdaiToGnoCall)) + } + calls.push(getAggregateCall(V2_REWARD_TOKEN, rewardAssetsCall)) + calls.push(getAggregateCall(V2_REWARD_TOKEN, penaltyAssetsCall)) + calls.push(getAggregateCall(V2_STAKED_TOKEN, principalAssetsCall)) + calls.push(getAggregateCall(V2_REWARD_TOKEN, rewardPerTokenCall)) + + const result = multicallContract.call('tryAggregate', 'tryAggregate(bool,(address,bytes)[]):((bool,bytes)[])', [ + ethereum.Value.fromBoolean(false), + ethereum.Value.fromArray(calls), + ]) + let resultValue = result[0].toTupleArray() + if (!resultValue[0].success) { + log.error('[Vault] getPoolStateUpdate failed updateStateCall={}', [updateStateCall.toHexString()]) + assert(false, 'getPoolLatestRate failed') + } + + if (isGnosis) { + resultValue = resultValue.slice(2) + } else { + resultValue = resultValue.slice(1) + } + const rewardAssets = ethereum.decode('uint256', resultValue[0].returnData)!.toBigInt() + const penaltyAssets = ethereum.decode('uint256', resultValue[1].returnData)!.toBigInt() + const principalAssets = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() + const rewardRate = ethereum.decode('uint256', resultValue[3].returnData)!.toBigInt() + const totalAssets = principalAssets.plus(rewardAssets) + let penaltyRate = BigInt.zero() + if (totalAssets.gt(BigInt.zero())) { + penaltyRate = BigInt.fromString(WAD).times(penaltyAssets).div(totalAssets) + } + const newRate = BigInt.fromString(WAD).plus(rewardRate).minus(penaltyRate) + return [newRate, rewardAssets, principalAssets, penaltyAssets] +} diff --git a/src/entities/vaults.ts b/src/entities/vaults.ts index d1c61cb..a57de02 100644 --- a/src/entities/vaults.ts +++ b/src/entities/vaults.ts @@ -1,20 +1,28 @@ -import { Address, BigDecimal, BigInt, DataSourceContext, ethereum, log } from '@graphprotocol/graph-ts' +import { Address, BigDecimal, BigInt, Bytes, ethereum, log } from '@graphprotocol/graph-ts' import { + BlocklistVault as BlocklistVaultTemplate, Erc20Vault as Erc20VaultTemplate, PrivateVault as PrivateVaultTemplate, - BlocklistVault as BlocklistVaultTemplate, RestakeVault as RestakeVaultTemplate, - OwnMevEscrow as OwnMevEscrowTemplate, Vault as VaultTemplate, } from '../../generated/templates' import { VaultCreated } from '../../generated/templates/VaultFactory/VaultFactory' -import { OsTokenPosition, Vault, VaultsStat } from '../../generated/schema' +import { Vault, VaultSnapshot } from '../../generated/schema' import { createOrLoadNetwork } from './network' import { createTransaction } from './transaction' -import { WAD } from '../helpers/constants' +import { MULTICALL, WAD } from '../helpers/constants' import { createOrLoadOsTokenConfig } from './osTokenConfig' - -const vaultsStatId = '1' +import { Multicall as MulticallContract, TryAggregateCallReturnDataStruct } from '../../generated/Keeper/Multicall' +import { calculateAverage, getAggregateCall } from '../helpers/utils' + +const snapshotsPerWeek = 14 +const secondsInYear = '31536000' +const maxPercent = '100' +const updateStateSelector = '0x1a7ff553' +const totalAssetsSelector = '0x01e1d114' +const totalSharesSelector = '0x3a98ef39' +const convertToAssetsSelector = '0x07a2d13a' +const exitingAssetsSelector = '0xee3bd5df' export function createVault( event: VaultCreated, @@ -49,20 +57,19 @@ export function createVault( vault.capacity = capacity vault.feePercent = feePercent vault.feeRecipient = admin - vault.keysManager = admin // Deprecated vault.depositDataManager = admin vault.canHarvest = false vault.consensusReward = BigInt.zero() vault.lockedExecutionReward = BigInt.zero() vault.unlockedExecutionReward = BigInt.zero() - vault.unconvertedExecutionReward = BigInt.zero() vault.slashedMevReward = BigInt.zero() vault.totalShares = BigInt.zero() vault.score = BigDecimal.zero() vault.totalAssets = BigInt.zero() - vault.principalAssets = BigInt.zero() vault.rate = BigInt.fromString(WAD) vault.exitingAssets = BigInt.zero() + vault.exitingTickets = BigInt.zero() + vault.latestExitTicket = BigInt.zero() vault.isPrivate = isPrivate vault.isBlocklist = isBlocklist vault.isRestake = isRestake @@ -71,14 +78,8 @@ export function createVault( vault.isCollateralized = false vault.addressString = vaultAddressHex vault.createdAt = block.timestamp - vault.apySnapshotsCount = BigInt.zero() vault.apy = BigDecimal.zero() - vault.weeklyApy = BigDecimal.zero() - vault.executionApy = BigDecimal.zero() - vault.consensusApy = BigDecimal.zero() - vault.medianApy = BigDecimal.zero() - vault.medianExecutionApy = BigDecimal.zero() - vault.medianConsensusApy = BigDecimal.zero() + vault.apys = [] vault.blocklistCount = BigInt.zero() vault.whitelistCount = BigInt.zero() vault.isGenesis = false @@ -89,9 +90,6 @@ export function createVault( if (ownMevEscrow != Address.zero()) { vault.mevEscrow = event.params.ownMevEscrow - const context = new DataSourceContext() - context.setString('vault', vaultAddressHex) - OwnMevEscrowTemplate.createWithContext(ownMevEscrow, context) } if (isPrivate) { @@ -114,13 +112,12 @@ export function createVault( VaultTemplate.create(vaultAddress) const network = createOrLoadNetwork() - network.vaultsTotal = network.vaultsTotal + 1 + let vaultIds = network.vaultIds + vaultIds.push(vaultAddressHex) + network.vaultIds = vaultIds + network.vaultsCount = network.vaultsCount + 1 network.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.vaultsCount = vaultsStat.vaultsCount.plus(BigInt.fromI32(1)) - vaultsStat.save() - createTransaction(event.transaction.hash.toHex()) log.info( @@ -139,29 +136,120 @@ export function createVault( ) } -export function createOrLoadOsTokenPosition(holder: Address, vaultAddress: Address): OsTokenPosition { - const osTokenPositionId = `${vaultAddress.toHex()}-${holder.toHex()}` - - let osTokenPosition = OsTokenPosition.load(osTokenPositionId) - if (osTokenPosition === null) { - osTokenPosition = new OsTokenPosition(osTokenPositionId) - osTokenPosition.shares = BigInt.zero() - osTokenPosition.address = holder - osTokenPosition.vault = vaultAddress.toHex() - osTokenPosition.save() +export function updateVaultApy( + vault: Vault, + fromTimestamp: BigInt | null, + toTimestamp: BigInt, + rateChange: BigInt, +): void { + if (fromTimestamp === null) { + // it's the first update, skip + return + } + const totalDuration = toTimestamp.minus(fromTimestamp) + if (totalDuration.isZero()) { + log.error('[Vault] updateVaultApy totalDuration is zero fromTimestamp={} toTimestamp={}', [ + fromTimestamp.toString(), + toTimestamp.toString(), + ]) + return } + const currentApy = new BigDecimal(rateChange) + .times(BigDecimal.fromString(secondsInYear)) + .times(BigDecimal.fromString(maxPercent)) + .div(BigDecimal.fromString(WAD)) + .div(new BigDecimal(totalDuration)) + + let apys = vault.apys + apys.push(currentApy) + if (apys.length > snapshotsPerWeek) { + apys = apys.slice(apys.length - snapshotsPerWeek) + } + vault.apys = apys + vault.apy = calculateAverage(apys) +} - return osTokenPosition +export function convertSharesToAssets(vault: Vault, shares: BigInt): BigInt { + if (vault.totalShares.equals(BigInt.zero())) { + return shares + } + return shares.times(vault.totalAssets).div(vault.totalShares) } -export function createOrLoadVaultsStat(): VaultsStat { - let vaultsStat = VaultsStat.load(vaultsStatId) - if (vaultsStat === null) { - vaultsStat = new VaultsStat(vaultsStatId) - vaultsStat.totalAssets = BigInt.zero() - vaultsStat.vaultsCount = BigInt.zero() - vaultsStat.save() +export function getVaultStateUpdate( + vault: Vault, + rewardsRoot: Bytes, + reward: BigInt, + unlockedMevReward: BigInt, + proof: Array, +): Array { + const isV2Vault = vault.version.equals(BigInt.fromI32(2)) + const vaultAddr = Address.fromString(vault.id) + const updateStateCall = getUpdateStateCall(rewardsRoot, reward, unlockedMevReward, proof) + const convertToAssetsCall = getConvertToAssetsCall(BigInt.fromString(WAD)) + const totalAssetsCall = Bytes.fromHexString(totalAssetsSelector) + const totalSharesCall = Bytes.fromHexString(totalSharesSelector) + const exitingAssetsCall = Bytes.fromHexString(exitingAssetsSelector) + + const multicallContract = MulticallContract.bind(Address.fromString(MULTICALL)) + let calls: Array = [getAggregateCall(vaultAddr, updateStateCall)] + calls.push(getAggregateCall(vaultAddr, convertToAssetsCall)) + calls.push(getAggregateCall(vaultAddr, totalAssetsCall)) + calls.push(getAggregateCall(vaultAddr, totalSharesCall)) + if (isV2Vault) { + calls.push(getAggregateCall(vaultAddr, exitingAssetsCall)) + } + + const result = multicallContract.call('tryAggregate', 'tryAggregate(bool,(address,bytes)[]):((bool,bytes)[])', [ + ethereum.Value.fromBoolean(false), + ethereum.Value.fromArray(calls), + ]) + let resultValue = result[0].toTupleArray() + if (!resultValue[0].success) { + log.error('[Vault] getVaultStateUpdate failed for vault={} updateStateCall={}', [ + vault.id, + updateStateCall.toHexString(), + ]) + assert(false, 'executeVaultUpdateState failed') } + resultValue = resultValue.slice(1) + + const newRate = ethereum.decode('uint256', resultValue[0].returnData)!.toBigInt() + const totalAssets = ethereum.decode('uint256', resultValue[1].returnData)!.toBigInt() + const totalShares = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() + const exitingAssets = isV2Vault + ? ethereum.decode('uint128', resultValue[3].returnData)!.toBigInt() + : vault.exitingAssets + return [newRate, totalAssets, totalShares, exitingAssets] +} + +export function getUpdateStateCall( + rewardsRoot: Bytes, + reward: BigInt, + unlockedMevReward: BigInt, + proof: Array, +): Bytes { + const updateStateArray: Array = [ + ethereum.Value.fromFixedBytes(rewardsRoot), + ethereum.Value.fromSignedBigInt(reward), + ethereum.Value.fromUnsignedBigInt(unlockedMevReward), + ethereum.Value.fromFixedBytesArray(proof), + ] + // Encode the tuple + const encodedUpdateStateArgs = ethereum.encode(ethereum.Value.fromTuple(changetype(updateStateArray))) + return Bytes.fromHexString(updateStateSelector).concat(encodedUpdateStateArgs as Bytes) +} + +function getConvertToAssetsCall(shares: BigInt): Bytes { + const encodedConvertToAssetsArgs = ethereum.encode(ethereum.Value.fromUnsignedBigInt(shares)) + return Bytes.fromHexString(convertToAssetsSelector).concat(encodedConvertToAssetsArgs as Bytes) +} - return vaultsStat +export function snapshotVault(vault: Vault, assetsDiff: BigInt, rewardsTimestamp: BigInt): void { + const vaultSnapshot = new VaultSnapshot(rewardsTimestamp.toString()) + vaultSnapshot.timestamp = rewardsTimestamp.toI64() + vaultSnapshot.vault = vault.id + vaultSnapshot.earnedAssets = assetsDiff + vaultSnapshot.totalAssets = vault.totalAssets + vaultSnapshot.save() } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 82b37e2..b5f2d49 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,147 +1,25 @@ -import { Address, Bytes, BigInt, ethereum, log } from '@graphprotocol/graph-ts' -import { GENESIS_VAULT, NETWORK, V2_REWARD_TOKEN, V2_STAKED_TOKEN, WAD } from './constants' -import { Vault } from '../../generated/schema' -import { Vault as VaultContract } from '../../generated/Keeper/Vault' -import { Multicall as MulticallContract, TryAggregateCallReturnDataStruct } from '../../generated/Keeper/Multicall' +import { Address, BigDecimal, Bytes, ethereum } from '@graphprotocol/graph-ts' -export function isGnosisNetwork(): boolean { - return NETWORK == 'chiado' || NETWORK == 'gnosis' -} - -const multicallContractAddr = Address.fromString('0xcA11bde05977b3631167028862bE2a173976CA11') -const updateStateSelector = '0x1a7ff553' -const totalAssetsSelector = '0x01e1d114' -const totalSharesSelector = '0x3a98ef39' -const convertToAssetsSelector = '0x07a2d13a' -const swapXdaiToGnoSelector = '0xb0d11302' -const poolRewardAssetsSelector = '0x18160ddd' -const poolPrincipalAssetsSelector = '0x18160ddd' -const poolPenaltyAssetsSelector = '0xe6af61c8' - -export function getVaultTotalAssets(vault: Vault): BigInt { - const vaultAddr = Address.fromString(vault.id) - const vaultContract = VaultContract.bind(vaultAddr) - return vaultContract.totalAssets() -} - -export function getVaultStateUpdate( - vault: Vault, - rewardsRoot: Bytes, - reward: BigInt, - unlockedMevReward: BigInt, - proof: Array, -): Array { - const isGnosis = isGnosisNetwork() - const vaultAddr = Address.fromString(vault.id) - const updateStateCall = getUpdateStateCall(rewardsRoot, reward, unlockedMevReward, proof) - const convertToAssetsCall = getConvertToAssetsCall(BigInt.fromString(WAD)) - const totalAssetsCall = Bytes.fromHexString(totalAssetsSelector) - const totalSharesCall = Bytes.fromHexString(totalSharesSelector) - const swapXdaiToGnoCall = Bytes.fromHexString(swapXdaiToGnoSelector) - - const multicallContract = MulticallContract.bind(multicallContractAddr) - let calls: Array = [getAggregateCall(vaultAddr, updateStateCall)] - if (isGnosis) { - calls.push(getAggregateCall(vaultAddr, swapXdaiToGnoCall)) - } - calls.push(getAggregateCall(vaultAddr, convertToAssetsCall)) - calls.push(getAggregateCall(vaultAddr, totalAssetsCall)) - calls.push(getAggregateCall(vaultAddr, totalSharesCall)) - - const result = multicallContract.call('tryAggregate', 'tryAggregate(bool,(address,bytes)[]):((bool,bytes)[])', [ - ethereum.Value.fromBoolean(false), - ethereum.Value.fromArray(calls), - ]) - const resultValue = result[0].toTupleArray() - if (!resultValue[0].success) { - log.error('[Vault] getVaultStateUpdate failed for vault={} updateStateCall={}', [ - vault.id, - updateStateCall.toHexString(), - ]) - assert(false, 'executeVaultUpdateState failed') - } - - let newRate: BigInt, totalAssets: BigInt, totalShares: BigInt - if (isGnosis) { - newRate = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() - totalAssets = ethereum.decode('uint256', resultValue[3].returnData)!.toBigInt() - totalShares = ethereum.decode('uint256', resultValue[4].returnData)!.toBigInt() - } else { - newRate = ethereum.decode('uint256', resultValue[1].returnData)!.toBigInt() - totalAssets = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() - totalShares = ethereum.decode('uint256', resultValue[3].returnData)!.toBigInt() - } - return [newRate, totalAssets, totalShares] +export function getAggregateCall(target: Address, data: Bytes): ethereum.Value { + const struct: Array = [ethereum.Value.fromAddress(target), ethereum.Value.fromBytes(data)] + return ethereum.Value.fromTuple(changetype(struct)) } -export function getPoolStateUpdate( - rewardsRoot: Bytes, - reward: BigInt, - unlockedMevReward: BigInt, - proof: Array, -): Array { - const isGnosis = isGnosisNetwork() - const rewardAssetsCall = Bytes.fromHexString(poolRewardAssetsSelector) - const principalAssetsCall = Bytes.fromHexString(poolPrincipalAssetsSelector) - const penaltyAssetsCall = Bytes.fromHexString(poolPenaltyAssetsSelector) - const updateStateCall = getUpdateStateCall(rewardsRoot, reward, unlockedMevReward, proof) - const swapXdaiToGnoCall = Bytes.fromHexString(swapXdaiToGnoSelector) - - const multicallContract = MulticallContract.bind(multicallContractAddr) - let calls: Array = [getAggregateCall(GENESIS_VAULT, updateStateCall)] - if (isGnosis) { - calls.push(getAggregateCall(GENESIS_VAULT, swapXdaiToGnoCall)) +export function calculateAverage(values: Array): BigDecimal { + if (values.length === 0) { + return BigDecimal.zero() } - calls.push(getAggregateCall(V2_REWARD_TOKEN, rewardAssetsCall)) - calls.push(getAggregateCall(V2_REWARD_TOKEN, penaltyAssetsCall)) - calls.push(getAggregateCall(V2_STAKED_TOKEN, principalAssetsCall)) - const result = multicallContract.call('tryAggregate', 'tryAggregate(bool,(address,bytes)[]):((bool,bytes)[])', [ - ethereum.Value.fromBoolean(false), - ethereum.Value.fromArray(calls), - ]) - const resultValue = result[0].toTupleArray() - if (!resultValue[0].success) { - log.error('[Vault] getPoolStateUpdate failed updateStateCall={}', [updateStateCall.toHexString()]) - assert(false, 'getPoolLatestRate failed') - } + // Start with a sum of zero. + let sum: BigDecimal = BigDecimal.zero() - let rewardAssets: BigInt, principalAssets: BigInt, penaltyAssets: BigInt - if (isGnosis) { - rewardAssets = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() - penaltyAssets = ethereum.decode('uint256', resultValue[3].returnData)!.toBigInt() - principalAssets = ethereum.decode('uint256', resultValue[4].returnData)!.toBigInt() - } else { - rewardAssets = ethereum.decode('uint256', resultValue[1].returnData)!.toBigInt() - penaltyAssets = ethereum.decode('uint256', resultValue[2].returnData)!.toBigInt() - principalAssets = ethereum.decode('uint256', resultValue[3].returnData)!.toBigInt() + // Iterate over all values to calculate the sum. + for (let i = 0; i < values.length; i++) { + sum = sum.plus(values[i]) } - let newRate = BigInt.fromString(WAD) - if (principalAssets.gt(BigInt.fromI32(0))) { - newRate = newRate.times(rewardAssets.plus(principalAssets).minus(penaltyAssets)).div(principalAssets) - } - - return [newRate, rewardAssets, principalAssets, penaltyAssets] -} -function getUpdateStateCall(rewardsRoot: Bytes, reward: BigInt, unlockedMevReward: BigInt, proof: Array): Bytes { - const updateStateArray: Array = [ - ethereum.Value.fromFixedBytes(rewardsRoot), - ethereum.Value.fromSignedBigInt(reward), - ethereum.Value.fromUnsignedBigInt(unlockedMevReward), - ethereum.Value.fromFixedBytesArray(proof), - ] - // Encode the tuple - const encodedUpdateStateArgs = ethereum.encode(ethereum.Value.fromTuple(changetype(updateStateArray))) - return Bytes.fromHexString(updateStateSelector).concat(encodedUpdateStateArgs as Bytes) -} - -function getConvertToAssetsCall(shares: BigInt): Bytes { - const encodedConvertToAssetsArgs = ethereum.encode(ethereum.Value.fromUnsignedBigInt(shares)) - return Bytes.fromHexString(convertToAssetsSelector).concat(encodedConvertToAssetsArgs as Bytes) -} - -function getAggregateCall(target: Address, data: Bytes): ethereum.Value { - const struct: Array = [ethereum.Value.fromAddress(target), ethereum.Value.fromBytes(data)] - return ethereum.Value.fromTuple(changetype(struct)) + // Divide the sum by the number of values to get the average. + // Note: BigDecimal division needs to handle scale/precision appropriately. + // Here, 'values.length' is converted to a BigDecimal for division. + return sum.div(BigDecimal.fromString(values.length.toString())) } diff --git a/src/mappings/depositDataRegistry.ts b/src/mappings/depositDataRegistry.ts index 542650c..50147cf 100644 --- a/src/mappings/depositDataRegistry.ts +++ b/src/mappings/depositDataRegistry.ts @@ -14,8 +14,6 @@ export function handleDepositDataManagerUpdated(event: DepositDataManagerUpdated // Vault must exist at the time of the event const vault = Vault.load(vaultAddress) as Vault vault.depositDataManager = depositDataManager - // Update deprecated vault keys manager - vault.keysManager = depositDataManager vault.save() createTransaction(event.transaction.hash.toHex()) @@ -34,8 +32,6 @@ export function handleDepositDataMigrated(event: DepositDataMigrated): void { // Vault must exist at the time of the event const vault = Vault.load(vaultAddress) as Vault vault.depositDataRoot = depositDataRoot - // Update deprecated validators root - vault.validatorsRoot = depositDataRoot // zero address is when the default deposit data manager was used (admin) if (depositDataManager.equals(Address.zero())) { @@ -43,8 +39,6 @@ export function handleDepositDataMigrated(event: DepositDataMigrated): void { } else { vault.depositDataManager = depositDataManager } - // Update deprecated vault keys manager - vault.keysManager = vault.depositDataManager vault.save() createTransaction(event.transaction.hash.toHex()) @@ -67,8 +61,6 @@ export function handleDepositDataRootUpdated(event: DepositDataRootUpdated): voi return } vault.depositDataRoot = depositDataRoot - // Update deprecated validators root - vault.validatorsRoot = depositDataRoot vault.save() createTransaction(event.transaction.hash.toHex()) diff --git a/src/mappings/erc20Token.ts b/src/mappings/erc20Token.ts new file mode 100644 index 0000000..5864a00 --- /dev/null +++ b/src/mappings/erc20Token.ts @@ -0,0 +1,97 @@ +import { Address, BigInt, log, store } from '@graphprotocol/graph-ts' +import { Transfer } from '../../generated/OsToken/Erc20Token' +import { createOrLoadSwiseTokenHolder, createTokenTransfer } from '../entities/tokenTransfer' +import { OS_TOKEN, SWISE_TOKEN } from '../helpers/constants' +import { convertOsTokenSharesToAssets, createOrLoadOsToken, createOrLoadOsTokenHolder } from '../entities/osToken' +import { createOrLoadNetwork, createOrLoadUser } from '../entities/network' + +export function handleTransfer(event: Transfer): void { + const tokenAddress = event.address + let tokenSymbol: string + if (tokenAddress.equals(OS_TOKEN)) { + _handleOsTokenTransfer(event) + tokenSymbol = 'osToken' + } else if (tokenAddress.equals(SWISE_TOKEN)) { + _handleSwiseTokenTransfer(event) + tokenSymbol = 'SWISE' + } else { + log.error('[ERC20Token] Unknown token address {}', [tokenAddress.toHexString()]) + return + } + + createTokenTransfer( + event.transaction.hash.toHex(), + event.params.from, + event.params.to, + event.params.value, + event.block.timestamp, + tokenSymbol, + ) + + log.info('[ERC20Token] Transfer token={} from={} to={} amount={}', [ + tokenSymbol, + event.params.from.toHexString(), + event.params.to.toHexString(), + event.params.value.toString(), + ]) +} + +function _handleSwiseTokenTransfer(event: Transfer): void { + const from = event.params.from + const to = event.params.to + const amount = event.params.value + + if (from.notEqual(Address.zero())) { + const tokenHolderFrom = createOrLoadSwiseTokenHolder(from) + + tokenHolderFrom.balance = tokenHolderFrom.balance.minus(amount) + tokenHolderFrom.transfersCount = tokenHolderFrom.transfersCount.plus(BigInt.fromI32(1)) + tokenHolderFrom.save() + } + if (to.notEqual(Address.zero())) { + const tokenHolderTo = createOrLoadSwiseTokenHolder(to) + tokenHolderTo.balance = tokenHolderTo.balance.plus(amount) + tokenHolderTo.transfersCount = tokenHolderTo.transfersCount.plus(BigInt.fromI32(1)) + tokenHolderTo.save() + } +} + +function _handleOsTokenTransfer(event: Transfer): void { + const from = event.params.from + const to = event.params.to + const amount = event.params.value + + const osToken = createOrLoadOsToken() + if (from.notEqual(Address.zero())) { + const tokenHolderFrom = createOrLoadOsTokenHolder(osToken, from) + tokenHolderFrom.balance = tokenHolderFrom.balance.minus(amount) + tokenHolderFrom.assets = convertOsTokenSharesToAssets(osToken, tokenHolderFrom.balance) + tokenHolderFrom.transfersCount = tokenHolderFrom.transfersCount.plus(BigInt.fromI32(1)) + tokenHolderFrom.save() + + const user = createOrLoadUser(from) + if (tokenHolderFrom.balance.isZero() && user.vaultsCount === 0) { + const network = createOrLoadNetwork() + network.usersCount = network.usersCount - 1 + network.save() + store.remove('User', user.id) + } + } + if (to.notEqual(Address.zero())) { + const tokenHolderTo = createOrLoadOsTokenHolder(osToken, to) + tokenHolderTo.balance = tokenHolderTo.balance.plus(amount) + tokenHolderTo.assets = convertOsTokenSharesToAssets(osToken, tokenHolderTo.balance) + tokenHolderTo.transfersCount = tokenHolderTo.transfersCount.plus(BigInt.fromI32(1)) + tokenHolderTo.save() + + const user = createOrLoadUser(to) + if (!user.isOsTokenHolder && user.vaultsCount === 0 && tokenHolderTo.balance.gt(BigInt.zero())) { + const network = createOrLoadNetwork() + network.usersCount = network.usersCount + 1 + network.save() + + user.isOsTokenHolder = true + user.save() + } + } +} diff --git a/src/mappings/erc20Vault.ts b/src/mappings/erc20Vault.ts index 10a1545..54f068d 100644 --- a/src/mappings/erc20Vault.ts +++ b/src/mappings/erc20Vault.ts @@ -1,16 +1,32 @@ -import { Address, store, log } from '@graphprotocol/graph-ts' +import { Address, log } from '@graphprotocol/graph-ts' import { Transfer } from '../../generated/templates/Erc20Vault/Erc20Vault' -import { createAllocatorAction, createOrLoadAllocator } from '../entities/allocator' +import { Vault } from '../../generated/schema' +import { + AllocatorActionType, + createAllocatorAction, + createOrLoadAllocator, + getAllocatorLtv, + getAllocatorLtvStatus, + getAllocatorOsTokenMintApy, +} from '../entities/allocator' import { createTransaction } from '../entities/transaction' +import { convertSharesToAssets } from '../entities/vaults' +import { createOrLoadOsToken } from '../entities/osToken' +import { createOrLoadOsTokenConfig } from '../entities/osTokenConfig' +import { decreaseUserVaultsCount, increaseUserVaultsCount } from '../entities/network' // Event emitted on mint, burn or transfer shares between allocators export function handleTransfer(event: Transfer): void { const params = event.params + const vaultAddress = event.address + const vault = Vault.load(vaultAddress.toHex()) as Vault + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const from = params.from const to = params.to - const value = params.value - const vaultAddress = event.address + const shares = params.value + const assets = convertSharesToAssets(vault, shares) const zeroAddress = Address.zero() if (from.equals(zeroAddress) || to.equals(zeroAddress)) { @@ -19,25 +35,36 @@ export function handleTransfer(event: Transfer): void { } const allocatorFrom = createOrLoadAllocator(from, vaultAddress) - allocatorFrom.shares = allocatorFrom.shares.minus(value) + allocatorFrom.shares = allocatorFrom.shares.minus(shares) + allocatorFrom.assets = convertSharesToAssets(vault, allocatorFrom.shares) + allocatorFrom.ltv = getAllocatorLtv(allocatorFrom, osToken) + allocatorFrom.ltvStatus = getAllocatorLtvStatus(allocatorFrom, osTokenConfig) + allocatorFrom.osTokenMintApy = getAllocatorOsTokenMintApy(allocatorFrom, osToken.apy, osToken, osTokenConfig) + allocatorFrom.save() if (allocatorFrom.shares.isZero()) { - store.remove('Allocator', allocatorFrom.id) - } else { - allocatorFrom.save() + decreaseUserVaultsCount(allocatorFrom.address) } - createAllocatorAction(event, vaultAddress, 'TransferOut', from, null, value) + createAllocatorAction(event, vaultAddress, AllocatorActionType.TransferOut, from, assets, shares) const allocatorTo = createOrLoadAllocator(to, vaultAddress) - allocatorTo.shares = allocatorTo.shares.plus(value) + if (allocatorTo.shares.isZero() && !shares.isZero()) { + increaseUserVaultsCount(allocatorTo.address) + } + allocatorTo.shares = allocatorTo.shares.plus(shares) + allocatorTo.assets = convertSharesToAssets(vault, allocatorTo.shares) + allocatorTo.ltv = getAllocatorLtv(allocatorTo, osToken) + allocatorTo.ltvStatus = getAllocatorLtvStatus(allocatorTo, osTokenConfig) + allocatorTo.osTokenMintApy = getAllocatorOsTokenMintApy(allocatorTo, osToken.apy, osToken, osTokenConfig) allocatorTo.save() - createAllocatorAction(event, vaultAddress, 'TransferIn', to, null, value) + createAllocatorAction(event, vaultAddress, AllocatorActionType.TransferIn, to, assets, shares) createTransaction(event.transaction.hash.toHex()) - log.info('[Vault] Transfer vault={} from={} to={} value={}', [ + log.info('[Vault] Transfer vault={} from={} to={} shares={} assets={}', [ vaultAddress.toHex(), - params.from.toHex(), - params.to.toHex(), - params.value.toString(), + from.toHex(), + to.toHex(), + shares.toString(), + assets.toString(), ]) } diff --git a/src/mappings/exchangeRates.ts b/src/mappings/exchangeRates.ts new file mode 100644 index 0000000..1ed5037 --- /dev/null +++ b/src/mappings/exchangeRates.ts @@ -0,0 +1,73 @@ +import { Address, BigDecimal, BigInt, ethereum, log } from '@graphprotocol/graph-ts' +import { PriceFeed as PriceFeedContract } from '../../generated/ExchangeRates/PriceFeed' +import { + ASSETS_USD_PRICE_FEED, + EUR_USD_PRICE_FEED, + GBP_USD_PRICE_FEED, + DAI_USD_PRICE_FEED, + ZERO_ADDRESS, +} from '../helpers/constants' +import { ExchangeRateSnapshot } from '../../generated/schema' +import { createOrLoadNetwork } from '../entities/network' + +export function handleExchangeRates(block: ethereum.Block): void { + const decimals = BigDecimal.fromString('100000000') + + let assetsUsdRate = BigDecimal.zero() + let eurToUsdRate = BigDecimal.zero() + let gbpToUsdRate = BigDecimal.zero() + let daiToUsdRate = BigDecimal.zero() + let response: BigInt + let priceFeedContract: PriceFeedContract + if (ASSETS_USD_PRICE_FEED != ZERO_ADDRESS) { + priceFeedContract = PriceFeedContract.bind(Address.fromString(ASSETS_USD_PRICE_FEED)) + response = priceFeedContract.latestAnswer() + assetsUsdRate = new BigDecimal(response).div(decimals) + } + + if (EUR_USD_PRICE_FEED != ZERO_ADDRESS) { + priceFeedContract = PriceFeedContract.bind(Address.fromString(EUR_USD_PRICE_FEED)) + response = priceFeedContract.latestAnswer() + eurToUsdRate = new BigDecimal(response).div(decimals) + } + + if (GBP_USD_PRICE_FEED != ZERO_ADDRESS) { + priceFeedContract = PriceFeedContract.bind(Address.fromString(GBP_USD_PRICE_FEED)) + response = priceFeedContract.latestAnswer() + gbpToUsdRate = new BigDecimal(response).div(decimals) + } + + if (DAI_USD_PRICE_FEED != ZERO_ADDRESS) { + priceFeedContract = PriceFeedContract.bind(Address.fromString(DAI_USD_PRICE_FEED)) + response = priceFeedContract.latestAnswer() + daiToUsdRate = new BigDecimal(response).div(decimals) + } + + const zero = BigDecimal.zero() + const one = BigDecimal.fromString('1') + const usdToEurRate = eurToUsdRate.gt(zero) ? one.div(eurToUsdRate) : zero + const usdToGbpRate = gbpToUsdRate.gt(zero) ? one.div(gbpToUsdRate) : zero + const usdToDaiRate = daiToUsdRate.gt(zero) ? one.div(daiToUsdRate) : zero + + const network = createOrLoadNetwork() + network.assetsUsdRate = assetsUsdRate + network.usdToEurRate = usdToEurRate + network.usdToGbpRate = usdToGbpRate + network.usdToDaiRate = usdToDaiRate + network.save() + + const exchangeRateSnapshot = new ExchangeRateSnapshot(block.timestamp.toString()) + exchangeRateSnapshot.timestamp = block.timestamp.toI64() + exchangeRateSnapshot.assetsUsdRate = assetsUsdRate + exchangeRateSnapshot.usdToDaiRate = usdToDaiRate + exchangeRateSnapshot.usdToEurRate = usdToEurRate + exchangeRateSnapshot.usdToGbpRate = usdToGbpRate + exchangeRateSnapshot.save() + + log.info('[ExchangeRates] assetsUsdRate={} usdToEurRate={} usdToGbpRate={} usdToDaiRate={}', [ + assetsUsdRate.toString(), + usdToEurRate.toString(), + usdToGbpRate.toString(), + usdToDaiRate.toString(), + ]) +} diff --git a/src/mappings/gnoVault.ts b/src/mappings/gnoVault.ts index bc49c6b..5771b0a 100644 --- a/src/mappings/gnoVault.ts +++ b/src/mappings/gnoVault.ts @@ -1,22 +1,57 @@ import { BigInt, log } from '@graphprotocol/graph-ts' -import { Vault } from '../../generated/schema' +import { Allocator, Vault } from '../../generated/schema' import { XdaiSwapped } from '../../generated/templates/GnoVault/GnoVault' +import { convertSharesToAssets, snapshotVault } from '../entities/vaults' +import { createOrLoadNetwork } from '../entities/network' +import { + getAllocatorLtv, + getAllocatorLtvStatus, + getAllocatorOsTokenMintApy, + snapshotAllocator, +} from '../entities/allocator' +import { createOrLoadOsTokenConfig } from '../entities/osTokenConfig' +import { createOrLoadOsToken } from '../entities/osToken' // Event emitted when xDAI is swapped to GNO export function handleXdaiSwapped(event: XdaiSwapped): void { const params = event.params const vaultAddress = event.address - let gnoAssets = params.assets + const timestamp = event.block.timestamp + const gnoAssets = params.assets const xdaiAssets = params.amount const vault = Vault.load(vaultAddress.toHex()) as Vault - vault.unconvertedExecutionReward = vault.unconvertedExecutionReward.le(xdaiAssets) - ? BigInt.zero() - : vault.unconvertedExecutionReward.minus(xdaiAssets) - - vault.principalAssets = vault.principalAssets.plus(gnoAssets) + vault.totalAssets = vault.totalAssets.plus(gnoAssets) vault.save() + snapshotVault(vault, gnoAssets, timestamp) + + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(gnoAssets) + network.totalEarnedAssets = network.totalEarnedAssets.plus(gnoAssets) + network.save() + + // update allocators + const osToken = createOrLoadOsToken() + let allocator: Allocator + let allocatorAssetsDiff: BigInt + let allocatorNewAssets: BigInt + let allocators: Array = vault.allocators.load() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + for (let j = 0; j < allocators.length; j++) { + allocator = allocators[j] + if (allocator.shares.isZero()) { + continue + } + allocatorNewAssets = convertSharesToAssets(vault, allocator.shares) + allocatorAssetsDiff = allocatorNewAssets.minus(allocator.assets) + allocator.assets = allocatorNewAssets + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + snapshotAllocator(allocator, osToken, osTokenConfig, allocatorAssetsDiff, BigInt.zero(), timestamp) + } log.info('[GnoVault] XdaiSwapped vault={} xdai={} gno={}', [ vaultAddress.toHexString(), diff --git a/src/mappings/keeper.ts b/src/mappings/keeper.ts index ed69439..3ff7339 100644 --- a/src/mappings/keeper.ts +++ b/src/mappings/keeper.ts @@ -9,15 +9,6 @@ import { JSONValue, log, } from '@graphprotocol/graph-ts' - -import { Vault } from '../../generated/schema' -import { Harvested, RewardsUpdated, ValidatorsApproval } from '../../generated/Keeper/Keeper' -import { - FoxVault as FoxVaultTemplate, - RewardSplitterFactory as RewardSplitterFactoryTemplate, - VaultFactory as VaultFactoryTemplate, -} from '../../generated/templates' -import { updatePoolApy, updateVaultApy } from '../entities/apySnapshots' import { BLOCKLIST_ERC20_VAULT_FACTORY_V2, BLOCKLIST_VAULT_FACTORY_V2, @@ -29,22 +20,47 @@ import { PRIV_ERC20_VAULT_FACTORY_V2, PRIV_VAULT_FACTORY_V1, PRIV_VAULT_FACTORY_V2, + RESTAKE_BLOCKLIST_ERC20_VAULT_FACTORY_V2, + RESTAKE_BLOCKLIST_VAULT_FACTORY_V2, + RESTAKE_ERC20_VAULT_FACTORY_V2, + RESTAKE_PRIV_ERC20_VAULT_FACTORY_V2, + RESTAKE_PRIV_VAULT_FACTORY_V2, + RESTAKE_VAULT_FACTORY_V2, REWARD_SPLITTER_FACTORY_V1, REWARD_SPLITTER_FACTORY_V2, VAULT_FACTORY_V1, VAULT_FACTORY_V2, - RESTAKE_VAULT_FACTORY_V2, - RESTAKE_PRIV_VAULT_FACTORY_V2, - RESTAKE_BLOCKLIST_VAULT_FACTORY_V2, - RESTAKE_ERC20_VAULT_FACTORY_V2, - RESTAKE_PRIV_ERC20_VAULT_FACTORY_V2, - RESTAKE_BLOCKLIST_ERC20_VAULT_FACTORY_V2, - ZERO_ADDRESS, WAD, + ZERO_ADDRESS, } from '../helpers/constants' -import { getPoolStateUpdate, getVaultStateUpdate, getVaultTotalAssets, isGnosisNetwork } from '../helpers/utils' -import { createOrLoadVaultsStat } from '../entities/vaults' -import { createOrLoadV2Pool } from '../entities/v2pool' +import { + FoxVault as FoxVaultTemplate, + RewardSplitterFactory as RewardSplitterFactoryTemplate, + VaultFactory as VaultFactoryTemplate, +} from '../../generated/templates' +import { Allocator, OsTokenHolder, Vault } from '../../generated/schema' +import { + convertOsTokenSharesToAssets, + createOrLoadOsToken, + snapshotOsToken, + snapshotOsTokenHolder, + updateOsTokenApy, + updateOsTokenTotalAssets, +} from '../entities/osToken' +import { + getAllocatorsMintedShares, + snapshotAllocator, + getAllocatorLtv, + getAllocatorOsTokenMintApy, + getAllocatorLtvStatus, +} from '../entities/allocator' +import { createOrLoadNetwork, isGnosisNetwork } from '../entities/network' +import { Harvested, RewardsUpdated, ValidatorsApproval } from '../../generated/Keeper/Keeper' +import { convertSharesToAssets, getVaultStateUpdate, snapshotVault, updateVaultApy } from '../entities/vaults' +import { createOrLoadV2Pool, getPoolStateUpdate, updatePoolApy } from '../entities/v2pool' +import { createOrLoadOsTokenConfig } from '../entities/osTokenConfig' +import { updateExitRequests } from '../entities/exitRequests' +import { updateRewardSplitters } from '../entities/rewardSplitter' const IS_PRIVATE_KEY = 'isPrivate' const IS_ERC20_KEY = 'isErc20' @@ -187,11 +203,37 @@ export function updateRewards( rewardsRoot: Bytes, updateTimestamp: BigInt, rewardsIpfsHash: string, + newAvgRewardPerSecond: BigInt, + block: ethereum.Block, ): void { const vaultRewards = value.toObject().mustGet('vaults').toArray() - const vaultsStat = createOrLoadVaultsStat() + const network = createOrLoadNetwork() const isGnosis = isGnosisNetwork() const v2Pool = createOrLoadV2Pool() + + // process OsToken rewards update + const osToken = createOrLoadOsToken() + updateOsTokenApy(osToken, newAvgRewardPerSecond) + const osTokenTotalAssetsDiff = updateOsTokenTotalAssets(osToken, updateTimestamp, block) + osToken.save() + snapshotOsToken(osToken, osTokenTotalAssetsDiff, updateTimestamp) + + // update assets of all the osToken holders + const osTokenHolders: Array = osToken.holders.load() + let osTokenHolder: OsTokenHolder + let osTokenAssetsBefore: BigInt + for (let i = 0; i < osTokenHolders.length; i++) { + osTokenHolder = osTokenHolders[i] + if (osTokenHolder.balance.isZero()) { + continue + } + osTokenAssetsBefore = osTokenHolder.assets + osTokenHolder.assets = convertOsTokenSharesToAssets(osToken, osTokenHolder.balance) + osTokenHolder.save() + snapshotOsTokenHolder(osTokenHolder, osTokenHolder.assets.minus(osTokenAssetsBefore), updateTimestamp) + } + + // process vault rewards for (let i = 0; i < vaultRewards.length; i++) { // load vault object const vaultReward = vaultRewards[i].toObject() @@ -203,8 +245,7 @@ export function updateRewards( } // extract vault reward data - const lockedMevReward = - vault.mevEscrow === null ? vaultReward.mustGet('locked_mev_reward').toBigInt() : BigInt.zero() + const lockedMevReward = !vault.mevEscrow ? vaultReward.mustGet('locked_mev_reward').toBigInt() : BigInt.zero() const unlockedMevReward = vaultReward.mustGet('unlocked_mev_reward').toBigInt() const consensusReward = vaultReward.mustGet('consensus_reward').toBigInt() const proof = vaultReward @@ -230,16 +271,18 @@ export function updateRewards( } // fetch new principal, total assets and rate - let newRate: BigInt, newTotalAssets: BigInt, newTotalShares: BigInt + let newRate: BigInt, newTotalAssets: BigInt, newTotalShares: BigInt, newExitingAssets: BigInt if (vault.isGenesis && !v2Pool.migrated) { newRate = BigInt.fromString(WAD) newTotalAssets = BigInt.zero() newTotalShares = BigInt.zero() + newExitingAssets = BigInt.zero() } else { const stateUpdate = getVaultStateUpdate(vault, rewardsRoot, proofReward, proofUnlockedMevReward, proof) newRate = stateUpdate[0] newTotalAssets = stateUpdate[1] newTotalShares = stateUpdate[2] + newExitingAssets = stateUpdate[3] updateVaultApy(vault, vault.rewardsTimestamp, updateTimestamp, newRate.minus(vault.rate)) } @@ -249,9 +292,16 @@ export function updateRewards( slashedMevReward = slashedMevReward.plus(vault.lockedExecutionReward.minus(lockedMevReward)) } - vaultsStat.totalAssets = vaultsStat.totalAssets.minus(vault.totalAssets).plus(newTotalAssets) + // update vault + const maxPercent = BigInt.fromI32(10000) + const rewardsDiff = vault.totalAssets + .times(newRate.minus(vault.rate)) + .times(maxPercent.plus(BigInt.fromI32(vault.feePercent))) + .div(BigInt.fromString(WAD)) + .div(maxPercent) vault.totalAssets = newTotalAssets vault.totalShares = newTotalShares + vault.exitingAssets = newExitingAssets vault.rate = newRate vault.rewardsRoot = rewardsRoot vault.proofReward = proofReward @@ -266,6 +316,13 @@ export function updateRewards( vault.canHarvest = true vault.save() + network.totalAssets = network.totalAssets.minus(vault.totalAssets).plus(newTotalAssets) + network.totalEarnedAssets = network.totalEarnedAssets.plus(rewardsDiff) + + if (!vault.isGenesis || v2Pool.migrated) { + snapshotVault(vault, rewardsDiff, updateTimestamp) + } + // update v2 pool data if (vault.isGenesis && v2Pool.migrated) { const stateUpdate = getPoolStateUpdate(rewardsRoot, proofReward, proofUnlockedMevReward, proof) @@ -273,26 +330,82 @@ export function updateRewards( const newRewardAssets = stateUpdate[1] const newPrincipalAssets = stateUpdate[2] const newPenaltyAssets = stateUpdate[3] + const poolNewTotalAssets = newRewardAssets.plus(newPrincipalAssets).minus(newPenaltyAssets) + + network.totalAssets = network.totalAssets.plus(poolNewTotalAssets).minus(v2Pool.totalAssets) updatePoolApy(v2Pool, v2Pool.rewardsTimestamp, updateTimestamp, newRate.minus(v2Pool.rate)) v2Pool.rate = newRate v2Pool.principalAssets = newPrincipalAssets v2Pool.rewardAssets = newRewardAssets v2Pool.penaltyAssets = newPenaltyAssets - v2Pool.totalAssets = newRewardAssets.plus(newPrincipalAssets).minus(newPenaltyAssets) + v2Pool.totalAssets = poolNewTotalAssets v2Pool.rewardsTimestamp = updateTimestamp v2Pool.save() } + + // update allocators + let allocator: Allocator + let allocatorAssetsDiff: BigInt + let allocatorNewAssets: BigInt + let allocatorNewMintedOsTokenShares: BigInt + let allocatorMintedOsTokenSharesDiff: BigInt + let allocators: Array = vault.allocators.load() + const allocatorsMintedOsTokenShares = getAllocatorsMintedShares(vault, allocators) + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + for (let j = 0; j < allocators.length; j++) { + allocator = allocators[j] + if (allocator.shares.isZero()) { + continue + } + allocatorNewAssets = convertSharesToAssets(vault, allocator.shares) + allocatorAssetsDiff = allocatorNewAssets.minus(allocator.assets) + allocator.assets = allocatorNewAssets + + allocatorNewMintedOsTokenShares = allocatorsMintedOsTokenShares[j] + allocatorMintedOsTokenSharesDiff = allocatorNewMintedOsTokenShares.minus(allocator.mintedOsTokenShares) + allocator.mintedOsTokenShares = allocatorNewMintedOsTokenShares + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + snapshotAllocator( + allocator, + osToken, + osTokenConfig, + allocatorAssetsDiff, + allocatorMintedOsTokenSharesDiff, + updateTimestamp, + ) + } + + // update exit requests + updateExitRequests(vault, block) + + // update reward splitters + updateRewardSplitters(vault) } - vaultsStat.save() + network.save() } export function handleRewardsUpdated(event: RewardsUpdated): void { const rewardsRoot = event.params.rewardsRoot const rewardsIpfsHash = event.params.rewardsIpfsHash const updateTimestamp = event.params.updateTimestamp + const newAvgRewardPerSecond = event.params.avgRewardPerSecond - const data = ipfs.cat(rewardsIpfsHash) as Bytes - updateRewards(json.fromBytes(data), rewardsRoot, updateTimestamp, rewardsIpfsHash) + let data: Bytes | null = ipfs.cat(rewardsIpfsHash) + while (data === null) { + log.warning('[Keeper] RewardsUpdated ipfs.cat failed, retrying', []) + data = ipfs.cat(rewardsIpfsHash) + } + updateRewards( + json.fromBytes(data as Bytes), + rewardsRoot, + updateTimestamp, + rewardsIpfsHash, + newAvgRewardPerSecond, + event.block, + ) log.info('[Keeper] RewardsUpdated rewardsRoot={} rewardsIpfsHash={} updateTimestamp={}', [ rewardsRoot.toHex(), rewardsIpfsHash, @@ -311,17 +424,14 @@ export function handleHarvested(event: Harvested): void { return } vault.canHarvest = (vault.rewardsRoot as Bytes).notEqual(event.params.rewardsRoot) + vault.save() if (vault.isGenesis) { const v2Pool = createOrLoadV2Pool() if (!v2Pool.migrated) { v2Pool.migrated = true v2Pool.save() } - vault.principalAssets = getVaultTotalAssets(vault) - } else { - vault.principalAssets = vault.principalAssets.plus(totalAssetsDelta) } - vault.save() log.info('[Keeper] Harvested vault={} totalAssetsDelta={}', [vaultAddress, totalAssetsDelta.toString()]) } @@ -339,3 +449,13 @@ export function handleValidatorsApproval(event: ValidatorsApproval): void { log.info('[Keeper] ValidatorsApproval vault={}', [vaultAddress]) } + +export function handleExitRequests(block: ethereum.Block): void { + const network = createOrLoadNetwork() + let vault: Vault + for (let i = 0; i < network.vaultIds.length; i++) { + vault = Vault.load(network.vaultIds[i]) as Vault + updateExitRequests(vault, block) + } + log.info('[ExitRequests] Sync exit requests at block={}', [block.number.toString()]) +} diff --git a/src/mappings/mevEscrow.ts b/src/mappings/mevEscrow.ts deleted file mode 100644 index 466d458..0000000 --- a/src/mappings/mevEscrow.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { log, dataSource } from '@graphprotocol/graph-ts' - -import { Vault } from '../../generated/schema' -import { Harvested } from '../../generated/templates/OwnMevEscrow/OwnMevEscrow' -import { isGnosisNetwork } from '../helpers/utils' - -// Event emitted on OwnMevEscrow harvesting rewards -export function handleHarvested(event: Harvested): void { - // ignore for gnosis networks - if (isGnosisNetwork()) return - - const totalAssetsDelta = event.params.assets - const context = dataSource.context() - const vaultId = context.getString('vault') - const vault = Vault.load(vaultId) as Vault - vault.principalAssets = vault.principalAssets.plus(totalAssetsDelta) - vault.save() - log.info('[OwnMevEscrow] Harvested vault={} totalAssetsDelta={}', [vaultId, totalAssetsDelta.toString()]) -} diff --git a/src/mappings/osToken.ts b/src/mappings/osToken.ts index b326fcc..f9c0bc4 100644 --- a/src/mappings/osToken.ts +++ b/src/mappings/osToken.ts @@ -1,67 +1,17 @@ import { log } from '@graphprotocol/graph-ts' -import { Transfer } from '../../generated/Erc20Token/Erc20Token' -import { - AvgRewardPerSecondUpdated, - FeePercentUpdated, - StateUpdated, -} from '../../generated/OsTokenVaultController/OsTokenVaultController' -import { updateOsTokenApy } from '../entities/apySnapshots' -import { createTokenTransfer } from '../entities/tokenTransfer' -import { createOrLoadOsToken, isSupportedOsTokenHolder, createOrLoadOsTokenHolder } from '../entities/osToken' - -export function handleAvgRewardPerSecondUpdated(event: AvgRewardPerSecondUpdated): void { - const newAvgRewardPerSecond = event.params.avgRewardPerSecond - const osToken = createOrLoadOsToken() - - // update OsToken - updateOsTokenApy(osToken, newAvgRewardPerSecond, event.block.timestamp) - osToken.save() - - log.info('[OsTokenController] AvgRewardPerSecondUpdated avgRewardPerSecond={}', [newAvgRewardPerSecond.toString()]) -} +import { FeePercentUpdated, StateUpdated } from '../../generated/OsTokenVaultController/OsTokenVaultController' +import { convertOsTokenSharesToAssets, createOrLoadOsToken } from '../entities/osToken' export function handleStateUpdated(event: StateUpdated): void { const shares = event.params.treasuryShares const osToken = createOrLoadOsToken() + osToken.totalAssets = osToken.totalAssets.plus(convertOsTokenSharesToAssets(osToken, shares)) osToken.totalSupply = osToken.totalSupply.plus(shares) osToken.save() log.info('[OsTokenController] StateUpdated treasuryShares={}', [shares.toString()]) } -export function handleTransfer(event: Transfer): void { - if (isSupportedOsTokenHolder(event.params.from)) { - let fromHolder = createOrLoadOsTokenHolder(event.params.from) - - fromHolder.shares = fromHolder.shares.minus(event.params.value) - fromHolder.timestamp = event.block.timestamp - fromHolder.save() - } - - if (isSupportedOsTokenHolder(event.params.to)) { - let toHolder = createOrLoadOsTokenHolder(event.params.to) - - toHolder.shares = toHolder.shares.plus(event.params.value) - toHolder.timestamp = event.block.timestamp - toHolder.save() - } - - createTokenTransfer( - event.transaction.hash.toHex(), - event.params.from, - event.params.to, - event.params.value, - event.block.timestamp, - 'osToken', - ) - - log.info('[OsToken] Transfer from={} to={} amount={}', [ - event.params.from.toHexString(), - event.params.to.toHexString(), - event.params.value.toString(), - ]) -} - export function handleFeePercentUpdated(event: FeePercentUpdated): void { const osToken = createOrLoadOsToken() osToken.feePercent = event.params.feePercent diff --git a/src/mappings/osTokenConfig.ts b/src/mappings/osTokenConfig.ts index fc50759..45c1c18 100644 --- a/src/mappings/osTokenConfig.ts +++ b/src/mappings/osTokenConfig.ts @@ -3,30 +3,19 @@ import { Vault } from '../../generated/schema' import { createOrLoadOsTokenConfig } from '../entities/osTokenConfig' import { OsTokenConfigUpdated as OsTokenConfigV1Updated } from '../../generated/OsTokenConfigV1/OsTokenConfigV1' import { OsTokenConfigUpdated as OsTokenConfigV2Updated } from '../../generated/OsTokenConfigV2/OsTokenConfigV2' - -export function updateOsTokenConfig(version: string, ltvPercent: BigInt, liqThresholdPercent: BigInt): void { - const osTokenConfig = createOrLoadOsTokenConfig(version) - - osTokenConfig.ltvPercent = ltvPercent - osTokenConfig.liqThresholdPercent = liqThresholdPercent - - osTokenConfig.save() - - log.info('[OsTokenConfig] OsTokenConfigUpdated version={} ltvPercent={} liqThresholdPercent={}', [ - version, - ltvPercent.toString(), - liqThresholdPercent.toString(), - ]) -} +import { updateAllocatorsLtvStatus } from '../entities/allocator' export function handleOsTokenConfigV1Updated(event: OsTokenConfigV1Updated): void { const ltvPercent = event.params.ltvPercent const liqThresholdPercent = event.params.liqThresholdPercent const multiplier = BigInt.fromString('100000000000000') - const modifiedLiqThresholdPercent = BigInt.fromI32(liqThresholdPercent).times(multiplier) - - updateOsTokenConfig('1', BigInt.fromI32(ltvPercent), modifiedLiqThresholdPercent) + updateOsTokenConfig( + '1', + BigInt.fromI32(ltvPercent).times(multiplier), + BigInt.fromI32(liqThresholdPercent).times(multiplier), + ) + updateAllocatorsLtvStatus() } export function handleOsTokenConfigV2Updated(event: OsTokenConfigV2Updated): void { @@ -48,9 +37,19 @@ export function handleOsTokenConfigV2Updated(event: OsTokenConfigV2Updated): voi vault.save() } + updateAllocatorsLtvStatus() +} + +function updateOsTokenConfig(version: string, ltvPercent: BigInt, liqThresholdPercent: BigInt): void { + const osTokenConfig = createOrLoadOsTokenConfig(version) + + osTokenConfig.ltvPercent = ltvPercent + osTokenConfig.liqThresholdPercent = liqThresholdPercent + + osTokenConfig.save() - log.info('[OsTokenConfig] OsTokenConfigV2Updated vault={} ltvPercent={} liqThresholdPercent={}', [ - vaultAddress, + log.info('[OsTokenConfig] OsTokenConfigUpdated version={} ltvPercent={} liqThresholdPercent={}', [ + version, ltvPercent.toString(), liqThresholdPercent.toString(), ]) diff --git a/src/mappings/rewardSplitter.ts b/src/mappings/rewardSplitter.ts index b12d53d..6c6ee79 100644 --- a/src/mappings/rewardSplitter.ts +++ b/src/mappings/rewardSplitter.ts @@ -1,10 +1,15 @@ -import { log, BigInt } from '@graphprotocol/graph-ts' +import { BigInt, log } from '@graphprotocol/graph-ts' import { RewardSplitter as RewardSplitterTemplate } from '../../generated/templates' -import { SharesIncreased, SharesDecreased } from '../../generated/templates/RewardSplitter/RewardSplitter' +import { + RewardsWithdrawn, + SharesDecreased, + SharesIncreased, +} from '../../generated/templates/RewardSplitter/RewardSplitter' import { RewardSplitterCreated } from '../../generated/templates/RewardSplitterFactory/RewardSplitterFactory' -import { RewardSplitter } from '../../generated/schema' +import { RewardSplitter, Vault } from '../../generated/schema' import { createTransaction } from '../entities/transaction' import { createOrLoadRewardSplitterShareHolder } from '../entities/rewardSplitter' +import { convertSharesToAssets } from '../entities/vaults' // Event emitted on RewardSplitter contract creation export function handleRewardSplitterCreated(event: RewardSplitterCreated): void { @@ -18,6 +23,7 @@ export function handleRewardSplitterCreated(event: RewardSplitterCreated): void rewardSplitter.totalShares = BigInt.zero() rewardSplitter.owner = owner rewardSplitter.vault = vault + rewardSplitter.lastSnapshotTimestamp = event.block.timestamp rewardSplitter.save() createTransaction(txHash) @@ -43,7 +49,7 @@ export function handleSharesIncreased(event: SharesIncreased): void { rewardSplitter.totalShares = rewardSplitter.totalShares.plus(shares) rewardSplitter.save() - const shareHolder = createOrLoadRewardSplitterShareHolder(account, rewardSplitterAddress) + const shareHolder = createOrLoadRewardSplitterShareHolder(account, rewardSplitterAddress, rewardSplitter.vault) shareHolder.shares = shareHolder.shares.plus(shares) shareHolder.save() @@ -69,7 +75,7 @@ export function handleSharesDecreased(event: SharesDecreased): void { rewardSplitter.totalShares = rewardSplitter.totalShares.minus(shares) rewardSplitter.save() - const shareHolder = createOrLoadRewardSplitterShareHolder(account, rewardSplitterAddress) + const shareHolder = createOrLoadRewardSplitterShareHolder(account, rewardSplitterAddress, rewardSplitter.vault) shareHolder.shares = shareHolder.shares.minus(shares) shareHolder.save() @@ -82,3 +88,32 @@ export function handleSharesDecreased(event: SharesDecreased): void { shares.toString(), ]) } + +// Event emitted on RewardSplitter rewards withdrawal +export function handleRewardsWithdrawn(event: RewardsWithdrawn): void { + const params = event.params + const account = params.account + const withdrawnVaultShares = params.amount + const rewardSplitterAddress = event.address + const rewardSplitterAddressHex = rewardSplitterAddress.toHex() + + const rewardSplitter = RewardSplitter.load(rewardSplitterAddressHex) as RewardSplitter + const vault = Vault.load(rewardSplitter.vault) as Vault + + const shareHolder = createOrLoadRewardSplitterShareHolder(account, rewardSplitterAddress, rewardSplitter.vault) + shareHolder.earnedVaultShares = shareHolder.earnedVaultShares.minus(withdrawnVaultShares) + if (shareHolder.earnedVaultShares.lt(BigInt.zero())) { + shareHolder.earnedVaultShares = BigInt.zero() + } + shareHolder.earnedVaultAssets = convertSharesToAssets(vault, shareHolder.earnedVaultShares) + shareHolder.save() + + const txHash = event.transaction.hash.toHex() + createTransaction(txHash) + + log.info('[RewardSplitter] RewardsWithdrawn rewardSplitter={} account={} withdrawnVaultShares={}', [ + rewardSplitterAddressHex, + account.toHex(), + withdrawnVaultShares.toString(), + ]) +} diff --git a/src/mappings/swiseToken.ts b/src/mappings/swiseToken.ts deleted file mode 100644 index 8a562af..0000000 --- a/src/mappings/swiseToken.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { log } from '@graphprotocol/graph-ts' -import { Transfer } from '../../generated/Erc20Token/Erc20Token' -import { createTokenTransfer } from '../entities/tokenTransfer' - -export function handleTransfer(event: Transfer): void { - createTokenTransfer( - event.transaction.hash.toHex(), - event.params.from, - event.params.to, - event.params.value, - event.block.timestamp, - 'SWISE', - ) - - log.info('[SwiseToken] Transfer from={} to={} amount={}', [ - event.params.from.toHexString(), - event.params.to.toHexString(), - event.params.value.toString(), - ]) -} diff --git a/src/mappings/v2pool.ts b/src/mappings/v2pool.ts index 4207b27..b1d55ab 100644 --- a/src/mappings/v2pool.ts +++ b/src/mappings/v2pool.ts @@ -1,4 +1,4 @@ -import { Address, BigInt, log } from '@graphprotocol/graph-ts' +import { Address, BigInt, log, store } from '@graphprotocol/graph-ts' import { RewardsUpdated as RewardsUpdatedV0, RewardsUpdated1 as RewardsUpdatedV1, @@ -6,43 +6,88 @@ import { Transfer as RewardTokenTransfer, } from '../../generated/V2RewardToken/V2RewardToken' import { Transfer as StakedTokenTransfer } from '../../generated/V2StakedToken/V2StakedToken' -import { createOrLoadV2Pool } from '../entities/v2pool' +import { createOrLoadV2Pool, createOrLoadV2PoolUser } from '../entities/v2pool' import { WAD } from '../helpers/constants' +import { createOrLoadNetwork, decreaseUserVaultsCount, increaseUserVaultsCount } from '../entities/network' export function handleRewardsUpdatedV0(event: RewardsUpdatedV0): void { - let pool = createOrLoadV2Pool() - pool.rewardAssets = event.params.totalRewards - pool.totalAssets = pool.principalAssets.plus(pool.rewardAssets) + const newTotalRewards = event.params.totalRewards + const pool = createOrLoadV2Pool() + + const rewardsDiff = newTotalRewards.minus(pool.rewardAssets) + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(rewardsDiff) + network.totalEarnedAssets = network.totalEarnedAssets.plus(rewardsDiff) + network.save() + + pool.rewardAssets = newTotalRewards + pool.totalAssets = pool.principalAssets.plus(newTotalRewards) pool.rate = BigInt.fromString(WAD).times(pool.totalAssets).div(pool.principalAssets) pool.save() - log.info('[V2 Pool] RewardsUpdated V0 totalRewards={}', [pool.rewardAssets.toString()]) + log.info('[V2 Pool] RewardsUpdated V0 totalRewards={}', [newTotalRewards.toString()]) } export function handleRewardsUpdatedV1(event: RewardsUpdatedV1): void { - let pool = createOrLoadV2Pool() - pool.rewardAssets = event.params.totalRewards - pool.totalAssets = pool.principalAssets.plus(pool.rewardAssets) + const newTotalRewards = event.params.totalRewards + const pool = createOrLoadV2Pool() + + const rewardsDiff = newTotalRewards.minus(pool.rewardAssets) + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(rewardsDiff) + network.totalEarnedAssets = network.totalEarnedAssets.plus(rewardsDiff) + network.save() + + pool.rewardAssets = newTotalRewards + pool.totalAssets = pool.principalAssets.plus(newTotalRewards) pool.rate = BigInt.fromString(WAD).times(pool.totalAssets).div(pool.principalAssets) pool.save() - log.info('[V2 Pool] RewardsUpdated V1 totalRewards={}', [pool.rewardAssets.toString()]) + log.info('[V2 Pool] RewardsUpdated V1 totalRewards={}', [newTotalRewards.toString()]) } export function handleRewardsUpdatedV2(event: RewardsUpdatedV2): void { const pool = createOrLoadV2Pool() if (!pool.migrated) { - pool.rewardAssets = event.params.totalRewards - pool.totalAssets = pool.principalAssets.plus(pool.rewardAssets) + const newTotalRewards = event.params.totalRewards + + const rewardsDiff = newTotalRewards.minus(pool.rewardAssets) + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(rewardsDiff) + network.totalEarnedAssets = network.totalEarnedAssets.plus(rewardsDiff) + network.save() + + pool.rewardAssets = newTotalRewards + pool.totalAssets = pool.principalAssets.plus(newTotalRewards) pool.rate = BigInt.fromString(WAD).times(pool.totalAssets).div(pool.principalAssets) pool.save() + log.info('[V2 Pool] RewardsUpdated V2 rewardAssets={}', [newTotalRewards.toString()]) } - log.info('[V2 Pool] RewardsUpdated V2 rewardAssets={}', [pool.rewardAssets.toString()]) } export function handleRewardTokenTransfer(event: RewardTokenTransfer): void { - const isBurn = event.params.to == Address.zero() - if (!isBurn) { + const from = event.params.from + const to = event.params.to + const amount = event.params.value + + if (from.notEqual(Address.zero())) { + const v2PoolUser = createOrLoadV2PoolUser(from) + v2PoolUser.balance = v2PoolUser.balance.minus(amount) + if (v2PoolUser.balance.le(BigInt.zero())) { + decreaseUserVaultsCount(from) + } + store.remove('V2PoolUser', v2PoolUser.id) + } + if (to.notEqual(Address.zero())) { + const v2PoolUser = createOrLoadV2PoolUser(to) + if (v2PoolUser.balance.isZero() && !amount.isZero()) { + increaseUserVaultsCount(to) + } + v2PoolUser.balance = v2PoolUser.balance.plus(amount) + v2PoolUser.save() + } + + if (!to.equals(Address.zero())) { // handle only burn events return } @@ -53,29 +98,47 @@ export function handleRewardTokenTransfer(event: RewardTokenTransfer): void { pool.totalAssets = pool.totalAssets.minus(value) pool.save() - log.info('[V2 Pool] StakedToken burn amount={}', [value.toString()]) + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.minus(value) + network.save() + + log.info('[V2 Pool] RewardToken burn amount={}', [value.toString()]) } export function handleStakedTokenTransfer(event: StakedTokenTransfer): void { - const isMint = event.params.from == Address.zero() - const isBurn = event.params.to == Address.zero() - if (!(isMint || isBurn)) { - // handle only mint and burn events - return - } + const from = event.params.from + const to = event.params.to + const amount = event.params.value + const network = createOrLoadNetwork() + const pool = createOrLoadV2Pool() - let pool = createOrLoadV2Pool() - let value = event.params.value - if (isMint) { - pool.principalAssets = pool.principalAssets.plus(value) - pool.totalAssets = pool.totalAssets.plus(value) - log.info('[V2 Pool] StakedToken mint amount={}', [value.toString()]) + if (from.equals(Address.zero())) { + pool.principalAssets = pool.principalAssets.plus(amount) + pool.totalAssets = pool.totalAssets.plus(amount) + network.totalAssets = network.totalAssets.plus(amount) + log.info('[V2 Pool] StakedToken mint amount={}', [amount.toString()]) + } else { + const v2PoolUser = createOrLoadV2PoolUser(from) + v2PoolUser.balance = v2PoolUser.balance.minus(amount) + if (v2PoolUser.balance.le(BigInt.zero())) { + decreaseUserVaultsCount(from) + } + store.remove('V2PoolUser', v2PoolUser.id) } - if (isBurn) { - pool.principalAssets = pool.principalAssets.minus(value) - pool.totalAssets = pool.totalAssets.minus(value) - log.info('[V2 Pool] StakedToken burn amount={}', [value.toString()]) + if (to.equals(Address.zero())) { + pool.principalAssets = pool.principalAssets.minus(amount) + pool.totalAssets = pool.totalAssets.minus(amount) + network.totalAssets = network.totalAssets.minus(amount) + log.info('[V2 Pool] StakedToken burn amount={}', [amount.toString()]) + } else { + const v2PoolUser = createOrLoadV2PoolUser(to) + if (v2PoolUser.balance.isZero() && !amount.isZero()) { + increaseUserVaultsCount(to) + } + v2PoolUser.balance = v2PoolUser.balance.plus(amount) + v2PoolUser.save() } pool.save() + network.save() } diff --git a/src/mappings/vault.ts b/src/mappings/vault.ts index eb073ad..0ba586e 100644 --- a/src/mappings/vault.ts +++ b/src/mappings/vault.ts @@ -1,17 +1,11 @@ -import { Address, BigDecimal, BigInt, DataSourceContext, ipfs, json, log, store } from '@graphprotocol/graph-ts' +import { Address, BigDecimal, BigInt, ipfs, json, log } from '@graphprotocol/graph-ts' import { ExitRequest, Vault } from '../../generated/schema' +import { BlocklistVault as BlocklistVaultTemplate, Vault as VaultTemplate } from '../../generated/templates' import { - BlocklistVault as BlocklistVaultTemplate, - OwnMevEscrow as OwnMevEscrowTemplate, - Vault as VaultTemplate, -} from '../../generated/templates' -import { - CheckpointCreated, Deposited, ExitedAssetsClaimed, ExitQueueEntered as V1ExitQueueEntered, - V2ExitQueueEntered, FeeRecipientUpdated, FeeSharesMinted, Initialized, @@ -22,19 +16,26 @@ import { OsTokenMinted, OsTokenRedeemed, Redeemed, - ValidatorsRootUpdated, + V2ExitQueueEntered, ValidatorsManagerUpdated, - ExitingAssetsPenalized, + ValidatorsRootUpdated, } from '../../generated/templates/Vault/Vault' import { GenesisVaultCreated, Migrated } from '../../generated/GenesisVault/GenesisVault' import { EthFoxVaultCreated } from '../../generated/templates/FoxVault/FoxVault' import { updateMetadata } from '../entities/metadata' import { createTransaction } from '../entities/transaction' -import { createAllocatorAction, createOrLoadAllocator } from '../entities/allocator' -import { createOrLoadNetwork } from '../entities/network' -import { createOrLoadOsTokenPosition, createOrLoadVaultsStat } from '../entities/vaults' -import { createOrLoadOsToken } from '../entities/osToken' +import { + AllocatorActionType, + createAllocatorAction, + createOrLoadAllocator, + getAllocatorLtv, + getAllocatorLtvStatus, + getAllocatorOsTokenMintApy, +} from '../entities/allocator' +import { createOrLoadNetwork, decreaseUserVaultsCount, increaseUserVaultsCount } from '../entities/network' +import { convertSharesToAssets } from '../entities/vaults' +import { convertOsTokenSharesToAssets, createOrLoadOsToken } from '../entities/osToken' import { DEPOSIT_DATA_REGISTRY, WAD } from '../helpers/constants' import { createOrLoadOsTokenConfig } from '../entities/osTokenConfig' @@ -47,22 +48,36 @@ export function handleDeposited(event: Deposited): void { const vaultAddress = event.address const vault = Vault.load(vaultAddress.toHex()) as Vault + const isVaultCreation = vault.totalShares.isZero() && vault.totalAssets.isZero() vault.totalAssets = vault.totalAssets.plus(assets) - vault.principalAssets = vault.principalAssets.plus(assets) vault.totalShares = vault.totalShares.plus(shares) vault.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.plus(assets) - vaultsStat.save() + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(assets) + network.save() + + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const allocator = createOrLoadAllocator(receiver, vaultAddress) + if (allocator.shares.isZero() && !shares.isZero()) { + increaseUserVaultsCount(receiver) + } allocator.shares = allocator.shares.plus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() const txHash = event.transaction.hash.toHex() - createAllocatorAction(event, vaultAddress, 'Deposited', receiver, assets, shares) + if (isVaultCreation) { + createAllocatorAction(event, vaultAddress, AllocatorActionType.VaultCreated, receiver, assets, shares) + } else { + createAllocatorAction(event, vaultAddress, AllocatorActionType.Deposited, receiver, assets, shares) + } createTransaction(txHash) @@ -84,21 +99,31 @@ export function handleRedeemed(event: Redeemed): void { const vault = Vault.load(vaultAddress.toHex()) as Vault vault.totalAssets = vault.totalAssets.minus(assets) - vault.principalAssets = vault.principalAssets.minus(assets) vault.totalShares = vault.totalShares.minus(shares) vault.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.minus(assets) - vaultsStat.save() + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.minus(assets) + network.save() const allocator = createOrLoadAllocator(owner, vaultAddress) allocator.shares = allocator.shares.minus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() + if (allocator.shares.isZero()) { + decreaseUserVaultsCount(allocator.address) + } + const txHash = event.transaction.hash.toHex() - createAllocatorAction(event, vaultAddress, 'Redeemed', owner, assets, shares) + createAllocatorAction(event, vaultAddress, AllocatorActionType.Redeemed, owner, assets, shares) createTransaction(txHash) @@ -181,7 +206,6 @@ export function handleValidatorsRootUpdated(event: ValidatorsRootUpdated): void const vault = Vault.load(vaultAddress) as Vault - vault.validatorsRoot = validatorsRoot vault.depositDataRoot = validatorsRoot vault.save() @@ -220,7 +244,6 @@ export function handleKeysManagerUpdated(event: KeysManagerUpdated): void { const vault = Vault.load(vaultAddress) as Vault - vault.keysManager = keysManager vault.depositDataManager = keysManager vault.save() @@ -259,22 +282,35 @@ export function handleV1ExitQueueEntered(event: V1ExitQueueEntered): void { const positionTicket = params.positionTicket const shares = params.shares const vaultAddress = event.address.toHex() - - // Update vault queued shares const vault = Vault.load(vaultAddress) as Vault + const assets = convertSharesToAssets(vault, shares) + + // if it's ERC-20 vault shares are updated in Transfer event handler if (!vault.isErc20) { - // if it's ERC-20 vault shares are updated in Transfer event handler + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const allocator = createOrLoadAllocator(owner, event.address) allocator.shares = allocator.shares.minus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() + + if (allocator.shares.isZero()) { + decreaseUserVaultsCount(allocator.address) + } } const timestamp = event.block.timestamp - createAllocatorAction(event, event.address, 'ExitQueueEntered', owner, null, shares) + createAllocatorAction(event, event.address, AllocatorActionType.ExitQueueEntered, owner, assets, shares) createTransaction(event.transaction.hash.toHex()) + vault.latestExitTicket = positionTicket.plus(shares) + vault.save() + // Create exit request const exitRequestId = `${vaultAddress}-${positionTicket}` const exitRequest = new ExitRequest(exitRequestId) @@ -282,11 +318,15 @@ export function handleV1ExitQueueEntered(event: V1ExitQueueEntered): void { exitRequest.vault = vaultAddress exitRequest.owner = owner exitRequest.receiver = receiver - exitRequest.totalShares = shares - exitRequest.totalAssets = BigInt.zero() + exitRequest.totalAssets = assets + exitRequest.exitedAssets = BigInt.zero() exitRequest.positionTicket = positionTicket exitRequest.isV2Position = false + exitRequest.exitQueueIndex = null exitRequest.timestamp = timestamp + exitRequest.isClaimable = false + exitRequest.isClaimed = false + exitRequest.lastSnapshotTimestamp = timestamp exitRequest.save() log.info('[Vault] V1ExitQueueEntered vault={} owner={} shares={}', [vaultAddress, owner.toHex(), shares.toString()]) @@ -308,22 +348,39 @@ export function handleV2ExitQueueEntered(event: V2ExitQueueEntered): void { const vault = Vault.load(vaultAddress) as Vault vault.totalShares = vault.totalShares.minus(shares) vault.totalAssets = vault.totalAssets.minus(assets) - vault.principalAssets = vault.principalAssets.minus(assets) + let exitingTickets: BigInt + if (vault.exitingAssets.le(BigInt.zero())) { + exitingTickets = assets + } else { + exitingTickets = assets.times(vault.exitingTickets).div(vault.exitingAssets) + } vault.exitingAssets = vault.exitingAssets.plus(assets) + vault.exitingTickets = vault.exitingTickets.plus(exitingTickets) + vault.latestExitTicket = positionTicket.plus(exitingTickets) vault.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.minus(assets) - vaultsStat.save() + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.minus(assets) + network.save() // Update allocator shares + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const allocator = createOrLoadAllocator(owner, event.address) allocator.shares = allocator.shares.minus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() + if (allocator.shares.isZero()) { + decreaseUserVaultsCount(allocator.address) + } + const timestamp = event.block.timestamp - createAllocatorAction(event, event.address, 'ExitQueueEntered', owner, assets, shares) + createAllocatorAction(event, event.address, AllocatorActionType.ExitQueueEntered, owner, assets, shares) createTransaction(event.transaction.hash.toHex()) @@ -334,11 +391,15 @@ export function handleV2ExitQueueEntered(event: V2ExitQueueEntered): void { exitRequest.vault = vaultAddress exitRequest.owner = owner exitRequest.receiver = receiver - exitRequest.totalShares = BigInt.zero() exitRequest.totalAssets = assets + exitRequest.exitedAssets = BigInt.zero() exitRequest.positionTicket = positionTicket exitRequest.isV2Position = true + exitRequest.exitQueueIndex = null exitRequest.timestamp = timestamp + exitRequest.isClaimable = false + exitRequest.isClaimed = false + exitRequest.lastSnapshotTimestamp = timestamp exitRequest.save() log.info('[Vault] V2ExitQueueEntered vault={} owner={} shares={} assets={}', [ @@ -358,9 +419,10 @@ export function handleExitedAssetsClaimed(event: ExitedAssetsClaimed): void { const prevPositionTicket = params.prevPositionTicket const newPositionTicket = params.newPositionTicket const claimedAssets = params.withdrawnAssets + const claimedTickets = prevPositionTicket.minus(newPositionTicket) const vaultAddress = event.address.toHex() - createAllocatorAction(event, event.address, 'ExitedAssetsClaimed', receiver, claimedAssets, null) + createAllocatorAction(event, event.address, AllocatorActionType.ExitedAssetsClaimed, receiver, claimedAssets, null) createTransaction(event.transaction.hash.toHex()) @@ -368,59 +430,41 @@ export function handleExitedAssetsClaimed(event: ExitedAssetsClaimed): void { const prevExitRequest = ExitRequest.load(prevExitRequestId) as ExitRequest const isExitQueueRequestResolved = newPositionTicket.equals(BigInt.zero()) - const isV2ExitRequest = prevExitRequest.totalShares.equals(BigInt.zero()) - - if (isV2ExitRequest) { + if (prevExitRequest.isV2Position) { // Update vault shares and assets const vault = Vault.load(vaultAddress) as Vault vault.exitingAssets = vault.exitingAssets.minus(claimedAssets) + vault.exitingTickets = vault.exitingTickets.minus(claimedTickets) vault.save() } if (!isExitQueueRequestResolved) { const nextExitQueueRequestId = `${vaultAddress}-${newPositionTicket}` - let withdrawnShares: BigInt = BigInt.zero() - let withdrawnAssets: BigInt = BigInt.zero() - if (isV2ExitRequest) { - withdrawnAssets = claimedAssets - } else { - withdrawnShares = newPositionTicket.minus(prevPositionTicket) - } - const nextExitRequest = new ExitRequest(nextExitQueueRequestId) - nextExitRequest.vault = vaultAddress nextExitRequest.owner = prevExitRequest.owner nextExitRequest.timestamp = prevExitRequest.timestamp nextExitRequest.receiver = receiver nextExitRequest.positionTicket = newPositionTicket nextExitRequest.isV2Position = prevExitRequest.isV2Position - nextExitRequest.totalShares = prevExitRequest.totalShares.minus(withdrawnShares) - nextExitRequest.totalAssets = prevExitRequest.totalAssets.minus(withdrawnAssets) + nextExitRequest.totalAssets = prevExitRequest.totalAssets.minus(claimedAssets) + nextExitRequest.exitedAssets = BigInt.zero() + nextExitRequest.exitQueueIndex = null + nextExitRequest.isClaimable = false + nextExitRequest.isClaimed = false + nextExitRequest.lastSnapshotTimestamp = prevExitRequest.lastSnapshotTimestamp nextExitRequest.save() } - store.remove('ExitRequest', prevExitRequestId) - - log.info('[Vault] ExitedAssetsClaimed vault={} withdrawnAssets={}', [vaultAddress, claimedAssets.toString()]) -} - -// Event emitted when shares burned. After that assets become available for claim -export function handleCheckpointCreated(event: CheckpointCreated): void { - const params = event.params + prevExitRequest.isClaimable = false + prevExitRequest.isClaimed = true + prevExitRequest.save() - const burnedShares = params.shares - const exitedAssets = params.assets - const vaultAddress = event.address.toHex() - - const vault = Vault.load(vaultAddress) as Vault - vault.principalAssets = vault.principalAssets.minus(exitedAssets) - vault.save() - - log.info('[Vault] CheckpointCreated vault={} burnedShares={} exitedAssets={}', [ + log.info('[Vault] ExitedAssetsClaimed vault={} prevPositionTicket={} newPositionTicket={} claimedAssets={}', [ vaultAddress, - burnedShares.toString(), - exitedAssets.toString(), + prevPositionTicket.toString(), + newPositionTicket.toString(), + claimedAssets.toString(), ]) } @@ -428,51 +472,56 @@ export function handleCheckpointCreated(event: CheckpointCreated): void { export function handleFeeSharesMinted(event: FeeSharesMinted): void { const params = event.params const vaultAddress = event.address + const vaultAddressHex = vaultAddress.toHex() const receiver = params.receiver const assets = params.assets const shares = params.shares + const vault = Vault.load(vaultAddressHex) as Vault + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const allocator = createOrLoadAllocator(receiver, vaultAddress) + if (allocator.shares.isZero() && !shares.isZero()) { + increaseUserVaultsCount(allocator.address) + } allocator.shares = allocator.shares.plus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() log.info('[Vault] FeeSharesMinted vault={} receiver={} assets={} shares={}', [ - vaultAddress.toHex(), + vaultAddressHex, receiver.toHex(), assets.toString(), shares.toString(), ]) } -export function handleExitingAssetsPenalized(event: ExitingAssetsPenalized): void { - const vaultAddress = event.address - const penaltyAssets = event.params.penalty - - const vault = Vault.load(vaultAddress.toHex()) as Vault - vault.exitingAssets = vault.exitingAssets.minus(penaltyAssets) - vault.save() - - log.info('[Vault] ExitingAssetsPenalized vault={} penaltyAssets={}', [vaultAddress.toHex(), penaltyAssets.toString()]) -} - export function handleOsTokenMinted(event: OsTokenMinted): void { const holder = event.params.caller const shares = event.params.shares const assets = event.params.assets - const txHash = event.transaction.hash.toHex() - createTransaction(txHash) - - createAllocatorAction(event, event.address, 'OsTokenMinted', holder, assets, shares) - - const osTokenPosition = createOrLoadOsTokenPosition(holder, event.address) - osTokenPosition.shares = osTokenPosition.shares.plus(shares) - osTokenPosition.save() - const osToken = createOrLoadOsToken() + osToken.totalAssets = osToken.totalAssets.plus(convertOsTokenSharesToAssets(osToken, shares)) osToken.totalSupply = osToken.totalSupply.plus(shares) osToken.save() + const vault = Vault.load(event.address.toHex()) as Vault + const allocator = createOrLoadAllocator(holder, event.address) + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + allocator.mintedOsTokenShares = allocator.mintedOsTokenShares.plus(shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + + createAllocatorAction(event, event.address, AllocatorActionType.OsTokenMinted, holder, assets, shares) + const txHash = event.transaction.hash.toHex() + createTransaction(txHash) + log.info('[Vault] OsTokenMinted holder={} shares={}', [holder.toHex(), shares.toString()]) } @@ -481,19 +530,28 @@ export function handleOsTokenBurned(event: OsTokenBurned): void { const assets = event.params.assets const shares = event.params.shares - const txHash = event.transaction.hash.toHex() - createTransaction(txHash) - - createAllocatorAction(event, event.address, 'OsTokenBurned', holder, assets, shares) - - const osTokenPosition = createOrLoadOsTokenPosition(holder, event.address) - osTokenPosition.shares = osTokenPosition.shares.lt(shares) ? BigInt.zero() : osTokenPosition.shares.minus(shares) - osTokenPosition.save() - const osToken = createOrLoadOsToken() + osToken.totalAssets = osToken.totalAssets.minus(convertOsTokenSharesToAssets(osToken, shares)) osToken.totalSupply = osToken.totalSupply.minus(shares) osToken.save() + const vault = Vault.load(event.address.toHex()) as Vault + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + const allocator = createOrLoadAllocator(holder, event.address) + allocator.mintedOsTokenShares = allocator.mintedOsTokenShares.minus(shares) + if (allocator.mintedOsTokenShares.lt(BigInt.zero())) { + allocator.mintedOsTokenShares = BigInt.zero() + } + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + + const txHash = event.transaction.hash.toHex() + createTransaction(txHash) + + createAllocatorAction(event, event.address, AllocatorActionType.OsTokenBurned, holder, assets, shares) + log.info('[Vault] OsTokenBurned holder={} shares={}', [holder.toHex(), shares.toString()]) } @@ -505,28 +563,40 @@ export function handleOsTokenLiquidated(event: OsTokenLiquidated): void { const vaultAddress = event.address.toHex() const vault = Vault.load(vaultAddress) as Vault + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) vault.totalShares = vault.totalShares.minus(withdrawnShares) vault.totalAssets = vault.totalAssets.minus(withdrawnAssets) - vault.principalAssets = vault.principalAssets.minus(withdrawnAssets) vault.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.minus(withdrawnAssets) - vaultsStat.save() - - const txHash = event.transaction.hash.toHex() - createTransaction(txHash) - - createAllocatorAction(event, event.address, 'OsTokenLiquidated', holder, null, shares) - - const osTokenPosition = createOrLoadOsTokenPosition(holder, event.address) - osTokenPosition.shares = osTokenPosition.shares.lt(shares) ? BigInt.zero() : osTokenPosition.shares.minus(shares) - osTokenPosition.save() + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.minus(withdrawnAssets) + network.save() const osToken = createOrLoadOsToken() + osToken.totalAssets = osToken.totalAssets.minus(convertOsTokenSharesToAssets(osToken, shares)) osToken.totalSupply = osToken.totalSupply.minus(shares) osToken.save() + const allocator = createOrLoadAllocator(holder, event.address) + allocator.shares = allocator.shares.minus(withdrawnShares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.mintedOsTokenShares = allocator.mintedOsTokenShares.minus(shares) + if (allocator.mintedOsTokenShares.lt(BigInt.zero())) { + allocator.mintedOsTokenShares = BigInt.zero() + } + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + + if (allocator.shares.isZero()) { + decreaseUserVaultsCount(allocator.address) + } + + const txHash = event.transaction.hash.toHex() + createTransaction(txHash) + + createAllocatorAction(event, event.address, AllocatorActionType.OsTokenLiquidated, holder, null, shares) log.info('[Vault] OsTokenLiquidated holder={} shares={}', [holder.toHex(), shares.toString()]) } @@ -540,26 +610,39 @@ export function handleOsTokenRedeemed(event: OsTokenRedeemed): void { const vault = Vault.load(vaultAddress) as Vault vault.totalShares = vault.totalShares.minus(withdrawnShares) vault.totalAssets = vault.totalAssets.minus(withdrawnAssets) - vault.principalAssets = vault.principalAssets.minus(withdrawnAssets) vault.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.minus(withdrawnAssets) - vaultsStat.save() - - const txHash = event.transaction.hash.toHex() - createTransaction(txHash) - - createAllocatorAction(event, event.address, 'OsTokenRedeemed', holder, null, shares) - - const osTokenPosition = createOrLoadOsTokenPosition(holder, event.address) - osTokenPosition.shares = osTokenPosition.shares.lt(shares) ? BigInt.zero() : osTokenPosition.shares.minus(shares) - osTokenPosition.save() + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.minus(withdrawnAssets) + network.save() const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) + osToken.totalAssets = osToken.totalAssets.minus(convertOsTokenSharesToAssets(osToken, shares)) osToken.totalSupply = osToken.totalSupply.minus(shares) osToken.save() + const allocator = createOrLoadAllocator(holder, event.address) + allocator.shares = allocator.shares.minus(withdrawnShares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.mintedOsTokenShares = allocator.mintedOsTokenShares.minus(shares) + if (allocator.mintedOsTokenShares.lt(BigInt.zero())) { + allocator.mintedOsTokenShares = BigInt.zero() + } + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) + allocator.save() + + if (allocator.shares.isZero()) { + decreaseUserVaultsCount(allocator.address) + } + + const txHash = event.transaction.hash.toHex() + createTransaction(txHash) + + createAllocatorAction(event, event.address, AllocatorActionType.OsTokenRedeemed, holder, null, shares) + log.info('[Vault] OsTokenRedeemed holder={} shares={}', [holder.toHex(), shares.toString()]) } @@ -578,20 +661,19 @@ export function handleGenesisVaultCreated(event: GenesisVaultCreated): void { vault.capacity = capacity vault.feePercent = feePercent vault.feeRecipient = admin - vault.keysManager = admin // Deprecated vault.depositDataManager = admin vault.consensusReward = BigInt.zero() vault.lockedExecutionReward = BigInt.zero() vault.unlockedExecutionReward = BigInt.zero() - vault.unconvertedExecutionReward = BigInt.zero() vault.canHarvest = false vault.slashedMevReward = BigInt.zero() vault.totalShares = BigInt.zero() vault.score = BigDecimal.zero() vault.rate = BigInt.fromString(WAD) vault.totalAssets = BigInt.zero() - vault.principalAssets = BigInt.zero() vault.exitingAssets = BigInt.zero() + vault.exitingTickets = BigInt.zero() + vault.latestExitTicket = BigInt.zero() vault.isPrivate = false vault.isBlocklist = false vault.isErc20 = false @@ -600,14 +682,8 @@ export function handleGenesisVaultCreated(event: GenesisVaultCreated): void { vault.isCollateralized = true vault.addressString = vaultAddressHex vault.createdAt = event.block.timestamp - vault.apySnapshotsCount = BigInt.zero() vault.apy = BigDecimal.zero() - vault.weeklyApy = BigDecimal.zero() - vault.executionApy = BigDecimal.zero() - vault.consensusApy = BigDecimal.zero() - vault.medianApy = BigDecimal.zero() - vault.medianExecutionApy = BigDecimal.zero() - vault.medianConsensusApy = BigDecimal.zero() + vault.apys = [] vault.blocklistCount = BigInt.zero() vault.whitelistCount = BigInt.zero() vault.isGenesis = true @@ -620,13 +696,12 @@ export function handleGenesisVaultCreated(event: GenesisVaultCreated): void { VaultTemplate.create(vaultAddress) const network = createOrLoadNetwork() - network.vaultsTotal = network.vaultsTotal + 1 + network.vaultsCount = network.vaultsCount + 1 + let vaultIds = network.vaultIds + vaultIds.push(vaultAddressHex) + network.vaultIds = vaultIds network.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.vaultsCount = vaultsStat.vaultsCount.plus(BigInt.fromI32(1)) - vaultsStat.save() - createTransaction(event.transaction.hash.toHex()) log.info('[GenesisVault] GenesisVaultCreated address={} admin={} feePercent={} capacity={}', [ @@ -653,20 +728,19 @@ export function handleFoxVaultCreated(event: EthFoxVaultCreated): void { vault.capacity = capacity vault.feePercent = feePercent vault.feeRecipient = admin - vault.keysManager = admin // Deprecated vault.depositDataManager = admin vault.consensusReward = BigInt.zero() vault.lockedExecutionReward = BigInt.zero() vault.unlockedExecutionReward = BigInt.zero() - vault.unconvertedExecutionReward = BigInt.zero() vault.canHarvest = false vault.slashedMevReward = BigInt.zero() vault.totalShares = BigInt.zero() vault.score = BigDecimal.zero() vault.rate = BigInt.fromString(WAD) vault.totalAssets = BigInt.zero() - vault.principalAssets = BigInt.zero() vault.exitingAssets = BigInt.zero() + vault.exitingTickets = BigInt.zero() + vault.latestExitTicket = BigInt.zero() vault.isPrivate = false vault.isBlocklist = true vault.isErc20 = false @@ -676,14 +750,8 @@ export function handleFoxVaultCreated(event: EthFoxVaultCreated): void { vault.mevEscrow = ownMevEscrow vault.addressString = vaultAddressHex vault.createdAt = event.block.timestamp - vault.apySnapshotsCount = BigInt.zero() vault.apy = BigDecimal.zero() - vault.weeklyApy = BigDecimal.zero() - vault.executionApy = BigDecimal.zero() - vault.consensusApy = BigDecimal.zero() - vault.medianApy = BigDecimal.zero() - vault.medianExecutionApy = BigDecimal.zero() - vault.medianConsensusApy = BigDecimal.zero() + vault.apys = [] vault.isGenesis = false vault.blocklistManager = admin vault.blocklistCount = BigInt.zero() @@ -697,18 +765,13 @@ export function handleFoxVaultCreated(event: EthFoxVaultCreated): void { VaultTemplate.create(vaultAddress) BlocklistVaultTemplate.create(vaultAddress) - const context = new DataSourceContext() - context.setString('vault', vaultAddressHex) - OwnMevEscrowTemplate.createWithContext(ownMevEscrow, context) - const network = createOrLoadNetwork() - network.vaultsTotal = network.vaultsTotal + 1 + network.vaultsCount = network.vaultsCount + 1 + let vaultIds = network.vaultIds + vaultIds.push(vaultAddressHex) + network.vaultIds = vaultIds network.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.vaultsCount = vaultsStat.vaultsCount.plus(BigInt.fromI32(1)) - vaultsStat.save() - createTransaction(event.transaction.hash.toHex()) log.info('[FoxVault] EthFoxVaultCreated address={} admin={} feePercent={} capacity={} ownMevEscrow={}', [ @@ -730,24 +793,31 @@ export function handleMigrated(event: Migrated): void { const vault = Vault.load(vaultAddress.toHex()) as Vault vault.totalAssets = vault.totalAssets.plus(assets) - vault.principalAssets = vault.principalAssets.plus(assets) vault.totalShares = vault.totalShares.plus(shares) vault.save() + const network = createOrLoadNetwork() + network.totalAssets = network.totalAssets.plus(assets) + network.save() + + const osToken = createOrLoadOsToken() + const osTokenConfig = createOrLoadOsTokenConfig(vault.osTokenConfig) const allocator = createOrLoadAllocator(receiver, vaultAddress) + if (allocator.shares.isZero() && !shares.isZero()) { + increaseUserVaultsCount(allocator.address) + } allocator.shares = allocator.shares.plus(shares) + allocator.assets = convertSharesToAssets(vault, allocator.shares) + allocator.ltv = getAllocatorLtv(allocator, osToken) + allocator.ltvStatus = getAllocatorLtvStatus(allocator, osTokenConfig) + allocator.osTokenMintApy = getAllocatorOsTokenMintApy(allocator, osToken.apy, osToken, osTokenConfig) allocator.save() - const vaultsStat = createOrLoadVaultsStat() - vaultsStat.totalAssets = vaultsStat.totalAssets.plus(assets) - vaultsStat.save() - const txHash = event.transaction.hash.toHex() - - createAllocatorAction(event, vaultAddress, 'Migrated', receiver, assets, shares) - createTransaction(txHash) + createAllocatorAction(event, vaultAddress, AllocatorActionType.Migrated, receiver, assets, shares) + log.info('[GenesisVault] Migrated vault={} receiver={} assets={} shares={}', [ vaultAddress.toHex(), receiver.toHex(), diff --git a/src/schema.graphql b/src/schema.graphql index 054aabb..0e3b575 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,3 +1,10 @@ +enum LtvStatus { + Healthy + Moderate + Risky + Unhealthy +} + """ Vault allocator """ @@ -11,39 +18,23 @@ type Allocator @entity { "The allocator's shares amount" shares: BigInt! - "The vault of the allocator" - vault: Vault! -} + "The allocator's assets amount" + assets: BigInt! -""" -Vault OsToken position -""" -type OsTokenPosition @entity { - "-" - id: ID! + "The allocator's minted OsToken shares amount" + mintedOsTokenShares: BigInt! - "The holder's address" - address: Bytes! + "The allocator's LTV percent. Will be null if there are no OsTokens minted" + ltv: BigDecimal! - "The holder's shares amount" - shares: BigInt! + "The allocator's LTV descriptive status" + ltvStatus: LtvStatus! - "The vault of the holder" - vault: Vault! -} - -""" -OsToken holder -""" -type OsTokenHolder @entity { - "The address of the OsToken holder" - id: ID! + "The allocator's OsToken mint APY" + osTokenMintApy: BigDecimal! - "The total holder shares" - shares: BigInt! - - "The timestamp when the holder was updated" - timestamp: BigInt! + "The vault of the allocator" + vault: Vault! } """ @@ -70,19 +61,36 @@ type TokenTransfer @entity { } """ -Token holder +OsToken holder """ -type TokenHolder @entity { - "-" +type OsTokenHolder @entity { + "" id: ID! - "The token symbol" - tokenSymbol: String! + "The OsToken holder balance (shares)" + balance: BigInt! - "The token holder address" - address: Bytes! + "The OsToken holder assets" + assets: BigInt! + + "The OsToken instance" + osToken: OsToken! + + "The total number of OsToken transfers" + transfersCount: BigInt! +} - "The total number of token transfers" +""" +SwiseToken holder +""" +type SwiseTokenHolder @entity { + "" + id: ID! + + "The SwiseToken holder balance" + balance: BigInt! + + "The total number of SwiseToken transfers" transfersCount: BigInt! } @@ -105,12 +113,15 @@ type ExitRequest @entity { "The address that will receive exited assets" receiver: Bytes! - "The number of shares queued for exit. Will be zero for V2 vaults as shares are burned when user enters exit queue." - totalShares: BigInt! - "The total number of assets queued for exit" totalAssets: BigInt! + "The total number of assets that has exited" + exitedAssets: BigInt! + + "The exit queue index that must be used to claim the exit. Will be null if the exit is not claimable" + exitQueueIndex: BigInt + "The timestamp when the exit request was created" timestamp: BigInt! @@ -119,6 +130,15 @@ type ExitRequest @entity { "The estimated withdrawal timestamp. Managed by the backend service." withdrawalTimestamp: BigInt + + "Defines whether the exit request is claimable" + isClaimable: Boolean! + + "Defines whether the exit request is claimed" + isClaimed: Boolean! + + "The timestamp of the last exit request snapshot" + lastSnapshotTimestamp: BigInt! } """ @@ -161,18 +181,12 @@ type Vault @entity { "The address of the vault's validators manager" validatorsManager: Bytes - "The address of the vault's keys manager (deprecated)" - keysManager: Bytes! - "The address of the vault's deposit data manager. If it's null, then the vault uses own validators manager." depositDataManager: Bytes! "The MEV and priority fees escrow address. If it's null, then the vault uses shared MEV escrow." mevEscrow: Bytes - "The vault validators merkle tree root (deprecated)" - validatorsRoot: Bytes - "The vault deposit data merkle tree root" depositDataRoot: Bytes @@ -209,9 +223,6 @@ type Vault @entity { "The vault execution reward" unlockedExecutionReward: BigInt! - "The vault execution reward that must be converted (only for Gnosis)" - unconvertedExecutionReward: BigInt! - "The vault slashed MEV reward in the smoothing pool" slashedMevReward: BigInt! @@ -224,12 +235,12 @@ type Vault @entity { "The vault exit requests" exitRequests: [ExitRequest!]! @derivedFrom(field: "vault") - "The vault apy snapshots" - apySnapshots: [VaultApySnapshot!]! @derivedFrom(field: "vault") - "The vault eigen pods (only for vaults with isRestake=true)" eigenPods: [EigenPod!]! @derivedFrom(field: "vault") + "The vault reward splitters" + rewardSplitters: [RewardSplitter!]! @derivedFrom(field: "vault") + "The total number of shares" totalShares: BigInt! @@ -239,15 +250,18 @@ type Vault @entity { "The total number of assets" totalAssets: BigInt! - "The number of assets used for rewardPerAsset calculation" - principalAssets: BigInt! - "The current exchange rate for 10^18 amount" rate: BigInt! "The total number of assets that are exiting (in V2 vaults)" exitingAssets: BigInt! + "The total number of tickets that are exiting (in V2 vaults)" + exitingTickets: BigInt! + + "The latest exit ticket number" + latestExitTicket: BigInt! + "Indicates whether the Vault is private" isPrivate: Boolean! @@ -296,29 +310,11 @@ type Vault @entity { "Whether the vault is a genesis vault (v2 pool migration)" isGenesis: Boolean! - "The total number of APY snapshots" - apySnapshotsCount: BigInt! - "The vault average weekly total APY" apy: BigDecimal! - "(deprecated) The vault average weekly total APY" - weeklyApy: BigDecimal! - - "The vault average weekly consensus APY" - consensusApy: BigDecimal! - - "The vault average weekly execution APY" - executionApy: BigDecimal! - - "The vault median weekly total APY" - medianApy: BigDecimal! - - "The vault median weekly execution APY" - medianExecutionApy: BigDecimal! - - "The vault median weekly consensus APY" - medianConsensusApy: BigDecimal! + "The list of vault APY snapshot" + apys: [BigDecimal!]! "The total number of vault blocklisted accounts" blocklistCount: BigInt! @@ -368,29 +364,6 @@ type AllocatorAction @entity { createdAt: BigInt! } -type VaultApySnapshot @entity { - "-" - id: ID! - - "The vault APY for the period" - apy: BigDecimal! - - "The vault consensus APY for the period" - consensusApy: BigDecimal! - - "The vault execution APY for the period" - executionApy: BigDecimal! - - "The period start epoch timestamp" - fromEpochTimestamp: BigInt! - - "The period end epoch timestamp" - toEpochTimestamp: BigInt! - - "The snapshot's vault" - vault: Vault -} - """ OsToken data """ @@ -401,8 +374,8 @@ type OsToken @entity { "The OsToken APY" apy: BigDecimal! - "The OsToken borrow APY" - borrowApy: BigDecimal! + "The list of OsToken APY snapshot" + apys: [BigDecimal!]! "The OsToken fee percent" feePercent: Int! @@ -410,36 +383,60 @@ type OsToken @entity { "The OsToken total supply" totalSupply: BigInt! - "The total number of OsToken snapshots" - snapshotsCount: BigInt! + "The OsToken total assets" + totalAssets: BigInt! + + "The OsToken holders" + holders: [OsTokenHolder!]! @derivedFrom(field: "osToken") + + "The timestamp of the last rewards update" + lastUpdateTimestamp: BigInt! } """ -Snapshot of OsToken average reward per second +Network data """ -type OsTokenSnapshot @entity { - "The counter of the snapshot" +type Network @entity { + "Always 0" id: ID! - "The OsToken average reward per second" - avgRewardPerSecond: BigInt! + "The total assets locked in the network" + totalAssets: BigInt! - "The OsToken average borrow reward per second" - borrowRewardPerSecond: BigInt! + "The total assets earned in the network" + totalEarnedAssets: BigInt! - "The timestamp the snapshot was created at" - createdAt: BigInt! + "Total number of vaults" + vaultsCount: Int! + + "The USD rate of the assets (e.g. ETH, GNO)" + assetsUsdRate: BigDecimal! + + "The USD to DAI rate" + usdToDaiRate: BigDecimal! + + "The USD to EUR rate" + usdToEurRate: BigDecimal! + + "The USD to GBP rate" + usdToGbpRate: BigDecimal! + + "The non repeated addresses of all the vaults" + vaultIds: [String!]! + + "The total number of non repeated vault allocators and osToken holders" + usersCount: Int! } -""" -Network data -""" -type Network @entity { - "Always 0" +type User @entity { + "User address" id: ID! - "Total vaults" - vaultsTotal: Int! + "The total number of vaults, where user is an allocator" + vaultsCount: Int! + + "Defines whether the user holds any osToken shares" + isOsTokenHolder: Boolean! } """ @@ -494,6 +491,9 @@ type RewardSplitter @entity { "Shareholders of the reward splitter" shareHolders: [RewardSplitterShareHolder!]! @derivedFrom(field: "rewardSplitter") + + "Last snapshot timestamp" + lastSnapshotTimestamp: BigInt! } """ @@ -506,11 +506,20 @@ type RewardSplitterShareHolder @entity { "The reward splitter" rewardSplitter: RewardSplitter! + "The address of the vault" + vault: Vault! + "The address of shareholder" address: Bytes! "The amount of shares" shares: BigInt! + + "The amount of earned vault shares" + earnedVaultShares: BigInt! + + "The amount of earned vault assets" + earnedVaultAssets: BigInt! } """ @@ -546,40 +555,28 @@ type V2Pool @entity { "Whether V2 Pool has migrated to V3" migrated: Boolean! - "The total number of APY snapshots" - apySnapshotsCount: BigInt! - "The current exchange rate for 10^18 staked token" rate: BigInt! "The pool average weekly total APY" apy: BigDecimal! - "(deprecated) The pool average weekly total APY" - weeklyApy: BigDecimal! - - "The pool average weekly consensus APY" - consensusApy: BigDecimal! - - "The pool average weekly execution APY" - executionApy: BigDecimal! + "The list of pool APY snapshot" + apys: [BigDecimal!]! "Last rewards update timestamp" rewardsTimestamp: BigInt } """ -Vaults statistics +StakeWise V2 pool user """ -type VaultsStat @entity { - "1" +type V2PoolUser @entity { + "The address of the user" id: ID! - "The total assets locked in vaults" - totalAssets: BigInt! - - "The total number of vaults" - vaultsCount: BigInt! + "The balance of the user" + balance: BigInt! } """ @@ -632,3 +629,159 @@ type OsTokenConfig @entity { "The vault osToken liquidation threshold percent" liqThresholdPercent: BigInt! } + +""" +The snapshot of the allocator state +""" +type AllocatorSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + allocator: Allocator! + earnedAssets: BigInt! + totalAssets: BigInt! + ltv: BigDecimal! +} + +""" +The aggregation of the allocator snapshots +""" +type AllocatorStats @aggregation(intervals: ["day"], source: "AllocatorSnapshot") { + id: Int8! + timestamp: Timestamp! + allocator: Allocator! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") + ltv: BigDecimal! @aggregate(fn: "last", arg: "ltv") +} + +""" +The snapshot of the OsToken state +""" +type OsTokenSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + earnedAssets: BigInt! + totalAssets: BigInt! +} + +""" +The aggregation of the OsToken snapshots +""" +type OsTokenStats @aggregation(intervals: ["day"], source: "OsTokenSnapshot") { + id: Int8! + timestamp: Timestamp! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") +} + +""" +The snapshot of the OsTokenHolder state +""" +type OsTokenHolderSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + osTokenHolder: OsTokenHolder! + earnedAssets: BigInt! + totalAssets: BigInt! +} + +""" +The aggregation of the OsTokenHolder snapshots +""" +type OsTokenHolderStats @aggregation(intervals: ["day"], source: "OsTokenHolderSnapshot") { + id: Int8! + timestamp: Timestamp! + osTokenHolder: OsTokenHolder! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") +} + +""" +The snapshot of the Vault state +""" +type VaultSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + vault: Vault! + earnedAssets: BigInt! + totalAssets: BigInt! +} + +""" +The aggregation of the Vault snapshots +""" +type VaultStats @aggregation(intervals: ["day"], source: "VaultSnapshot") { + id: Int8! + timestamp: Timestamp! + vault: Vault! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") +} + +""" +The snapshot of the ExitRequest state +""" +type ExitRequestSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + exitRequest: ExitRequest! + earnedAssets: BigInt! + totalAssets: BigInt! +} + +""" +The aggregation of the ExitRequest snapshots +""" +type ExitRequestStats @aggregation(intervals: ["day"], source: "ExitRequestSnapshot") { + id: Int8! + timestamp: Timestamp! + exitRequest: ExitRequest! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") +} + +""" +The snapshot of the RewardSplitterShareHolder state +""" +type RewardSplitterShareHolderSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + rewardSpliterShareHolder: RewardSplitterShareHolder! + earnedAssets: BigInt! + totalAssets: BigInt! +} + +""" +The aggregation of the RewardSplitterShareHolder snapshots +""" +type RewardSplitterShareHolderStats @aggregation(intervals: ["day"], source: "RewardSplitterShareHolderSnapshot") { + id: Int8! + timestamp: Timestamp! + rewardSpliterShareHolder: RewardSplitterShareHolder! + earnedAssets: BigInt! @aggregate(fn: "sum", arg: "earnedAssets") + totalAssets: BigInt! @aggregate(fn: "last", arg: "totalAssets") +} + +""" +The snapshot of the exchange rates +""" +type ExchangeRateSnapshot @entity(timeseries: true) { + id: Int8! + timestamp: Timestamp! + assetsUsdRate: BigDecimal! + usdToDaiRate: BigDecimal! + usdToEurRate: BigDecimal! + usdToGbpRate: BigDecimal! +} + +""" +The aggregation of the ExchangeRate snapshots +""" +type ExchangeRateStats @aggregation(intervals: ["day"], source: "ExchangeRateSnapshot") { + id: Int8! + timestamp: Timestamp! + assetsUsdRate: BigDecimal! @aggregate(fn: "last", arg: "assetsUsdRate") + usdToDaiRate: BigDecimal! @aggregate(fn: "last", arg: "usdToDaiRate") + usdToEurRate: BigDecimal! @aggregate(fn: "last", arg: "usdToEurRate") + usdToGbpRate: BigDecimal! @aggregate(fn: "last", arg: "usdToGbpRate") +} diff --git a/src/subgraph.template.yaml b/src/subgraph.template.yaml index 2530dbc..b69dd2b 100644 --- a/src/subgraph.template.yaml +++ b/src/subgraph.template.yaml @@ -1,4 +1,4 @@ -specVersion: 0.0.8 +specVersion: 1.2.0 description: The liquid staking protocol repository: https://github.com/stakewise/v3-subgraph schema: @@ -7,7 +7,56 @@ features: - ipfsOnEthereumContracts dataSources: - kind: ethereum/contract - name: Erc20Token + name: ExitRequests + network: {{ network }} + source: + address: '{{ genesisVault.address }}' + abi: Vault + startBlock: {{ genesisVault.startBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.8 + language: wasm/assemblyscript + file: ./mappings/keeper.ts + entities: + - Network + - Vault + - ExitRequest + - ExitRequestSnapshot + abis: + - name: Vault + file: ./abis/Vault.json + blockHandlers: + - handler: handleExitRequests + filter: + kind: polling + every: {{ pollingBlocksInterval }} + - kind: ethereum/contract + name: ExchangeRates + network: {{ network }} + source: + address: '{{ genesisVault.address }}' + abi: PriceFeed + startBlock: {{ genesisVault.startBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.8 + language: wasm/assemblyscript + file: ./mappings/exchangeRates.ts + entities: + - Network + - ExchangeRateSnapshot + - ExchangeRateStats + abis: + - name: PriceFeed + file: ./abis/PriceFeed.json + blockHandlers: + - handler: handleExchangeRates + filter: + kind: polling + every: {{ pollingBlocksInterval }} + - kind: ethereum/contract + name: OsToken network: {{ network }} source: address: '{{ osToken.address }}' @@ -17,11 +66,13 @@ dataSources: kind: ethereum/events apiVersion: 0.0.8 language: wasm/assemblyscript - file: ./mappings/osToken.ts + file: ./mappings/erc20Token.ts entities: - - OsTokenHolder + - User + - Network + - OsToken - TokenTransfer - - TokenHolder + - OsTokenHolder abis: - name: Erc20Token file: ./abis/Erc20Token.json @@ -39,10 +90,10 @@ dataSources: kind: ethereum/events apiVersion: 0.0.8 language: wasm/assemblyscript - file: ./mappings/swiseToken.ts + file: ./mappings/erc20Token.ts entities: - TokenTransfer - - TokenHolder + - SwiseTokenHolder abis: - name: Erc20Token file: ./abis/Erc20Token.json @@ -62,16 +113,33 @@ dataSources: language: wasm/assemblyscript file: ./mappings/keeper.ts entities: + - Network + - OsToken + - OsTokenConfig + - OsTokenSnapshot + - OsTokenHolder + - OsTokenHolderSnapshot - Vault - - DaySnapshot + - VaultSnapshot - V2Pool + - Allocator + - AllocatorSnapshot + - ExitRequest + - ExitRequestSnapshot + - RewardSplitter + - RewardSplitterShareHolder + - RewardSplitterShareHolderSnapshot abis: - name: Keeper file: ./abis/Keeper.json - name: Vault file: ./abis/Vault.json + - name: OsTokenVaultController + file: ./abis/OsTokenVaultController.json - name: Multicall file: ./abis/Multicall.json + - name: RewardSplitter + file: ./abis/RewardSplitter.json eventHandlers: - event: RewardsUpdated(indexed address,indexed bytes32,uint256,uint64,uint64,string) handler: handleRewardsUpdated @@ -97,13 +165,10 @@ dataSources: file: ./mappings/osToken.ts entities: - OsToken - - OsTokenSnapshot abis: - name: OsTokenVaultController file: ./abis/OsTokenVaultController.json eventHandlers: - - event: AvgRewardPerSecondUpdated(uint256) - handler: handleAvgRewardPerSecondUpdated - event: StateUpdated(uint256,uint256,uint256) handler: handleStateUpdated - event: FeePercentUpdated(uint16) @@ -121,7 +186,10 @@ dataSources: language: wasm/assemblyscript file: ./mappings/osTokenConfig.ts entities: + - Network - Vault + - OsTokenConfig + - Allocator abis: - name: OsTokenConfigV1 file: ./abis/OsTokenConfigV1.json @@ -141,7 +209,10 @@ dataSources: language: wasm/assemblyscript file: ./mappings/osTokenConfig.ts entities: + - Network - Vault + - OsTokenConfig + - Allocator abis: - name: OsTokenConfigV2 file: ./abis/OsTokenConfigV2.json @@ -161,13 +232,14 @@ dataSources: language: wasm/assemblyscript file: ./mappings/vault.ts entities: + - Network + - User - Vault - Allocator - - DaySnapshot - AllocatorAction - Transaction - - Network - - VaultsStat + - OsToken + - OsTokenConfig abis: - name: GenesisVault file: ./abis/GenesisVault.json @@ -190,6 +262,9 @@ dataSources: file: ./mappings/v2pool.ts entities: - V2Pool + - V2PoolUser + - Network + - User abis: - name: V2RewardToken file: ./abis/V2RewardToken.json @@ -216,6 +291,9 @@ dataSources: file: ./mappings/v2pool.ts entities: - V2Pool + - V2PoolUser + - Network + - User abis: - name: V2StakedToken file: ./abis/V2StakedToken.json @@ -328,6 +406,7 @@ templates: - Vault - Network - Transaction + - OsTokenConfig abis: - name: VaultFactory file: ./abis/VaultFactory.json @@ -364,12 +443,14 @@ templates: language: wasm/assemblyscript file: ./mappings/vault.ts entities: + - Network + - User + - OsToken + - OsTokenConfig - Vault - Allocator - - ExitRequest - - DaySnapshot - - OsToken - AllocatorAction + - ExitRequest - Transaction abis: - name: Vault @@ -395,10 +476,6 @@ templates: handler: handleKeysManagerUpdated - event: ValidatorsManagerUpdated(indexed address,indexed address) handler: handleValidatorsManagerUpdated - - event: CheckpointCreated(uint256,uint256) - handler: handleCheckpointCreated - - event: ExitingAssetsPenalized(uint256) - handler: handleExitingAssetsPenalized - event: FeeSharesMinted(address,uint256,uint256) handler: handleFeeSharesMinted - event: OsTokenMinted(indexed address,address,uint256,uint256,address) @@ -466,7 +543,13 @@ templates: language: wasm/assemblyscript file: ./mappings/erc20Vault.ts entities: + - Network + - User + - Vault + - OsToken + - OsTokenConfig - Allocator + - AllocatorAction - Transaction abis: - name: Erc20Vault @@ -485,9 +568,13 @@ templates: language: wasm/assemblyscript file: ./mappings/vault.ts entities: - - Vault - Network - - VaultsStat + - User + - Vault + - OsToken + - OsTokenConfig + - Allocator + - AllocatorAction - Transaction abis: - name: FoxVault @@ -506,8 +593,13 @@ templates: language: wasm/assemblyscript file: ./mappings/gnoVault.ts entities: + - Network - Vault - - Transaction + - OsToken + - OsTokenConfig + - VaultSnapshot + - Allocator + - AllocatorSnapshot abis: - name: GnoVault file: ./abis/GnoVault.json @@ -538,24 +630,6 @@ templates: handler: handleRestakeOperatorsManagerUpdated - event: RestakeWithdrawalsManagerUpdated(address) handler: handleRestakeWithdrawalsManagerUpdated - - kind: ethereum/contract - name: OwnMevEscrow - network: {{ network }} - source: - abi: OwnMevEscrow - mapping: - kind: ethereum/events - apiVersion: 0.0.8 - language: wasm/assemblyscript - file: ./mappings/mevEscrow.ts - entities: - - Vault - abis: - - name: OwnMevEscrow - file: ./abis/OwnMevEscrow.json - eventHandlers: - - event: Harvested(uint256) - handler: handleHarvested - kind: ethereum/contract name: RewardSplitter network: {{ network }} @@ -578,3 +652,5 @@ templates: handler: handleSharesIncreased - event: SharesDecreased(indexed address,uint256) handler: handleSharesDecreased + - event: RewardsWithdrawn(indexed address,uint256) + handler: handleRewardsWithdrawn