-
Notifications
You must be signed in to change notification settings - Fork 6.3k
feat(solid): Terra2 TVL adapter #16057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
/* SOLID Protocol (Terra, phoenix-1) — Collateral-only TVL adapter | ||
* - Counts only collateral balances in SOLID vaults | ||
* - ampLUNA priced via Eris exchange_rate with $3k min-effective filter | ||
* - bLUNA priced via weighted-average of Astroport & White Whale pools | ||
* (each pool must pass a min-liquidity filter in LUNA USD terms) | ||
*/ | ||
|
||
const axios = require("axios"); | ||
const { sumSingleBalance } = require("@defillama/sdk/build/generalUtil"); | ||
|
||
// ------- Config ------- | ||
const LCD = "https://terra-api.cosmosrescue.dev:8443"; // Terra LCD (community endpoint) | ||
|
||
/** Thresholds */ | ||
const AMP_MIN_USD = 3000; // ampLUNA: min effective USD to include | ||
const BLUNA_MIN_USD = 3000; // per-pool min (based on LUNA-side reserve * LUNA USD) | ||
|
||
// ------- Addresses ------- | ||
const ADDR = { | ||
ampLUNA: { | ||
token: "terra1ecgazyd0waaj3g7l9cmy5gulhxkps2gmxu9ghducvuypjq68mq2s5lvsct", | ||
custody: "terra18uxq2k6wpsqythpakz5n6ljnuzyehrt775zkdclrtdtv6da63gmskqn7dq", | ||
}, | ||
bLUNA: { | ||
token: "terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml", | ||
custody: "terra1fyfrqdf58nf4fev2amrdrytq5d63njulfa7sm75c0zu4pnr693dsqlr7p9", | ||
// bLUNA–LUNA pairs used for weighted price (by LUNA reserve) | ||
pairs: [ | ||
"terra1h32epkd72x7st0wk49z35qlpsxf26pw4ydacs8acq6uka7hgshmq7z7vl9", // Astroport | ||
"terra1j5znhs9jeyty9u9jcagl3vefkvzwqp6u9tq9a3e5qrz4gmj2udyqp0z0xc", // White Whale | ||
], | ||
}, | ||
USDC: { | ||
token: "ibc/2C962DAB9F57FE0921435426AE75196009FAA1981BF86991203C8411F8980FDB", | ||
custody: "terra1hdu4t2mrrv98rwdzps40va7me3xjme32upcw36x4cda8tx9cee9qrwdhsl", | ||
}, | ||
wETH: { | ||
token: "ibc/BC8A77AFBD872FDC32A348D3FB10CC09277C266CFE52081DE341C7EC6752E674", | ||
custody: "terra1xyxxg9z8eep6xkfts4sp7gper677glz0md4wd9krj4d8dllmut8q8tjjrl", | ||
}, | ||
wBTC: { | ||
token: "ibc/05D299885B07905B6886F554B39346EA6761246076A1120B1950049B92B922DD", | ||
custody: "terra1jksfmpavp09wwla8xffera3q7z49ef6r2jx9lu29mwvl64g34ljs7u2hln", | ||
}, | ||
wSOL: { | ||
token: "terra1ctelwayk6t2zu30a8v9kdg3u2gr0slpjdfny5pjp7m3tuquk32ysugyjdg", | ||
custody: "terra1e32q545j90agakl32mtkacq05990cnr54czj8wp0wv3nttkrhwlqr9spf5", | ||
}, | ||
wBNB: { | ||
token: "terra1xc7ynquupyfcn43sye5pfmnlzjcw2ck9keh0l2w2a4rhjnkp64uq4pr388", | ||
custody: "terra1fluajm00hwu9wyy8yuyf4zag7x5pw95vdlgkhh8w03pfzqj6hapsx4673t", | ||
}, | ||
}; | ||
|
||
// ------- Helpers ------- | ||
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64"); | ||
|
||
async function smartQuery(contract, msg) { | ||
const url = `${LCD}/cosmwasm/wasm/v1/contract/${contract}/smart?query_msg=${b64(msg)}`; | ||
const { data } = await axios.get(url); | ||
return data.data || data; | ||
} | ||
|
||
async function bankBalances(address) { | ||
const url = `${LCD}/cosmos/bank/v1beta1/balances/${address}?pagination.limit=1000`; | ||
const { data } = await axios.get(url); | ||
return data.balances || []; | ||
} | ||
|
||
async function cw20Balance(token, address) { | ||
const r = await smartQuery(token, { balance: { address } }); | ||
return BigInt(r.balance || "0"); | ||
} | ||
|
||
async function cw20Decimals(token) { | ||
const r = await smartQuery(token, { token_info: {} }); | ||
return r.decimals ?? r.token_info?.decimals ?? 6; | ||
} | ||
|
||
// LUNA USD price | ||
async function getLunaUsd() { | ||
try { | ||
const { data } = await axios.get("https://coins.llama.fi/prices/current/coingecko:terra-luna-2"); | ||
return data?.coins?.["coingecko:terra-luna-2"]?.price ?? 0; | ||
} catch { | ||
return 0; | ||
} | ||
} | ||
|
||
// ampLUNA exchange rate from Eris API | ||
async function getAmpLunaRate() { | ||
try { | ||
const { data } = await axios.get("https://api.erisprotocol.com/terra/amplifier/LUNA"); | ||
const rate = Number(data?.exchange_rate); | ||
return rate > 0 ? rate : 1; | ||
} catch { | ||
return 1; | ||
} | ||
} | ||
|
||
// bLUNA rate (weighted avg) | ||
async function getBLunaRateWeighted(lunaUsd) { | ||
const bluna = ADDR.bLUNA.token; | ||
let totalWeighted = 0; | ||
let totalWeight = 0; | ||
|
||
for (const pair of ADDR.bLUNA.pairs) { | ||
try { | ||
const pool = await smartQuery(pair, { pool: {} }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is giving me for both pools listed
Is this expected? Using https://terra-api.cosmosrescue.dev:8443 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im aware this is an old commit, trying to derive bLuna price |
||
const assets = pool.assets || []; | ||
if (assets.length !== 2) continue; | ||
|
||
let lunaRes = 0, blunaRes = 0; | ||
for (const a of assets) { | ||
if (a.info?.native_token?.denom === "uluna") lunaRes = Number(a.amount) / 1e6; | ||
if (a.info?.token?.contract_addr === bluna) blunaRes = Number(a.amount) / 1e6; | ||
} | ||
if (lunaRes <= 0 || blunaRes <= 0) continue; | ||
|
||
if (lunaUsd * lunaRes < BLUNA_MIN_USD) continue; | ||
|
||
const rate = lunaRes / blunaRes; | ||
totalWeighted += rate * lunaRes; | ||
totalWeight += lunaRes; | ||
} catch (e) { | ||
// skip this pair if query fails | ||
continue; | ||
} | ||
} | ||
|
||
if (totalWeight === 0) return 1; | ||
return totalWeighted / totalWeight; | ||
} | ||
|
||
// ------- TVL ------- | ||
async function tvl() { | ||
const balances = {}; | ||
const lunaUsd = await getLunaUsd(); | ||
|
||
const [ampRate, bRate] = await Promise.all([ | ||
getAmpLunaRate(), | ||
getBLunaRateWeighted(lunaUsd), | ||
]); | ||
|
||
// ampLUNA | ||
{ | ||
const [bal, dec] = await Promise.all([ | ||
cw20Balance(ADDR.ampLUNA.token, ADDR.ampLUNA.custody), | ||
cw20Decimals(ADDR.ampLUNA.token), | ||
]); | ||
const amt = Number(bal) / 10 ** dec; | ||
const lunaEq = amt * ampRate; | ||
const usdVal = lunaEq * lunaUsd; | ||
if (usdVal >= AMP_MIN_USD) { | ||
sumSingleBalance(balances, "uluna", lunaEq); | ||
} | ||
} | ||
|
||
// bLUNA | ||
{ | ||
const [bal, dec] = await Promise.all([ | ||
cw20Balance(ADDR.bLUNA.token, ADDR.bLUNA.custody), | ||
cw20Decimals(ADDR.bLUNA.token), | ||
]); | ||
const amt = Number(bal) / 10 ** dec; | ||
const lunaEq = amt * bRate; | ||
if (lunaEq > 0) sumSingleBalance(balances, "uluna", lunaEq); | ||
} | ||
|
||
// IBC tokens | ||
for (const k of ["USDC", "wETH", "wBTC"]) { | ||
const { token: denom, custody } = ADDR[k]; | ||
const list = await bankBalances(custody); | ||
const coin = list.find((c) => c.denom === denom); | ||
const amt = coin ? Number(coin.amount) / 1e6 : 0; | ||
if (amt > 0) sumSingleBalance(balances, denom, amt); | ||
} | ||
|
||
// Wormhole cw20 | ||
for (const k of ["wSOL", "wBNB"]) { | ||
const { token, custody } = ADDR[k]; | ||
const [bal, dec] = await Promise.all([cw20Balance(token, custody), cw20Decimals(token)]); | ||
const amt = Number(bal) / 10 ** dec; | ||
if (amt > 0) sumSingleBalance(balances, token, amt); | ||
} | ||
|
||
return balances; | ||
} | ||
|
||
module.exports = { | ||
timetravel: false, | ||
misrepresentedTokens: false, | ||
methodology: | ||
"TVL = sum of SOLID collateral vault balances (ampLUNA, bLUNA, USDC (Noble IBC), wETH.axl, wBTC.axl, wSOL.wh, wBNB.wh). ampLUNA uses Eris exchange_rate with a $3k min-effective filter. bLUNA uses a liquidity-weighted average rate from Astroport & White Whale bLUNA–LUNA pools, each required to pass a $3k min-liquidity filter. Balances are read from custody contracts on Terra (phoenix-1).", | ||
terra: { tvl }, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are we fetching price here instead of just returning a token balance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback. For standard tokens I agree we should only return balances.
However, ampLUNA and bLUNA are LSTs (liquid staking derivatives) of LUNA, so their value is not 1:1 with LUNA but determined by an exchange rate.
Without applying the exchange rate, the TVL would be under/over-reported.
That’s why we fetch both balance and price (exchange rate) here directly.
If you prefer, we can instead return balances only and set up separate price feeds for ampLUNA and bLUNA, but this seemed the simpler integration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm getting 501s from the Terra LCD when I try to test this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will move ampLUNA and bLUNA pricing into our coin prices server. Is there a better way to get the bLUNA rate than reading pool weights?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the adapter to return balances only and removed all pricing/exchange-rate logic.
Some Terra LCDs return 501/5xx. For local testing, please use:
https://terra-api.cosmosrescue.dev:8443
Examples:
GET /cosmwasm/wasm/v1/contract//smart/<base64({"balance":{"address":""}})>
GET /cosmos/bank/v1beta1/balances/
You can also override via: export TERRA_LCD=https://terra-api.cosmosrescue.dev:8443
For LSTs please prefer on-chain exchange rate over pool weights.
bLUNA:
price(bLUNA) = exchange_rate * price(LUNA)
exchange_rate can be obtained from the hub/minter contract state
(either a direct "exchange_rate" field, or computed as total_bonded / total_shares).
ampLUNA:
price(ampLUNA) = exchange_rate * price(LUNA)
exchange_rate available via Eris public API (and on-chain as well).
Happy to provide exact hub/minter contract addresses & query schema if helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result of the updated code is below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey could you give me more details on how to fetch the
exchange_rate
for bLUNA please? Thanks for your helpThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bLUNA doesn’t expose a direct exchange_rate endpoint like ampLUNA (Eris API).
Instead, we calculate its effective rate via liquidity-weighted pool prices:
• Astroport bLUNA–LUNA pool (contract: terra1h32epkd72x7st0wk49z35qlpsxf26pw4ydacs8acq6uka7hgshmq7z7vl9)
• White Whale bLUNA–LUNA pool (contract: terra1j5znhs9jeyty9u9jcagl3vefkvzwqp6u9tq9a3e5qrz4gmj2udyqp0z0xc)
For each pool:
1. Query pool reserves (via LCD wasm/contract/{pair}/smart with { pool: {} }).
2. Extract LUNA reserve (uluna) and bLUNA reserve (CW20 address terra17aj4ty4sz4yhgm08na8drc0v03v2jwr3waxcqrwhajj729zhl7zqnpc0ml).
3. Compute the price as LUNA_reserve / bLUNA_reserve.
4. Weight the rate by the LUNA-side liquidity, to avoid small pools skewing results.
5. Skip pools if their LUNA-side liquidity < $3k (to filter out dust).
6. Final bLUNA exchange_rate = weighted average across valid pools.
This way, the exchange_rate dynamically reflects the actual market ratio of bLUNA to LUNA.