Skip to content

Add staking data to vault snapshot hooks#316

Merged
murderteeth merged 4 commits intomainfrom
feat/staking-data
Jan 28, 2026
Merged

Add staking data to vault snapshot hooks#316
murderteeth merged 4 commits intomainfrom
feat/staking-data

Conversation

@matheus1lva
Copy link
Collaborator

@matheus1lva matheus1lva commented Jan 25, 2026

Summary

Implements staking rewards data indexing for Yearn vaults across multiple chains. This adds support for four types of staking systems (VeYFI, Juiced, V3 Staking, OP Boost) using an event-driven discovery pattern, and embeds staking APR and reward data directly in vault snapshot hooks.

Closes #305

Implementation Details

Staking Registry Discovery (Event-Driven)

  • Added 4 registry ABIs with event hooks to discover staking pools:
    • yearn/staking/registry/juiced - Ethereum, Arbitrum, Polygon
    • yearn/staking/registry/v3 - Ethereum, Arbitrum, Polygon
    • yearn/staking/registry/opboost - Optimism
    • yearn/staking/registry/veyfi - Ethereum (special handling to query stakingToken)
  • Event hooks create stakingPool things when StakingPoolAdded or Register events are detected

Staking Pool Snapshots

  • Added yearn/staking/pool ABI and snapshot hook
  • Fetches reward token data, prices, and calculates APR using: APR = (rewardPerDuration * rewardPrice) / (vaultPrice * totalStaked) / 10^decimals * (secondsPerYear / rewardDuration)
  • Returns reward metadata including address, symbol, decimals, price, APR, per-week distribution, and completion status

Vault Integration

  • Updated V2 and V3 vault snapshot hooks to query associated staking pools
  • Embedded staking data in vault snapshots with fallback: {available: false, rewards: []}
  • Added GraphQL types: Staking and StakingReward

Critical Bug Fixes

  • Fixed SQL query using non-existent column t.incept_block(t.defaults->>'inceptBlock')::bigint
  • Fixed import paths in staking pool hook (6 levels deep)
  • Fixed price extraction to destructure priceUsd property
  • Fixed bigint type compatibility by wrapping decimals in Number()

How to review

  1. Start with config changes: Review config/abis.yaml to understand registry sources and chains
  2. Registry event hooks: Check packages/ingest/abis/yearn/staking/registry/*/event/hook.ts for thing creation logic
  3. Staking pool snapshot: Review packages/ingest/abis/yearn/staking/pool/snapshot/hook.ts for APR calculation
  4. Vault integration: Check V2/V3 vault snapshot hooks for staking query and embedding
  5. GraphQL types: Review packages/web/app/api/gql/typeDefs/vault.ts for API schema

Files to focus on:

  • APR calculation logic in staking/pool/snapshot/hook.ts (lines 100-120)
  • SQL queries in vault hooks (lines 86-94 for V3, 116-124 for V2)
  • VeYFI registry hook which has different event structure

Test plan

Automated:

  • Existing tests should pass (no behavioral changes to existing vault data)
  • TypeScript compilation successful
  • Linting passes

Manual:

  1. Start dev environment: make dev
  2. Run fanout: Navigate to Ingest → fanout abis (run 2-3 times for full initialization)
  3. Verify staking pool discovery:
    SELECT COUNT(*) FROM thing WHERE label = 'stakingPool';
    -- Should show staking pools discovered from registry events
  4. Verify staking pool snapshots:
    SELECT address, snapshot->>'available' as available 
    FROM snapshot s 
    JOIN thing t ON s.chain_id = t.chain_id AND s.address = t.address 
    WHERE t.label = 'stakingPool' LIMIT 5;
  5. Test GraphQL API:
    query {
      vault(chainId: 1, address: "0x...") {
        staking {
          available
          source
          rewards {
            symbol
            apr
            perWeek
          }
        }
      }
    }
  6. Test REST API:
    curl http://localhost:3001/api/rest/snapshot/1/<vault-address> | jq '.staking'

Manual testing

➜  kong git:(feat/staking-data) ✗ curl http://localhost:3000/api/rest/snapshot/1/0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3709    0  3709    0     0   132k      0 --:--:-- --:--:-- --:--:--  134k
{
  "chainId": 1,
  "address": "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204",
  "v3": true,
  "asset": {
    "name": "USD Coin",
    "symbol": "USDC",
    "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "chainId": 1,
    "decimals": "6"
  },
  "erc4626": true,
  "factory": "0x444045c5C13C246e117eD36437303cac8E250aB0",
  "decimals": 6,
  "vaultType": "1",
  "apiVersion": "3.0.2",
  "inceptTime": "1710258455",
  "inceptBlock": "19419991",
  "name": "USDC-1 yVault",
  "symbol": "yvUSDC-1",
  "FACTORY": "0x444045c5C13C246e117eD36437303cac8E250aB0",
  "blockTime": "1769369915",
  "totalDebt": "35521803940413",
  "totalIdle": "0",
  "accountant": "0x5A74Cb32D36f2f517DB6f7b0A0591e09b22cDE69",
  "isShutdown": false,
  "blockNumber": "24313974",
  "totalAssets": "35521803940413",
  "totalSupply": "32430813166841",
  "role_manager": "0xb3bd6B2E61753C311EFbCF0111f75D29706D9a41",
  "deposit_limit": "50000000000000",
  "pricePerShare": "1095310",
  "unlockedShares": "10188125400",
  "DOMAIN_SEPARATOR": "0xe617f6b05d0a04984902525f2611c0df06fcba6a6e9bea1205da8a7145106a2c",
  "lastProfitUpdate": "1769131751",
  "get_default_queue": [
    "0x00C8a649C9837523ebb406Ceb17a6378Ab5C74cF",
    "0x39c0aEc5738ED939876245224aFc7E09C8480a52",
    "0x694E47AFD14A64661a04eee674FB331bCDEF3737",
    "0x074134A2784F4F66b6ceD6f68849382990Ff3215",
    "0x25f893276544d86a82b1ce407182836F45cb6673",
    "0x522478B54046aB7197880F2626b74a96d45B9B02",
    "0x888239Ffa9a0613F9142C808aA9F7d1948a14f75",
    "0x694cdD19EBee7A974BA8fE3AF8B383bb256F2858"
  ],
  "use_default_queue": false,
  "minimum_total_idle": "0",
  "future_role_manager": "0x0000000000000000000000000000000000000000",
  "profitMaxUnlockTime": "864000",
  "profitUnlockingRate": "42777772461748309",
  "deposit_limit_module": "0x0000000000000000000000000000000000000000",
  "fullProfitUnlockDate": "1769401543",
  "withdraw_limit_module": "0x0000000000000000000000000000000000000000",
  "fees": {
    "managementFee": 0,
    "performanceFee": 1000
  },
  "meta": {
    "kind": "Multi Strategy",
    "name": "USDC-1 yVault",
    "type": "Yearn Vault",
    "token": {
      "name": "USD Coin",
      "symbol": "USDC",
      "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "chainId": 1,
      "category": "",
      "decimals": 6,
      "description": "",
      "displayName": "USD Coin",
      "displaySymbol": "USDC"
    },
    "isPool": false,
    "address": "0xbe53a109b494e5c9f97b9cd39fe969be68bf6204",
    "chainId": 1,
    "category": "Stablecoin",
    "isHidden": false,
    "registry": "0xff31a1b020c868f6ea3f61eb953344920eeca3af",
    "uiNotice": "",
    "inclusion": {
      "isSet": true,
      "isCove": false,
      "isGimme": false,
      "isYearn": true,
      "isKatana": false,
      "isMorpho": false,
      "isYearnJuiced": false,
      "isPoolTogether": false,
      "isPublicERC4626": false
    },
    "isBoosted": false,
    "isRetired": false,
    "migration": {
      "target": "0xbe53a109b494e5c9f97b9cd39fe969be68bf6204",
      "contract": "0x0000000000000000000000000000000000000000",
      "available": false
    },
    "protocols": [],
    "sourceURI": "",
    "stability": {
      "stability": "Unknown"
    },
    "description": "Multi strategy USDC vault. <br/><br/>Multi strategy vaults are (wait for it) vaults that contain multiple strategies. Multi strategy vaults give the vault creator flexibility to balance risk and opportunity across multiple different strategies.",
    "displayName": "USDC",
    "isAutomated": false,
    "isAggregator": false,
    "displaySymbol": "yvUSDC",
    "isHighlighted": true,
    "shouldUseV2APR": true
  },
  "risk": {
    "riskLevel": 1,
    "riskScore": {
      "review": 0,
      "comment": "",
      "testing": 0,
      "complexity": 0,
      "riskExposure": 0,
      "centralizationRisk": 0,
      "externalProtocolTvl": 0,
      "protocolIntegration": 0,
      "externalProtocolType": 0,
      "externalProtocolAudit": 0,
      "externalProtocolLongevity": 0,
      "externalProtocolCentralisation": 0
    }
  },
  "debts": [],
  "roles": [
    {
      "account": "0xb3bd6B2E61753C311EFbCF0111f75D29706D9a41",
      "roleMask": "57896044618658097711785492504343953926634992332820282019728792003956564819968"
    }
  ],
  "staking": {
    "rewards": [],
    "available": false
  },
  "allocator": "0x1400D5C76D0A630368c8548172e5130983AAA0Ef",
  "sparklines": {
    "apy": [],
    "tvl": []
  },
  "strategies": [],
  "performance": {
    "oracle": {
      "apr": 0,
      "apy": 0
    }
  }
}

- Fix SQL column reference in vault hooks: use (t.defaults->>'inceptBlock')::bigint instead of t.incept_block
- Correct import paths in staking pool hook (6 levels deep)
- Fix price extraction to destructure priceUsd property
- Wrap decimals in Number() for bigint type compatibility

This resolves snapshot extraction failures that prevented staking data from being indexed.
@vercel
Copy link

vercel bot commented Jan 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
kong Ready Ready Preview, Comment Jan 28, 2026 2:29pm

Request Review

@matheus1lva
Copy link
Collaborator Author

For vaults that has real rewards i couldnt find any easily, is there any easy to test on fetching rewards dat?

@rossgalloway
Copy link
Collaborator

rossgalloway commented Jan 26, 2026

For vaults that has real rewards i couldnt find any easily, is there any easy to test on fetching rewards dat?

These should have staking enabled:
0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204 (yvUSDC-1)
0x028eC7330ff87667b6dfb0D94b954c820195336c (yvDAI-1)
0xc56413869c6CDf96496f2b1eF801fEDBdFA7dDB0 (yvWETH-1)
0xBF319dDC2Edc1Eb6FDf9910E39b37Be221C8805F (yvcrvUSD-2)

@murderteeth murderteeth self-requested a review January 27, 2026 04:52
Copy link
Collaborator

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i indexed just usdc-1 0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204 on mainnet + the staking contracts. i run this gql


query {
  vault(chainId: 1, address: "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204") {
    staking {
      available
      source
      rewards {
        symbol
        apr
        perWeek
      }
    }
  }
}

i get this error

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Staking.source.",
      "locations": [
        {
          "line": 8,
          "column": 7
        }
      ],
      "path": [
        "vault",
        "staking",
        "source"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "Error: Cannot return null for non-nullable field Staking.source.",
          "    at completeValue (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:601:13)",
          "    at executeField (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:498:19)",
          "    at executeFields (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:412:22)",
          "    at completeObjectValue (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:912:10)",
          "    at completeValue (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:642:12)",
          "    at executeField (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:498:19)",
          "    at executeFields (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:412:22)",
          "    at completeObjectValue (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:912:10)",
          "    at completeValue (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:642:12)",
          "    at eval (webpack-internal:///(rsc)/../../node_modules/graphql/execution/execute.mjs:495:9)",
          "    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)",
          "    at async Promise.all (index 0)",
          "    at async execute (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/requestPipeline.js:249:37)",
          "    at async processGraphQLRequest (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/requestPipeline.js:178:32)",
          "    at async internalExecuteOperation (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/ApolloServer.js:646:16)",
          "    at async runHttpQuery (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/runHttpQuery.js:141:29)",
          "    at async runPotentiallyBatchedHttpQuery (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/httpBatching.js:40:16)",
          "    at async ApolloServer.executeHTTPGraphQLRequest (webpack-internal:///(rsc)/../../node_modules/@apollo/server/dist/esm/ApolloServer.js:557:20)"
        ]
      }
    }
  ],
  "data": {
    "vault": {
      "chainId": 1,
      "address": "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204",
      "name": "USDC-1 yVault",
      "staking": null
    }
  }
}

@matheus1lva
Copy link
Collaborator Author

Ok so it took me some time to understand why the hell that vault for eg was not pulling rewards.

So here i have it:

c%                                                                                                                                                                                                   
➜  web git:(feat/staking-data) ✗ curl http://localhost:3000/api/rest/snapshot/1/0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3973    0  3973    0     0   4489      0 --:--:-- --:--:-- --:--:--  4489
{
  "chainId": 1,
  "address": "0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204",
  "v3": true,
  "asset": {
    "name": "USD Coin",
    "symbol": "USDC",
    "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "chainId": 1,
    "decimals": "6"
  },
  "erc4626": true,
  "factory": "0x444045c5C13C246e117eD36437303cac8E250aB0",
  "decimals": 6,
  "vaultType": "1",
  "apiVersion": "3.0.2",
  "inceptTime": "1710258455",
  "inceptBlock": "19419991",
  "name": "USDC-1 yVault",
  "symbol": "yvUSDC-1",
  "FACTORY": "0x444045c5C13C246e117eD36437303cac8E250aB0",
  "blockTime": "1769532035",
  "totalDebt": "36086917526370",
  "totalIdle": "0",
  "accountant": "0x5A74Cb32D36f2f517DB6f7b0A0591e09b22cDE69",
  "isShutdown": false,
  "blockNumber": "24327425",
  "totalAssets": "36086917526370",
  "totalSupply": "32942475985752",
  "role_manager": "0xb3bd6B2E61753C311EFbCF0111f75D29706D9a41",
  "deposit_limit": "50000000000000",
  "pricePerShare": "1095452",
  "unlockedShares": "3024490339",
  "DOMAIN_SEPARATOR": "0xe617f6b05d0a04984902525f2611c0df06fcba6a6e9bea1205da8a7145106a2c",
  "lastProfitUpdate": "1769400167",
  "get_default_queue": [
    "0x00C8a649C9837523ebb406Ceb17a6378Ab5C74cF",
    "0x39c0aEc5738ED939876245224aFc7E09C8480a52",
    "0x694E47AFD14A64661a04eee674FB331bCDEF3737",
    "0x074134A2784F4F66b6ceD6f68849382990Ff3215",
    "0x25f893276544d86a82b1ce407182836F45cb6673",
    "0x522478B54046aB7197880F2626b74a96d45B9B02",
    "0x888239Ffa9a0613F9142C808aA9F7d1948a14f75",
    "0x694cdD19EBee7A974BA8fE3AF8B383bb256F2858"
  ],
  "use_default_queue": false,
  "minimum_total_idle": "0",
  "future_role_manager": "0x0000000000000000000000000000000000000000",
  "profitMaxUnlockTime": "864000",
  "profitUnlockingRate": "22935741342883345",
  "deposit_limit_module": "0x0000000000000000000000000000000000000000",
  "fullProfitUnlockDate": "1770261597",
  "withdraw_limit_module": "0x0000000000000000000000000000000000000000",
  "fees": {
    "managementFee": 0,
    "performanceFee": 1000
  },
  "meta": {
    "kind": "Multi Strategy",
    "name": "USDC-1 yVault",
    "type": "Yearn Vault",
    "token": {
      "name": "USD Coin",
      "symbol": "USDC",
      "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
      "chainId": 1,
      "category": "",
      "decimals": 6,
      "description": "",
      "displayName": "USD Coin",
      "displaySymbol": "USDC"
    },
    "isPool": false,
    "address": "0xbe53a109b494e5c9f97b9cd39fe969be68bf6204",
    "chainId": 1,
    "category": "Stablecoin",
    "isHidden": false,
    "registry": "0xff31a1b020c868f6ea3f61eb953344920eeca3af",
    "uiNotice": "",
    "inclusion": {
      "isSet": true,
      "isCove": false,
      "isGimme": false,
      "isYearn": true,
      "isKatana": false,
      "isMorpho": false,
      "isYearnJuiced": false,
      "isPoolTogether": false,
      "isPublicERC4626": false
    },
    "isBoosted": false,
    "isRetired": false,
    "migration": {
      "target": "0xbe53a109b494e5c9f97b9cd39fe969be68bf6204",
      "contract": "0x0000000000000000000000000000000000000000",
      "available": false
    },
    "protocols": [],
    "sourceURI": "",
    "stability": {
      "stability": "Unknown"
    },
    "description": "Multi strategy USDC vault. <br/><br/>Multi strategy vaults are (wait for it) vaults that contain multiple strategies. Multi strategy vaults give the vault creator flexibility to balance risk and opportunity across multiple different strategies.",
    "displayName": "USDC",
    "isAutomated": false,
    "isAggregator": false,
    "displaySymbol": "yvUSDC",
    "isHighlighted": true,
    "shouldUseV2APR": true
  },
  "risk": {
    "riskLevel": 1,
    "riskScore": {
      "review": 0,
      "comment": "",
      "testing": 0,
      "complexity": 0,
      "riskExposure": 0,
      "centralizationRisk": 0,
      "externalProtocolTvl": 0,
      "protocolIntegration": 0,
      "externalProtocolType": 0,
      "externalProtocolAudit": 0,
      "externalProtocolLongevity": 0,
      "externalProtocolCentralisation": 0
    }
  },
  "debts": [],
  "roles": [
    {
      "account": "0xb3bd6B2E61753C311EFbCF0111f75D29706D9a41",
      "roleMask": "57896044618658097711785492504343953926634992332820282019728792003956564819968"
    }
  ],
  "staking": {
    "source": "VeYFI",
    "address": "0x622fA41799406B120f9a40dA843D358b7b2CFEE3",
    "rewards": [
      {
        "apr": 0,
        "name": "Discount YFI",
        "price": 2256.07409,
        "symbol": "dYFI",
        "address": "0x41252E8691e964f7DE35156B68493bAb6797a275",
        "perWeek": 0,
        "decimals": "18",
        "finishedAt": "1770249600",
        "isFinished": false
      }
    ],
    "available": true
  },
  "allocator": "0x1400D5C76D0A630368c8548172e5130983AAA0Ef",
  "sparklines": {
    "apy": [],
    "tvl": []
  },
  "strategies": [],
  "performance": {
    "oracle": {
      "apr": 0,
      "apy": 0
    }
  }
}

And on the graphql playground:

{
  "data": {
    "vault": {
      "staking": {
        "address": "0x622fA41799406B120f9a40dA843D358b7b2CFEE3",
        "available": true,
        "source": "VeYFI",
        "rewards": [
          {
            "address": "0x41252E8691e964f7DE35156B68493bAb6797a275",
            "name": "Discount YFI",
            "symbol": "dYFI",
            "decimals": 18,
            "price": 2256.07409,
            "isFinished": false,
            "finishedAt": "1770249600",
            "apr": 0,
            "perWeek": 0
          }
        ]
      }
    }
  }
}

@murderteeth murderteeth self-requested a review January 28, 2026 07:40
Copy link
Collaborator

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can see staking data for 0xBe53A109B494E5c9f97b9Cd39Fe969BE68BF6204 now! but i also notice when running the indexer that the opboost hook is failing


ZodError: [
{
  "code": "custom",
  "fatal": true,
  "path": [
    "stakingPool"
  ],
  "message": "Invalid input"
}
]
  at Object.get error [as error] (/home/murdertxxth/git/kong/node_modules/zod/lib/types.js:43:31)
  at ZodObject.parse (/home/murdertxxth/git/kong/node_modules/zod/lib/types.js:143:22)
  at Object.process (/home/murdertxxth/git/kong/packages/ingest/abis/yearn/staking/registry/opboost/event/hook.ts:15:6)
  at EvmLogsExtractor.extract (/home/murdertxxth/git/kong/packages/ingest/extract/evmlogs.ts:74:39)
  at processTicksAndRejections (node:internal/process/task_queues:105:5)
  at async handler (/home/murdertxxth/git/kong/packages/ingest/extract/index.ts:32:7)
  at async Worker.bullmq_1.Worker [as processFn] (/home/murdertxxth/git/kong/packages/lib/mq.ts:90:7)
  at async Worker.processJob (/home/murdertxxth/git/kong/node_modules/bullmq/src/classes/worker.ts:741:22)
  at async Worker.retryIfFailed (/home/murdertxxth/git/kong/node_modules/bullmq/src/classes/worker.ts:944:16) {
issues: [
    {
      code: 'custom',
      fatal: true,
      path: [Array],
      message: 'Invalid input'
    }
  ],
  addIssue: [Function (anonymous)],
  addIssues: [Function (anonymous)],
  errors: [
    {
      code: 'custom',
      fatal: true,
      path: [Array],
      message: 'Invalid input'
    }
  ]
}

@matheus1lva
Copy link
Collaborator Author

Apparently i had a left over chain.local which i forgot T.T, removed it and also updated the abi.

Copy link
Collaborator

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. let me merge tho. indexing the pools will take some time, so we need to disable the fanout cron, index the pools manually, then re-enable the cron.

@murderteeth murderteeth merged commit ed5a91d into main Jan 28, 2026
3 checks passed
@murderteeth murderteeth deleted the feat/staking-data branch January 28, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add staking data to vault snapshot hooks

3 participants