Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/adaptors/canto-lending/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +218 to +223

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect poolInfo output shape: confirm supplyRate/borrowRate/comp*Speeds are per-block, 1e18-scaled
fd -a 'canto' src/adaptors/canto-lending
rg -nP -C4 '\b(supplyRate|borrowRate|compSupplySpeeds|compBorrowSpeeds|supplyRatePerBlock|borrowRatePerBlock|getCash)\b' src/adaptors/canto-lending

Repository: DefiLlama/yield-server

Length of output: 9426


🌐 Web query:

Canto blockchain average block time seconds

💡 Result:

The Canto blockchain's average block time has evolved due to network upgrades. Historically, the Canto EVM maintained a block time of approximately 6 seconds [1][2]. Recent infrastructure updates, specifically through the "Callisto" upgrade, transitioned the network to the Cosmos SDK v0.50 with the ABCI 2.0 interface and Optimistic Block Execution. This upgrade significantly reduced block times to less than 2 seconds [3]. Furthermore, the Canto roadmap for its "Cyclone Stack" includes additional performance improvements—such as Optimistic Parallel Execution and MemIAVL integration—intended to achieve even faster, sub-second block times [3]. Because block times can fluctuate based on network conditions and specific validator performance, real-time block explorers may display slightly varying averages, often ranging between 5 and 6 seconds in older snapshots or depending on the indexing status of the explorer [4][5][6]. However, the technical target and optimized performance for the current architecture are significantly lower, moving toward sub-second finality [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant section of the adaptor
sed -n '200,310p' src/adaptors/canto-lending/index.js

# Look for any repo-local note about Canto block time or APY annualization
rg -n "block time|YEARLY_BLOCKS|calculateApy|supplyRatePerBlock|borrowRatePerBlock|compSupplySpeeds|compBorrowSpeeds" src README.md docs . 2>/dev/null || true

Repository: DefiLlama/yield-server

Length of output: 50378


Replace the hardcoded 6s block interval
calculateApy and calculateRewardApy annualize per-block rates with a fixed BLOCK_TIME = 6; Canto’s block time no longer stays pinned to that assumption, so the APY drifts unless this comes from chain config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/canto-lending/index.js` around lines 218 - 223, The APY math in
calculateApy (and the related calculateRewardApy path) is using a hardcoded
6-second BLOCK_TIME, which should instead come from chain configuration. Update
the annualization logic to read the block interval from the existing Canto chain
config or a shared config source, and make sure both APY helpers use that
dynamic value consistently.


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) {
Expand All @@ -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()]
Expand All @@ -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;

Expand Down
114 changes: 114 additions & 0 deletions src/adaptors/valdora-finance/index.js
Original file line number Diff line number Diff line change
@@ -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;
};
Comment on lines +39 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silent fallback masks upstream failures.

getAverageValidatorCommission returns 0 when every validator query fails (Line 43-44), and getStzigApr returns 0 when bondedZig is falsy (Line 57). Elsewhere in this same file, apy() explicitly throws on invalid/missing data (Lines 76, 82, 86). If the LCD is degraded or all 4 validator queries fail, this silently computes a 0 commission (inflating APR) or 0 APR (understating it) instead of surfacing the failure like the rest of the module does.

Suggested consistency fix
   if (!commissions.length) return 0;
+  // consider throwing instead, to match the fail-fast pattern used in apy()
+  // throw new Error('Unable to fetch validator commissions');
   return commissions.reduce((sum, value) => sum + value, 0) / commissions.length;

Also applies to: 56-57

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/valdora-finance/index.js` around lines 39 - 45,
`getAverageValidatorCommission` and `getStzigApr` are silently defaulting to 0
when upstream data is missing or all validator queries fail, unlike `apy()`
which throws on invalid input. Update these helpers to detect empty/failed
results as an error condition and throw a descriptive failure instead of
returning 0, using the existing functions `getAverageValidatorCommission`,
`getStzigApr`, and `apy` as the consistency points for the behavior change.


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',
};
Loading