Skip to content
Open
Changes from 2 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
196 changes: 196 additions & 0 deletions projects/solid/index.js
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;
Copy link
Collaborator

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?

Copy link
Author

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.

Copy link
Collaborator

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

Copy link
Collaborator

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?

Copy link
Author

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.

  • ampLUNA/bLUNA (and other assets) are now reported as raw balances.
  • Pricing, including LST exchange rates, should be handled by DefiLlama coin-prices infra.
  • Added TERRA_LCD env var (default: https://terra-api.cosmosrescue.dev:8443).
  • CW20 balances via wasm smart queries; IBC/native via bank balances.
  • Keys use "terra2:<cw20_contract>" and "terra2:<ibc_denom>".

Some Terra LCDs return 501/5xx. For local testing, please use:
https://terra-api.cosmosrescue.dev:8443
Examples:

  • CW20 smart query:
    GET /cosmwasm/wasm/v1/contract//smart/<base64({"balance":{"address":""}})>
  • Bank balances:
    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.

Copy link
Author

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

image

Copy link
Collaborator

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 help

Copy link
Author

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.

} 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: {} });
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is giving me for both pools listed

{
  code: 12,
  message: "Not Implemented",
  details: [
  ],
}

Is this expected? Using https://terra-api.cosmosrescue.dev:8443

Copy link
Collaborator

Choose a reason for hiding this comment

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