Skip to content
Open
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
139 changes: 139 additions & 0 deletions src/adaptors/orai-quant-terminal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const utils = require('../utils');

const LCD_ENDPOINT = 'https://lcd.orai.io';
const CHAIN = 'arbitrum';
const PROJECT = 'orai-quant-terminal';
const APP_URL = 'https://quant.orai.io/vault';

// Each entry maps a vault to its stats contract.
// Data is read from statsContract, while pool identity uses vaultAddress.
const POOLS_CONFIG = [
{
statsContract: 'orai1rzfk6fd6d5zhm77cshdtr0vsuyu0qe0dg36evysklx8n6q8h38psxywppw',
vaultAddress: '0xd3A1C2Bd6E1d163A7380D701c946aDCd82DD95b1',
symbol: 'USDC',
poolMeta: 'Golden Rhythm Vault',
underlyingTokens: [],
url: "https://quant.orai.io/vault-v3/0xd3a1c2bd6e1d163a7380d701c946adcd82dd95b1",
},
{
statsContract: 'orai1rzfk6fd6d5zhm77cshdtr0vsuyu0qe0dg36evysklx8n6q8h38psxywppw',
vaultAddress: '0xf90E1b849bFB17D57abDb07438d68ac787B5C587',
symbol: 'USDC',
poolMeta: 'Polymarket Vault',
underlyingTokens: [],
url: "https://quant.orai.io/vault-v2/0xf90e1b849bfb17d57abdb07438d68ac787b5c587",
},
{
statsContract: 'orai1rzfk6fd6d5zhm77cshdtr0vsuyu0qe0dg36evysklx8n6q8h38psxywppw',
vaultAddress: '0x5424293637Cc59ad7580aD1caC46e28D4801a587',
symbol: 'USDC',
poolMeta: 'XAU Alpha Vault',
underlyingTokens: [],
url: "https://quant.orai.io/vault-v2/0x5424293637cc59ad7580ad1cac46e28d4801a587",
}
];

function toQueryPath(queryMsg) {
return encodeURIComponent(
Buffer.from(JSON.stringify(queryMsg)).toString('base64')
);
}

async function queryContract({ contract, queryMsg }) {
const queryPath = toQueryPath(queryMsg);
const url = `${LCD_ENDPOINT}/cosmwasm/wasm/v1/contract/${contract}/smart/${queryPath}`;
const response = await utils.getData(url);
return response?.data ?? response;
}

async function queryVaultStats(contract, vaultAddress) {
const queryMsg =
{ get_vault_stats: { vault_address: vaultAddress.toLowerCase() } };
return queryContract({ contract, queryMsg });
}

function extractFirstNumber(payload, depth = 0) {
if (depth > 8 || payload === null || payload === undefined) return null;
if (typeof payload === 'number' && Number.isFinite(payload)) return payload;
if (typeof payload === 'string') {
const value = Number(payload);
return Number.isFinite(value) ? value : null;
}
if (Array.isArray(payload)) {
for (const item of payload) {
const value = extractFirstNumber(item, depth + 1);
if (value !== null) return value;
}
return null;
}
if (typeof payload !== 'object') return null;

for (const value of Object.values(payload)) {
const number = extractFirstNumber(value, depth + 1);
if (number !== null) return number;
}
return null;
}
Comment on lines +56 to +77
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/adaptors/orai-quant-terminal/index.js

Repository: DefiLlama/yield-server

Length of output: 112


🏁 Script executed:

cat -n src/adaptors/orai-quant-terminal/index.js | sed -n '50,130p'

Repository: DefiLlama/yield-server

Length of output: 3214


🏁 Script executed:

fd -e js -e json -e md -e txt | xargs grep -l "get_vault_stats" 2>/dev/null | head -20

Repository: DefiLlama/yield-server

Length of output: 108


🏁 Script executed:

grep -r "get_vault_stats" src/adaptors/orai-quant-terminal/ -A 5 -B 5

Repository: DefiLlama/yield-server

Length of output: 892


🏁 Script executed:

head -50 src/adaptors/orai-quant-terminal/index.js

Repository: DefiLlama/yield-server

Length of output: 1847


🏁 Script executed:

find src/adaptors/orai-quant-terminal -type f

Repository: DefiLlama/yield-server

Length of output: 108


🏁 Script executed:

tail -60 src/adaptors/orai-quant-terminal/index.js

Repository: DefiLlama/yield-server

Length of output: 1692


Lock extraction to known response schema keys instead of relying on generic fallback.

When vaultStatsData lacks top-level tvl or apy keys, the code falls back to searching the entire response object with extractFirstNumber(). This recursively returns the first finite number found (via Object.values() iteration), which could silently pick any numeric field like decimals, block_height, or fee_bps, producing valid-looking but completely wrong tvlUsd or apy values. Downstream validation at line 120 only checks isFinite() and > 0, insufficient to catch this.

Instead, restrict extractNumericByKeys to an explicit whitelist of valid keys (e.g. ['tvl', 'apy', 'apr', 'total_value_locked']) and return null if none match, rather than falling back to the generic DFS search.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adaptors/orai-quant-terminal/index.js` around lines 64 - 85, The current
extractor (extractFirstNumber) can pick any numeric field by DFS and causes
wrong tvl/apy; modify the extraction flow so that extractNumericByKeys (or the
caller that uses extractFirstNumber) first checks only an explicit whitelist of
allowed keys (e.g., 'tvl','apy','apr','total_value_locked','total_value'),
searching those keys on the response object (and shallow/nested predictable
shapes) and returning null if none present; remove or disable the generic
Object.values DFS fallback in extractFirstNumber (or stop calling it when no
whitelisted keys matched) so arbitrary numeric fields like 'decimals' or
'block_height' are not returned. Ensure the functions to change are
extractNumericByKeys and/or extractFirstNumber and keep the return contract
(null or finite number) so downstream checks still work.


function extractNumericByKeys(payload, keys) {
if (!payload || typeof payload !== 'object') return null;
for (const key of keys) {
if (!(key in payload)) continue;
const value = extractFirstNumber(payload[key]);
if (value !== null) return value;
}
return null;
}

function normalizeApyPercent(value) {
if (!Number.isFinite(value)) return null;
if (value < 0) return null;
// Assume APY is already percent; only tiny values are treated as ratio inputs.
return value < 0.01 ? value * 100 : value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function buildPoolData(config) {
const vaultStatsData = await queryVaultStats(
config.statsContract,
config.vaultAddress
);

const tvlSource = vaultStatsData?.tvl ?? vaultStatsData;
const apySource = vaultStatsData?.apy ?? vaultStatsData;

const tvlRaw = extractNumericByKeys(tvlSource, ['tvl']) ?? extractFirstNumber(tvlSource);
const tvlUsd = tvlRaw / 1e6;

const apyRaw = extractNumericByKeys(apySource, ['apy']) ?? extractFirstNumber(apySource);

const apy = normalizeApyPercent(apyRaw);

if (!Number.isFinite(tvlUsd) || tvlUsd <= 0 || apy === null) return null;

return {
pool: `${config.vaultAddress}-${CHAIN}`,
chain: utils.formatChain(CHAIN),
project: PROJECT,
symbol: utils.formatSymbol(config.symbol),
tvlUsd,
apy,
underlyingTokens: config.underlyingTokens,
poolMeta: config.poolMeta,
url: config.url,
token: config.vaultAddress,
};
}

async function apy() {
const settledPools = await Promise.allSettled(POOLS_CONFIG.map(buildPoolData));
return settledPools
.filter((result) => result.status === 'fulfilled' && result.value)
.map((result) => result.value);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

module.exports = {
timetravel: false,
apy,
url: APP_URL,
};
Loading