|
| 1 | +const sdk = require('@defillama/sdk'); |
| 2 | +const axios = require('axios'); |
| 3 | +const utils = require('../utils'); |
| 4 | + |
| 5 | +const vaults = [ |
| 6 | + { |
| 7 | + name: 'Lido Golden Goose Vault', |
| 8 | + symbol: 'GGV', |
| 9 | + vault: '0xef417FCE1883c6653E7dC6AF7c6F85CCDE84Aa09', |
| 10 | + chain: 'ethereum', |
| 11 | + decimals: 18, |
| 12 | + underlyingTokens: [ |
| 13 | + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', // stETH |
| 14 | + '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', // wstETH |
| 15 | + ], |
| 16 | + url: 'https://app.veda.tech/vaults/lido-golden-goose-vault', |
| 17 | + }, |
| 18 | + { |
| 19 | + name: 'Lombard DeFi Vault', |
| 20 | + symbol: 'LBTCv', |
| 21 | + vault: '0x5401b8620E5FB570064CA9114fd1e135fd77D57c', |
| 22 | + chain: 'ethereum', |
| 23 | + decimals: 8, |
| 24 | + apiUrl: |
| 25 | + 'https://bff.prod.lombard-fi.com/sevenseas-api/daily-data/all/0x5401b8620E5FB570064CA9114fd1e135fd77D57c/0/latest', |
| 26 | + underlyingTokens: [ |
| 27 | + '0x8236a87084f8B84306f72007F36F2618A5634494', // LBTC |
| 28 | + '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', // WBTC |
| 29 | + ], |
| 30 | + url: 'https://app.veda.tech/vaults/lombard-defi-vault', |
| 31 | + }, |
| 32 | + { |
| 33 | + name: 'PlasmaUSD', |
| 34 | + symbol: 'PlasmaUSD', |
| 35 | + vault: '0xd1074E0AE85610dDBA0147e29eBe0D8E5873a000', |
| 36 | + chain: 'ethereum', |
| 37 | + displayChain: 'plasma', |
| 38 | + chains: ['ethereum', 'plasma'], |
| 39 | + decimals: 6, |
| 40 | + underlyingTokens: [ |
| 41 | + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC |
| 42 | + '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT |
| 43 | + '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI |
| 44 | + ], |
| 45 | + url: 'https://app.veda.tech/vaults/plasmausd', |
| 46 | + }, |
| 47 | + { |
| 48 | + name: 'Veda USD Vault', |
| 49 | + symbol: 'vedaUSD', |
| 50 | + vault: '0x71b9601d96B7e43C434d07D4AE1Aa26650920aA7', |
| 51 | + chain: 'ethereum', |
| 52 | + decimals: 6, |
| 53 | + underlyingTokens: [ |
| 54 | + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC |
| 55 | + ], |
| 56 | + url: 'https://app.veda.tech/vaults/usd-vault', |
| 57 | + }, |
| 58 | +]; |
| 59 | + |
| 60 | +const SECONDS_PER_DAY = 86400; |
| 61 | + |
| 62 | +const calculateApr = (currentRate, pastRate, days) => { |
| 63 | + if (!pastRate || pastRate <= 0 || currentRate === pastRate) return null; |
| 64 | + return ((currentRate - pastRate) / pastRate / days) * 365 * 100; |
| 65 | +}; |
| 66 | + |
| 67 | +const getBlock = async (timestamp, chain = 'ethereum') => { |
| 68 | + try { |
| 69 | + const response = await axios.get( |
| 70 | + `https://coins.llama.fi/block/${chain}/${timestamp}` |
| 71 | + ); |
| 72 | + return response.data.height; |
| 73 | + } catch (e) { |
| 74 | + console.error(`Error fetching block for ${chain} at ${timestamp}:`, e.message); |
| 75 | + return null; |
| 76 | + } |
| 77 | +}; |
| 78 | + |
| 79 | +const getAccountant = async (vaultAddress, chain) => { |
| 80 | + try { |
| 81 | + const hookResult = await sdk.api.abi.call({ |
| 82 | + target: vaultAddress, |
| 83 | + abi: 'function hook() view returns (address)', |
| 84 | + chain, |
| 85 | + }); |
| 86 | + |
| 87 | + if (hookResult.output === '0x0000000000000000000000000000000000000000') { |
| 88 | + return null; |
| 89 | + } |
| 90 | + |
| 91 | + const accountantResult = await sdk.api.abi.call({ |
| 92 | + target: hookResult.output, |
| 93 | + abi: 'function accountant() view returns (address)', |
| 94 | + chain, |
| 95 | + }); |
| 96 | + return accountantResult.output; |
| 97 | + } catch (e) { |
| 98 | + return null; |
| 99 | + } |
| 100 | +}; |
| 101 | + |
| 102 | +const getRate = async (accountant, chain, block) => { |
| 103 | + try { |
| 104 | + const result = await sdk.api.abi.call({ |
| 105 | + target: accountant, |
| 106 | + abi: 'function getRate() view returns (uint256)', |
| 107 | + chain, |
| 108 | + block: block || undefined, |
| 109 | + }); |
| 110 | + return Number(result.output); |
| 111 | + } catch (e) { |
| 112 | + return null; |
| 113 | + } |
| 114 | +}; |
| 115 | + |
| 116 | +const getTotalSupply = async (vault) => { |
| 117 | + if (vault.chains) { |
| 118 | + const supplies = await Promise.all( |
| 119 | + vault.chains.map(async (chain) => { |
| 120 | + try { |
| 121 | + const result = await sdk.api.abi.call({ |
| 122 | + target: vault.vault, |
| 123 | + abi: 'erc20:totalSupply', |
| 124 | + chain, |
| 125 | + }); |
| 126 | + return Number(result.output) / Math.pow(10, vault.decimals); |
| 127 | + } catch (e) { |
| 128 | + return 0; |
| 129 | + } |
| 130 | + }) |
| 131 | + ); |
| 132 | + return supplies.reduce((sum, s) => sum + s, 0); |
| 133 | + } |
| 134 | + |
| 135 | + const result = await sdk.api.abi.call({ |
| 136 | + target: vault.vault, |
| 137 | + abi: 'erc20:totalSupply', |
| 138 | + chain: vault.chain, |
| 139 | + }); |
| 140 | + return Number(result.output) / Math.pow(10, vault.decimals); |
| 141 | +}; |
| 142 | + |
| 143 | +const getTvl = async (vault, totalSupply, currentRate) => { |
| 144 | + const vaultKey = `${vault.chain}:${vault.vault}`; |
| 145 | + const underlyingKey = `${vault.chain}:${vault.underlyingTokens[0]}`; |
| 146 | + const priceRes = await axios.get( |
| 147 | + `https://coins.llama.fi/prices/current/${vaultKey},${underlyingKey}` |
| 148 | + ); |
| 149 | + |
| 150 | + if (priceRes.data.coins[vaultKey]?.price) { |
| 151 | + return totalSupply * priceRes.data.coins[vaultKey].price; |
| 152 | + } |
| 153 | + if (priceRes.data.coins[underlyingKey]?.price) { |
| 154 | + const rate = currentRate / Math.pow(10, vault.decimals); |
| 155 | + return totalSupply * rate * priceRes.data.coins[underlyingKey].price; |
| 156 | + } |
| 157 | + return 0; |
| 158 | +}; |
| 159 | + |
| 160 | +const getApyFromApi = async (apiUrl) => { |
| 161 | + try { |
| 162 | + const response = await axios.get(apiUrl); |
| 163 | + const data = response.data; |
| 164 | + if (!data || data.length === 0) return null; |
| 165 | + |
| 166 | + const latest = data[0]; |
| 167 | + let apyBase7d = null; |
| 168 | + if (data.length >= 7) { |
| 169 | + const recent7d = data.slice(0, 7); |
| 170 | + apyBase7d = |
| 171 | + recent7d.reduce((sum, d) => sum + (d.daily_apy || 0), 0) / |
| 172 | + recent7d.length; |
| 173 | + } |
| 174 | + |
| 175 | + return { |
| 176 | + apyBase: latest.daily_apy || 0, |
| 177 | + apyBase7d, |
| 178 | + }; |
| 179 | + } catch (e) { |
| 180 | + return null; |
| 181 | + } |
| 182 | +}; |
| 183 | + |
| 184 | +const apy = async () => { |
| 185 | + const now = Math.floor(Date.now() / 1000); |
| 186 | + const [block1d, block7d] = await Promise.all([ |
| 187 | + getBlock(now - SECONDS_PER_DAY), |
| 188 | + getBlock(now - 7 * SECONDS_PER_DAY), |
| 189 | + ]); |
| 190 | + |
| 191 | + const pools = []; |
| 192 | + |
| 193 | + for (const vault of vaults) { |
| 194 | + try { |
| 195 | + const accountant = await getAccountant(vault.vault, vault.chain); |
| 196 | + if (!accountant) continue; |
| 197 | + |
| 198 | + const currentRate = await getRate(accountant, vault.chain, null); |
| 199 | + if (!currentRate) continue; |
| 200 | + |
| 201 | + const totalSupply = await getTotalSupply(vault); |
| 202 | + const tvlUsd = await getTvl(vault, totalSupply, currentRate); |
| 203 | + |
| 204 | + let apyBase, apyBase7d; |
| 205 | + |
| 206 | + if (vault.apiUrl) { |
| 207 | + const apiApy = await getApyFromApi(vault.apiUrl); |
| 208 | + apyBase = apiApy?.apyBase || 0; |
| 209 | + apyBase7d = apiApy?.apyBase7d || 0; |
| 210 | + } else { |
| 211 | + // Calculate APY from on-chain rate changes (skip if blocks unavailable) |
| 212 | + const [rate1d, rate7d] = await Promise.all([ |
| 213 | + block1d ? getRate(accountant, vault.chain, block1d) : null, |
| 214 | + block7d ? getRate(accountant, vault.chain, block7d) : null, |
| 215 | + ]); |
| 216 | + const apr1d = calculateApr(currentRate, rate1d, 1); |
| 217 | + const apr7d = calculateApr(currentRate, rate7d, 7); |
| 218 | + apyBase = apr1d || apr7d || 0; |
| 219 | + apyBase7d = apr7d || 0; |
| 220 | + } |
| 221 | + |
| 222 | + const poolChain = vault.displayChain || vault.chain; |
| 223 | + pools.push({ |
| 224 | + pool: `${vault.vault}-${poolChain}`.toLowerCase(), |
| 225 | + chain: utils.formatChain(poolChain), |
| 226 | + project: 'veda', |
| 227 | + symbol: vault.symbol, |
| 228 | + tvlUsd, |
| 229 | + apyBase, |
| 230 | + apyBase7d, |
| 231 | + underlyingTokens: vault.underlyingTokens, |
| 232 | + url: vault.url, |
| 233 | + }); |
| 234 | + } catch (e) { |
| 235 | + console.error(`Error processing ${vault.name}:`, e.message); |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + return pools; |
| 240 | +}; |
| 241 | + |
| 242 | +module.exports = { |
| 243 | + timetravel: false, |
| 244 | + apy, |
| 245 | + url: 'https://veda.tech/', |
| 246 | +}; |
0 commit comments