diff --git a/src/adaptors/canto-lending/index.js b/src/adaptors/canto-lending/index.js index 897dc4bd1a..5883a904df 100644 --- a/src/adaptors/canto-lending/index.js +++ b/src/adaptors/canto-lending/index.js @@ -215,13 +215,20 @@ const getPrices = async (chain, addresses) => { }, {}); }; -function calculateApy(rate, price = 1, tvl = 1) { - // supply rate per block * number of blocks per year +function calculateApy(rate) { const BLOCK_TIME = 6; const YEARLY_BLOCKS = (365 * 24 * 60 * 60) / BLOCK_TIME; - const safeTvl = tvl === 0 ? 1 : tvl; - const apy = (((rate / 1e18) * YEARLY_BLOCKS * price) / safeTvl) * 100; - return apy; + + return (Math.pow(rate / 1e18 + 1, YEARLY_BLOCKS) - 1) * 100; +} + +function calculateRewardApy(speed, rewardPrice, totalUsd) { + const BLOCK_TIME = 6; + const YEARLY_BLOCKS = (365 * 24 * 60 * 60) / BLOCK_TIME; + + if (!Number.isFinite(totalUsd) || totalUsd <= 0) return null; + + return (((speed / 1e18) * YEARLY_BLOCKS * rewardPrice) / totalUsd) * 100; } function calculateTvl(cash, borrows, reserves, price, decimals) { @@ -233,6 +240,10 @@ function calculateTvl(cash, borrows, reserves, price, decimals) { return tvl; } +function calculateAvailableTvl(cash, price, decimals) { + return (parseFloat(cash) / decimals) * price; +} + const getApy = async () => { const wCantoUsd = getUsdPrice( (await getPrices('canto', [WCANTO]))[WCANTO.toLowerCase()] @@ -253,16 +264,19 @@ const getApy = async () => { pool.price, pool.underlyingTokenDecimals ); - const tvlUsd = totalSupplyUsd - totalBorrowUsd; - const availableBorrowUsd = - (parseFloat(pool.getCash) / pool.underlyingTokenDecimals) * pool.price; + const tvlUsd = calculateAvailableTvl( + pool.getCash, + pool.price, + pool.underlyingTokenDecimals + ); + const availableBorrowUsd = tvlUsd; const apyBase = calculateApy(pool.supplyRate); const apyReward = Number.isFinite(wCantoUsd) - ? calculateApy(pool.compSupplySpeeds, wCantoUsd, totalSupplyUsd) + ? calculateRewardApy(pool.compSupplySpeeds, wCantoUsd, totalSupplyUsd) : null; const apyBaseBorrow = calculateApy(pool.borrowRate); const apyRewardBorrow = Number.isFinite(wCantoUsd) - ? calculateApy(pool.compBorrowSpeeds, wCantoUsd, totalBorrowUsd) + ? calculateRewardApy(pool.compBorrowSpeeds, wCantoUsd, totalBorrowUsd) : null; const ltv = parseInt(pool.collateralFactor) / 1e18; diff --git a/src/adaptors/valdora-finance/index.js b/src/adaptors/valdora-finance/index.js new file mode 100644 index 0000000000..91029e930f --- /dev/null +++ b/src/adaptors/valdora-finance/index.js @@ -0,0 +1,114 @@ +const utils = require('../utils'); + +const PROJECT = 'valdora-finance'; +const CHAIN = 'ZIGChain'; +const LCD = 'https://public-zigchain-lcd.numia.xyz'; + +const STAKER_CONTRACT = + 'zig18nnde5tpn76xj3wm53n0tmuf3q06nruj3p6kdemcllzxqwzkpqzqk7ue55'; +const STZIG_DENOM = + 'coin.zig109f7g2rzl2aqee7z6gffn8kfe9cpqx0mjkk7ethmx8m2hq4xpe9snmaam2.stzig'; +const ZIG_PRICE_KEY = 'zigchain:uzig'; +const DECIMALS = 1e6; +const PERFORMANCE_FEE = 0.10; +const COMMUNITY_TAX = 0.02; + +const VALIDATORS = [ + 'zigvaloper18vykgjgcmp2z4xzkt6mh74glrpd7qda8fqldrl', + 'zigvaloper1vd9ljpsgq5ev7yf7r6tu7t237qqpf2vehp4kvp', + 'zigvaloper1jh6jve7n4pu9vxnmr67eg4m6qk7d7s4lf4uu0j', + 'zigvaloper15pwqnx4hgkwq839xv3jgjxh9aj2wdg8p8dgy76', +]; + +const get = (path) => utils.getData(`${LCD}${path}`); + +const queryContract = async (contract, data) => { + const query = Buffer.from(JSON.stringify(data)).toString('base64'); + const response = await get(`/cosmwasm/wasm/v1/contract/${contract}/smart/${query}`); + return response.data; +}; + +const getAverageValidatorCommission = async () => { + const results = await Promise.allSettled( + VALIDATORS.map(async (validator) => { + const data = await get(`/cosmos/staking/v1beta1/validators/${validator}`); + return Number(data.validator?.commission?.commission_rates?.rate || 0); + }) + ); + + const commissions = results + .filter((r) => r.status === 'fulfilled') + .map((r) => r.value); + + if (!commissions.length) return 0; + return commissions.reduce((sum, value) => sum + value, 0) / commissions.length; +}; + +const getStzigApr = async () => { + const [annualProvisions, stakingPool, averageCommission] = await Promise.all([ + get('/cosmos/mint/v1beta1/annual_provisions'), + get('/cosmos/staking/v1beta1/pool'), + getAverageValidatorCommission(), + ]); + + const annualProvisionsZig = Number(annualProvisions.annual_provisions) / DECIMALS; + const bondedZig = Number(stakingPool.pool?.bonded_tokens || 0) / DECIMALS; + + if (!bondedZig) return 0; + return ( + (annualProvisionsZig / bondedZig) * + (1 - COMMUNITY_TAX) * + (1 - averageCommission) * + (1 - PERFORMANCE_FEE) * + 100 + ); +}; + +const apy = async () => { + const [fundsRaised, apyBase, totalSupply, priceData] = await Promise.all([ + queryContract(STAKER_CONTRACT, { funds_raised: {} }), + getStzigApr(), + queryContract(STAKER_CONTRACT, { total_supply: {} }), + utils.getPriceApiData(`/prices/current/${ZIG_PRICE_KEY}`), + ]); + + const zigPrice = priceData.coins[ZIG_PRICE_KEY]?.price; + if (!zigPrice) throw new Error('Unable to fetch ZIG price'); + + const fundsRaisedValue = Number(fundsRaised.funds_raised); + const totalSupplyValue = Number(totalSupply.total_supply); + + if (!Number.isFinite(fundsRaisedValue) || fundsRaisedValue < 0) { + throw new Error('Invalid funds_raised contract response'); + } + + if (!Number.isFinite(totalSupplyValue) || totalSupplyValue <= 0) { + throw new Error('Invalid total_supply contract response'); + } + + const tvlUsd = (fundsRaisedValue / DECIMALS) * zigPrice; + const pricePerShare = fundsRaisedValue / totalSupplyValue; + + return [ + { + pool: `${STZIG_DENOM}-${CHAIN}`.toLowerCase(), + chain: CHAIN, + project: PROJECT, + symbol: 'stZIG', + tvlUsd, + apyBase, + pricePerShare, + underlyingTokens: ['uzig'], + searchTokenOverride: STZIG_DENOM, + isIntrinsicSource: true, + url: 'https://valdora.finance/stake', + }, + ].filter((pool) => utils.keepFinite(pool)); +}; + +module.exports = { + protocolId: '6991', + timetravel: false, + apy, + url: 'https://valdora.finance/stake', +};