diff --git a/src/adaptors/termmax/index.js b/src/adaptors/termmax/index.js index 5b92d0e31b..174d1e0f69 100644 --- a/src/adaptors/termmax/index.js +++ b/src/adaptors/termmax/index.js @@ -1,53 +1,93 @@ const axios = require('axios'); const sdk = require('@defillama/sdk'); const { default: BigNumber } = require('bignumber.js'); +const { Interface } = require('ethers/lib/utils'); + +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const EVENTS = { + V1: { + CreateVault: + 'event CreateVault(address indexed vault, address indexed creator, (address admin,address curator,uint256 timelock,address asset,uint256 maxCapacity,string name,string symbol,uint64 performanceFeeRate) indexed initialParams)', + }, + V2: { + VaultCreated: + 'event VaultCreated(address indexed vault, address indexed creator, (address admin,address curator,address guardian,uint256 timelock,address asset,address pool,uint256 maxCapacity,string name,string symbol,uint64 performanceFeeRate,uint64 minApy) initialParams)', + }, +}; +const v1iface = new Interface([EVENTS.V1.CreateVault]); +const v2iface = new Interface([EVENTS.V2.VaultCreated]); const VAULTS = { - Ethereum: { + ethereum: { alias: 'eth', chain: 'ethereum', chainId: 1, - addresses: [ - '0x984408C88a9B042BF3e2ddf921Cd1fAFB4b735D1', - '0xDEB8a9C0546A01b7e5CeE8e44Fd0C8D8B96a1f6e', - '0xdC4d99aB6c69943b4E17431357AbC5b54B4C2F56', - '0xDAdeAcC03a59639C0ecE5ec4fF3BC0d9920A47eC', + vaultFactory: [ + { + address: '0x01D8C1e0584751085a876892151Bf8490e862E3E', + fromBlock: 22174789, + }, + { + address: '0x4778CBf91d8369843281c8f5a2D7b56d1420dFF5', + fromBlock: 22283092, + }, + ], + vaultFactoryV2: [ + { + address: '0xF2BDa87CA467eB90A1b68f824cB136baA68a8177', + fromBlock: 23445703, + }, + { + address: '0x5b8B26a6734B5eABDBe6C5A19580Ab2D0424f027', + fromBlock: 23488637, + }, ], }, - Arbitrum: { + arbitrum: { alias: 'arb', chain: 'arbitrum', chainId: 42161, - addresses: [ - '0xc94b752839a22D2C44E99e298671dd4B2aDd11b3', - '0x8c5161f287Cbc9Afa48bC8972eE8CC0a755fcAdC', + vaultFactory: [ + { + address: '0x929CBcb8150aD59DB63c92A7dAEc07b30d38bA79', + fromBlock: 322193571, + }, + ], + vaultFactoryV2: [ + { + address: '0xa7c93162962D050098f4BB44E88661517484C5EB', + fromBlock: 385228046, + }, ], }, - Binance: { + bsc: { alias: 'bnb', chain: 'bsc', chainId: 56, - addresses: [ - '0x86c958CAc8aeE37dE62715691c0D597c710Eca51', - '0x89653E6523fB73284353252b41AE580E6f96dFad', + vaultFactory: [ + { + address: '0x48bCd27e208dC973C3F56812F762077A90E88Cea', + fromBlock: 50519690, + }, + ], + vaultFactoryV2: [ + { + address: '0x1401049368eD6AD8194f8bb7E41732c4620F170b', + fromBlock: 63192842, + }, ], }, }; -const VAULTS_V1 = { - Ethereum: { - alias: 'eth', - chain: 'ethereum', - chainId: 1, - addresses: [ - '0x6aa8b4366d3BcFa51473677e9C961BAbADCec4e3', - '0x09400f3a0b358eb408060f5a3272ed7e3b7664e0', - '0x581AA6F8D7498E68E02D4fB6abFA7972c9d86286', - '0xf4B8dCD6509B3Cb87DA9C491cB0728a2F3088d3F', - '0xB3Bad0e31E08395eaaE76bE47985D074A4112F38', - '0x0d6b6280915313e7322dcb4d71d511a93a75d98d', - ], - }, +const VAULT_BLACKLIST = { + arbitrum: [ + '0x8531dC1606818A3bc3D26207a63641ac2F1f6Dc8', // misconfigured asset + ], + ethereum: [], + bsc: [ + '0xe5E01B82904a49Ce5a670c1B7488C3f29433088a', // misconfigured asset + ], }; async function getMerklOpportunities() { @@ -57,297 +97,479 @@ async function getMerklOpportunities() { return res.data.filter((o) => o.status === 'LIVE'); } -async function apy() { - const opportunities = await getMerklOpportunities(); +async function getPrices(chain, addresses) { + const priceMap = new Map(); - const pools = []; - const promises = []; - for (const [chain, vaultData] of Object.entries(VAULTS)) { + const tasks = []; + for (const address of addresses) { + const url = new URL( + `https://coins.llama.fi/prices/current/${chain}:${address}` + ); + tasks.push( + axios.get(url).then((response) => { + const priceKey = `${chain}:${address}`; + priceMap.set(address, response.data.coins[priceKey]?.price || 0); + }) + ); + } + await Promise.all(tasks); + + return priceMap; +} + +async function getVaultV1Addresses(chain, blockNumber) { + const { vaultFactory } = VAULTS[chain]; + + const addresses = []; + + const tasks = []; + for (const factory of vaultFactory) { const task = async () => { - const calls = vaultData.addresses.map((address) => ({ - target: address, - })); - const [apr, asset, decimals, names, totalAssets] = await Promise.all([ - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'apr', - type: 'function', - inputs: [], - outputs: [{ type: 'uint256' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'asset', - type: 'function', - inputs: [], - outputs: [{ type: 'address' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'decimals', - type: 'function', - inputs: [], - outputs: [{ type: 'uint8' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'name', - type: 'function', - inputs: [], - outputs: [{ type: 'string' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'totalAssets', - type: 'function', - inputs: [], - outputs: [{ type: 'uint256' }], - }, - chain: vaultData.chain, - }), - ]); - const assetNames = await sdk.api.abi.multiCall({ - calls: asset.output.map((o) => ({ target: o.output })), - abi: { - name: 'symbol', - type: 'function', - inputs: [], - outputs: [{ type: 'string' }], - }, - chain: vaultData.chain, + const { output } = await sdk.api2.util.getLogs({ + target: factory.address, + topic: '', + fromBlock: factory.fromBlock, + toBlock: blockNumber, + keys: [], + topics: [v1iface.getEventTopic('CreateVault')], + chain, }); - - const assetAddresses = new Set(asset.output.map((o) => o.output)); - const priceMap = new Map(); - { - const promises = []; - for (const assetAddress of assetAddresses) { - const url = new URL( - `https://coins.llama.fi/prices/current/${vaultData.chain}:${assetAddress}` - ); - promises.push( - axios.get(url).then((response) => { - const priceKey = `${vaultData.chain}:${assetAddress}`; - priceMap.set( - assetAddress, - response.data.coins[priceKey]?.price || 0 - ); - }) - ); - } - await Promise.all(promises); + const events = output + .filter((e) => !e.removed) + .map((e) => v1iface.parseLog(e)); + for (const { args } of events) { + const [vault] = args; + addresses.push(vault); } + }; + tasks.push(task()); + } + await Promise.all(tasks); - for (let i = 0; i < vaultData.addresses.length; i++) { - const address = vaultData.addresses[i]; - const assetAddress = asset.output[i].output; - - const readableApr = new BigNumber(apr.output[i].output) - .div(new BigNumber(10).pow(6)) // actual decimals for APR is 8 - .toNumber(); - const tvlUsd = new BigNumber(totalAssets.output[i].output) - .div(new BigNumber(10).pow(decimals.output[i].output)) - .times(priceMap.get(assetAddress) || 0) - .toNumber(); - - const url = new URL( - `https://app.termmax.ts.finance/earn/${ - vaultData.alias - }/${address.toLowerCase()}` - ); - url.searchParams.set('chain', vaultData.alias); - - const pool = { - pool: `${address}-${chain.toLowerCase()}`, - chain, - project: 'termmax', - symbol: assetNames.output[i].output, - tvlUsd, - apyBase: readableApr, - url: String(url), - underlyingTokens: [assetAddress], - poolMeta: names.output[i].output, - }; - - const opportunity = opportunities.find( - (o) => - o.chainId === vaultData.chainId && - o.identifier.toLowerCase() === address.toLowerCase() - ); - if (opportunity) { - pool.apyReward = opportunity.apr; - - const breakdowns = - (opportunity.rewardsRecord && - opportunity.rewardsRecord.breakdowns) || - []; - pool.rewardTokens = breakdowns - .map((b) => b.token.address) - .filter((a) => a); - } - - pools.push(pool); - } + return addresses; +} + +async function getVaultsV1({ alias, chain, chainId, number, opportunities }) { + const vaults = []; + + const addresses = await getVaultV1Addresses(chain, number).then((addresses) => + addresses.filter((a) => !VAULT_BLACKLIST[chain].includes(a)) + ); + const calls = addresses.map((target) => ({ target })); + const [aprs, assets, decimalses, names, totalAssetses] = await Promise.all([ + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint256:apr', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'address:asset', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint8:decimals', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'string:name', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint256:totalAssets', + }), + ]); + + const [assetNames, priceMap] = await Promise.all([ + sdk.api.abi.multiCall({ + chain, + calls: assets.output.map((a) => ({ target: a.output })), + abi: 'string:symbol', + }), + getPrices( + chain, + assets.output.map((a) => a.output) + ), + ]); + + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i]; + const assetAddress = assets.output[i].output; + + const readableApr = new BigNumber(aprs.output[i].output) + .div(new BigNumber(10).pow(6)) // actual decimals for APR is 8 + .toNumber(); + const tvlUsd = new BigNumber(totalAssetses.output[i].output) + .div(new BigNumber(10).pow(decimalses.output[i].output)) + .times(priceMap.get(assetAddress) || 0) + .toNumber(); + + const url = new URL( + `https://app.termmax.ts.finance/earn/${alias}/${address.toLowerCase()}` + ); + url.searchParams.set('chain', alias); + + const vault = { + pool: `${address}-${chain.toLowerCase()}`, + chain, + project: 'termmax', + symbol: assetNames.output[i].output, + tvlUsd, + apyBase: readableApr, + url: String(url), + underlyingTokens: [assetAddress], + poolMeta: names.output[i].output, }; - promises.push(task()); + + const opportunity = opportunities.find( + (o) => + o.chainId === chainId && + o.identifier.toLowerCase() === address.toLowerCase() + ); + if (opportunity) { + vault.apyReward = opportunity.apr; + + const breakdowns = + (opportunity.rewardsRecord && opportunity.rewardsRecord.breakdowns) || + []; + vault.rewardTokens = breakdowns + .map((b) => b.token.address) + .filter((a) => a); + } + + vaults.push(vault); } - for (const [chain, vaultData] of Object.entries(VAULTS_V1)) { + + return vaults; +} + +async function getVaultV2Addresses(chain, blockNumber) { + const { vaultFactoryV2 } = VAULTS[chain]; + + const addresses = []; + + const tasks = []; + for (const factory of vaultFactoryV2) { const task = async () => { - const calls = vaultData.addresses.map((address) => ({ - target: address, - })); - const [apy, asset, decimals, names, totalAssets] = await Promise.all([ - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'apy', - type: 'function', - inputs: [], - outputs: [{ type: 'uint256' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'asset', - type: 'function', - inputs: [], - outputs: [{ type: 'address' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'decimals', - type: 'function', - inputs: [], - outputs: [{ type: 'uint8' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'name', - type: 'function', - inputs: [], - outputs: [{ type: 'string' }], - }, - chain: vaultData.chain, - }), - sdk.api.abi.multiCall({ - calls, - abi: { - name: 'totalAssets', - type: 'function', - inputs: [], - outputs: [{ type: 'uint256' }], - }, - chain: vaultData.chain, - }), - ]); - const assetNames = await sdk.api.abi.multiCall({ - calls: asset.output.map((o) => ({ target: o.output })), + const { output } = await sdk.api2.util.getLogs({ + target: factory.address, + topic: '', + fromBlock: factory.fromBlock, + toBlock: blockNumber, + keys: [], + topics: [v2iface.getEventTopic('VaultCreated')], + chain, + }); + const events = output + .filter((e) => !e.removed) + .map((e) => v2iface.parseLog(e)); + for (const { args } of events) { + const [vault] = args; + addresses.push(vault); + } + }; + tasks.push(task()); + } + await Promise.all(tasks); + + return addresses; +} + +async function getAaveVaultEffectiveApy({ + aavePool, + apy, + assetAddress, + chain, + poolAddress, + vaultAddress, +}) { + const aToken = await sdk.api.abi + .call({ + target: poolAddress, + abi: 'address:aToken', + chain, + }) + .then((r) => r.output) + .catch(() => NULL_ADDRESS); + if (aToken === NULL_ADDRESS) return apy; + + const [assetsInThirdPool, idle] = await Promise.all([ + sdk.api.abi + .call({ + target: aToken, abi: { - name: 'symbol', - type: 'function', - inputs: [], - outputs: [{ type: 'string' }], + inputs: [{ type: 'address' }], + name: 'balanceOf', + outputs: [{ type: 'uint256' }], }, - chain: vaultData.chain, + params: [poolAddress], + chain, + }) + .then((r) => r.output), + sdk.api.abi + .call({ + target: assetAddress, + abi: { + inputs: [{ type: 'address' }], + name: 'balanceOf', + outputs: [{ type: 'uint256' }], + }, + params: [poolAddress], + chain, + }) + .then((r) => r.output), + ]); + + const idleFund = new BigNumber(assetsInThirdPool).plus(idle); + if (idleFund.isZero()) return apy; + + const passiveRatio = new BigNumber(assetsInThirdPool).div(idleFund); + + const currentLiquidityRate = await sdk.api.abi + .call({ + target: aavePool, + abi: { + inputs: [{ type: 'address' }], + name: 'getReserveData', + outputs: [ + { + components: [ + { + components: [{ type: 'uint256', name: 'data' }], + name: 'configuration', + type: 'tuple', + }, + { type: 'uint128', name: 'liquidityIndex' }, + { type: 'uint128', name: 'currentLiquidityRate' }, + { type: 'uint128', name: 'variableBorrowIndex' }, + { type: 'uint128', name: 'currentVariableBorrowRate' }, + { type: 'uint128', name: 'currentStableBorrowRate' }, + { type: 'uint40', name: 'lastUpdateTimestamp' }, + { type: 'uint16', name: 'id' }, + { type: 'address', name: 'aTokenAddress' }, + { type: 'address', name: 'stableDebtTokenAddress' }, + { type: 'address', name: 'variableDebtTokenAddress' }, + { type: 'address', name: 'interestRateStrategyAddress' }, + { type: 'uint128', name: 'accruedToTreasury' }, + { type: 'uint128', name: 'unbacked' }, + { type: 'uint128', name: 'isolationModeTotalDebt' }, + ], + name: 'res', + type: 'tuple', + }, + ], + }, + params: [assetAddress], + chain, + }) + .then((r) => r.output.currentLiquidityRate); + + const passiveApy = new BigNumber(currentLiquidityRate).div( + new BigNumber(10).pow(27) + ); + return new BigNumber(apy).plus(passiveApy.times(passiveRatio)).toNumber(); +} + +async function getVaultEffectiveApy({ + apy, + assetAddress, + chain, + chainId, + poolAddress, + vaultAddress, +}) { + const aavePool = await sdk.api.abi + .call({ + target: poolAddress, + abi: 'address:aavePool', + chain, + }) + .then((r) => r.output) + .catch(() => NULL_ADDRESS); + if (aavePool !== NULL_ADDRESS) + return await getAaveVaultEffectiveApy({ + apy, + assetAddress, + chain, + poolAddress, + vaultAddress, + aavePool, + }); + + return apy; +} + +async function getVaultsV2({ alias, chain, chainId, number, opportunities }) { + const vaults = []; + + const addresses = await getVaultV2Addresses(chain, number).then((addresses) => + addresses.filter((a) => !VAULT_BLACKLIST[chain].includes(a)) + ); + const calls = addresses.map((target) => ({ target })); + const [apys, assets, decimalses, names, totalAssetses, pools] = + await Promise.all([ + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint256:apy', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'address:asset', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint8:decimals', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'string:name', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'uint256:totalAssets', + }), + sdk.api.abi.multiCall({ + chain, + calls, + abi: 'address:pool', + }), + ]); + + const [assetNames, priceMap] = await Promise.all([ + sdk.api.abi.multiCall({ + chain, + calls: assets.output.map((a) => ({ target: a.output })), + abi: 'string:symbol', + }), + getPrices( + chain, + assets.output.map((a) => a.output) + ), + ]); + + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i]; + const assetAddress = assets.output[i].output; + + let apy = new BigNumber(apys.output[i].output) + .div(new BigNumber(10).pow(8)) + .toNumber(); + if (pools.output[i].output !== NULL_ADDRESS) { + apy = await getVaultEffectiveApy({ + apy, + assetAddress, + chain, + chainId, + poolAddress: pools.output[i].output, + vaultAddress: address, }); + } + const tvlUsd = new BigNumber(totalAssetses.output[i].output) + .div(new BigNumber(10).pow(decimalses.output[i].output)) + .times(priceMap.get(assetAddress) || 0) + .toNumber(); - const assetAddresses = new Set(asset.output.map((o) => o.output)); - const priceMap = new Map(); - { - const promises = []; - for (const assetAddress of assetAddresses) { - const url = new URL( - `https://coins.llama.fi/prices/current/${vaultData.chain}:${assetAddress}` - ); - promises.push( - axios.get(url).then((response) => { - const priceKey = `${vaultData.chain}:${assetAddress}`; - priceMap.set( - assetAddress, - response.data.coins[priceKey]?.price || 0 - ); - }) - ); - } - await Promise.all(promises); - } + const url = new URL( + `https://app.termmax.ts.finance/earn/${alias}/${address.toLowerCase()}` + ); + url.searchParams.set('chain', alias); - for (let i = 0; i < vaultData.addresses.length; i++) { - const address = vaultData.addresses[i]; - const assetAddress = asset.output[i].output; - - const readableApy = new BigNumber(apy.output[i].output) - .div(new BigNumber(10).pow(6)) - .toNumber(); - const tvlUsd = new BigNumber(totalAssets.output[i].output) - .div(new BigNumber(10).pow(decimals.output[i].output)) - .times(priceMap.get(assetAddress) || 0) - .toNumber(); - - const url = new URL( - `https://app.termmax.ts.finance/earn/${ - vaultData.alias - }/${address.toLowerCase()}` - ); - url.searchParams.set('chain', vaultData.alias); - - const pool = { - pool: `${address}-${chain.toLowerCase()}`, - chain, - project: 'termmax', - symbol: assetNames.output[i].output, - tvlUsd, - apyBase: readableApy, - url: String(url), - underlyingTokens: [assetAddress], - poolMeta: names.output[i].output, - }; - - const opportunity = opportunities.find( - (o) => - o.chainId === vaultData.chainId && - o.identifier.toLowerCase() === address.toLowerCase() - ); - if (opportunity) { - pool.apyReward = opportunity.apr; - - const breakdowns = - (opportunity.rewardsRecord && - opportunity.rewardsRecord.breakdowns) || - []; - pool.rewardTokens = breakdowns - .map((b) => b.token.address) - .filter((a) => a); - } - - pools.push(pool); - } + const vault = { + pool: `${address}-${chain.toLowerCase()}`, + chain, + project: 'termmax', + symbol: assetNames.output[i].output, + tvlUsd, + apyBase: apy, + url: String(url), + underlyingTokens: [assetAddress], + poolMeta: names.output[i].output, + }; + + const opportunity = opportunities.find( + (o) => + o.chainId === chainId && + o.identifier.toLowerCase() === address.toLowerCase() + ); + if (opportunity) { + vault.apyReward = opportunity.apr; + + const breakdowns = + (opportunity.rewardsRecord && opportunity.rewardsRecord.breakdowns) || + []; + vault.rewardTokens = breakdowns + .map((b) => b.token.address) + .filter((a) => a); + } + + vaults.push(vault); + } + + return vaults; +} + +async function getVaultsOnChain(chain, chainId, alias) { + const vaultsOnChain = []; + + const [opportunities, { number }] = await Promise.all([ + getMerklOpportunities(), + sdk.api.util.getLatestBlock(chain), + ]); + + const tasks = []; + { + const taskV1 = async () => { + const vaultsV1 = await getVaultsV1({ + alias, + chain, + chainId, + number, + opportunities, + }); + for (const vault of vaultsV1) vaultsOnChain.push(vault); + }; + tasks.push(taskV1()); + } + { + const taskV2 = async () => { + const vaultsV2 = await getVaultsV2({ + alias, + chain, + chainId, + number, + opportunities, + }); + for (const vault of vaultsV2) vaultsOnChain.push(vault); + }; + tasks.push(taskV2()); + } + await Promise.all(tasks); + + return vaultsOnChain; +} + +async function apy() { + const vaults = []; + const tasks = []; + for (const key of Object.keys(VAULTS)) { + const task = async () => { + const { alias, chain, chainId } = VAULTS[key]; + const vaultsOnChain = await getVaultsOnChain(chain, chainId, alias); + for (const vault of vaultsOnChain) vaults.push(vault); }; - promises.push(task()); + tasks.push(task()); } - await Promise.all(promises); - return pools; + await Promise.all(tasks); + return vaults; } module.exports = {