diff --git a/src/adaptors/single-finance/abis.js b/src/adaptors/single-finance/abis.js new file mode 100644 index 0000000..9ca7cc6 --- /dev/null +++ b/src/adaptors/single-finance/abis.js @@ -0,0 +1,219 @@ +module.exports = { + VaultABI: [ + { + inputs: [], + name: 'totalToken', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [ + { + internalType: 'uint8', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'vaultDebtVal', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [ + { + internalType: 'uint8', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'config', + outputs: [ + { + internalType: 'contract IVaultConfig', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ], + + Erc20ABI: [ + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + ], + ConfigurableInterestVaultConfigABI: [ + { + inputs: [ + { + internalType: 'uint256', + name: 'debt', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'floating', + type: 'uint256', + }, + ], + name: 'getInterestRate', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getReservePoolBps', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ], + BigBangABI: [ + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'poolInfo', + outputs: [ + { + internalType: 'uint256', + name: 'allocPoint', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'lastRewardBlock', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'accRewardPerShare', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalAllocPoint', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'singlePerBlock', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ], +}; diff --git a/src/adaptors/single-finance/index.js b/src/adaptors/single-finance/index.js new file mode 100644 index 0000000..0c9cdf6 --- /dev/null +++ b/src/adaptors/single-finance/index.js @@ -0,0 +1,300 @@ +const sdk = require('@defillama/sdk'); +const utils = require('../utils'); +const superagent = require('superagent'); + +const { + VaultABI, + ConfigurableInterestVaultConfigABI, + Erc20ABI, + BigBangABI, +} = require('./abis'); + +const poolApiEndpoint = 'https://api.singlefinance.io/api/vaults'; + +const singleToken = { + cronos: '0x0804702a4E749d39A35FDe73d1DF0B1f1D6b8347'.toLowerCase(), + fantom: '0x8cc97b50fe87f31770bcdcd6bc8603bc1558380b'.toLowerCase(), +}; + +const singleVaultAddress = { + cronos: '0x3710000815c45d715af84f35919a6f2a901b7548'.toLowerCase(), + fantom: '0xE87158503f831244E67af248E02bb1cc1CEfA841'.toLowerCase(), +}; + +const bigBang = { + cronos: '0x1Ae8a7C582C3f9F9117d5Bc2863F2eD16cBd29cb'.toLowerCase(), + fantom: '0x7C770a787B430582AbccE88886e9E4E24A457A61'.toLowerCase(), +}; + +const blocksPerYear = (secondsPerBlock) => + (60 / secondsPerBlock) * 60 * 24 * 365; + +const blocksPerYears = { + cronos: blocksPerYear(5.8), + fantom: blocksPerYear(5.8), +}; + +const availablePools = { + cronos: ['CRO', 'USDC', 'VVS', 'MMF', 'USDT', 'VERSA', 'ARGO', 'bCRO'], + fantom: ['FTM', 'USDC', 'fUSDT'], + // cronos: ['CRO',], + // fantom:[], +}; + +const getApyBase = ( + borrowingInterest, + totalBorrow, + totalSupply, + lendingPerformanceFee +) => { + return ( + borrowingInterest * + (totalBorrow / totalSupply) * + (1 - lendingPerformanceFee) * + 100 + ); +}; + +const getApyReward = ( + stakedTvl, + allocPoint, + totalAllocPoint, + singlePerBlock, + singlePrice, + blocksPerYear +) => { + return ( + (((allocPoint / totalAllocPoint) * singlePerBlock * singlePrice) / + stakedTvl) * + blocksPerYear * + 100 + ); +}; + +const getPrices = async (addresses) => { + const prices = ( + await superagent.post('https://coins.llama.fi/prices').send({ + coins: addresses, + }) + ).body.coins; + + const priceItems = Object.entries(prices).reduce( + (acc, [name, price]) => ({ + ...acc, + [name.split(':')[1]]: price.price, + }), + {} + ); + + return priceItems; +}; + +const secondToYearlyInterestRate = (rate) => + (Number(rate) * (60 * 60 * 24 * 365)) / 1000000000000000000; + +const getPriceInUsd = (item, decimal, price) => { + return (Number(item) / 10 ** Number(decimal)) * price; +}; + +const multiCallOutput = async (abi, calls, chain) => { + const res = await sdk.api.abi.multiCall({ + abi: abi, + calls: calls, + chain: chain, + }); + + return res.output.map(({ output }) => output); +}; + +const getApy = async (chain) => { + const allPools = await utils.getData( + `${poolApiEndpoint}?chainid=${ + chain === 'fantom' ? '250' : chain === 'cronos' ? '25' : undefined + }` + ); + + const pools = allPools.data.filter((pool) => { + const symbol = pool.token.symbol; + return availablePools[chain].includes(symbol); + }); + + const singlePrice = await getPrices([chain + ':' + singleToken[chain]]); + + const totalSupply = await multiCallOutput( + VaultABI.find(({ name }) => name === 'totalToken'), + pools.map((pool) => ({ target: pool.address })), + chain + ); + + const totalSupplyToken = await multiCallOutput( + VaultABI.find(({ name }) => name === 'totalSupply'), + pools.map((pool) => ({ target: pool.address })), + chain + ); + + const totalBorrowed = await multiCallOutput( + VaultABI.find(({ name }) => name === 'vaultDebtVal'), + pools.map((pool) => ({ target: pool.address })), + chain + ); + + const decimals = await multiCallOutput( + VaultABI.find(({ name }) => name === 'decimals'), + pools.map((pool) => ({ target: pool.address })), + chain + ); + + const configs = await multiCallOutput( + VaultABI.find(({ name }) => name === 'config'), + pools.map((pool) => ({ target: pool.address })), + chain + ); + + const totalToken = await multiCallOutput( + Erc20ABI.find(({ name }) => name === 'balanceOf'), + pools.map((pool) => ({ + target: pool.token.id, + params: [pool.address], + })), + chain + ); + + const interestRate = await multiCallOutput( + ConfigurableInterestVaultConfigABI.find( + ({ name }) => name === 'getInterestRate' + ), + configs.map((config, i) => { + return { + target: config, + params: [totalBorrowed[i].toString(), totalToken[i].toString()], + }; + }), + chain + ); + const totalIbStaked = await multiCallOutput( + VaultABI.find(({ name }) => name === 'balanceOf'), + configs.map((config, i) => { + return { + target: pools[i].address, + params: bigBang[chain], + }; + }), + chain + ); + + const lendingPerformanceFeeBps = await multiCallOutput( + ConfigurableInterestVaultConfigABI.find( + ({ name }) => name === 'getReservePoolBps' + ), + configs.map((config, i) => { + return { + target: config, + }; + }), + chain + ); + + const allocPoint = await multiCallOutput( + BigBangABI.find(({ name }) => name === 'poolInfo'), + pools.map((pool) => ({ + params: [pool.fairLaunchPoolId], + target: bigBang[chain], + })), + chain + ); + + const totalAllocPoint = await multiCallOutput( + BigBangABI.find(({ name }) => name === 'totalAllocPoint'), + [{ target: bigBang[chain] }], + chain + ); + + const singlePerBlock = await multiCallOutput( + BigBangABI.find(({ name }) => name === 'singlePerBlock'), + [{ target: bigBang[chain] }], + chain + ); + + const singleVaultDeimals = await multiCallOutput( + VaultABI.find(({ name }) => name === 'decimals'), + [{ target: singleVaultAddress[chain] }], + chain + ); + + const prices = await getPrices( + pools.map((pool) => { + return chain + ':' + pool.token.id; + }) + ); + + const res = pools.map((pool, idx) => { + const tokenAddress = pool.token.id.toLowerCase(); + const totalSupplyUsd = getPriceInUsd( + totalSupply[idx], + decimals[idx], + prices[tokenAddress] + ); + const totalBorrowUsd = getPriceInUsd( + totalBorrowed[idx], + decimals[idx], + prices[tokenAddress] + ); + const lendingPerformanceFee = Number(lendingPerformanceFeeBps[idx]) / 10000; + + const apyBase = getApyBase( + secondToYearlyInterestRate(interestRate[idx]), + totalBorrowed[idx], + totalSupply[idx], + lendingPerformanceFee + ); + + let conversionRate = 1; + + if (totalSupplyToken[idx] && Number(totalSupplyToken[idx]) > 0) { + conversionRate = totalSupply[idx] / totalSupplyToken[idx]; + } + + const stakedTvl = + (Number(totalIbStaked[idx]) / Math.pow(10, decimals[idx])) * + prices[tokenAddress] * + conversionRate; + + const apyReward = getApyReward( + stakedTvl, + allocPoint[idx].allocPoint, + totalAllocPoint[0], + singlePerBlock[0] / Math.pow(10, singleVaultDeimals[0]), + singlePrice[singleToken[chain]], + blocksPerYears[chain] + ); + + return { + pool: pool.address, + chain: utils.formatChain(chain), + project: 'single-finance', + symbol: pool.token.symbol, + tvlUsd: totalSupplyUsd - totalBorrowUsd, + apyBaseBorrow: secondToYearlyInterestRate(interestRate[idx]) * 100, + totalSupplyUsd, + totalBorrowUsd, + apyBase, + apyReward: chain === 'cronos' ? apyReward : 0, + underlyingTokens: [pool.token.id], + rewardTokens: chain === 'cronos' ? [singleToken[chain]] : [], + ltv: 0, + }; + }); + return res; +}; + +const apy = async () => { + const cronosPools = await getApy('cronos'); + const fantomPools = await getApy('fantom'); + return [...cronosPools, ...fantomPools]; +}; + +module.exports = { + timetravel: false, + apy: apy, + url: 'https://app.singlefinance.io/lend', +};