From 5585c5ab03b3e9a043469e11d654530812c61635 Mon Sep 17 00:00:00 2001 From: permapod-rsk <223239333+permapod-rsk@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:33:23 +0200 Subject: [PATCH 1/5] Add Valdora Finance yield adapter --- src/adaptors/valdora-finance/index.js | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/adaptors/valdora-finance/index.js diff --git a/src/adaptors/valdora-finance/index.js b/src/adaptors/valdora-finance/index.js new file mode 100644 index 0000000000..670d290aee --- /dev/null +++ b/src/adaptors/valdora-finance/index.js @@ -0,0 +1,87 @@ +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 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 commissions = await Promise.all( + VALIDATORS.map(async (validator) => { + const data = await get(`/cosmos/staking/v1beta1/validators/${validator}`); + return Number(data.validator?.commission?.commission_rates?.rate || 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 - averageCommission) * 100; +}; + +const apy = async () => { + const [fundsRaised, apyBase, priceData] = await Promise.all([ + queryContract(STAKER_CONTRACT, { funds_raised: {} }), + getStzigApr(), + 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 tvlUsd = (Number(fundsRaised.funds_raised || 0) / DECIMALS) * zigPrice; + + return [ + { + pool: `${STZIG_DENOM}-${CHAIN}`.toLowerCase(), + chain: CHAIN, + project: PROJECT, + symbol: 'stZIG', + tvlUsd, + apyBase, + 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', +}; From 13e7291e775f9f0f8c8df6ffb13e4693fe06feaa Mon Sep 17 00:00:00 2001 From: permapod-rsk <223239333+permapod-rsk@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:48:44 +0200 Subject: [PATCH 2/5] valdora-finance: address review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch to Promise.allSettled for validator commission fetching so that a single unresponsive validator does not abort the entire APY pipeline; failed results are filtered out and the average is computed over the remaining ones (returns 0 if all fail) - Deduct the 10% performance fee and the 2% community tax from the APR formula; the displayed APY now reflects net yield for the staker - Add pricePerShare field by querying the exchange_rate from the staker contract, covering the stZIG→ZIG conversion ratio for liquid assets - Validator addresses remain hardcoded for now: the chain does not expose a public endpoint to enumerate Valdora's curated validator set --- src/adaptors/valdora-finance/index.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/adaptors/valdora-finance/index.js b/src/adaptors/valdora-finance/index.js index 670d290aee..ac9fe3c6b6 100644 --- a/src/adaptors/valdora-finance/index.js +++ b/src/adaptors/valdora-finance/index.js @@ -10,6 +10,8 @@ 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', @@ -27,13 +29,18 @@ const queryContract = async (contract, data) => { }; const getAverageValidatorCommission = async () => { - const commissions = await Promise.all( + 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; }; @@ -48,13 +55,20 @@ const getStzigApr = async () => { const bondedZig = Number(stakingPool.pool?.bonded_tokens || 0) / DECIMALS; if (!bondedZig) return 0; - return (annualProvisionsZig / bondedZig) * (1 - averageCommission) * 100; + return ( + (annualProvisionsZig / bondedZig) * + (1 - COMMUNITY_TAX) * + (1 - averageCommission) * + (1 - PERFORMANCE_FEE) * + 100 + ); }; const apy = async () => { - const [fundsRaised, apyBase, priceData] = await Promise.all([ + const [fundsRaised, apyBase, exchangeRate, priceData] = await Promise.all([ queryContract(STAKER_CONTRACT, { funds_raised: {} }), getStzigApr(), + queryContract(STAKER_CONTRACT, { exchange_rate: {} }), utils.getPriceApiData(`/prices/current/${ZIG_PRICE_KEY}`), ]); @@ -62,6 +76,7 @@ const apy = async () => { if (!zigPrice) throw new Error('Unable to fetch ZIG price'); const tvlUsd = (Number(fundsRaised.funds_raised || 0) / DECIMALS) * zigPrice; + const pricePerShare = Number(exchangeRate?.exchange_rate || 1); return [ { @@ -71,6 +86,7 @@ const apy = async () => { symbol: 'stZIG', tvlUsd, apyBase, + pricePerShare, underlyingTokens: ['uzig'], searchTokenOverride: STZIG_DENOM, isIntrinsicSource: true, From d0409e437eb726e6dbc7762c3fe7afb17e0eb038 Mon Sep 17 00:00:00 2001 From: permapod-rsk <223239333+permapod-rsk@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:51:25 +0200 Subject: [PATCH 3/5] fix: replace non-existent exchange_rate query with total_supply The contract does not expose an exchange_rate query endpoint, causing HTTP 500 errors. Compute pricePerShare from funds_raised / total_supply instead. --- src/adaptors/valdora-finance/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/adaptors/valdora-finance/index.js b/src/adaptors/valdora-finance/index.js index ac9fe3c6b6..29a2b3a68e 100644 --- a/src/adaptors/valdora-finance/index.js +++ b/src/adaptors/valdora-finance/index.js @@ -65,18 +65,20 @@ const getStzigApr = async () => { }; const apy = async () => { - const [fundsRaised, apyBase, exchangeRate, priceData] = await Promise.all([ + const [fundsRaised, apyBase, totalSupply, priceData] = await Promise.all([ queryContract(STAKER_CONTRACT, { funds_raised: {} }), getStzigApr(), - queryContract(STAKER_CONTRACT, { exchange_rate: {} }), + 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 tvlUsd = (Number(fundsRaised.funds_raised || 0) / DECIMALS) * zigPrice; - const pricePerShare = Number(exchangeRate?.exchange_rate || 1); + const fundsRaisedValue = Number(fundsRaised.funds_raised || 0); + const totalSupplyValue = Number(totalSupply.total_supply || 1); + const tvlUsd = (fundsRaisedValue / DECIMALS) * zigPrice; + const pricePerShare = fundsRaisedValue / totalSupplyValue; return [ { From 4f796c1afa056e6203fb8153ebb0489a06ac90e6 Mon Sep 17 00:00:00 2001 From: permapod-rsk <223239333+permapod-rsk@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:31:36 +0200 Subject: [PATCH 4/5] valdora-finance: validate contract balances --- src/adaptors/valdora-finance/index.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/adaptors/valdora-finance/index.js b/src/adaptors/valdora-finance/index.js index 29a2b3a68e..91029e930f 100644 --- a/src/adaptors/valdora-finance/index.js +++ b/src/adaptors/valdora-finance/index.js @@ -75,8 +75,17 @@ const apy = async () => { const zigPrice = priceData.coins[ZIG_PRICE_KEY]?.price; if (!zigPrice) throw new Error('Unable to fetch ZIG price'); - const fundsRaisedValue = Number(fundsRaised.funds_raised || 0); - const totalSupplyValue = Number(totalSupply.total_supply || 1); + 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; From e46bc830c9fb1f1f245e6f76f36de52e7de6dc5d Mon Sep 17 00:00:00 2001 From: permapod-rsk <223239333+permapod-rsk@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:58:57 +0200 Subject: [PATCH 5/5] canto-lending: use available liquidity tvl --- src/adaptors/canto-lending/index.js | 34 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) 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;