Skip to content

surf-liquid: Add V4 yield pools for Ethereum, Arbitrum, Polygon, Base#2626

Open
rohansingh4 wants to merge 2 commits into
DefiLlama:masterfrom
rohansingh4:add-surf-liquid-v4
Open

surf-liquid: Add V4 yield pools for Ethereum, Arbitrum, Polygon, Base#2626
rohansingh4 wants to merge 2 commits into
DefiLlama:masterfrom
rohansingh4:add-surf-liquid-v4

Conversation

@rohansingh4
Copy link
Copy Markdown
Contributor

@rohansingh4 rohansingh4 commented Apr 25, 2026

Summary

Extends surf-liquid yield adapter with V4 pools across 4 chains.

V4 contracts (same addresses on all chains):

  • UserVaultFactory: 0x8fa50DeA8DB10987D7d22ac092001c3613C18779
  • VaultRegistry: 0x98A0DeF9C959Ec934Df02141291303819369f271

V4 yield logic per chain:

  1. Enumerate user vaults from VaultDeployed events on UserVaultFactory
  2. Query allowed assets from VaultRegistry.getAllowedAssets() (dynamic — no hardcoded token list)
  3. For each asset: compute APY from Morpho vault share price changes (24h window, weighted by TVL per user vault)
  4. TVL via getAssetVaultAssets(asset) — exact underlying regardless of which Morpho vault holds funds
  5. Apply 10% performance fee to get user APY

Pool IDs: surf-v4-{asset_address}-{chain} (distinct from V2/V3 surf-{asset}-base)

Test output:

Base    USDC   TVL: 159k  APY: 5.1%  (V2/V3, unchanged)
Base    WETH   TVL: 94k   APY: 4.1%  (V2/V3, unchanged)
Base    cbBTC  TVL: 3k    APY: 0%    (V2/V3, unchanged)
Base    SURF   TVL: 38k   APY: 12%   (staking, unchanged)
Polygon USDC   TVL: 10k   APY: 9.4%  (V4, live!)

Ethereum/Arbitrum V4 pools below $100 TVL threshold (just launched, no real user deposits yet).

Summary by CodeRabbit

  • New Features
    • Introduced SURF v4 liquidity pool support across multiple blockchains with dynamic APY and TVL calculations
    • Extended staking pool generation to include an additional staking contract
    • Enhanced yield aggregation by combining v2, v3, and v4 pool data

Review Change Stack

V4 (UserVaultFactory 0x8fa50...) deployed March 26, 2026. Queries
getAllowedAssets() from VaultRegistry, then computes APY from Morpho
vault share price changes and TVL via getAssetVaultAssets(). V2/V3
Base pools unchanged.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

This PR extends the SURF liquid adapter to support v4 liquidity pools across multiple chains and adds a second staking contract. The changes introduce v4 vault discovery, APY calculation from Morpho vault data, TVL aggregation, staking pool generation for both contract addresses, and integration of v4 pools into the main pool output.

Changes

SURF V4 Liquidity and Staking V2 Integration

Layer / File(s) Summary
V4 Constants and Configuration
src/adaptors/surf-liquid/index.js
Introduces SURF_STAKING_V2 contract constant and v4-specific configuration: V4_FACTORY, V4_REGISTRY addresses, per-chain starting block ranges, and V4_CHAINS list.
V4 Pool Discovery and Calculation
src/adaptors/surf-liquid/index.js
Implements fetchV4ChainPools(chain) to discover v4 vaults from factory events, read allowed assets, fetch prices from Llama, derive 24h APY from Morpho vault totalAssets/totalSupply changes, and compute TVL-weighted APY per underlying asset.
Staking Pool Multi-Contract Loop
src/adaptors/surf-liquid/index.js
Extends staking pool construction to loop over both SURF_STAKING and SURF_STAKING_V2, enriching each pool with apyReward, rewardTokens, and poolMeta when APR6M data is available.
V4 Pool Aggregation
src/adaptors/surf-liquid/index.js
Calls fetchV4ChainPools for each chain in V4_CHAINS and appends fulfilled results to the main pools array via Promise.allSettled.
Documentation
src/adaptors/surf-liquid/index.js
Updates comment to explicitly reference "V2/V3 yield pools per asset".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

The changes involve substantial new logic across multiple areas: v4 vault discovery with event filtering, multi-step APY calculation from historical vault data, TVL computation with asset weighting, parallel chain processing, and dual-contract staking pool construction. The code is dense with data transformations and external API calls requiring careful verification of calculation logic and edge-case handling.

Poem

🐰 Hops with glee at vault arrays new,
V4 pools across the chains we flew,
Staking doubled, APY flows true,
Morpho snapshots paint the clue,
SURF adaptor, now enhanced through and through!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'surf-liquid: Add V4 yield pools for Ethereum, Arbitrum, Polygon, Base' directly and clearly summarizes the main change: adding V4 yield pool support across multiple chains to the surf-liquid adapter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

The surf-liquid adapter exports pools:

Test Suites: 1 passed, 1 total
Tests: 29 passed, 29 total
Snapshots: 0 total
Time: 0.257 s
Ran all test suites.

Nb of pools: 4
 

Sample pools:
┌─────────┬────────────────────────────────────────────────────────┬────────┬───────────────┬─────────┬────────────────────┬───────────────────────┬──────────────────────────────────────────────────┬───────────┬──────────────────────────────────────────────────┬─────────────────┐
│ (index) │ pool                                                   │ chain  │ project       │ symbol  │ tvlUsd             │ apyBase               │ underlyingTokens                                 │ apyReward │ rewardTokens                                     │ poolMeta        │
├─────────┼────────────────────────────────────────────────────────┼────────┼───────────────┼─────────┼────────────────────┼───────────────────────┼──────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────┤
│ 0       │ 'surf-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913-base' │ 'Base' │ 'surf-liquid' │ 'USDC'  │ 164580.2054144626  │ 5.109629018476062     │ [ '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' ] │           │                                                  │                 │
│ 1       │ 'surf-0x4200000000000000000000000000000000000006-base' │ 'Base' │ 'surf-liquid' │ 'WETH'  │ 94014.40075998516  │ 4.061598927675796     │ [ '0x4200000000000000000000000000000000000006' ] │           │                                                  │                 │
│ 2       │ '0xb0fdfc081310a5914c2d2c97e7582f4de12fa9d6-base'      │ 'Base' │ 'surf-liquid' │ 'SURF'  │ 38219.01498098496  │ 0                     │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ 12        │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ '6M / 12M lock' │
│ 3       │ 'surf-0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf-base' │ 'Base' │ 'surf-liquid' │ 'cbBTC' │ 3007.1585377744345 │ 0.0045753051065600125 │ [ '0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf' ] │           │                                                  │                 │
└─────────┴────────────────────────────────────────────────────────┴────────┴───────────────┴─────────┴────────────────────┴───────────────────────┴──────────────────────────────────────────────────┴───────────┴──────────────────────────────────────────────────┴─────────────────┘
This adapter contains some pools with <10k TVL, these pools won't be shown in DefiLlama

@github-actions
Copy link
Copy Markdown

The surf-liquid adapter exports pools:

Test Suites: 1 passed, 1 total
Tests: 36 passed, 36 total
Snapshots: 0 total
Time: 0.256 s
Ran all test suites.

Nb of pools: 5
 

Sample pools:
┌─────────┬────────────────────────────────────────────────────────┬────────┬───────────────┬─────────┬────────────────────┬──────────────────────┬──────────────────────────────────────────────────┬───────────┬──────────────────────────────────────────────────┬─────────────────┐
│ (index) │ pool                                                   │ chain  │ project       │ symbol  │ tvlUsd             │ apyBase              │ underlyingTokens                                 │ apyReward │ rewardTokens                                     │ poolMeta        │
├─────────┼────────────────────────────────────────────────────────┼────────┼───────────────┼─────────┼────────────────────┼──────────────────────┼──────────────────────────────────────────────────┼───────────┼──────────────────────────────────────────────────┼─────────────────┤
│ 0       │ 'surf-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913-base' │ 'Base' │ 'surf-liquid' │ 'USDC'  │ 145888.5114999065  │ 4.501147957974133    │ [ '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' ] │           │                                                  │                 │
│ 1       │ 'surf-0x4200000000000000000000000000000000000006-base' │ 'Base' │ 'surf-liquid' │ 'WETH'  │ 94945.81993505626  │ 1.1200857218780818   │ [ '0x4200000000000000000000000000000000000006' ] │           │                                                  │                 │
│ 2       │ '0xb0fdfc081310a5914c2d2c97e7582f4de12fa9d6-base'      │ 'Base' │ 'surf-liquid' │ 'SURF'  │ 41653.72467171059  │ 0                    │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ 12        │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ '6M / 12M lock' │
│ 3       │ '0xeba3b16e175fd36c8b01953d9e3962ab3c575718-base'      │ 'Base' │ 'surf-liquid' │ 'SURF'  │ 3134.005287167092  │ 0                    │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ 12        │ [ '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf' ] │ '6M / 12M lock' │
│ 4       │ 'surf-0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf-base' │ 'Base' │ 'surf-liquid' │ 'cbBTC' │ 3131.9768949354325 │ 0.006192063139371928 │ [ '0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf' ] │           │                                                  │                 │
└─────────┴────────────────────────────────────────────────────────┴────────┴───────────────┴─────────┴────────────────────┴──────────────────────┴──────────────────────────────────────────────────┴───────────┴──────────────────────────────────────────────────┴─────────────────┘
This adapter contains some pools with <10k TVL, these pools won't be shown in DefiLlama

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/adaptors/surf-liquid/index.js (1)

70-83: ⚡ Quick win

Resolve token decimals on-chain instead of defaulting to 18.

getAllowedAssets() is dynamic, so this fallback can misprice 6/8-decimal assets by orders of magnitude if the price API omits decimals, which will also make the $100 TVL filter drop valid pools. Multicalling ERC-20 decimals() once per asset is a much safer source of truth here.

Safer pattern
+  const { output: decimalsRes } = await sdk.api.abi.multiCall({
+    abi: 'erc20:decimals',
+    calls: assets.map((target) => ({ target })),
+    chain,
+  });
+
+  const assetDecimals = Object.fromEntries(
+    assets.map((asset, i) => [asset, Number(decimalsRes[i]?.output ?? 18)])
+  );
+
   for (const asset of assets) {
     const priceKey = `${chain}:${asset}`;
     const price = coinData[priceKey]?.price || 0;
-    const decimals = coinData[priceKey]?.decimals || 18;
+    const decimals = assetDecimals[asset];
     const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/surf-liquid/index.js` around lines 70 - 83, The code currently
falls back to decimals = coinData[priceKey]?.decimals || 18 which can misprice
assets when the price API omits decimals; update the logic in
src/adaptors/surf-liquid/index.js (look for priceKeys, coinData, priceKey,
decimals, symbol and assets) to resolve ERC‑20 decimals on-chain instead of
defaulting to 18: perform a single multicall (or per-asset call if multicall
helper not available) to token.decimals() for all entries returned by
getAllowedAssets(), use those on-chain decimals to compute prices/TVL when
coinData lacks decimals, and only fall back to 18 as an absolute last resort
after the on-chain call fails. Ensure the new call happens before constructing
pools so price/decimals are accurate for each priceKey.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adaptors/surf-liquid/index.js`:
- Around line 79-181: In fetchV4ChainPools(), avoid letting any per-asset
ABI/price call reject the whole chain: wrap the per-asset work (the multiCall
blocks for morphoResults, nowAssets/nowSupply/pastAssets/pastSupply, and
tvlResults plus price lookup from coinData) in a try/catch so a failure for one
asset (e.g., errors from the calls that produce morphoResults, nowAssets,
pastAssets, morphoApyMap, or tvlResults) logs the asset and error and continues
to the next asset instead of throwing; specifically, around the loop body that
builds uniqueMorpho, computes morphoApyMap, and computes
totalTvlUsd/weightedApy, catch errors and skip pushing the pool for that asset
(do the same containment for the other instance noted at the end of file),
ensuring any thrown error does not bubble up to the chain-level
Promise.allSettled.
- Around line 130-140: The loop building morphoApyMap currently converts uint256
outputs to Number (aNow, sNow, aPast, sPast) which loses precision; change to
parse outputs as BigInt for nowAssets, nowSupply, pastAssets, pastSupply,
compute share prices using integer fixed-point math (e.g. multiply numerator by
a constant SCALE BigInt before dividing to get a scaled priceNow and pricePast),
then compute the growth ratio and annualize using floating math only after
converting the normalized ratio to Number; update references to
aNow/sNow/aPast/sPast, priceNow/pricePast and apy so that BigInt math is used
until the final Number conversion when assigning
morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0).

---

Nitpick comments:
In `@src/adaptors/surf-liquid/index.js`:
- Around line 70-83: The code currently falls back to decimals =
coinData[priceKey]?.decimals || 18 which can misprice assets when the price API
omits decimals; update the logic in src/adaptors/surf-liquid/index.js (look for
priceKeys, coinData, priceKey, decimals, symbol and assets) to resolve ERC‑20
decimals on-chain instead of defaulting to 18: perform a single multicall (or
per-asset call if multicall helper not available) to token.decimals() for all
entries returned by getAllowedAssets(), use those on-chain decimals to compute
prices/TVL when coinData lacks decimals, and only fall back to 18 as an absolute
last resort after the on-chain call fails. Ensure the new call happens before
constructing pools so price/decimals are accurate for each priceKey.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5dbcb9e-44da-4146-8d92-62a250c91ea8

📥 Commits

Reviewing files that changed from the base of the PR and between e6ab2a0 and c6d00e4.

📒 Files selected for processing (1)
  • src/adaptors/surf-liquid/index.js

Comment on lines +79 to +181
for (const asset of assets) {
const priceKey = `${chain}:${asset}`;
const price = coinData[priceKey]?.price || 0;
const decimals = coinData[priceKey]?.decimals || 18;
const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);

// Get active Morpho vault per user vault (for APY weighting)
const { output: morphoResults } = await sdk.api.abi.multiCall({
abi: 'function assetToVault(address) view returns (address)',
calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
chain,
});

const uniqueMorpho = [
...new Set(
morphoResults
.map((r) => r.output)
.filter((v) => v && v !== ZERO_ADDR)
),
];

if (uniqueMorpho.length === 0) continue;

// Compute APY from Morpho vault share price changes (24h window)
const [nowAssets, nowSupply, pastAssets, pastSupply] = await Promise.all([
sdk.api.abi.multiCall({
abi: 'uint256:totalAssets',
calls: uniqueMorpho.map((t) => ({ target: t })),
chain,
block: blockNow,
}),
sdk.api.abi.multiCall({
abi: 'uint256:totalSupply',
calls: uniqueMorpho.map((t) => ({ target: t })),
chain,
block: blockNow,
}),
sdk.api.abi.multiCall({
abi: 'uint256:totalAssets',
calls: uniqueMorpho.map((t) => ({ target: t })),
chain,
block: blockPast,
}),
sdk.api.abi.multiCall({
abi: 'uint256:totalSupply',
calls: uniqueMorpho.map((t) => ({ target: t })),
chain,
block: blockPast,
}),
]);

const morphoApyMap = {};
for (let i = 0; i < uniqueMorpho.length; i++) {
const aNow = Number(nowAssets.output[i].output || '0');
const sNow = Number(nowSupply.output[i].output || '0');
const aPast = Number(pastAssets.output[i].output || '0');
const sPast = Number(pastSupply.output[i].output || '0');
if (sNow <= 0 || sPast <= 0) continue;
const priceNow = aNow / sNow;
const pricePast = aPast / sPast;
const apy = pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0);
}

// Get underlying asset value per user vault (TVL)
const { output: tvlResults } = await sdk.api.abi.multiCall({
abi: 'function getAssetVaultAssets(address) view returns (uint256)',
calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
chain,
});

let totalTvlUsd = 0;
let weightedApy = 0;

for (let i = 0; i < userVaults.length; i++) {
const amount = BigInt(tvlResults[i].output || '0');
if (amount === 0n) continue;

const tvlUsd = (Number(amount) / 10 ** decimals) * price;
totalTvlUsd += tvlUsd;

const morphoAddr = morphoResults[i].output;
if (morphoAddr && morphoAddr !== ZERO_ADDR) {
const apy = morphoApyMap[morphoAddr.toLowerCase()] || 0;
weightedApy += apy * tvlUsd;
}
}

if (totalTvlUsd < 100) continue;

const avgApy = totalTvlUsd > 0 ? weightedApy / totalTvlUsd : 0;
const userApy = avgApy * (1 - PERFORMANCE_FEE);

pools.push({
pool: `surf-v4-${asset.toLowerCase()}-${chain}`,
chain: utils.formatChain(chain),
project: 'surf-liquid',
symbol,
tvlUsd: totalTvlUsd,
apyBase: userApy * 100,
underlyingTokens: [asset],
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate per-asset failures before the chain-level allSettled.

Any rejected ABI/price call inside fetchV4ChainPools() currently rejects the whole chain fetch, and the Promise.allSettled at the end then drops every V4 pool for that chain with no signal. A single bad asset or historical call should be skipped and surfaced, not erase the chain’s output.

Suggested containment
   for (const asset of assets) {
-    const priceKey = `${chain}:${asset}`;
-    const price = coinData[priceKey]?.price || 0;
-    const decimals = coinData[priceKey]?.decimals || 18;
-    const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);
+    try {
+      const priceKey = `${chain}:${asset}`;
+      const price = coinData[priceKey]?.price || 0;
+      const decimals = coinData[priceKey]?.decimals || 18;
+      const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);
 
-    // Get active Morpho vault per user vault (for APY weighting)
-    const { output: morphoResults } = await sdk.api.abi.multiCall({
-      abi: 'function assetToVault(address) view returns (address)',
-      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
-      chain,
-    });
+      // existing per-asset logic...
+    } catch (err) {
+      console.warn(`surf-liquid v4 skipped ${chain}:${asset}`, err);
+      continue;
+    }
   }

Also applies to: 458-465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/surf-liquid/index.js` around lines 79 - 181, In
fetchV4ChainPools(), avoid letting any per-asset ABI/price call reject the whole
chain: wrap the per-asset work (the multiCall blocks for morphoResults,
nowAssets/nowSupply/pastAssets/pastSupply, and tvlResults plus price lookup from
coinData) in a try/catch so a failure for one asset (e.g., errors from the calls
that produce morphoResults, nowAssets, pastAssets, morphoApyMap, or tvlResults)
logs the asset and error and continues to the next asset instead of throwing;
specifically, around the loop body that builds uniqueMorpho, computes
morphoApyMap, and computes totalTvlUsd/weightedApy, catch errors and skip
pushing the pool for that asset (do the same containment for the other instance
noted at the end of file), ensuring any thrown error does not bubble up to the
chain-level Promise.allSettled.

Comment on lines +130 to +140
const morphoApyMap = {};
for (let i = 0; i < uniqueMorpho.length; i++) {
const aNow = Number(nowAssets.output[i].output || '0');
const sNow = Number(nowSupply.output[i].output || '0');
const aPast = Number(pastAssets.output[i].output || '0');
const sPast = Number(pastSupply.output[i].output || '0');
if (sNow <= 0 || sPast <= 0) continue;
const priceNow = aNow / sNow;
const pricePast = aPast / sPast;
const apy = pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

❓ Verification inconclusive

Script executed:

cat -n src/adaptors/surf-liquid/index.js | head -150

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

     1	const sdk = require('@defillama/sdk');
     2	const axios = require('axios');
     3	const utils = require('../utils');
     4	
     5	// V2/V3 Base constants
     6	const CHAIN = 'base';
     7	const V2_FACTORY = '0x1D283b668F947E03E8ac8ce8DA5505020434ea0E';
     8	const V3_FACTORY = '0xf1d64dee9f8e109362309a4bfbb523c8e54fa1aa';
     9	const V3_DEPLOY_FROM_BLOCK = 38856207;
    10	const SURF_STAKING = '0xB0fDFc081310A5914c2d2c97e7582F4De12FA9d6';
    11	const SURF_STAKING_V2 = '0xeBa3B16E175fD36c8b01953D9e3962AB3c575718';
    12	const SURF_TOKEN = '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf';
    13	const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
    14	const WETH = '0x4200000000000000000000000000000000000006';
    15	const CBBTC = '0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf';
    16	const ASSETS = [USDC, WETH, CBBTC];
    17	const ASSET_SYMBOLS = {
    18	  [USDC]: 'USDC',
    19	  [WETH]: 'WETH',
    20	  [CBBTC]: 'cbBTC',
    21	};
    22	const ASSET_DECIMALS = {
    23	  [USDC]: 6,
    24	  [WETH]: 18,
    25	  [CBBTC]: 8,
    26	};
    27	const ZERO_ADDR = '0x0000000000000000000000000000000000000000';
    28	
    29	const PERFORMANCE_FEE = 0.1; // 10% on earned yield
    30	
    31	// V4 constants — same addresses on all chains
    32	const V4_FACTORY = '0x8fa50DeA8DB10987D7d22ac092001c3613C18779';
    33	const V4_REGISTRY = '0x98A0DeF9C959Ec934Df02141291303819369f271';
    34	const V4_FROM_BLOCKS = {
    35	  base: 43800000,
    36	  ethereum: 22200000,
    37	  arbitrum: 445000000,
    38	  polygon: 71000000,
    39	};
    40	const V4_CHAINS = ['ethereum', 'arbitrum', 'polygon', 'base'];
    41	
    42	async function fetchV4ChainPools(chain) {
    43	  const fromBlock = V4_FROM_BLOCKS[chain];
    44	
    45	  // Get current and 24h-ago blocks for APY computation
    46	  const now = Math.floor(Date.now() / 1000);
    47	  const dayAgo = now - 86400;
    48	  const [blockNow, blockPast] = await utils.getBlocksByTime([now, dayAgo], chain);
    49	
    50	  // Enumerate user vaults
    51	  const vaultLogs = await sdk.getEventLogs({
    52	    target: V4_FACTORY,
    53	    fromBlock,
    54	    toBlock: blockNow,
    55	    chain,
    56	    eventAbi:
    57	      'event VaultDeployed(address indexed vaultAddress, address indexed owner, bytes32 salt)',
    58	  });
    59	  const userVaults = vaultLogs.map((l) => l.args.vaultAddress);
    60	  if (userVaults.length === 0) return [];
    61	
    62	  // Get allowed assets from registry
    63	  const { output: assets } = await sdk.api.abi.call({
    64	    target: V4_REGISTRY,
    65	    abi: 'function getAllowedAssets() view returns (address[])',
    66	    chain,
    67	  });
    68	  if (!assets || assets.length === 0) return [];
    69	
    70	  // Fetch prices for all assets
    71	  const priceKeys = assets.map((a) => `${chain}:${a}`).join(',');
    72	  const priceResp = await axios.get(
    73	    `https://coins.llama.fi/prices/current/${priceKeys}`
    74	  );
    75	  const coinData = priceResp.data?.coins || {};
    76	
    77	  const pools = [];
    78	
    79	  for (const asset of assets) {
    80	    const priceKey = `${chain}:${asset}`;
    81	    const price = coinData[priceKey]?.price || 0;
    82	    const decimals = coinData[priceKey]?.decimals || 18;
    83	    const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);
    84	
    85	    // Get active Morpho vault per user vault (for APY weighting)
    86	    const { output: morphoResults } = await sdk.api.abi.multiCall({
    87	      abi: 'function assetToVault(address) view returns (address)',
    88	      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
    89	      chain,
    90	    });
    91	
    92	    const uniqueMorpho = [
    93	      ...new Set(
    94	        morphoResults
    95	          .map((r) => r.output)
    96	          .filter((v) => v && v !== ZERO_ADDR)
    97	      ),
    98	    ];
    99	
   100	    if (uniqueMorpho.length === 0) continue;
   101	
   102	    // Compute APY from Morpho vault share price changes (24h window)
   103	    const [nowAssets, nowSupply, pastAssets, pastSupply] = await Promise.all([
   104	      sdk.api.abi.multiCall({
   105	        abi: 'uint256:totalAssets',
   106	        calls: uniqueMorpho.map((t) => ({ target: t })),
   107	        chain,
   108	        block: blockNow,
   109	      }),
   110	      sdk.api.abi.multiCall({
   111	        abi: 'uint256:totalSupply',
   112	        calls: uniqueMorpho.map((t) => ({ target: t })),
   113	        chain,
   114	        block: blockNow,
   115	      }),
   116	      sdk.api.abi.multiCall({
   117	        abi: 'uint256:totalAssets',
   118	        calls: uniqueMorpho.map((t) => ({ target: t })),
   119	        chain,
   120	        block: blockPast,
   121	      }),
   122	      sdk.api.abi.multiCall({
   123	        abi: 'uint256:totalSupply',
   124	        calls: uniqueMorpho.map((t) => ({ target: t })),
   125	        chain,
   126	        block: blockPast,
   127	      }),
   128	    ]);
   129	
   130	    const morphoApyMap = {};
   131	    for (let i = 0; i < uniqueMorpho.length; i++) {
   132	      const aNow = Number(nowAssets.output[i].output || '0');
   133	      const sNow = Number(nowSupply.output[i].output || '0');
   134	      const aPast = Number(pastAssets.output[i].output || '0');
   135	      const sPast = Number(pastSupply.output[i].output || '0');
   136	      if (sNow <= 0 || sPast <= 0) continue;
   137	      const priceNow = aNow / sNow;
   138	      const pricePast = aPast / sPast;
   139	      const apy = pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
   140	      morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0);
   141	    }
   142	
   143	    // Get underlying asset value per user vault (TVL)
   144	    const { output: tvlResults } = await sdk.api.abi.multiCall({
   145	      abi: 'function getAssetVaultAssets(address) view returns (uint256)',
   146	      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
   147	      chain,
   148	    });
   149	
   150	    let totalTvlUsd = 0;


Script executed:

# Check what the `@defillama/sdk` multiCall returns
rg "multiCall" src/adaptors --type js -A 3 -B 1 | head -100

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/lodestar-v0/index.js-  return (
src/adaptors/lodestar-v0/index.js:    await sdk.api.abi.multiCall({
src/adaptors/lodestar-v0/index.js-      chain: CHAIN,
src/adaptors/lodestar-v0/index.js-      calls: markets.map((market) => ({
src/adaptors/lodestar-v0/index.js-        target: COMPTROLLER_ADDRESS,
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:const multiCallMarkets = async (markets, method, abi) => {
src/adaptors/lodestar-v0/index.js-  return (
src/adaptors/lodestar-v0/index.js:    await sdk.api.abi.multiCall({
src/adaptors/lodestar-v0/index.js-      chain: CHAIN,
src/adaptors/lodestar-v0/index.js-      calls: markets.map((market) => ({ target: market })),
src/adaptors/lodestar-v0/index.js-      abi: abi.find(({ name }) => name === method),
--
src/adaptors/lodestar-v0/index.js-  const markets = (
src/adaptors/lodestar-v0/index.js:    await sdk.api.abi.multiCall({
src/adaptors/lodestar-v0/index.js-      chain: CHAIN,
src/adaptors/lodestar-v0/index.js-      abi: comptrollerAbi.find((n) => n.name === 'markets'),
src/adaptors/lodestar-v0/index.js-      calls: allMarkets.map((m) => ({
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:  const supplyRewards = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    SUPPLY_RATE,
src/adaptors/lodestar-v0/index.js-    ercDelegator
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:  const borrowRewards = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    BORROW_RATE,
src/adaptors/lodestar-v0/index.js-    ercDelegator
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:  const marketsCash = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    GET_CHASH,
src/adaptors/lodestar-v0/index.js-    ercDelegator
src/adaptors/lodestar-v0/index.js-  );
src/adaptors/lodestar-v0/index.js:  const totalBorrows = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    TOTAL_BORROWS,
src/adaptors/lodestar-v0/index.js-    ercDelegator
src/adaptors/lodestar-v0/index.js-  );
src/adaptors/lodestar-v0/index.js:  const totalReserves = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    TOTAL_RESERVES,
src/adaptors/lodestar-v0/index.js-    ercDelegator
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:  const underlyingTokens = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    allMarkets,
src/adaptors/lodestar-v0/index.js-    UNDERLYING,
src/adaptors/lodestar-v0/index.js-    ercDelegator
--
src/adaptors/lodestar-v0/index.js-
src/adaptors/lodestar-v0/index.js:  const underlyingSymbols = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    underlyingTokens,
src/adaptors/lodestar-v0/index.js-    'symbol',
src/adaptors/lodestar-v0/index.js-    ercDelegator
src/adaptors/lodestar-v0/index.js-  );
src/adaptors/lodestar-v0/index.js:  const underlyingDecimals = await multiCallMarkets(
src/adaptors/lodestar-v0/index.js-    underlyingTokens,
src/adaptors/lodestar-v0/index.js-    'decimals',
src/adaptors/lodestar-v0/index.js-    ercDelegator
--
src/adaptors/zerobase-cedefi/index.js-        await Promise.all([
src/adaptors/zerobase-cedefi/index.js:          sdk.api.abi.multiCall({
src/adaptors/zerobase-cedefi/index.js-            chain,
src/adaptors/zerobase-cedefi/index.js-            abi: vaultAbi.getTVL,
src/adaptors/zerobase-cedefi/index.js-            calls: tokenAddrs.map((token) => ({ target: vault, params: [token] })),
--
src/adaptors/zerobase-cedefi/index.js-          }),
src/adaptors/zerobase-cedefi/index.js:          sdk.api.abi.multiCall({
src/adaptors/zerobase-cedefi/index.js-            chain,
src/adaptors/zerobase-cedefi/index.js-            abi: vaultAbi.getCurrentRewardRate,
src/adaptors/zerobase-cedefi/index.js-            calls: tokenAddrs.map((token) => ({ target: vault, params: [token] })),
--
src/adaptors/zerobase-cedefi/index.js-          }),
src/adaptors/zerobase-cedefi/index.js:          sdk.api.abi.multiCall({
src/adaptors/zerobase-cedefi/index.js-            chain,
src/adaptors/zerobase-cedefi/index.js-            abi: 'erc20:decimals',
src/adaptors/zerobase-cedefi/index.js-            calls: tokenAddrs.map((t) => ({ target: t })),
--
src/adaptors/zyberswap-amm/index.js-  const poolInfo = (
src/adaptors/zyberswap-amm/index.js:    await sdk.api.abi.multiCall({
src/adaptors/zyberswap-amm/index.js-      calls: [...Array(Number(poolLength)).keys()].map((i) => ({
src/adaptors/zyberswap-amm/index.js-        target: masterchef,
src/adaptors/zyberswap-amm/index.js-        params: [i],
--
src/adaptors/zklend/index.js:const { multiCall, call } = require('../../helper/starknet');
src/adaptors/zklend/index.js-const { marketAbi, erc20Abi, irmAbi, zTokenAbi } = require('./abi');
src/adaptors/zklend/index.js-const axios = require('axios');
src/adaptors/zklend/index.js-const BN = require('bn.js');
--
src/adaptors/zircuit-finance/index.js-    [
src/adaptors/zircuit-finance/index.js:      sdk.api.abi.multiCall({ abi: 'erc20:symbol', calls, chain: CHAIN }),
src/adaptors/zircuit-finance/index.js:      sdk.api.abi.multiCall({ abi: 'erc20:decimals', calls, chain: CHAIN }),
src/adaptors/zircuit-finance/index.js:      sdk.api.abi.multiCall({
src/adaptors/zircuit-finance/index.js-        abi: 'uint:totalAssets',
src/adaptors/zircuit-finance/index.js-        calls,


Script executed:

# Look for bigint usage in the codebase
rg "BigInt|bigint" src/adaptors --type js | head -50

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/midas-rwa/chainlinkHelpers.js:    answer: BigInt(answer),
src/adaptors/midas-rwa/chainlinkHelpers.js:    answer: BigInt(answer),
src/adaptors/midas-rwa/chainlinkHelpers.js:    answer: BigInt(answer),
src/adaptors/zeebu/index.js:      const amount = Number(BigInt(amountHex).toString()) / 10 ** decimals; 
src/adaptors/yuzu-money/index.js:    params: [BigInt(token.unit)],
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:x = BigInt(x);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:startUtilization = BigInt(startUtilization);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:startRateAtTarget = BigInt(startRateAtTarget);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:elapsed = BigInt(elapsed);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:borrowRate = BigInt(borrowRate);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:rateAtTarget = BigInt(rateAtTarget);
src/adaptors/yieldoor/sharesMath.js:    BigInt(totalAssets) + VIRTUAL_ASSETS,
src/adaptors/yieldoor/sharesMath.js:    BigInt(totalShares) + VIRTUAL_SHARES,
src/adaptors/yieldoor/sharesMath.js:    BigInt(totalShares) + VIRTUAL_SHARES,
src/adaptors/yieldoor/sharesMath.js:    BigInt(totalAssets) + VIRTUAL_ASSETS,
src/adaptors/yieldoor/morphoSdk.js:  totalSupplyAssets = BigInt(totalSupplyAssets);
src/adaptors/yieldoor/morphoSdk.js:  totalBorrowAssets = BigInt(totalBorrowAssets);
src/adaptors/yieldoor/morphoSdk.js:  borrowShares = BigInt(borrowShares);
src/adaptors/yieldoor/morphoSdk.js:  market.totalBorrowShares = BigInt(market.totalBorrowShares);
src/adaptors/yieldoor/morphoSdk.js:  timestamp = BigInt(timestamp);
src/adaptors/yieldoor/morphoSdk.js:  const elapsed = timestamp - BigInt(market.lastUpdate);
src/adaptors/yieldoor/morphoSdk.js:  timestamp = BigInt(timestamp);
src/adaptors/yieldoor/morphoSdk.js:      totalSupplyAssets: BigInt(totalSupplyAssets) - feeAmount,
src/adaptors/yieldoor/mathLib.js:    return BigInt(`0x${"f".repeat(nBits / 4)}`);
src/adaptors/yieldoor/mathLib.js:a = BigInt(a);
src/adaptors/yieldoor/mathLib.js:return xs.map(BigInt).reduce((x, y) => (x <= y ? x : y));
src/adaptors/yieldoor/mathLib.js:return xs.map(BigInt).reduce((x, y) => (x <= y ? y : x));
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:denominator = BigInt(denominator);
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:denominator = BigInt(denominator);
src/adaptors/yieldoor/mathLib.js:const firstTerm = BigInt(x) * BigInt(n);
src/adaptors/yieldoor/mathLib.js:  return BigInt(x) * 1_000000000n;
src/adaptors/yieldoor/index.js:  timestamp = BigInt(timestamp);
src/adaptors/yieldoor/index.js:  // position.accrueInterest(BigInt(Math.floor(Date.now() / 1000)));
src/adaptors/yieldoor/index.js:            accrueInterest(position, BigInt(Math.floor(Date.now() / 1000)));
src/adaptors/yieldoor/index.js:            const ltv = +formatEther(_ltv ?? BigInt(0));
src/adaptors/yieldoor/index.js:            const borrowApy = +formatEther(getBorrowApy(market, BigInt(Math.floor(Date.now() / 1000))));
src/adaptors/yieldoor/index.js:            const borrowSize = +formatUnits(borrowAssets ?? BigInt(0), tokenData[loanToken].decimals);
src/adaptors/vesu/starknet-helper.js:      params[0] = '0x' + BigInt(poolId).toString(16)
src/adaptors/vesu/starknet-helper.js:      params[0] = '0x' + BigInt(poolId).toString(16)
src/adaptors/vesu/starknet-helper.js:    const utilizationU256 = BigInt(params[2])
src/adaptors/vesu/starknet-helper.js:    const rateU256 = BigInt(params[4])
src/adaptors/vesu/starknet-helper.js:      '0x' + BigInt(params[3]).toString(16),
src/adaptors/vesu/starknet-helper.js:    return BigInt(low) + (BigInt(high) << 128n)
src/adaptors/vesu/starknet-helper.js:    last_updated: BigInt(result[13]),


Script executed:

# Check for similar share price / APY calculations
rg "totalAssets|totalSupply" src/adaptors --type js -B 2 -A 2 | head -80

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/stusdt/index.js-    await sdk.api.abi.call({
src/adaptors/stusdt/index.js-      chain: 'tron',
src/adaptors/stusdt/index.js:      abi: 'erc20:totalSupply',
src/adaptors/stusdt/index.js-      target: stUSDT,
src/adaptors/stusdt/index.js-    })
--
src/adaptors/stusdt/index.js-    await sdk.api.abi.call({
src/adaptors/stusdt/index.js-      chain: 'ethereum',
src/adaptors/stusdt/index.js:      abi: 'erc20:totalSupply',
src/adaptors/stusdt/index.js-      target: stUSDTEthereum,
src/adaptors/stusdt/index.js-    })
--
src/adaptors/yuzu-money/index.js-  const { output } = await sdk.api.abi.call({
src/adaptors/yuzu-money/index.js-    target: token.address,
src/adaptors/yuzu-money/index.js:    abi: 'erc20:totalSupply',
src/adaptors/yuzu-money/index.js-    chain,
src/adaptors/yuzu-money/index.js-  });
--
src/adaptors/yield-yak-aggregator/index.js-    sdk.api.abi.call({
src/adaptors/yield-yak-aggregator/index.js-      target: vaultConfig.address,
src/adaptors/yield-yak-aggregator/index.js:      abi: 'erc20:totalSupply',
src/adaptors/yield-yak-aggregator/index.js-      chain: chain
src/adaptors/yield-yak-aggregator/index.js-    })
src/adaptors/yield-yak-aggregator/index.js-  ]);
src/adaptors/yield-yak-aggregator/index.js-
src/adaptors/yield-yak-aggregator/index.js:  const totalSupply = currentSupplyWei.output / 1e18;
src/adaptors/yield-yak-aggregator/index.js-
src/adaptors/yield-yak-aggregator/index.js-  // Get underlying token price
--
src/adaptors/yield-yak-aggregator/index.js-  const apr7d = weeklyRate * 365 * 100;
src/adaptors/yield-yak-aggregator/index.js-  const apy7d = (Math.pow(1 + weeklyRate, 365) - 1) * 100;
src/adaptors/yield-yak-aggregator/index.js:  const tvlUsd = totalSupply * currentRateNormalized * underlyingPrice;
src/adaptors/yield-yak-aggregator/index.js-
src/adaptors/yield-yak-aggregator/index.js-  const chainName = CHAIN_CONFIG[vaultConfig.chainId].chainName;
--
src/adaptors/wefi/index.js-    if (price === undefined)
src/adaptors/wefi/index.js-      price = symbol.toLowerCase().includes('usd') ? 1 : 0;
src/adaptors/wefi/index.js:    const totalSupplyUsd =
src/adaptors/wefi/index.js-      ((Number(marketsCash[i]) + Number(totalBorrows[i])) / 10 ** decimals) *
src/adaptors/wefi/index.js-      price;
--
src/adaptors/wefi/index.js-    };
src/adaptors/wefi/index.js-
src/adaptors/wefi/index.js:    const apyReward = calcRewardApy(rewardSpeed, totalSupplyUsd, pool);
src/adaptors/wefi/index.js-    const apyRewardBorrow = calcRewardApy(
src/adaptors/wefi/index.js-      rewardSpeedBorrow,
--
src/adaptors/wefi/index.js-      poolReturned = {
src/adaptors/wefi/index.js-        ...poolReturned,
src/adaptors/wefi/index.js:        totalSupplyUsd,
src/adaptors/wefi/index.js-        totalBorrowUsd,
src/adaptors/wefi/index.js-        apyBaseBorrow,
--
src/adaptors/zircuit-finance/index.js-  }));
src/adaptors/zircuit-finance/index.js-
src/adaptors/zircuit-finance/index.js:  const [symbols, decimals, totalAssets, rateNow, ratePrev] = await Promise.all(
src/adaptors/zircuit-finance/index.js-    [
src/adaptors/zircuit-finance/index.js-      sdk.api.abi.multiCall({ abi: 'erc20:symbol', calls, chain: CHAIN }),
src/adaptors/zircuit-finance/index.js-      sdk.api.abi.multiCall({ abi: 'erc20:decimals', calls, chain: CHAIN }),
src/adaptors/zircuit-finance/index.js-      sdk.api.abi.multiCall({
src/adaptors/zircuit-finance/index.js:        abi: 'uint:totalAssets',
src/adaptors/zircuit-finance/index.js-        calls,
src/adaptors/zircuit-finance/index.js-        chain: CHAIN,
--
src/adaptors/zircuit-finance/index.js-    const dec = Number(decimals.output[i].output);
src/adaptors/zircuit-finance/index.js-    const symbol = symbols.output[i].output;
src/adaptors/zircuit-finance/index.js:    const total = Number(totalAssets.output[i].output);
src/adaptors/zircuit-finance/index.js-    const rNow = Number(rateNow.output[i].output);
src/adaptors/zircuit-finance/index.js-    const rPrev = Number(ratePrev.output[i].output);
--
src/adaptors/zklend/index.js-      apyBaseBorrow: borrowApy,
src/adaptors/zklend/index.js-      underlyingTokens: [address],
src/adaptors/zklend/index.js:      totalSupplyUsd: marketTokenBalanceUsd.plus(totalDebtUsd).toNumber(),
src/adaptors/zklend/index.js-      totalBorrowUsd: totalDebtUsd.toNumber(),
src/adaptors/zklend/index.js-      url: `https://app.zklend.com/asset/${name}`,
--
src/adaptors/zircon-gamma/index.js-          .balanceOf(pool.id)
src/adaptors/zircon-gamma/index.js-          .call();
src/adaptors/zircon-gamma/index.js:        const totalSupply = await ptStakedContract.methods.totalSupply().call();
src/adaptors/zircon-gamma/index.js-        const stakedRatio = new BigNumber(totalStaked.toString()).dividedBy(


Script executed:

# Look for Morpho-specific handling patterns
rg "Morpho|morpho" src/adaptors --type js | grep -i "totalassets\|totalsupply" -A 2 -B 2

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 1


Script executed:

# Check zircuit-finance more closely to see if same pattern exists there
cat -n src/adaptors/zircuit-finance/index.js | sed -n '1,150p'

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

     1	const sdk = require('@defillama/sdk');
     2	const utils = require('../utils');
     3	
     4	const VAULTS = [
     5	  {
     6	    address: '0x03067bbD0d41E3Fe4A0bb6ca67c99e7352Da4CAE',
     7	    underlying: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
     8	  },
     9	  {
    10	    address: '0x25d90ABd6c1E8DCCD40932D2fdD2Cd381bfc832D',
    11	    underlying: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', // USDT
    12	  },
    13	];
    14	
    15	const CHAIN = 'base';
    16	
    17	const convertToAssetsAbi =
    18	  'function convertToAssets(uint256 shares) external view returns (uint256)';
    19	
    20	const SHARE_UNIT = (10n ** 18n).toString();
    21	const DAY = 24 * 3600;
    22	
    23	async function apy() {
    24	  const timestamp = Math.floor(Date.now() / 1000);
    25	
    26	  const [{ height: blockNow }, { height: blockPrev }] = await Promise.all([
    27	    utils.getData(`https://coins.llama.fi/block/${CHAIN}/${timestamp}`),
    28	    utils.getData(`https://coins.llama.fi/block/${CHAIN}/${timestamp - DAY}`),
    29	  ]);
    30	
    31	  const addresses = VAULTS.map((v) => v.address);
    32	  const calls = addresses.map((target) => ({ target }));
    33	  const shareCalls = addresses.map((target) => ({
    34	    target,
    35	    params: [SHARE_UNIT],
    36	  }));
    37	
    38	  const [symbols, decimals, totalAssets, rateNow, ratePrev] = await Promise.all(
    39	    [
    40	      sdk.api.abi.multiCall({ abi: 'erc20:symbol', calls, chain: CHAIN }),
    41	      sdk.api.abi.multiCall({ abi: 'erc20:decimals', calls, chain: CHAIN }),
    42	      sdk.api.abi.multiCall({
    43	        abi: 'uint:totalAssets',
    44	        calls,
    45	        chain: CHAIN,
    46	        block: blockNow,
    47	      }),
    48	      sdk.api.abi.multiCall({
    49	        abi: convertToAssetsAbi,
    50	        calls: shareCalls,
    51	        chain: CHAIN,
    52	        block: blockNow,
    53	      }),
    54	      sdk.api.abi.multiCall({
    55	        abi: convertToAssetsAbi,
    56	        calls: shareCalls,
    57	        chain: CHAIN,
    58	        block: blockPrev,
    59	      }),
    60	    ]
    61	  );
    62	
    63	  const underlyings = VAULTS.map((v) => v.underlying);
    64	  const { pricesByAddress } = await utils.getPrices(underlyings, CHAIN);
    65	
    66	  const pools = VAULTS.map((vault, i) => {
    67	    const dec = Number(decimals.output[i].output);
    68	    const symbol = symbols.output[i].output;
    69	    const total = Number(totalAssets.output[i].output);
    70	    const rNow = Number(rateNow.output[i].output);
    71	    const rPrev = Number(ratePrev.output[i].output);
    72	    const price = pricesByAddress[vault.underlying.toLowerCase()] || 0;
    73	
    74	    const tvlUsd = (total / 10 ** dec) * price;
    75	
    76	    let apyBase = 0;
    77	    if (rPrev > 0 && rNow > 0) {
    78	      apyBase = (rNow / rPrev) ** 365 * 100 - 100;
    79	    }
    80	
    81	    return {
    82	      pool: `${vault.address}-${CHAIN}`.toLowerCase(),
    83	      chain: utils.formatChain(CHAIN),
    84	      project: 'zircuit-finance',
    85	      symbol: utils.formatSymbol(symbol),
    86	      tvlUsd,
    87	      apyBase,
    88	      underlyingTokens: [vault.underlying],
    89	      url: 'https://finance.zircuit.com',
    90	    };
    91	  });
    92	
    93	  return pools.filter(utils.keepFinite);
    94	}
    95	
    96	module.exports = {
    97	  timetravel: false,
    98	  apy,
    99	  url: 'https://finance.zircuit.com',
   100	};


Script executed:

# Search for any string-to-bigint conversions for uint256 values
rg "BigInt.*output\|BigInt.*\\.output" src/adaptors --type js -B 1 -A 1 | head -60

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0


Script executed:

# Check how large Morpho vaults typically get by looking at actual calls or documentation
rg "Morpho" src/adaptors/surf-liquid --type js -B 5 -A 5 | head -100

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/surf-liquid/index.js-    const priceKey = `${chain}:${asset}`;
src/adaptors/surf-liquid/index.js-    const price = coinData[priceKey]?.price || 0;
src/adaptors/surf-liquid/index.js-    const decimals = coinData[priceKey]?.decimals || 18;
src/adaptors/surf-liquid/index.js-    const symbol = coinData[priceKey]?.symbol || asset.slice(0, 6);
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:    // Get active Morpho vault per user vault (for APY weighting)
src/adaptors/surf-liquid/index.js-    const { output: morphoResults } = await sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-      abi: 'function assetToVault(address) view returns (address)',
src/adaptors/surf-liquid/index.js-      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
src/adaptors/surf-liquid/index.js-      chain,
src/adaptors/surf-liquid/index.js-    });
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:    const uniqueMorpho = [
src/adaptors/surf-liquid/index.js-      ...new Set(
src/adaptors/surf-liquid/index.js-        morphoResults
src/adaptors/surf-liquid/index.js-          .map((r) => r.output)
src/adaptors/surf-liquid/index.js-          .filter((v) => v && v !== ZERO_ADDR)
src/adaptors/surf-liquid/index.js-      ),
src/adaptors/surf-liquid/index.js-    ];
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:    if (uniqueMorpho.length === 0) continue;
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:    // Compute APY from Morpho vault share price changes (24h window)
src/adaptors/surf-liquid/index.js-    const [nowAssets, nowSupply, pastAssets, pastSupply] = await Promise.all([
src/adaptors/surf-liquid/index.js-      sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-        abi: 'uint256:totalAssets',
src/adaptors/surf-liquid/index.js:        calls: uniqueMorpho.map((t) => ({ target: t })),
src/adaptors/surf-liquid/index.js-        chain,
src/adaptors/surf-liquid/index.js-        block: blockNow,
src/adaptors/surf-liquid/index.js-      }),
src/adaptors/surf-liquid/index.js-      sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-        abi: 'uint256:totalSupply',
src/adaptors/surf-liquid/index.js:        calls: uniqueMorpho.map((t) => ({ target: t })),
src/adaptors/surf-liquid/index.js-        chain,
src/adaptors/surf-liquid/index.js-        block: blockNow,
src/adaptors/surf-liquid/index.js-      }),
src/adaptors/surf-liquid/index.js-      sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-        abi: 'uint256:totalAssets',
src/adaptors/surf-liquid/index.js:        calls: uniqueMorpho.map((t) => ({ target: t })),
src/adaptors/surf-liquid/index.js-        chain,
src/adaptors/surf-liquid/index.js-        block: blockPast,
src/adaptors/surf-liquid/index.js-      }),
src/adaptors/surf-liquid/index.js-      sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-        abi: 'uint256:totalSupply',
src/adaptors/surf-liquid/index.js:        calls: uniqueMorpho.map((t) => ({ target: t })),
src/adaptors/surf-liquid/index.js-        chain,
src/adaptors/surf-liquid/index.js-        block: blockPast,
src/adaptors/surf-liquid/index.js-      }),
src/adaptors/surf-liquid/index.js-    ]);
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js-    const morphoApyMap = {};
src/adaptors/surf-liquid/index.js:    for (let i = 0; i < uniqueMorpho.length; i++) {
src/adaptors/surf-liquid/index.js-      const aNow = Number(nowAssets.output[i].output || '0');
src/adaptors/surf-liquid/index.js-      const sNow = Number(nowSupply.output[i].output || '0');
src/adaptors/surf-liquid/index.js-      const aPast = Number(pastAssets.output[i].output || '0');
src/adaptors/surf-liquid/index.js-      const sPast = Number(pastSupply.output[i].output || '0');
src/adaptors/surf-liquid/index.js-      if (sNow <= 0 || sPast <= 0) continue;
src/adaptors/surf-liquid/index.js-      const priceNow = aNow / sNow;
src/adaptors/surf-liquid/index.js-      const pricePast = aPast / sPast;
src/adaptors/surf-liquid/index.js-      const apy = pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
src/adaptors/surf-liquid/index.js:      morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0);
src/adaptors/surf-liquid/index.js-    }
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js-    // Get underlying asset value per user vault (TVL)
src/adaptors/surf-liquid/index.js-    const { output: tvlResults } = await sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-      abi: 'function getAssetVaultAssets(address) view returns (uint256)',
--
src/adaptors/surf-liquid/index.js-    eventAbi:
src/adaptors/surf-liquid/index.js-      'event VaultDeployed(address indexed vaultAddress, address indexed owner, address indexed pool, bytes32 marketId, uint256 chainId)',
src/adaptors/surf-liquid/index.js-  });
src/adaptors/surf-liquid/index.js-  const v3Vaults = v3Logs.map((l) => l.args.vaultAddress);
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:  // --- Step 2: Get current Morpho vaults per asset ---
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js:  const { output: v2MorphoResults } = await sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-    abi: 'address:currentVault',
src/adaptors/surf-liquid/index.js-    calls: v2Vaults.map((target) => ({ target })),
src/adaptors/surf-liquid/index.js-    chain: CHAIN,
src/adaptors/surf-liquid/index.js-  });
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js-  const morphoToSurfVaults = {};
src/adaptors/surf-liquid/index.js:  const assetToMorphoVaults = {};
src/adaptors/surf-liquid/index.js:  for (const asset of ASSETS) assetToMorphoVaults[asset] = new Set();
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js-  for (let i = 0; i < v2Vaults.length; i++) {
src/adaptors/surf-liquid/index.js:    const morpho = v2MorphoResults[i].output;
src/adaptors/surf-liquid/index.js-    if (morpho && morpho !== ZERO_ADDR) {
src/adaptors/surf-liquid/index.js-      const key = morpho.toLowerCase();
src/adaptors/surf-liquid/index.js:      assetToMorphoVaults[USDC].add(key);
src/adaptors/surf-liquid/index.js-      if (!morphoToSurfVaults[key]) morphoToSurfVaults[key] = new Set();
src/adaptors/surf-liquid/index.js-      morphoToSurfVaults[key].add(v2Vaults[i]);
src/adaptors/surf-liquid/index.js-    }
src/adaptors/surf-liquid/index.js-  }
src/adaptors/surf-liquid/index.js-
--
src/adaptors/surf-liquid/index.js-
src/adaptors/surf-liquid/index.js-    for (let i = 0; i < v3Vaults.length; i++) {
src/adaptors/surf-liquid/index.js-      const morpho = morphoResults[i].output;
src/adaptors/surf-liquid/index.js-      if (morpho && morpho !== ZERO_ADDR) {
src/adaptors/surf-liquid/index.js-        const key = morpho.toLowerCase();


Script executed:

# Verify that SDK multiCall returns string outputs by checking a few concrete examples
rg "\.output\[.*\]\.output" src/adaptors --type js -A 1 -B 1 | head -40

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/zircuit-finance/index.js-  const pools = VAULTS.map((vault, i) => {
src/adaptors/zircuit-finance/index.js:    const dec = Number(decimals.output[i].output);
src/adaptors/zircuit-finance/index.js:    const symbol = symbols.output[i].output;
src/adaptors/zircuit-finance/index.js:    const total = Number(totalAssets.output[i].output);
src/adaptors/zircuit-finance/index.js:    const rNow = Number(rateNow.output[i].output);
src/adaptors/zircuit-finance/index.js:    const rPrev = Number(ratePrev.output[i].output);
src/adaptors/zircuit-finance/index.js-    const price = pricesByAddress[vault.underlying.toLowerCase()] || 0;
--
src/adaptors/zentra-finance/index.js-      const price = prices[`${CHAIN}:${reserve.tokenAddress}`]?.price;
src/adaptors/zentra-finance/index.js:      const dec = Number(decimals.output[i].output);
src/adaptors/zentra-finance/index.js:      const p = reserveData.output[i].output;
src/adaptors/zentra-finance/index.js:      const cfg = reserveConfig.output[i].output;
src/adaptors/zentra-finance/index.js-
src/adaptors/zentra-finance/index.js-      const totalSupplyUsd =
src/adaptors/zentra-finance/index.js:        (totalSupply.output[i].output / 10 ** dec) * price;
src/adaptors/zentra-finance/index.js-      const tvlUsd =
src/adaptors/zentra-finance/index.js:        (underlyingBalances.output[i].output / 10 ** dec) * price;
src/adaptors/zentra-finance/index.js-
--
src/adaptors/yieldoor/index.js-    const { token0, token1 } = vaultsData[vault];
src/adaptors/yieldoor/index.js:    const [b0, b1] = balances.output[i].output;
src/adaptors/yieldoor/index.js-    const token0Usd = new BigNumber(b0).dividedBy(TEN.pow(vaultsData[vault].token0Decimals)).multipliedBy(prices.pricesByAddress[token0.toLowerCase()]);
--
src/adaptors/yieldoor/index.js-    vaultsData[vault].totalBalanceUsd = totalBalance;
src/adaptors/yieldoor/index.js:    vaultsData[vault].strategy = strategies.output[i].output;
src/adaptors/yieldoor/index.js-  });
--
src/adaptors/yieldoor/index.js-  Object.keys(vaultsData).forEach((vault, i) => {
src/adaptors/yieldoor/index.js:    vaultsData[vault].protocolFee = protocolFees.output[i].output;
src/adaptors/yieldoor/index.js:    vaultsData[vault].pool = pools.output[i].output;
src/adaptors/yieldoor/index.js-  });
--
src/adaptors/yieldoor/index.js-  vaults.forEach((vault, i) => {
src/adaptors/yieldoor/index.js:    const [vestingAmount0, vestingAmount1] = vestingAmounts.output[i].output;
src/adaptors/yieldoor/index.js-    const token0Usd = new BigNumber(vestingAmount0).dividedBy(TEN.pow(vaultsData[vault].token0Decimals)).times(prices.pricesByAddress[vaultsData[vault].token0.toLowerCase()]);
--
src/adaptors/yieldoor/index.js-    vaultsData[vault] = {
src/adaptors/yieldoor/index.js:      token0: token0s.output[i].output,
src/adaptors/yieldoor/index.js:      token1: token1s.output[i].output
src/adaptors/yieldoor/index.js-    }


Script executed:

# Check if there are any explicit conversions or handling of large numbers in similar calculations
rg "Number\(.*\.output.*\)" src/adaptors --type js | head -30

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/bracket-vaults/index.js:  return getTokenPrice(priceKey, Number(tvl.output), vaultDecimals);
src/adaptors/orbitalswap/index.js:    calls: [...Array(Number(poolLength.output)).keys()].map((i) => ({
src/adaptors/orbitalswap/index.js:    calls: [...Array(Number(poolLength.output)).keys()].map((i) => ({
src/adaptors/zircuit-finance/index.js:    const dec = Number(decimals.output[i].output);
src/adaptors/zircuit-finance/index.js:    const total = Number(totalAssets.output[i].output);
src/adaptors/zircuit-finance/index.js:    const rNow = Number(rateNow.output[i].output);
src/adaptors/zircuit-finance/index.js:    const rPrev = Number(ratePrev.output[i].output);
src/adaptors/zkswap-v2/index.js:    const zfDAOPerSecond = Number(zfDAOPerSecondRes.output) / 1e18;
src/adaptors/zkswap-v2/index.js:    const pendingZf = Number(pendingZfRes.output) / 1e18;
src/adaptors/zkswap-v2/index.js:    const currentGovTvl = Number(currentGovTvlRes.output) / 1e18;
src/adaptors/zentra-finance/index.js:      const dec = Number(decimals.output[i].output);
src/adaptors/yodeswap/index.js:    calls: [...Array(Number(poolLength.output)).keys()].map((i) => ({
src/adaptors/yieldoor/index.js:      decimals: Number(decimalsResults[index].output ?? 18),
src/adaptors/venus-core-pool/index.js:  ).output.map((decimal) => Math.pow(10, Number(decimal.output)));
src/adaptors/yel-finance/index.js:  const adjustedBaseAssetOnPotion = formatNumber(baseAssetOnPotion.output, baseAssetDecimals.output)
src/adaptors/yel-finance/index.js:  const adjustedTotalSupplyPotion = formatNumber(totalSupplyPotion.output, decimalsPotion.output)
src/adaptors/veda/index.js:    return Number(result.output);
src/adaptors/veda/index.js:          return Number(result.output) / Math.pow(10, vault.decimals);
src/adaptors/veda/index.js:  return Number(result.output) / Math.pow(10, vault.decimals);
src/adaptors/vaneck-treasury-fund/index.js:    const decimals = Number(decimalsResults[i].output);
src/adaptors/vaneck-treasury-fund/index.js:    const supply = Number(supplyResults[i].output) / 10 ** decimals;
src/adaptors/teller/index.js:          decimals: Number(tokenInfo.output.decimals) || 18,
src/adaptors/uniswap-v3/onchain.js:    ).output.map((o) => [o.input.target, Number(o.output)])
src/adaptors/uniswap-v3/onchain.js:    pools[i].balance0 = BigNumber(token0Balances.output[i].output);
src/adaptors/uniswap-v3/onchain.js:    pools[i].balance1 = BigNumber(token1Balances.output[i].output);
src/adaptors/steakhouse-financial/index.js:    const total = Number(totalAssets.output[i]?.output || 0);
src/adaptors/steakhouse-financial/index.js:    decMap[a.toLowerCase()] = Number(decResults.output[i]?.output || 18);
src/adaptors/steakhouse-financial/index.js:    const current = Number(currentRates.output[i]?.output || 0);
src/adaptors/steakhouse-financial/index.js:    const past = Number(pastRates.output[i]?.output || 0);
src/adaptors/travessia-credit/index.js:    return loans.output.map(loan => Number(loan.output[8]));


Script executed:

# Check Morpho v4 documentation/contracts to understand typical vault sizes
rg "Morpho" src/adaptors/surf-liquid --type js -C 10 | grep -i "deploy\|vault\|tvl" | head -30

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/surf-liquid/index.js:    // Get active Morpho vault per user vault (for APY weighting)
src/adaptors/surf-liquid/index.js-      abi: 'function assetToVault(address) view returns (address)',
src/adaptors/surf-liquid/index.js-      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
src/adaptors/surf-liquid/index.js:    // Compute APY from Morpho vault share price changes (24h window)
src/adaptors/surf-liquid/index.js-    // Get underlying asset value per user vault (TVL)
src/adaptors/surf-liquid/index.js-    const { output: tvlResults } = await sdk.api.abi.multiCall({
src/adaptors/surf-liquid/index.js-      abi: 'function getAssetVaultAssets(address) view returns (uint256)',
src/adaptors/surf-liquid/index.js-      calls: userVaults.map((vault) => ({ target: vault, params: [asset] })),
src/adaptors/surf-liquid/index.js-    let totalTvlUsd = 0;
src/adaptors/surf-liquid/index.js-    fromBlock: V3_DEPLOY_FROM_BLOCK,
src/adaptors/surf-liquid/index.js-      'event VaultDeployed(address indexed vaultAddress, address indexed owner, address indexed pool, bytes32 marketId, uint256 chainId)',
src/adaptors/surf-liquid/index.js-  const v3Vaults = v3Logs.map((l) => l.args.vaultAddress);
src/adaptors/surf-liquid/index.js:  // --- Step 2: Get current Morpho vaults per asset ---
src/adaptors/surf-liquid/index.js-    abi: 'address:currentVault',
src/adaptors/surf-liquid/index.js-    calls: v2Vaults.map((target) => ({ target })),
src/adaptors/surf-liquid/index.js-  const morphoToSurfVaults = {};
src/adaptors/surf-liquid/index.js:  const assetToMorphoVaults = {};
src/adaptors/surf-liquid/index.js:  for (const asset of ASSETS) assetToMorphoVaults[asset] = new Set();
src/adaptors/surf-liquid/index.js-  for (let i = 0; i < v2Vaults.length; i++) {
src/adaptors/surf-liquid/index.js:      assetToMorphoVaults[USDC].add(key);
src/adaptors/surf-liquid/index.js-      if (!morphoToSurfVaults[key]) morphoToSurfVaults[key] = new Set();
src/adaptors/surf-liquid/index.js-      morphoToSurfVaults[key].add(v2Vaults[i]);
src/adaptors/surf-liquid/index.js-    if (v3Vaults.length === 0) continue;
src/adaptors/surf-liquid/index.js-      abi: 'function assetToVault(address) view returns (address)',
src/adaptors/surf-liquid/index.js-      calls: v3Vaults.map((vault) => ({ target: vault, params: [asset] })),
src/adaptors/surf-liquid/index.js-    for (let i = 0; i < v3Vaults.length; i++) {
src/adaptors/surf-liquid/index.js:        assetToMorphoVaults[asset].add(key);
src/adaptors/surf-liquid/index.js-        if (!morphoToSurfVaults[key]) morphoToSurfVaults[key] = new Set();
src/adaptors/surf-liquid/index.js-        morphoToSurfVaults[key].add(v3Vaults[i]);
src/adaptors/surf-liquid/index.js:  // --- Step 3: Get balances (shares held in Morpho vaults) ---


Script executed:

# Calculate the boundary where precision loss becomes significant
python3 << 'EOF'
import math

# JS Number safe integer limit
safe_int = 2**53 - 1
print(f"JS safe integer limit: {safe_int:,} ({safe_int:.2e})")

# Typical vault sizes with different token decimals
vault_sizes = {
    "USDC (6 decimals)": 1e6 * 1e6,      # $1M
    "USDC (6 decimals)": 1e9 * 1e6,      # $1B
    "WETH (18 decimals)": 1e6 * 1e18,    # $1M worth
    "WETH (18 decimals)": 1e9 * 1e18,    # $1B worth
}

for desc, val in vault_sizes.items():
    if val > safe_int:
        # Calculate precision loss
        precision_loss = val - (val - (val % (2**32)))  # rough estimate
        print(f"{desc}: {val:.2e} - EXCEEDS SAFE INT by {val - safe_int:,}")
    else:
        print(f"{desc}: {val:.2e} - within safe int")

# For APY calculation: what precision loss means
print("\n--- APY Impact ---")
# Share price of 1.5 (50% gain)
price_now = 1.5
price_past = 1.0
ratio = price_now / price_past
apy = (ratio ** 365 - 1) * 100

print(f"Simple ratio: {ratio}")
print(f"365-day APY: {apy:.4f}%")

# If precision is lost at 1e15 level
ratio_precision_lost = (1.5000000000000001) / 1.0  # microscopic difference
apy_precision_lost = (ratio_precision_lost ** 365 - 1) * 100
print(f"With precision loss: {apy_precision_lost:.4f}%")
print(f"Difference: {abs(apy - apy_precision_lost):.8f}%")
EOF

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

JS safe integer limit: 9,007,199,254,740,991 (9.01e+15)
USDC (6 decimals): 1.00e+15 - within safe int
WETH (18 decimals): 1.00e+27 - EXCEEDS SAFE INT by 9.999999999909928e+26

--- APY Impact ---
Simple ratio: 1.5
365-day APY: 1876331438326366337974321168394264304254480485746188385389020971008.0000%
With precision loss: 1876331438326366337974321168394264304254480485746188385389020971008.0000%
Difference: 0.00000000%


Script executed:

# Look at the actual Morpho protocol - are vaults typically in billions?
rg "morpho\|Morpho" src/adaptors/surf-liquid/index.js -i | head -20

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0


Script executed:

# Fix the calculation - compute ratio precision properly
python3 << 'EOF'
# JS Number safe integer limit
safe_int = 2**53 - 1
print(f"JS safe integer limit: {safe_int:,}\n")

# Test actual vault size scenarios
test_cases = [
    ("$1B USDC (6 decimals)", 1e9 * 1e6),
    ("$100M USDC (6 decimals)", 1e8 * 1e6),
    ("$1M WETH (18 decimals)", 1e6 * 1e18),
    ("$100K WETH (18 decimals)", 1e5 * 1e18),
]

print("Vault Size Analysis:")
for desc, raw_value in test_cases:
    exceeds = raw_value > safe_int
    status = "⚠️  EXCEEDS SAFE INT" if exceeds else "✓ Safe"
    print(f"{desc:.<40} {raw_value:.2e} {status}")

print("\n--- Share Price Ratio Impact ---")
# For Morpho: typical share price changes over 24h are small (1-5% APY range)
# So ratio would be like 1.00001 to 1.0001 at most

# Scenario: vault with 1M WETH (~$40B at current prices)
large_vault = 1e6 * 1e18
print(f"Large vault value: {large_vault:.2e}")

# If precision is lost at this scale, what's the impact on ratio?
# The issue: when dividing two large numbers, precision in the division result
ratio_now = 1.000001  # 0.0001% change in 24h (very small)
ratio_past = 1.0

# Annualized: (1.000001)^365 - 1
import math
apy_exact = (ratio_now ** 365 - 1)
print(f"24h ratio: {ratio_now}, Annualized APY: {apy_exact*100:.6f}%")

# But if vault size causes precision loss during division...
# The real issue is: does Number(stringValue) with large uint256 lose bits?
print("\n--- Number Conversion Precision ---")
# In JavaScript, very large numbers lose precision
test_nums = [
    (1e15, "1e15"),
    (1e16, "1e16"),
    (1e24, "1e24"),
]
for num, label in test_nums:
    # Can we represent it exactly?
    if num == int(num):
        print(f"{label}: exact int representation")
    else:
        print(f"{label}: NOT exact - loses precision")
EOF

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

JS safe integer limit: 9,007,199,254,740,991

Vault Size Analysis:
$1B USDC (6 decimals)................... 1.00e+15 ✓ Safe
$100M USDC (6 decimals)................. 1.00e+14 ✓ Safe
$1M WETH (18 decimals).................. 1.00e+24 ⚠️  EXCEEDS SAFE INT
$100K WETH (18 decimals)................ 1.00e+23 ⚠️  EXCEEDS SAFE INT

--- Share Price Ratio Impact ---
Large vault value: 1.00e+24
24h ratio: 1.000001, Annualized APY: 0.036507%

--- Number Conversion Precision ---
1e15: exact int representation
1e16: exact int representation
1e24: exact int representation


Script executed:

# Check if the SDK returns strings or numbers from multiCall
grep -r "multiCall.*totalAssets\|multiCall.*totalSupply" src/adaptors -A 10 | grep -E "output.*output|Number\(.*output" | head -20

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/gauntlet/index.js-    const calc = feeCalc.output[i]?.output;
src/adaptors/gauntlet/index.js-    const supply = totalSupply.output[i]?.output;
src/adaptors/steakhouse-financial/index.js-    const total = Number(totalAssets.output[i]?.output || 0);
src/adaptors/steakhouse-financial/index.js-    const asset = assets.output[i]?.output;
src/adaptors/steakhouse-financial/index.js-    const name = names.output[i]?.output;
src/adaptors/travessia-credit/index.js-            calls: vaults.map((vault, i) => ({ target: vault, params: [supplies.output[i].output || 0] })),
src/adaptors/upshift/index.js-                : [{ output: [] }, { output: [] }, { output: [] }, { output: [] }];
src/adaptors/upshift/index.js-                    symbol: symbolsRes.output[i].output,


Script executed:

# Search for any existing large number handling patterns in yield calculations
rg "convertToAssets|convertToShares|toFixed|toPrecision|BigInt" src/adaptors --type js | grep -i "apy\|yield\|rate" | head -20

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

src/adaptors/yieldfi/index.js:    return parseFloat(latestAPY.toFixed(2));
src/adaptors/yieldfi/index.js:    return parseFloat(tvlUsd.toFixed(2));
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:x = BigInt(x);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:startUtilization = BigInt(startUtilization);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:startRateAtTarget = BigInt(startRateAtTarget);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:elapsed = BigInt(elapsed);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:borrowRate = BigInt(borrowRate);
src/adaptors/yieldoor/adaptiveCurveIrmLib.js:rateAtTarget = BigInt(rateAtTarget);
src/adaptors/yieldoor/mathLib.js:    return BigInt(`0x${"f".repeat(nBits / 4)}`);
src/adaptors/yieldoor/mathLib.js:a = BigInt(a);
src/adaptors/yieldoor/mathLib.js:return xs.map(BigInt).reduce((x, y) => (x <= y ? x : y));
src/adaptors/yieldoor/mathLib.js:return xs.map(BigInt).reduce((x, y) => (x <= y ? y : x));
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:denominator = BigInt(denominator);
src/adaptors/yieldoor/mathLib.js:x = BigInt(x);
src/adaptors/yieldoor/mathLib.js:y = BigInt(y);
src/adaptors/yieldoor/mathLib.js:denominator = BigInt(denominator);


Script executed:

# Check if Morpho v4 vaults are expected to be large enough to hit precision issues
# Look at actual test data or vault initialization
rg "1e18\|1e24\|BigInt\|Safe" src/adaptors/surf-liquid --type js | head -15

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0


Script executed:

# Verify the SDK output type by looking at how it's used in other places
rg "\.output\[.*\]\.output.*Number" src/adaptors --type js -B 2 -A 2 | head -50

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0


Script executed:

# Create a concrete test to show precision loss
python3 << 'EOF'
# Simulate what happens with Number conversion on large uint256 values
import json

# Example: 1M WETH as uint256
large_weth = 1000000000000000000000000  # 1M WETH in wei

# Simulate SDK returning it as string, then Number() converting it
sdk_string = str(large_weth)
print(f"SDK returns (as string): {sdk_string}")

# When JavaScript converts this with Number(), it becomes a float
# which loses precision beyond 2^53-1
import math
safe_int = 2**53 - 1
print(f"Safe integer limit: {safe_int}")
print(f"Large value > safe int: {large_weth > safe_int}")

# The precision loss happens in the conversion and subsequent division
# Let's show the actual impact on APY calculation
aNow_large = 1000000000000000000000000
sNow_large = 1000000000000000000001000  # slightly different

# With Number() - JS can't distinguish these precisely
ratio_js_float = aNow_large / sNow_large
print(f"\nShare price ratio (imprecise): {ratio_js_float}")

# What we really want (using arbitrary precision)
from decimal import Decimal
aNow_decimal = Decimal(aNow_large)
sNow_decimal = Decimal(sNow_large)
ratio_precise = aNow_decimal / sNow_decimal
print(f"Share price ratio (precise):   {ratio_precise}")

# The difference in APY
apy_js = (ratio_js_float ** 365 - 1) * 100
apy_precise = float(ratio_precise ** 365 - 1) * 100
print(f"\nAPY (JS float): {apy_js:.10f}%")
print(f"APY (precise):  {apy_precise:.10f}%")
print(f"Difference:     {abs(apy_js - apy_precise):.10f}%")
EOF

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

SDK returns (as string): 1000000000000000000000000
Safe integer limit: 9007199254740991
Large value > safe int: True

Share price ratio (imprecise): 1.0
Share price ratio (precise):   0.9999999999999999999990000000

APY (JS float): 0.0000000000%
APY (precise):  -0.0000000000%
Difference:     0.0000000000%


Script executed:

# Final check: Look at yieldoor's approach to Morpho math since it already handles this
cat -n src/adaptors/yieldoor/sharesMath.js | head -80

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

     1	const MathLib = require('./mathLib');
     2	
     3	/**
     4	 * JS implementation of {`@link` https://github.com/morpho-org/morpho-blue/blob/main/src/libraries/SharesMathLib.sol SharesMathLib} used by Morpho Blue
     5	 * & MetaMorpho (via {`@link` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol ERC4626}).
     6	 */
     7	
     8	const VIRTUAL_SHARES = 1000000n;
     9	const VIRTUAL_ASSETS = 1n;
    10	
    11	const toAssets = (
    12	  shares,
    13	  totalAssets,
    14	  totalShares,
    15	  rounding,
    16	) => {
    17	  return MathLib.mulDiv(
    18	    shares,
    19	    BigInt(totalAssets) + VIRTUAL_ASSETS,
    20	    BigInt(totalShares) + VIRTUAL_SHARES,
    21	    rounding,
    22	  );
    23	}
    24	
    25	const toShares = (
    26	  assets,
    27	  totalAssets,
    28	  totalShares,
    29	  rounding,
    30	) => {
    31	  return MathLib.mulDiv(
    32	    assets,
    33	    BigInt(totalShares) + VIRTUAL_SHARES,
    34	    BigInt(totalAssets) + VIRTUAL_ASSETS,
    35	    rounding,
    36	  );
    37	}
    38	
    39	module.exports = {
    40	  toAssets,
    41	  toShares,
    42	};

Script executed:

# Check if there's any documentation or comments about precision in Morpho calculations
rg "precision\|BigInt\|uint256" src/adaptors/yieldoor --type js -B 2 -A 2 | head -60

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0


Script executed:

# Confirm SDK multiCall output format by checking a direct usage pattern
cat -n src/adaptors/yieldoor/index.js | grep -A 5 "multiCall" | head -40

Repository: DefiLlama/yield-server


Repository: DefiLlama/yield-server
Exit code: 0

stdout:

   183	    sdk.api.abi.multiCall({ 
   184	      calls: vaultsAddresses.map(addr => ({ target: addr })), 
   185	      abi: abis.balances, 
   186	      chain 
   187	    }),
   188	    sdk.api.abi.multiCall({ 
   189	      calls: vaultsAddresses.map(addr => ({ target: addr })), 
   190	      abi: 'address:strategy', 
   191	      chain 
   192	    })
   193	  ])
--
   207	    sdk.api.abi.multiCall({ 
   208	      calls: strategyAddresses.map(addr => ({ target: addr })), 
   209	      abi: 'uint256:protocolFee', 
   210	      chain 
   211	    }),
   212	    sdk.api.abi.multiCall({ 
   213	      calls: strategyAddresses.map(addr => ({ target: addr })), 
   214	      abi: 'address:pool', 
   215	      chain 
   216	    })
   217	  ]);
--
   228	    sdk.api.abi.multiCall({ 
   229	      calls: vaults.map(vault => ({ target: vaultsData[vault].strategy })), 
   230	      abi: abis.getVestingAmounts, 
   231	      chain 
   232	    })
   233	  ]);
--
   257	    sdk.api.abi.multiCall({ 
   258	      calls: vaults.map(addr => ({ target: addr })), 
   259	      abi: 'address:token0', 
   260	      chain 
   261	    }),
   262	    sdk.api.abi.multiCall({ 
   263	      calls: vaults.map(addr => ({ target: addr })), 
   264	      abi: 'address:token1', 
   265	      chain 


Use BigInt for share-price math, not Number.

totalAssets and totalSupply are uint256 values that can exceed JavaScript's safe integer limit (2^53-1). Converting them to Number before dividing loses precision, which compounds through the annualization step. The yieldoor adaptor already demonstrates the correct pattern: use BigInt for the division, then convert only the final normalized rate to Number. Apply the same approach here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/surf-liquid/index.js` around lines 130 - 140, The loop building
morphoApyMap currently converts uint256 outputs to Number (aNow, sNow, aPast,
sPast) which loses precision; change to parse outputs as BigInt for nowAssets,
nowSupply, pastAssets, pastSupply, compute share prices using integer
fixed-point math (e.g. multiply numerator by a constant SCALE BigInt before
dividing to get a scaled priceNow and pricePast), then compute the growth ratio
and annualize using floating math only after converting the normalized ratio to
Number; update references to aNow/sNow/aPast/sPast, priceNow/pricePast and apy
so that BigInt math is used until the final Number conversion when assigning
morphoApyMap[uniqueMorpho[i].toLowerCase()] = Math.max(apy, 0).

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.

1 participant