Conversation
Pools: - USDC/WETH/cbBTC vault pools with Morpho lending APY (after 10% fee) - SURF staking pool (14% for 6M, 18% for 12M lock) APY is sourced from Morpho GraphQL API for the underlying vaults that Surf Liquid deposits into, with 10% performance fee deducted.
📝 WalkthroughWalkthroughAdds a Surf Liquid adaptor for Base that discovers Morpho V2/V3 vaults, maps Morpho-to-Surf vaults, aggregates balances, computes on-chain APY and USD TVL (via CoinGecko/llama), builds per-asset yield pools with a 10% performance fee, and adds a SURF staking pool. Changes
Sequence DiagramsequenceDiagram
participant Adaptor as Adaptor
participant V2Factory as Morpho V2<br/>Factory
participant V3Factory as Morpho V3<br/>Factory
participant MorphoAPI as Morpho API
participant StakingContract as SURF Staking<br/>Contract
participant PriceAPI as CoinGecko/llama
participant User as User/Consumer
Adaptor->>V2Factory: Discover vaults (total, infos, events)
Adaptor->>V3Factory: Discover vaults (total, infos, events)
Adaptor->>V2Factory: Map vaults (currentVault)
Adaptor->>V3Factory: Map vaults (assetToVault)
Adaptor->>Adaptor: Aggregate balances and mappings
Adaptor->>MorphoAPI: Query per-vault metrics (totalAssets, totalSupply, decimals)
MorphoAPI-->>Adaptor: Return vault metrics
Adaptor->>PriceAPI: Fetch asset prices
PriceAPI-->>Adaptor: Return prices
Adaptor->>StakingContract: Query totalStaked & subscription TVL
Adaptor->>Adaptor: Compute per-vault APY, convert shares, calculate TVL/USD, apply 10% fee, build pools
Adaptor->>User: Return array of pool objects (pools + poolMeta)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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)
📝 Coding Plan
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 Tip You can enable review details to help with troubleshooting, context usage and more.Enable the |
|
The surf-liquid adapter exports pools: Test Suites: 1 passed, 1 total |
|
Hi @rohansingh4 thanks for the PR. A tvl adapter is required before we can proceed with a yield adapter. I've added a couple of comments |
src/adaptors/surf-liquid/index.js
Outdated
| const ZERO_ADDR = '0x0000000000000000000000000000000000000000'; | ||
|
|
||
| const PERFORMANCE_FEE = 0.1; // 10% on earned yield | ||
| const STAKING_APY = 14; // conservative: 14% (6M), up to 18% (12M) |
There was a problem hiding this comment.
apy should be derived from onchain sources using defillama sdk where possible or a subgraph
There was a problem hiding this comment.
tvl adapter- DefiLlama/DefiLlama-Adapters#18401 (comment)
also i see our surfliquid page deprecetated tag is removed, which is great but please update our numbers meanwhile whichver is verified from your end, those with issues you can tell me I will try to resolved
src/adaptors/surf-liquid/index.js
Outdated
| const userApy = avgMorphoApy * (1 - PERFORMANCE_FEE); | ||
|
|
||
| pools.push({ | ||
| pool: `surf-liquid-${ASSET_SYMBOLS[asset].toLowerCase()}-${CHAIN}`, |
There was a problem hiding this comment.
pool should be made up of the receipt token address and chain
src/adaptors/surf-liquid/index.js
Outdated
|
|
||
| if (stakingTvl > 100) { | ||
| pools.push({ | ||
| pool: `surf-liquid-staking-${CHAIN}`, |
src/adaptors/surf-liquid/index.js
Outdated
|
|
||
| let morphoData = {}; | ||
| if (allMorphoAddresses.length > 0) { | ||
| const { vaults: morphoVaultsResp } = await request( |
There was a problem hiding this comment.
can we get morpho apy from onchain or subgraph
…endency - Replace Morpho GraphQL API with on-chain share price calculation (totalAssets/totalSupply at current vs 1-day-ago blocks) - Derive staking APR from on-chain contract (apr6Months, apr12Months, BASIS_POINTS) instead of hardcoded value - Change pool IDs to use contract addresses (factory/staking address + chain) - Remove graphql-request dependency - Get asset prices from coins.llama.fi instead of Morpho API
|
The surf-liquid adapter exports pools: Test Suites: 1 passed, 1 total |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/adaptors/surf-liquid/index.js (1)
239-239:⚠️ Potential issue | 🟠 MajorUse a stable address-based pool id, not symbol-derived suffixes.
Line 239 ties identity to
ASSET_SYMBOLS[...]; symbol changes/casing changes can create a new config key. Sincepoolis the dedupe key downstream, this risks pool history fragmentation. Prefer a canonical address-based id (asset or receipt token address + chain).Suggested fix
- pool: `${V3_FACTORY.toLowerCase()}-${ASSET_SYMBOLS[asset].toLowerCase()}-${CHAIN}`, + pool: `${asset.toLowerCase()}-${CHAIN}`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/surf-liquid/index.js` at line 239, The pool id currently uses ASSET_SYMBOLS[asset] which is unstable; change it to use a canonical token address (e.g., the asset or receipt token address) instead. Replace `${V3_FACTORY.toLowerCase()}-${ASSET_SYMBOLS[asset].toLowerCase()}-${CHAIN}` with a stable id built from the token address like `${V3_FACTORY.toLowerCase()}-${assetAddress.toLowerCase()}-${CHAIN}`, where assetAddress is resolved from your existing mapping or accessor (e.g., ASSET_ADDRESSES[asset] or getAssetAddress(asset)) and normalized (lowercase or checksum) before interpolation so pool dedupe uses the immutable address rather than a symbol.
🧹 Nitpick comments (1)
src/adaptors/surf-liquid/index.js (1)
85-91: Parallelize per-assetassetToVaultmulticalls to reduce RPC latency.The current loop awaits each asset call serially. This is safe but slower; a
Promise.alloverASSETSkeeps behavior and reduces runtime variance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/adaptors/surf-liquid/index.js` around lines 85 - 91, The loop over ASSETS currently awaits sdk.api.abi.multiCall for each asset sequentially; change it to run the per-asset multicalls in parallel by mapping ASSETS to an array of promises that call sdk.api.abi.multiCall (with the same abi 'function assetToVault(address) view returns (address)', calls built from v3Vaults.map(vault => ({ target: vault, params: [asset] })), and CHAIN) and then await Promise.all on that array; ensure you preserve the skip when v3Vaults.length === 0 (either check once before creating promises or keep a per-asset guard that returns a resolved value) and then process the resulting array of { output: morphoResults } responses the same way as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/adaptors/surf-liquid/index.js`:
- Around line 279-280: The APR calculation for stakingApr6M and stakingApr12M
divides by basisPoints without checking for zero/invalid, causing Infinity/NaN;
update the code around stakingApr6M/stakingApr12M to parse Number(basisPoints)
once (e.g., const bp = Number(basisPoints)) and if bp <= 0 or isNaN(bp) avoid
the division by setting stakingApr6M and stakingApr12M to a safe fallback (null
or 0) or skip the calculation and optionally log a warning; ensure you reference
and update the expressions that use apr6M, apr12M and basisPoints so they use
the validated bp variable.
- Around line 298-305: The if gate currently checks only stakingTvl but tvlUsd
uses stakingTvl + subscriptionTvl, so update the condition to apply the
visibility threshold to the combined TVL; compute a combinedTvl (stakingTvl +
subscriptionTvl) or directly change the guard to if (stakingTvl +
subscriptionTvl > 100) before pushing the pool object (the pools.push block that
creates the SURF_STAKING-CHAIN entry and sets tvlUsd), ensuring the same summed
value is used in both the check and the reported tvlUsd.
- Around line 174-188: The code is currently defaulting supply to '1'
(sNow/sPast) which can fabricate prices/apy; change the logic in the block that
computes aNow, sNow, aPast, sPast, priceNow, pricePast, apyVal and the
assignment to morphoData[addr] to treat missing or zero supply as invalid: do
not substitute '1' for supply—parse supply to a numeric (or BigInt) and if sNow
=== 0 or sPast === 0 or any supply field is missing/NaN then set apy to 0 (or
skip setting apy) and set totalSupply to BigInt(0) (and totalAssets to BigInt(0)
if assets missing) instead of using '1'; ensure the priceNow/pricePast and
Math.pow calculation are only executed when both supplies are >0 to avoid
synthetic APY and use the existing symbols (aNow, sNow, aPast, sPast, priceNow,
pricePast, apyVal, morphoData[addr], totalAssets, totalSupply) to localize the
changes.
---
Duplicate comments:
In `@src/adaptors/surf-liquid/index.js`:
- Line 239: The pool id currently uses ASSET_SYMBOLS[asset] which is unstable;
change it to use a canonical token address (e.g., the asset or receipt token
address) instead. Replace
`${V3_FACTORY.toLowerCase()}-${ASSET_SYMBOLS[asset].toLowerCase()}-${CHAIN}`
with a stable id built from the token address like
`${V3_FACTORY.toLowerCase()}-${assetAddress.toLowerCase()}-${CHAIN}`, where
assetAddress is resolved from your existing mapping or accessor (e.g.,
ASSET_ADDRESSES[asset] or getAssetAddress(asset)) and normalized (lowercase or
checksum) before interpolation so pool dedupe uses the immutable address rather
than a symbol.
---
Nitpick comments:
In `@src/adaptors/surf-liquid/index.js`:
- Around line 85-91: The loop over ASSETS currently awaits sdk.api.abi.multiCall
for each asset sequentially; change it to run the per-asset multicalls in
parallel by mapping ASSETS to an array of promises that call
sdk.api.abi.multiCall (with the same abi 'function assetToVault(address) view
returns (address)', calls built from v3Vaults.map(vault => ({ target: vault,
params: [asset] })), and CHAIN) and then await Promise.all on that array; ensure
you preserve the skip when v3Vaults.length === 0 (either check once before
creating promises or keep a per-asset guard that returns a resolved value) and
then process the resulting array of { output: morphoResults } responses the same
way as before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 09fe5cc3-80a1-4c38-b71a-3bd610c6b70a
📒 Files selected for processing (1)
src/adaptors/surf-liquid/index.js
| const aNow = Number(assetsNowRes.output[i].output || '0'); | ||
| const sNow = Number(supplyNowRes.output[i].output || '1'); | ||
| const aPast = Number(assetsPastRes.output[i].output || '0'); | ||
| const sPast = Number(supplyPastRes.output[i].output || '1'); | ||
|
|
||
| const priceNow = sNow > 0 ? aNow / sNow : 1; | ||
| const pricePast = sPast > 0 ? aPast / sPast : 1; | ||
| const apyVal = | ||
| pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0; | ||
|
|
||
| morphoData[addr] = { | ||
| apy: Math.max(apyVal, 0), | ||
| totalAssets: BigInt(assetsNowRes.output[i].output || '0'), | ||
| totalSupply: BigInt(supplyNowRes.output[i].output || '1'), | ||
| }; |
There was a problem hiding this comment.
Avoid synthetic APY when supply data is missing or zero.
On Line 175 and Line 177, defaulting totalSupply to '1' can fabricate share prices/APY if the call fails or returns zero. That can silently skew apyBase and downstream rankings.
Suggested fix
- const aNow = Number(assetsNowRes.output[i].output || '0');
- const sNow = Number(supplyNowRes.output[i].output || '1');
- const aPast = Number(assetsPastRes.output[i].output || '0');
- const sPast = Number(supplyPastRes.output[i].output || '1');
+ const aNowRaw = assetsNowRes.output[i]?.output ?? '0';
+ const sNowRaw = supplyNowRes.output[i]?.output ?? '0';
+ const aPastRaw = assetsPastRes.output[i]?.output ?? '0';
+ const sPastRaw = supplyPastRes.output[i]?.output ?? '0';
+
+ const aNow = Number(aNowRaw);
+ const sNow = Number(sNowRaw);
+ const aPast = Number(aPastRaw);
+ const sPast = Number(sPastRaw);
- const priceNow = sNow > 0 ? aNow / sNow : 1;
- const pricePast = sPast > 0 ? aPast / sPast : 1;
- const apyVal =
- pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
+ const hasValidSupply = sNow > 0 && sPast > 0;
+ const priceNow = hasValidSupply ? aNow / sNow : 0;
+ const pricePast = hasValidSupply ? aPast / sPast : 0;
+ const apyVal =
+ hasValidSupply && pricePast > 0 ? Math.pow(priceNow / pricePast, 365) - 1 : 0;
morphoData[addr] = {
apy: Math.max(apyVal, 0),
- totalAssets: BigInt(assetsNowRes.output[i].output || '0'),
- totalSupply: BigInt(supplyNowRes.output[i].output || '1'),
+ totalAssets: BigInt(aNowRaw),
+ totalSupply: BigInt(sNowRaw),
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adaptors/surf-liquid/index.js` around lines 174 - 188, The code is
currently defaulting supply to '1' (sNow/sPast) which can fabricate prices/apy;
change the logic in the block that computes aNow, sNow, aPast, sPast, priceNow,
pricePast, apyVal and the assignment to morphoData[addr] to treat missing or
zero supply as invalid: do not substitute '1' for supply—parse supply to a
numeric (or BigInt) and if sNow === 0 or sPast === 0 or any supply field is
missing/NaN then set apy to 0 (or skip setting apy) and set totalSupply to
BigInt(0) (and totalAssets to BigInt(0) if assets missing) instead of using '1';
ensure the priceNow/pricePast and Math.pow calculation are only executed when
both supplies are >0 to avoid synthetic APY and use the existing symbols (aNow,
sNow, aPast, sPast, priceNow, pricePast, apyVal, morphoData[addr], totalAssets,
totalSupply) to localize the changes.
src/adaptors/surf-liquid/index.js
Outdated
| const stakingApr6M = (Number(apr6M) / Number(basisPoints)) * 100; | ||
| const stakingApr12M = (Number(apr12M) / Number(basisPoints)) * 100; |
There was a problem hiding this comment.
Guard against zero BASIS_POINTS before APR division.
If BASIS_POINTS is zero (or invalid), Line 279/280 yields Infinity/NaN APY values.
Suggested fix
- const stakingApr6M = (Number(apr6M) / Number(basisPoints)) * 100;
- const stakingApr12M = (Number(apr12M) / Number(basisPoints)) * 100;
+ const bp = Number(basisPoints);
+ if (!bp) throw new Error('Invalid BASIS_POINTS from staking contract');
+ const stakingApr6M = (Number(apr6M) / bp) * 100;
+ const stakingApr12M = (Number(apr12M) / bp) * 100;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adaptors/surf-liquid/index.js` around lines 279 - 280, The APR
calculation for stakingApr6M and stakingApr12M divides by basisPoints without
checking for zero/invalid, causing Infinity/NaN; update the code around
stakingApr6M/stakingApr12M to parse Number(basisPoints) once (e.g., const bp =
Number(basisPoints)) and if bp <= 0 or isNaN(bp) avoid the division by setting
stakingApr6M and stakingApr12M to a safe fallback (null or 0) or skip the
calculation and optionally log a warning; ensure you reference and update the
expressions that use apr6M, apr12M and basisPoints so they use the validated bp
variable.
src/adaptors/surf-liquid/index.js
Outdated
| if (stakingTvl > 100) { | ||
| pools.push({ | ||
| pool: `${SURF_STAKING.toLowerCase()}-${CHAIN}`, | ||
| chain: utils.formatChain(CHAIN), | ||
| project: 'surf-liquid', | ||
| symbol: 'SURF', | ||
| tvlUsd: stakingTvl + subscriptionTvl, | ||
| apyBase: 0, |
There was a problem hiding this comment.
Apply the visibility threshold to the same TVL you report.
Line 298 gates on stakingTvl only, but Line 304 reports stakingTvl + subscriptionTvl. This can drop a valid pool even when reported TVL would pass the threshold.
Suggested fix
- if (stakingTvl > 100) {
+ const totalSurfTvl = stakingTvl + subscriptionTvl;
+ if (totalSurfTvl > 100) {
pools.push({
pool: `${SURF_STAKING.toLowerCase()}-${CHAIN}`,
chain: utils.formatChain(CHAIN),
project: 'surf-liquid',
symbol: 'SURF',
- tvlUsd: stakingTvl + subscriptionTvl,
+ tvlUsd: totalSurfTvl,
apyBase: 0,
apyReward: stakingApr6M,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (stakingTvl > 100) { | |
| pools.push({ | |
| pool: `${SURF_STAKING.toLowerCase()}-${CHAIN}`, | |
| chain: utils.formatChain(CHAIN), | |
| project: 'surf-liquid', | |
| symbol: 'SURF', | |
| tvlUsd: stakingTvl + subscriptionTvl, | |
| apyBase: 0, | |
| const totalSurfTvl = stakingTvl + subscriptionTvl; | |
| if (totalSurfTvl > 100) { | |
| pools.push({ | |
| pool: `${SURF_STAKING.toLowerCase()}-${CHAIN}`, | |
| chain: utils.formatChain(CHAIN), | |
| project: 'surf-liquid', | |
| symbol: 'SURF', | |
| tvlUsd: totalSurfTvl, | |
| apyBase: 0, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/adaptors/surf-liquid/index.js` around lines 298 - 305, The if gate
currently checks only stakingTvl but tvlUsd uses stakingTvl + subscriptionTvl,
so update the condition to apply the visibility threshold to the combined TVL;
compute a combinedTvl (stakingTvl + subscriptionTvl) or directly change the
guard to if (stakingTvl + subscriptionTvl > 100) before pushing the pool object
(the pools.push block that creates the SURF_STAKING-CHAIN entry and sets
tvlUsd), ensuring the same summed value is used in both the check and the
reported tvlUsd.
|
hi @rohansingh4, thanks for the changes! pls dbl check the code rabbit issues, remove hardcoded fallbacks for reward apy, better to be null than inaccurate values. Reward tokens should only be added to the pools when reward apy > 0 |
- Remove totalSupply fallback to '1'; skip vaults with zero supply instead of fabricating APY values - Guard BASIS_POINTS against zero before APR division - Use total TVL (staking + subscriptions) for visibility threshold - Only add rewardTokens/apyReward when reward APY > 0 - Clean up BigInt comparisons to use 0n literals
|
The surf-liquid adapter exports pools: Test Suites: 1 passed, 1 total |
Summary
Pools
How it works
Test results
Data sources
Summary by CodeRabbit