mirror of
https://github.com/alsk1992/CloddsBot.git
synced 2026-10-02 02:54:40 +08:00
fix: KaminoMarket.load() was called with a garbage argument in every position, breaking all Kamino lending
Audit of src/solana/kamino.ts, part of the ongoing Solana venue-integration coverage pass. The most severe finding of this audit series: KaminoMarket .load()'s real signature is (connection, marketAddress, recentSlotDurationMs: number, programId?, setupLocalTest?, withReserves?) — verified against the SDK's own market.d.ts — but every one of the 6 call sites (getKaminoMarkets, getKaminoObligation, depositToKamino, withdrawFromKamino, borrowFromKamino, repayToKamino) passed PROGRAM_ID as the third argument (where a number was expected) and never passed a real programId at all. The `as any` cast on the destructured import silenced TypeScript's arity check entirely, so this was invisible to typecheck. Confirmed live in two layers: - With the wrong arg order, getKaminoMarkets() always returned [] — first traced to calculateSupplyAPR()/calculateBorrowAPR() throwing "[DecimalError] Invalid argument: undefined" for every single reserve, no exceptions. - Fixing just that arity (those two methods need (slot, referralFeeBps), not zero args) surfaced the real, deeper bug: a different error, "Invalid borrow rate curve, could not identify the interpolation points," again on every reserve — because recentSlotDurationMs was receiving a garbage PublicKey object instead of a real millisecond duration, breaking the SDK's internal rate-curve interpolation entirely regardless of which reserve. Fixed by passing the SDK's own DEFAULT_RECENT_SLOT_DURATION_MS (=450) as the third argument and PROGRAM_ID as the fourth, at all 6 call sites. After the fix, getKaminoMarkets() returns all 58 real reserves with realistic live rates (e.g. SOL: 5.05% deposit / 6.6% borrow / 89.9% utilization / 74% LTV). This bug affected every Kamino lending function in the file, not just the read path — deposit/withdraw/borrow/repay all load the market the same way. Also added a per-reserve try/catch in getKaminoMarkets() (matching the per-item pattern getKaminoStrategies() below already uses) so one genuinely malformed reserve can't blank out the whole result the way this bug did. Known, non-blocking follow-up not addressed here: getKaminoStrategies/ getKaminoStrategy/getKaminoUserShares (the kliquidity-sdk vault side) hardcode tvl:'0', apy:0, tokenASymbol:'TokenA', tokenBSymbol:'TokenB', and every user position amount to '0', despite the SDK exposing real methods for this data (getStrategyAprApy, getStrategyShareData, getStrategyTokensHoldings) — confirmed these exist via the SDK's Kamino.d.ts. Not fixed here since properly wiring up TVL/APY/token-amount computation across three functions is a separate, larger scope than this fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3520347d44
commit
c6b0b7d944
+50
-20
@@ -156,11 +156,21 @@ export async function getKaminoMarkets(
|
||||
connection: Connection
|
||||
): Promise<KaminoMarketInfo[]> {
|
||||
try {
|
||||
const { KaminoMarket, PROGRAM_ID } = await import('@kamino-finance/klend-sdk');
|
||||
const { KaminoMarket, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } = await import('@kamino-finance/klend-sdk');
|
||||
|
||||
// KaminoMarket.load()'s real signature is (connection, marketAddress,
|
||||
// recentSlotDurationMs: number, programId?, ...) — verified against the
|
||||
// SDK's own market.d.ts. Every call site here used to pass PROGRAM_ID
|
||||
// as the third argument (a PublicKey where a number was expected) and
|
||||
// never passed a real programId at all, since the `as any` cast
|
||||
// silenced TypeScript's arity/type check. Confirmed live: this was the
|
||||
// actual root cause of "[DecimalError] Invalid argument: undefined" and
|
||||
// "Invalid borrow rate curve, could not identify the interpolation
|
||||
// points" errors on every single reserve.
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
@@ -168,22 +178,37 @@ export async function getKaminoMarkets(
|
||||
return [];
|
||||
}
|
||||
|
||||
// calculateSupplyAPR()/calculateBorrowAPR() require (slot, referralFeeBps)
|
||||
// — confirmed against the SDK's own reserve.d.ts. The old code called
|
||||
// them with zero arguments (the `as any` cast on `reserve` silenced
|
||||
// TypeScript's arity check), which threw "[DecimalError] Invalid
|
||||
// argument: undefined" for every single reserve, aborted this loop, and
|
||||
// bubbled up to the outer catch — meaning getKaminoMarkets() always
|
||||
// silently returned [] regardless of real reserve data. 0 referral bps
|
||||
// (no referral program integration here). The per-reserve try/catch
|
||||
// stays as defense against any genuinely malformed reserve, matching
|
||||
// the per-item pattern getKaminoStrategies() below already uses.
|
||||
const currentSlot = await connection.getSlot();
|
||||
const reserves: KaminoReserveInfo[] = [];
|
||||
for (const [, reserve] of market.reserves) {
|
||||
reserves.push({
|
||||
address: reserve.address.toBase58(),
|
||||
symbol: reserve.symbol || 'UNKNOWN',
|
||||
mint: reserve.getLiquidityMint().toBase58(),
|
||||
decimals: (reserve.state.liquidity.mintDecimals as BN).toNumber(),
|
||||
depositRate: (reserve as any).calculateSupplyAPR() * 100,
|
||||
borrowRate: (reserve as any).calculateBorrowAPR() * 100,
|
||||
totalDeposits: reserve.getTotalSupply().toString(),
|
||||
totalBorrows: reserve.getBorrowedAmount().toString(),
|
||||
availableLiquidity: reserve.getLiquidityAvailableAmount().toString(),
|
||||
utilizationRate: reserve.calculateUtilizationRatio() * 100,
|
||||
ltv: reserve.state.config.loanToValuePct,
|
||||
liquidationThreshold: reserve.state.config.liquidationThresholdPct,
|
||||
});
|
||||
try {
|
||||
reserves.push({
|
||||
address: reserve.address.toBase58(),
|
||||
symbol: reserve.symbol || 'UNKNOWN',
|
||||
mint: reserve.getLiquidityMint().toBase58(),
|
||||
decimals: (reserve.state.liquidity.mintDecimals as BN).toNumber(),
|
||||
depositRate: (reserve as any).calculateSupplyAPR(currentSlot, 0) * 100,
|
||||
borrowRate: (reserve as any).calculateBorrowAPR(currentSlot, 0) * 100,
|
||||
totalDeposits: reserve.getTotalSupply().toString(),
|
||||
totalBorrows: reserve.getBorrowedAmount().toString(),
|
||||
availableLiquidity: reserve.getLiquidityAvailableAmount().toString(),
|
||||
utilizationRate: reserve.calculateUtilizationRatio() * 100,
|
||||
ltv: reserve.state.config.loanToValuePct,
|
||||
liquidationThreshold: reserve.state.config.liquidationThresholdPct,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn({ error, reserve: reserve.address.toBase58() }, 'Skipping Kamino reserve that failed to load');
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
@@ -227,11 +252,12 @@ export async function getKaminoObligation(
|
||||
marketAddress?: string
|
||||
): Promise<KaminoObligationInfo | null> {
|
||||
try {
|
||||
const { KaminoMarket, VanillaObligation, PROGRAM_ID } = await import('@kamino-finance/klend-sdk');
|
||||
const { KaminoMarket, VanillaObligation, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } = await import('@kamino-finance/klend-sdk');
|
||||
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(marketAddress || KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
@@ -309,12 +335,13 @@ export async function depositToKamino(
|
||||
keypair: Keypair,
|
||||
params: KaminoDepositParams
|
||||
): Promise<KaminoLendingResult> {
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID } =
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } =
|
||||
await import('@kamino-finance/klend-sdk');
|
||||
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(params.marketAddress || KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
@@ -366,12 +393,13 @@ export async function withdrawFromKamino(
|
||||
keypair: Keypair,
|
||||
params: KaminoWithdrawParams
|
||||
): Promise<KaminoLendingResult> {
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID } =
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } =
|
||||
await import('@kamino-finance/klend-sdk');
|
||||
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(params.marketAddress || KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
@@ -423,12 +451,13 @@ export async function borrowFromKamino(
|
||||
keypair: Keypair,
|
||||
params: KaminoBorrowParams
|
||||
): Promise<KaminoLendingResult> {
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID } =
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } =
|
||||
await import('@kamino-finance/klend-sdk');
|
||||
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(params.marketAddress || KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
@@ -480,12 +509,13 @@ export async function repayToKamino(
|
||||
keypair: Keypair,
|
||||
params: KaminoRepayParams
|
||||
): Promise<KaminoLendingResult> {
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID } =
|
||||
const { KaminoMarket, KaminoAction, VanillaObligation, PROGRAM_ID, DEFAULT_RECENT_SLOT_DURATION_MS } =
|
||||
await import('@kamino-finance/klend-sdk');
|
||||
|
||||
const market = await KaminoMarket.load(
|
||||
connection,
|
||||
new PublicKey(params.marketAddress || KAMINO_MAIN_MARKET),
|
||||
DEFAULT_RECENT_SLOT_DURATION_MS,
|
||||
PROGRAM_ID as any
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user