Skip to content

Fix surf-liquid TVL: count idle underlying tokens in vault contracts#18665

Open
rohansingh4 wants to merge 2 commits intoDefiLlama:mainfrom
rohansingh4:fix-surf-liquid-idle-tvl
Open

Fix surf-liquid TVL: count idle underlying tokens in vault contracts#18665
rohansingh4 wants to merge 2 commits intoDefiLlama:mainfrom
rohansingh4:fix-surf-liquid-idle-tvl

Conversation

@rohansingh4
Copy link
Copy Markdown
Contributor

@rohansingh4 rohansingh4 commented Apr 6, 2026

Summary

Fixes a gap between on-chain TVL (Dune: $239k) and DefiLlama TVL ($130k).

The previous adapter only tracked funds deployed into Morpho vaults via currentVault() / assetToVault(asset). During rebalances, funds temporarily sit as raw USDC/WETH/cbBTC directly inside the Surf vault contract — these were completely missed.

Fix: Also track direct underlying token balances held by each Surf vault alongside the Morpho share positions.

No double-counting risk: when funds are deployed to Morpho, the vault holds Morpho ERC-4626 shares (not underlying tokens), so the direct token balance is $0. When funds are idle the Morpho share balance is $0. The two never overlap.

Changes

  • V2 vaults: also add [USDC, v2Vault] to tokensAndOwners
  • V3 vaults: also add [asset, v3Vault] for each asset alongside the Morpho share entry

Summary by CodeRabbit

  • Bug Fixes

    • More accurate TVL accounting by converting vault share balances into underlying token amounts and excluding zero/invalid vaults.
    • Improved discovery to always record valid Morpho-vault mappings and skip empty or zero-share entries.
  • Refactor

    • Reworked allocation-building and discovery flow for V2/V3 vaults to simplify processing and avoid incorrect inclusions.

Also track direct underlying token balances (USDC/WETH/cbBTC) held by
each Surf vault in addition to Morpho vault share positions. During
rebalances, funds temporarily sit as raw tokens in the vault — the
previous adapter missed these entirely since it only read currentVault()
/ assetToVault() Morpho allocations.

No double-counting risk: when funds are in Morpho the vault holds shares
(not underlying), so the direct token balance is zero, and vice versa.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Rewrote TVL computation to build an allocations list of {surfVault, morphoVault, asset}, convert Morpho ERC‑4626 share balances to underlying amounts via totalAssets/totalSupply, and include idle underlying balances for V2 and V3 while filtering out zero/invalid vault addresses.

Changes

Cohort / File(s) Summary
Surf Liquid TVL accounting
projects/surf-liquid/index.js
Replaced tokensAndOwners aggregation with an allocations array; added V2 filtering to skip zero/invalid vaults and include idle USDC; V2 derives Morpho via currentVault(); V3 maps ASSETSassetToVault(asset) across discovered V3 vaults and records allocations when non-zero; converts Morpho shares to underlying using totalAssets/totalSupply and early-continues on zero values. Areas to review: allocation construction, numeric division (shares → underlying), V2/V3 discovery/filtering, and early-return conditions.

Sequence Diagram(s)

sequenceDiagram
    participant Scraper as Script
    participant Chain as Blockchain/Logs
    participant Surf as SurfVault (V2/V3)
    participant Morpho as MorphoERC4626
    participant API as api (add)

    Scraper->>Chain: discover V2/V3 vaults (factory logs)
    Scraper->>Surf: for each V2 vault: call currentVault()
    Surf-->>Scraper: morphoVault address (or 0)
    alt morphoVault != 0
        Scraper->>Morpho: balanceOf(surfVault) -> shares
        Morpho-->>Scraper: shares
        Scraper->>Morpho: totalAssets(), totalSupply()
        Morpho-->>Scraper: assets,supply
        Scraper->>API: add(asset, shares * assets / supply)
    else morphoVault == 0
        Scraper->>API: add(idleUnderlyingBalance)
    end
    loop V3 per asset
        Scraper->>Surf: assetToVault(asset) across V3 vaults
        Surf-->>Scraper: morphoVault (or 0)
        alt morphoVault != 0
            Scraper->>Morpho: balanceOf(surfVault) -> shares
            Morpho-->>Scraper: shares
            Scraper->>API: add(asset, computedUnderlying)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I counted vaults by moonlit code tonight,
Hop-scoped shares to underlying light,
Skipped the zeros, bobbed for each token,
Quietly added what Morpho had spoken —
Tiny hops, tidy sums — hooray, done right! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing TVL calculation by counting idle underlying tokens in vault contracts, which directly addresses the core issue.
Description check ✅ Passed The description clearly explains the problem (TVL undercounting), the root cause (missing idle tokens), and the solution with no double-counting risk. It provides sufficient technical detail for code review.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

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

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.

@llamabutler
Copy link
Copy Markdown

The adapter at projects/surf-liquid exports TVL:

base                      130.10 k
base-staking              43.62 k
staking                   43.62 k

total                    130.10 k 

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.

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

26-33: Filter invalid V2 vault addresses before fetching currentVault.

Line 26 skips invalid vaults only after the currentVault multicall is already built from all entries, which can trigger avoidable bad-target calls. Move validation earlier and only query valid vaults.

♻️ Suggested refactor
-  const v2Owners = v2Infos.map((info) => info[0]);
-  const v2MorphoVaults = await api.multiCall({
-    abi: "address:currentVault",
-    calls: v2Owners.map((target) => ({ target })),
-  });
-  for (let i = 0; i < v2Owners.length; i++) {
-    if (!v2Owners[i] || v2Owners[i] === ZERO_ADDR) continue;
+  const v2Owners = v2Infos.map((info) => info[0]);
+  const validV2Owners = v2Owners.filter((v) => v && v !== ZERO_ADDR);
+  const v2MorphoVaults = await api.multiCall({
+    abi: "address:currentVault",
+    calls: validV2Owners.map((target) => ({ target })),
+  });
+  for (let i = 0; i < validV2Owners.length; i++) {
     // Count Morpho vault shares held by this V2 vault
     if (v2MorphoVaults[i] && v2MorphoVaults[i] !== ZERO_ADDR) {
-      tokensAndOwners.push([v2MorphoVaults[i], v2Owners[i]]);
+      tokensAndOwners.push([v2MorphoVaults[i], validV2Owners[i]]);
     }
     // Also count idle USDC sitting directly in the vault (e.g. during rebalances)
-    tokensAndOwners.push([USDC, v2Owners[i]]);
+    tokensAndOwners.push([USDC, validV2Owners[i]]);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/surf-liquid/index.js` around lines 26 - 33, The code currently
builds the currentVault multicall using all entries then filters invalid vaults
later, causing bad-target calls; update the logic so you validate each vault
index before adding it to any multicall or tokensAndOwners: check v2Owners[i]
and v2Owners[i] !== ZERO_ADDR (and skip when v2MorphoVaults[i] is falsy or
ZERO_ADDR before pushing morpho entries) and only include those validated
indices when constructing the currentVault multicall and when pushing into
tokensAndOwners; adjust the loop that references v2Owners, v2MorphoVaults,
ZERO_ADDR and tokensAndOwners so invalid vaults are excluded up-front.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@projects/surf-liquid/index.js`:
- Around line 26-33: The code currently builds the currentVault multicall using
all entries then filters invalid vaults later, causing bad-target calls; update
the logic so you validate each vault index before adding it to any multicall or
tokensAndOwners: check v2Owners[i] and v2Owners[i] !== ZERO_ADDR (and skip when
v2MorphoVaults[i] is falsy or ZERO_ADDR before pushing morpho entries) and only
include those validated indices when constructing the currentVault multicall and
when pushing into tokensAndOwners; adjust the loop that references v2Owners,
v2MorphoVaults, ZERO_ADDR and tokensAndOwners so invalid vaults are excluded
up-front.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f567d279-367f-4e43-a727-cf8f2ba5ef19

📥 Commits

Reviewing files that changed from the base of the PR and between 765107d and 110ec6e.

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

@rohansingh4
Copy link
Copy Markdown
Contributor Author

rohansingh4 commented Apr 6, 2026

Hi @RohanNero
What was wrong:

sumTokens was passed Morpho vault contract addresses as "tokens." DefiLlama doesn't have price data
for these custom Morpho vaults (they're not on CoinGecko/tracked by DefiLlama's price feeds), so they
priced at $0. Only vaults it happened to recognise got counted — which is why it showed $130k
instead of the real ~$239k.

The fix:

The adapter now manually unwraps Morpho ERC-4626 shares:
underlying = shares × totalAssets / totalSupply
Then adds USDC/WETH/cbBTC amounts directly to the API. DefiLlama always knows the prices of these
base assets, so TVL should now match Dune's ~$239k.

Previously used sumTokens with Morpho vault addresses as tokens.
DefiLlama lacks price data for these custom Morpho vaults, causing TVL
to be severely undercounted (~$130k vs actual ~$239k).

Fix: compute underlying value (shares * totalAssets / totalSupply)
directly in the adapter and add USDC/WETH/cbBTC amounts explicitly.
DefiLlama always has prices for these base assets.
@llamabutler
Copy link
Copy Markdown

The adapter at projects/surf-liquid exports TVL:

base                      206.00 k
base-staking              43.62 k
staking                   43.62 k

total                    206.00 k 

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: 1

🧹 Nitpick comments (2)
projects/surf-liquid/index.js (2)

49-50: Consider moving empty check outside the loop.

The v3Vaults.length === 0 check runs on every iteration of the ASSETS loop. Moving it outside would avoid redundant checks and the unnecessary api.multiCall setup.

♻️ Minor efficiency improvement
-  for (const asset of ASSETS) {
-    if (v3Vaults.length === 0) continue;
+  if (v3Vaults.length > 0) {
+    for (const asset of ASSETS) {
       const morphoVaults = await api.multiCall({
         abi: "function assetToVault(address) view returns (address)",
         calls: v3Vaults.map((vault) => ({ target: vault, params: [asset] })),
       });
       for (let i = 0; i < v3Vaults.length; i++) {
         if (morphoVaults[i] && morphoVaults[i] !== ZERO_ADDR) {
           allocations.push({ surfVault: v3Vaults[i], morphoVault: morphoVaults[i], asset });
         }
       }
     }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/surf-liquid/index.js` around lines 49 - 50, The loop over ASSETS
currently checks v3Vaults.length === 0 on every iteration causing redundant work
and repeated api.multiCall setup; move the empty-check for v3Vaults outside the
for (const asset of ASSETS) loop and early-return or continue before entering
the loop so you only construct api.multiCall (and related per-vault logic) when
v3Vaults is non-empty; update references to ASSETS, v3Vaults and api.multiCall
accordingly so no per-asset unnecessary checks or multiCall initialization
occurs.

112-112: Update methodology if idle token tracking is added.

If you add the direct underlying token balance tracking (idle tokens), consider updating the methodology to reflect this:

-  methodology: "TVL counts Morpho vault deposits across V2 and V3 Surf Liquid vaults. Staking includes SURF staked and SURF subscriptions.",
+  methodology: "TVL counts Morpho vault deposits and idle underlying tokens across V2 and V3 Surf Liquid vaults. Staking includes SURF staked and SURF subscriptions.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/surf-liquid/index.js` at line 112, The methodology string currently
in the exported metadata (the methodology property in
projects/surf-liquid/index.js) doesn't mention idle token tracking; if you add
direct underlying token balance tracking (idle tokens), update that methodology
text to explain how idle tokens are counted/separated from TVL and staking
metrics (e.g., specify whether idle underlying balances are included in TVL or
reported separately and how subscriptions vs idle balances are handled), then
replace the existing methodology value in the same exported object so it clearly
documents the new idle token handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@projects/surf-liquid/index.js`:
- Around line 33-60: The code currently only records Morph o vault allocations
in the allocations array (built by v2MorphoVaults loop and v3 morphoVaults loop)
but does not track idle underlying token balances sitting directly in Surf
vaults; after the existing allocation-building loops (after the V3 loop), add
logic to register direct token-owner pairs so api.sumTokens can pick them up:
for V2, add USDC with each v2Vault as owner (use v2Vaults array and USDC
constant), and for V3, for each asset in ASSETS add the pair (asset, v3Vault)
using v3Vaults; ensure you still exclude ZERO_ADDR entries and reuse the same
arrays/variables (v2Vaults, v3Vaults, ASSETS, USDC, ZERO_ADDR) so sumTokens can
be called with those owner/token pairs to capture idle balances.

---

Nitpick comments:
In `@projects/surf-liquid/index.js`:
- Around line 49-50: The loop over ASSETS currently checks v3Vaults.length === 0
on every iteration causing redundant work and repeated api.multiCall setup; move
the empty-check for v3Vaults outside the for (const asset of ASSETS) loop and
early-return or continue before entering the loop so you only construct
api.multiCall (and related per-vault logic) when v3Vaults is non-empty; update
references to ASSETS, v3Vaults and api.multiCall accordingly so no per-asset
unnecessary checks or multiCall initialization occurs.
- Line 112: The methodology string currently in the exported metadata (the
methodology property in projects/surf-liquid/index.js) doesn't mention idle
token tracking; if you add direct underlying token balance tracking (idle
tokens), update that methodology text to explain how idle tokens are
counted/separated from TVL and staking metrics (e.g., specify whether idle
underlying balances are included in TVL or reported separately and how
subscriptions vs idle balances are handled), then replace the existing
methodology value in the same exported object so it clearly documents the new
idle token handling.
🪄 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: eababd02-35b1-4964-aa26-d6d45d776a15

📥 Commits

Reviewing files that changed from the base of the PR and between 110ec6e and feb4e77.

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

Comment on lines +33 to 60
// --- Build (surfVault, morphoVault, asset) allocations ---

const allocations = []; // { surfVault, morphoVault, asset }

// V2: currentVault() → USDC only
const v2MorphoVaults = await api.multiCall({
abi: "address:currentVault",
calls: v2Vaults.map((target) => ({ target })),
});
for (let i = 0; i < v2Vaults.length; i++) {
if (v2MorphoVaults[i] && v2MorphoVaults[i] !== ZERO_ADDR) {
allocations.push({ surfVault: v2Vaults[i], morphoVault: v2MorphoVaults[i], asset: USDC });
}
}

// V3: assetToVault(asset) for each asset
for (const asset of ASSETS) {
if (v3Vaults.length === 0) continue;
const morphoVaults = await api.multiCall({
abi: "function assetToVault(address) view returns (address)",
calls: v3Vaults.map((vault) => ({ target: vault, params: [asset] })),
});
for (let i = 0; i < v3Vaults.length; i++) {
if (morphoVaults[i] && morphoVaults[i] !== ZERO_ADDR) {
tokensAndOwners.push([morphoVaults[i], v3Vaults[i]]);
allocations.push({ surfVault: v3Vaults[i], morphoVault: morphoVaults[i], 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 | 🔴 Critical

Missing direct underlying token balance tracking as stated in PR objectives.

The PR objectives explicitly state:

  • V2 vaults: add [USDC, v2Vault] to track direct USDC balances
  • V3 vaults: add [asset, v3Vault] for each asset to track direct token balances during idle periods

However, the current implementation only builds Morpho vault allocations. To capture idle tokens sitting directly in Surf vault contracts (when funds aren't deployed to Morpho), you need to also call api.sumTokens with the Surf vault addresses as owners of the underlying assets.

🐛 Proposed fix to add idle token balance tracking

After the allocations loop (around line 60), add:

+  // --- Track idle underlying tokens held directly by Surf vaults ---
+  const tokensAndOwners = [];
+  
+  // V2 vaults hold USDC when idle
+  for (const v2Vault of v2Vaults) {
+    tokensAndOwners.push([USDC, v2Vault]);
+  }
+  
+  // V3 vaults can hold any asset when idle
+  for (const v3Vault of v3Vaults) {
+    for (const asset of ASSETS) {
+      tokensAndOwners.push([asset, v3Vault]);
+    }
+  }
+  
+  await api.sumTokens({ tokensAndOwners });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/surf-liquid/index.js` around lines 33 - 60, The code currently only
records Morph o vault allocations in the allocations array (built by
v2MorphoVaults loop and v3 morphoVaults loop) but does not track idle underlying
token balances sitting directly in Surf vaults; after the existing
allocation-building loops (after the V3 loop), add logic to register direct
token-owner pairs so api.sumTokens can pick them up: for V2, add USDC with each
v2Vault as owner (use v2Vaults array and USDC constant), and for V3, for each
asset in ASSETS add the pair (asset, v3Vault) using v3Vaults; ensure you still
exclude ZERO_ADDR entries and reuse the same arrays/variables (v2Vaults,
v3Vaults, ASSETS, USDC, ZERO_ADDR) so sumTokens can be called with those
owner/token pairs to capture idle balances.

@RohanNero RohanNero self-assigned this Apr 10, 2026
@rohansingh4
Copy link
Copy Markdown
Contributor Author

@RohanNero this is urgent — TVL is breaking to near-zero on every rebalance.

Root cause: the current live adapter prices Morpho vault tokens via DefiLlama's price database. Custom Morpho vaults have no price data, so TVL shows ~$0 after each rebalance to a new vault. This has now happened multiple times.

This PR's fix: manually compute underlying = shares × totalAssets / totalSupply and add USDC/WETH/cbBTC directly. These base asset prices are always available regardless of which Morpho vault is used. Once merged this issue will not recur.

Current TVL showing ~$3,899 (should be ~$204k). Requesting urgent review.

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.

3 participants