Skip to content
Merged
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
122 changes: 122 additions & 0 deletions projects/trufin-vaults/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const { getConnection, decodeAccount, getAssociatedTokenAddress } = require("../helper/solana");
const { PublicKey } = require("@solana/web3.js");
const { tickToPrice } = require("../helper/utils/tick");

const VAULT_PROGRAM_ID = new PublicKey("5CjtbqE3tE6LDnPU1HKyvnzPsNVkyxF4H6tPRUrTQDX3");
const RAYDIUM_CLMM_PROGRAM_ID = new PublicKey("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK");
const WSOL_MINT = "So11111111111111111111111111111111111111112";
const TRUSOL_MINT = "6umRHtiuBd1PC6HQhfH9ioNsqY4ihZncZXNPiGu3d3rN";
const TRUSOL_STAKE_POOL = new PublicKey("EyKyx9LKz7Qbp6PSbBRoMdt8iNYp8PvFVupQTQRMY9AM");

const vaultStatePda = PublicKey.findProgramAddressSync([Buffer.from("vault_state")], VAULT_PROGRAM_ID)[0];
const vaultSolPda = PublicKey.findProgramAddressSync([Buffer.from("vault_sol")], VAULT_PROGRAM_ID)[0];
const vaultWsolAta = getAssociatedTokenAddress(WSOL_MINT, vaultSolPda.toString());
const vaultTrusolAta = getAssociatedTokenAddress(TRUSOL_MINT, vaultSolPda.toString());

// VaultState Borsh layout offsets (after 8-byte Anchor discriminator):
// [8] vault_sol_bump: u8
// [9] current_position_tick_lower: i32
// [13] current_position_tick_upper: i32
// [17] fee: u64
// [25] treasury_trusol_fees: u64
// [33] treasury_wsol_fees: u64
// [41] treasury: Pubkey (32)
// [73] is_paused: bool
// [74] raydium_pool_address: Pubkey (32)
function decodeVaultState(data) {
const tickLower = data.readInt32LE(9);
const tickUpper = data.readInt32LE(13);
const poolAddress = new PublicKey(data.slice(74, 106));
return { tickLower, tickUpper, poolAddress };
}

function derivePositionNftMint(tickLower, tickUpper) {
const tickLowerBuf = Buffer.alloc(4);
tickLowerBuf.writeInt32LE(tickLower);
const tickUpperBuf = Buffer.alloc(4);
tickUpperBuf.writeInt32LE(tickUpper);
return PublicKey.findProgramAddressSync(
[Buffer.from("position_nft_mint"), vaultStatePda.toBuffer(), tickLowerBuf, tickUpperBuf],
VAULT_PROGRAM_ID,
)[0];
}

function derivePersonalPosition(nftMint) {
return PublicKey.findProgramAddressSync(
[Buffer.from("position"), nftMint.toBuffer()],
RAYDIUM_CLMM_PROGRAM_ID,
)[0];
}

function readTokenAccountAmount(accountInfo) {
if (!accountInfo) return 0;
return Number(accountInfo.data.readBigUInt64LE(64));
}

function getPositionAmounts(tickLower, tickUpper, tickCurrent, liquidity) {
const sa = tickToPrice(tickLower / 2);
const sb = tickToPrice(tickUpper / 2);

let amount0 = 0;
let amount1 = 0;

if (tickCurrent < tickLower) {
amount0 = liquidity * (sb - sa) / (sa * sb);
} else if (tickCurrent < tickUpper) {
const sp = tickToPrice(tickCurrent) ** 0.5;
amount0 = liquidity * (sb - sp) / (sp * sb);
amount1 = liquidity * (sp - sa);
} else {
amount1 = liquidity * (sb - sa);
}

return { amount0: Math.floor(amount0), amount1: Math.floor(amount1) };
}

async function raydiumVaultTvl() {
const connection = getConnection();

const vaultStateAccount = await connection.getAccountInfo(vaultStatePda);
const { tickLower, tickUpper, poolAddress } = decodeVaultState(vaultStateAccount.data);

const nftMint = derivePositionNftMint(tickLower, tickUpper);
const personalPositionPda = derivePersonalPosition(nftMint);

const [solBalance, wsolAccount, trusolAccount, positionAccount, poolAccount, stakePoolAccount] =
await Promise.all([
connection.getBalance(vaultSolPda),
connection.getAccountInfo(new PublicKey(vaultWsolAta)),
connection.getAccountInfo(new PublicKey(vaultTrusolAta)),
connection.getAccountInfo(personalPositionPda),
connection.getAccountInfo(poolAddress),
connection.getAccountInfo(TRUSOL_STAKE_POOL),
]);

const wsolBalance = readTokenAccountAmount(wsolAccount);
const trusolBalance = readTokenAccountAmount(trusolAccount);

const stakePoolTotalLamports = Number(stakePoolAccount.data.readBigUInt64LE(258));
const stakePoolTokenSupply = Number(stakePoolAccount.data.readBigUInt64LE(266));
const trusolPrice = stakePoolTotalLamports / stakePoolTokenSupply;

const position = decodeAccount("raydiumPositionInfo", positionAccount);
const pool = decodeAccount("raydiumCLMM", poolAccount);

const { amount0, amount1 } = getPositionAmounts(
position.tickLower, position.tickUpper, pool.tickCurrent,
Number(position.liquidity.toString()),
);

const positionWsol = amount0 + Number(position.tokenFeesOwedA.toString());
const positionTrusol = amount1 + Number(position.tokenFeesOwedB.toString());

const totalSol = solBalance + wsolBalance + positionWsol
+ (trusolBalance + positionTrusol) * trusolPrice;
Comment on lines +104 to +116
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

Map CLMM side A/B to WSOL/truSOL before valuing the position.

amount0/amount1 and tokenFeesOwedA/B are pool-side values, but this code hard-codes side A as WSOL and side B as truSOL. If the Raydium pool uses the opposite mint ordering, the position portion of TVL is mispriced. Read the pool’s token ordering first, then route each side into the WSOL/truSOL buckets before applying trusolPrice.

🧭 Minimal shape of the fix
-  const positionWsol = amount0 + Number(position.tokenFeesOwedA.toString());
-  const positionTrusol = amount1 + Number(position.tokenFeesOwedB.toString());
+  const isWsolTokenA = /* compare the decoded pool token ordering against WSOL_MINT/TRUSOL_MINT */;
+
+  const positionWsol = isWsolTokenA
+    ? amount0 + Number(position.tokenFeesOwedA.toString())
+    : amount1 + Number(position.tokenFeesOwedB.toString());
+  const positionTrusol = isWsolTokenA
+    ? amount1 + Number(position.tokenFeesOwedB.toString())
+    : amount0 + Number(position.tokenFeesOwedA.toString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/trufin-vaults/index.js` around lines 104 - 116, The code assumes
pool side A is WSOL and B is truSOL when computing positionWsol/positionTrusol;
instead, inspect the pool's token ordering (e.g., pool.tokenMintA and
pool.tokenMintB) after decodeAccount("raydiumCLMM", poolAccount) and map
amount0/amount1 and tokenFeesOwedA/tokenFeesOwedB to the correct buckets
accordingly before valuing: if tokenMintA equals WSOL_MINT then treat
amount0+tokenFeesOwedA as positionWsol and amount1+tokenFeesOwedB as
positionTrusol, otherwise swap them; then compute positionWsol/positionTrusol
and totalSol using trusolPrice as before so TVL uses the correct side mapping
for getPositionAmounts outputs and tokenFeesOwedA/B.


return { solana: totalSol / 1e9 };
}

module.exports = {
methodology: "Counts the TVL of the TruFin Raydium Vault.",
solana: { tvl: raydiumVaultTvl },
Comment on lines +121 to +123
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

Mark this adapter as latest-only.

raydiumVaultTvl always reads live Solana RPC state and never consumes historical inputs. Without timetravel: false, historical runs can replay current TVL as if it were past data.

🗓️ Minimal export fix
 module.exports = {
+  timetravel: false,
   methodology: "Counts the TVL of the TruFin Raydium Vault.",
   solana: { tvl: raydiumVaultTvl },
 };

Based on learnings: In DefiLlama adapters, timetravel is a valid export key.

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

In `@projects/trufin-vaults/index.js` around lines 121 - 123, The adapter exports
live-only Solana TVL via module.exports with solana.tvl set to raydiumVaultTvl
but does not mark it latest-only; add the timetravel flag to the export by
setting timetravel: false alongside tvl (i.e., update the exported solana object
that references raydiumVaultTvl to include timetravel: false) so historical runs
won't replay current RPC state.

Comment on lines +116 to +121
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

Match the solana.tvl return contract.

On Line 121 this function is already mounted under the Solana chain. Returning { solana: ... } again on Line 116 is what llamabutler is flagging as “exports TVL values twice”; return the native/token balance payload directly instead of another chain-scoped wrapper.

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

In `@projects/trufin-vaults/index.js` around lines 116 - 121, raydiumVaultTvl is
already exported under module.exports as solana.tvl, so its return should be the
native/token balance payload itself rather than a chain-scoped wrapper; update
the return in raydiumVaultTvl to return the balances object (e.g., the mapping
of token identifiers to amounts or the native token balance) directly instead of
returning { solana: ... }, leaving module.exports = { methodology: ..., solana:
{ tvl: raydiumVaultTvl } } unchanged.

};
Comment on lines +119 to +122
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

Flag the Raydium overlap as external double counting.

This vault TVL is capital parked inside a Raydium CLMM position, and projects/raydium/index.js:1-35 already sums every CLMM pool’s vaultA/vaultB balances. Add doublecounted: true so the overlap is surfaced correctly on the protocol page.

🔁 Minimal export fix
 module.exports = {
+  doublecounted: true,
   methodology: "Counts the TVL of the TruFin Raydium Vault.",
   solana: { tvl: raydiumVaultTvl },
 };

Based on learnings: doublecounted: true remains appropriate when a vault deploys assets into an external protocol that DefiLlama tracks independently.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
module.exports = {
methodology: "Counts the TVL of the TruFin Raydium Vault.",
solana: { tvl: raydiumVaultTvl },
};
module.exports = {
doublecounted: true,
methodology: "Counts the TVL of the TruFin Raydium Vault.",
solana: { tvl: raydiumVaultTvl },
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projects/trufin-vaults/index.js` around lines 119 - 122, The module export
for this project currently exposes methodology and solana: { tvl:
raydiumVaultTvl } but does not mark that the TVL is actually counted inside
Raydium CLMM pools; update the exported object (module.exports) to include
doublecounted: true so the overlap with projects/raydium/index.js is surfaced
(i.e., add doublecounted: true at the top-level export alongside methodology and
solana).

Loading