From 77d3e8eb94ad91de670c019e0e28b1c57fbb8bcb Mon Sep 17 00:00:00 2001 From: Behrang Norouzinia Date: Thu, 23 Jul 2026 19:46:10 +0330 Subject: [PATCH 1/2] Hipo: fix APY halved by outdated treasury state semantics Since Hipo's treasury upgrade, get_treasury_state returns the pool exchange rate before/after the latest round (previous_rate/current_rate) at stack positions 11/12, replacing last_staked/last_recovered. The rate advances once per validation round, so the growth must be compounded per single round duration; the old 2-round duration halved the APY (showed ~8.3% instead of the actual ~17.3%). Also adds apyBaseInception derived from the launch exchange rate of 1.0. Co-Authored-By: Claude Fable 5 --- src/adaptors/hipo/index.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/adaptors/hipo/index.js b/src/adaptors/hipo/index.js index 03043e6c08..33f6978646 100644 --- a/src/adaptors/hipo/index.js +++ b/src/adaptors/hipo/index.js @@ -2,6 +2,9 @@ const utils = require('../utils'); const address = 'EQCLyZHP4Xe8fpchQz76O-_RmUhaVc_9BAoGyJrwJrcbz2eZ'; +// hGRAM launched at an exchange rate of 1.0 GRAM +const launchTimestamp = 1698685200; + module.exports = { protocolId: '3722', timetravel: false, @@ -42,23 +45,27 @@ module.exports = { ); } - const lastStaked = Number(getTreasuryState.stack[11].value); - const lastRecovered = Number(getTreasuryState.stack[12].value); + // The treasury stores the hGRAM/GRAM exchange rate before and after the + // latest round's loan repayments (fixed-point, 1e9 = 1.0). The rate is + // updated once per validation round, so the growth between the two rates + // accrued over a single round duration. + const previousRate = Number(getTreasuryState.stack[11].value); + const currentRate = Number(getTreasuryState.stack[12].value); const currentRoundSince = Number(getTimes.stack[0].value); const nextRoundSince = Number(getTimes.stack[3].value); - const duration = 2 * (nextRoundSince - currentRoundSince); + const roundDuration = nextRoundSince - currentRoundSince; const year = 365 * 24 * 60 * 60; - const compoundingFrequency = year / duration; + const compoundingFrequency = year / roundDuration; const apyBase = - (Math.pow( - lastRecovered / lastStaked || 1, - compoundingFrequency - ) - - 1) * + (Math.pow(currentRate / previousRate || 1, compoundingFrequency) - 1) * 100; + const yearsSinceLaunch = (Date.now() / 1000 - launchTimestamp) / year; + const apyBaseInception = + (Math.pow(currentRate / 1e9, 1 / yearsSinceLaunch) - 1) * 100; + return [ { pool: (address + '-ton').toLowerCase(), @@ -67,6 +74,7 @@ module.exports = { symbol: 'hGRAM', tvlUsd, apyBase, + apyBaseInception, underlyingTokens: ['EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'], // native TON }, ]; From 0344873eb838b128d1b62981120aa07b70ef8e1b Mon Sep 17 00:00:00 2001 From: Behrang Norouzinia Date: Fri, 24 Jul 2026 10:44:04 +0330 Subject: [PATCH 2/2] Hipo: validate treasury rates and round duration Throw on non-finite or non-positive rates and round duration instead of falling back to a 0% APY when treasury data is malformed. Co-Authored-By: Claude Fable 5 --- src/adaptors/hipo/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/adaptors/hipo/index.js b/src/adaptors/hipo/index.js index 33f6978646..612b98f236 100644 --- a/src/adaptors/hipo/index.js +++ b/src/adaptors/hipo/index.js @@ -55,12 +55,22 @@ module.exports = { const currentRoundSince = Number(getTimes.stack[0].value); const nextRoundSince = Number(getTimes.stack[3].value); + if (!Number.isFinite(previousRate) || previousRate <= 0) { + throw new Error('Invalid previous rate: ' + previousRate); + } + if (!Number.isFinite(currentRate) || currentRate <= 0) { + throw new Error('Invalid current rate: ' + currentRate); + } + const roundDuration = nextRoundSince - currentRoundSince; + if (!Number.isFinite(roundDuration) || roundDuration <= 0) { + throw new Error('Invalid round duration: ' + roundDuration); + } + const year = 365 * 24 * 60 * 60; const compoundingFrequency = year / roundDuration; const apyBase = - (Math.pow(currentRate / previousRate || 1, compoundingFrequency) - 1) * - 100; + (Math.pow(currentRate / previousRate, compoundingFrequency) - 1) * 100; const yearsSinceLaunch = (Date.now() / 1000 - launchTimestamp) / year; const apyBaseInception =