-
Notifications
You must be signed in to change notification settings - Fork 1.1k
surf-liquid: Add V4 yield pools for Ethereum, Arbitrum, Polygon, Base #2626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,13 @@ const sdk = require('@defillama/sdk'); | |
| const axios = require('axios'); | ||
| const utils = require('../utils'); | ||
|
|
||
| // V2/V3 Base constants | ||
| const CHAIN = 'base'; | ||
|
|
||
| const V2_FACTORY = '0x1D283b668F947E03E8ac8ce8DA5505020434ea0E'; | ||
| const V3_FACTORY = '0xf1d64dee9f8e109362309a4bfbb523c8e54fa1aa'; | ||
| const V3_DEPLOY_FROM_BLOCK = 38856207; | ||
| const SURF_STAKING = '0xB0fDFc081310A5914c2d2c97e7582F4De12FA9d6'; | ||
| const SURF_STAKING_V2 = '0xeBa3B16E175fD36c8b01953D9e3962AB3c575718'; | ||
| const SURF_TOKEN = '0xcdca2eaae4a8a6b83d7a3589946c2301040dafbf'; | ||
| const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'; | ||
| const WETH = '0x4200000000000000000000000000000000000006'; | ||
|
|
@@ -27,10 +28,168 @@ const ZERO_ADDR = '0x0000000000000000000000000000000000000000'; | |
|
|
||
| const PERFORMANCE_FEE = 0.1; // 10% on earned yield | ||
|
|
||
| // V4 constants — same addresses on all chains | ||
| const V4_FACTORY = '0x8fa50DeA8DB10987D7d22ac092001c3613C18779'; | ||
| const V4_REGISTRY = '0x98A0DeF9C959Ec934Df02141291303819369f271'; | ||
| const V4_FROM_BLOCKS = { | ||
| base: 43800000, | ||
| ethereum: 22200000, | ||
| arbitrum: 445000000, | ||
| polygon: 71000000, | ||
| }; | ||
| const V4_CHAINS = ['ethereum', 'arbitrum', 'polygon', 'base']; | ||
|
|
||
| async function fetchV4ChainPools(chain) { | ||
| const fromBlock = V4_FROM_BLOCKS[chain]; | ||
|
|
||
| // Get current and 24h-ago blocks for APY computation | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const dayAgo = now - 86400; | ||
| const [blockNow, blockPast] = await utils.getBlocksByTime([now, dayAgo], chain); | ||
|
|
||
| // Enumerate user vaults | ||
| const vaultLogs = await sdk.getEventLogs({ | ||
| target: V4_FACTORY, | ||
| fromBlock, | ||
| toBlock: blockNow, | ||
| chain, | ||
| eventAbi: | ||
| 'event VaultDeployed(address indexed vaultAddress, address indexed owner, bytes32 salt)', | ||
| }); | ||
| const userVaults = vaultLogs.map((l) => l.args.vaultAddress); | ||
| if (userVaults.length === 0) return []; | ||
|
|
||
| // Get allowed assets from registry | ||
| const { output: assets } = await sdk.api.abi.call({ | ||
| target: V4_REGISTRY, | ||
| abi: 'function getAllowedAssets() view returns (address[])', | ||
| chain, | ||
| }); | ||
| if (!assets || assets.length === 0) return []; | ||
|
|
||
| // Fetch prices for all assets | ||
| const priceKeys = assets.map((a) => `${chain}:${a}`).join(','); | ||
| const priceResp = await axios.get( | ||
| `https://coins.llama.fi/prices/current/${priceKeys}` | ||
| ); | ||
| const coinData = priceResp.data?.coins || {}; | ||
|
|
||
| const pools = []; | ||
|
|
||
| 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], | ||
| }); | ||
| } | ||
|
Comment on lines
+79
to
+181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isolate per-asset failures before the chain-level Any rejected ABI/price call inside 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 |
||
|
|
||
| return pools; | ||
| } | ||
|
|
||
| const apy = async () => { | ||
| // ===================================================================== | ||
| // V2/V3 Base pools | ||
| // ===================================================================== | ||
|
|
||
| // --- Step 1: Discover Surf Liquid vault addresses --- | ||
|
|
||
| // V2 vaults from factory | ||
| const { output: totalV2 } = await sdk.api.abi.call({ | ||
| target: V2_FACTORY, | ||
| abi: 'uint256:getTotalVaults', | ||
|
|
@@ -47,7 +206,6 @@ const apy = async () => { | |
| }); | ||
| const v2Vaults = v2Infos.map((info) => info.output[0]); | ||
|
|
||
| // V3 vaults from factory deploy events | ||
| const currentBlock = (await sdk.api.util.getLatestBlock(CHAIN)).number; | ||
| const v3Logs = await sdk.getEventLogs({ | ||
| target: V3_FACTORY, | ||
|
|
@@ -61,7 +219,6 @@ const apy = async () => { | |
|
|
||
| // --- Step 2: Get current Morpho vaults per asset --- | ||
|
|
||
| // V2 vaults -> currentVault() (USDC only) | ||
| const { output: v2MorphoResults } = await sdk.api.abi.multiCall({ | ||
| abi: 'address:currentVault', | ||
| calls: v2Vaults.map((target) => ({ target })), | ||
|
|
@@ -202,7 +359,7 @@ const apy = async () => { | |
| prices[asset] = priceResp.data?.coins?.[key]?.price || 0; | ||
| } | ||
|
|
||
| // --- Step 6: Build yield pools per asset --- | ||
| // --- Step 6: Build V2/V3 yield pools per asset --- | ||
|
|
||
| const pools = []; | ||
|
|
||
|
|
@@ -248,65 +405,64 @@ const apy = async () => { | |
| }); | ||
| } | ||
|
|
||
| // --- Step 7: SURF Staking pool (APR from on-chain) --- | ||
|
|
||
| const [ | ||
| { output: totalStaked }, | ||
| { output: apr6M }, | ||
| { output: apr12M }, | ||
| { output: basisPoints }, | ||
| ] = await Promise.all([ | ||
| sdk.api.abi.call({ | ||
| target: SURF_STAKING, | ||
| abi: 'uint256:totalStaked', | ||
| chain: CHAIN, | ||
| }), | ||
| sdk.api.abi.call({ | ||
| target: SURF_STAKING, | ||
| abi: 'uint256:apr6Months', | ||
| chain: CHAIN, | ||
| }), | ||
| sdk.api.abi.call({ | ||
| target: SURF_STAKING, | ||
| abi: 'uint256:apr12Months', | ||
| chain: CHAIN, | ||
| }), | ||
| sdk.api.abi.call({ | ||
| target: SURF_STAKING, | ||
| abi: 'uint256:BASIS_POINTS', | ||
| chain: CHAIN, | ||
| }), | ||
| ]); | ||
|
|
||
| const bp = Number(basisPoints); | ||
| const stakingApr6M = bp > 0 ? (Number(apr6M) / bp) * 100 : null; | ||
| const stakingApr12M = bp > 0 ? (Number(apr12M) / bp) * 100 : null; | ||
| // --- Step 7: SURF Staking pools (v1 + v2) --- | ||
|
|
||
| const surfPriceKey = `${CHAIN}:${SURF_TOKEN}`; | ||
| const surfPriceResp = await axios.get( | ||
| `https://coins.llama.fi/prices/current/${surfPriceKey}?searchWidth=24h` | ||
| ); | ||
| const surfPrice = surfPriceResp.data?.coins?.[surfPriceKey]?.price || 0; | ||
| const stakingTvl = (Number(totalStaked) / 1e18) * surfPrice; | ||
|
|
||
| if (stakingTvl > 100) { | ||
| const stakingPool = { | ||
| pool: `${SURF_STAKING.toLowerCase()}-${CHAIN}`, | ||
| chain: utils.formatChain(CHAIN), | ||
| project: 'surf-liquid', | ||
| symbol: 'SURF', | ||
| tvlUsd: stakingTvl, | ||
| apyBase: 0, | ||
| underlyingTokens: [SURF_TOKEN], | ||
| }; | ||
| for (const stakingAddr of [SURF_STAKING, SURF_STAKING_V2]) { | ||
| const [ | ||
| { output: totalStaked }, | ||
| { output: apr6M }, | ||
| { output: apr12M }, | ||
| { output: basisPoints }, | ||
| ] = await Promise.all([ | ||
| sdk.api.abi.call({ target: stakingAddr, abi: 'uint256:totalStaked', chain: CHAIN }), | ||
| sdk.api.abi.call({ target: stakingAddr, abi: 'uint256:apr6Months', chain: CHAIN }), | ||
| sdk.api.abi.call({ target: stakingAddr, abi: 'uint256:apr12Months', chain: CHAIN }), | ||
| sdk.api.abi.call({ target: stakingAddr, abi: 'uint256:BASIS_POINTS', chain: CHAIN }), | ||
| ]); | ||
|
|
||
| const bp = Number(basisPoints); | ||
| const stakingApr6M = bp > 0 ? (Number(apr6M) / bp) * 100 : null; | ||
| const stakingTvl = (Number(totalStaked) / 1e18) * surfPrice; | ||
|
|
||
| if (stakingTvl > 100) { | ||
| const stakingPool = { | ||
| pool: `${stakingAddr.toLowerCase()}-${CHAIN}`, | ||
| chain: utils.formatChain(CHAIN), | ||
| project: 'surf-liquid', | ||
| symbol: 'SURF', | ||
| tvlUsd: stakingTvl, | ||
| apyBase: 0, | ||
| underlyingTokens: [SURF_TOKEN], | ||
| }; | ||
|
|
||
| if (stakingApr6M != null && stakingApr6M > 0) { | ||
| stakingPool.apyReward = stakingApr6M; | ||
| stakingPool.rewardTokens = [SURF_TOKEN]; | ||
| stakingPool.poolMeta = '6M / 12M lock'; | ||
| } | ||
|
|
||
| if (stakingApr6M != null && stakingApr6M > 0) { | ||
| stakingPool.apyReward = stakingApr6M; | ||
| stakingPool.rewardTokens = [SURF_TOKEN]; | ||
| stakingPool.poolMeta = '6M / 12M lock'; | ||
| pools.push(stakingPool); | ||
| } | ||
| } | ||
|
|
||
| pools.push(stakingPool); | ||
| // ===================================================================== | ||
| // V4 pools — all chains (Base, Ethereum, Arbitrum, Polygon) | ||
| // ===================================================================== | ||
|
|
||
| const v4Results = await Promise.allSettled( | ||
| V4_CHAINS.map((chain) => fetchV4ChainPools(chain)) | ||
| ); | ||
|
|
||
| for (const result of v4Results) { | ||
| if (result.status === 'fulfilled') { | ||
| pools.push(...result.value); | ||
| } | ||
| } | ||
|
|
||
| return pools; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Verification inconclusive
Script executed:
cat -n src/adaptors/surf-liquid/index.js | head -150Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 1
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
Script executed:
Repository: DefiLlama/yield-server
Repository: DefiLlama/yield-server
Exit code: 0
stdout:
Use BigInt for share-price math, not
Number.totalAssetsandtotalSupplyare uint256 values that can exceed JavaScript's safe integer limit (2^53-1). Converting them toNumberbefore dividing loses precision, which compounds through the annualization step. Theyieldooradaptor already demonstrates the correct pattern: useBigIntfor the division, then convert only the final normalized rate toNumber. Apply the same approach here.🤖 Prompt for AI Agents