Fix surf-liquid TVL: count idle underlying tokens in vault contracts#18665
Fix surf-liquid TVL: count idle underlying tokens in vault contracts#18665rohansingh4 wants to merge 2 commits intoDefiLlama:mainfrom
Conversation
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.
📝 WalkthroughWalkthroughRewrote 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
The adapter at projects/surf-liquid exports TVL: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
projects/surf-liquid/index.js (1)
26-33: Filter invalid V2 vault addresses before fetchingcurrentVault.Line 26 skips invalid vaults only after the
currentVaultmulticall 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
📒 Files selected for processing (1)
projects/surf-liquid/index.js
|
Hi @RohanNero sumTokens was passed Morpho vault contract addresses as "tokens." DefiLlama doesn't have price data The fix: The adapter now manually unwraps Morpho ERC-4626 shares: |
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.
|
The adapter at projects/surf-liquid exports TVL: |
There was a problem hiding this comment.
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 === 0check runs on every iteration of the ASSETS loop. Moving it outside would avoid redundant checks and the unnecessaryapi.multiCallsetup.♻️ 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
📒 Files selected for processing (1)
projects/surf-liquid/index.js
| // --- 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 }); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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 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 Current TVL showing ~$3,899 (should be ~$204k). Requesting urgent review. |
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
[USDC, v2Vault]to tokensAndOwners[asset, v3Vault]for each asset alongside the Morpho share entrySummary by CodeRabbit
Bug Fixes
Refactor