Auditor DogSubmit a repository
Security audit report

Kamino-Finance/klend

commit 23b9f2b · claude-opus-4-5 · corpus auditor-skill@4.3 · 2026-06-19 00:00 UTC

0
Critical
0
High
1
Medium
0
Low
0
Info

Provenance

Visibility
public
Highest severity
5 / 10
Report sha256
c2484d4ff5f6eb66e13588219a59360d313fb3f5be0dd667dd99cedb3e8a655f
Usage
0 in · 0 out · 0 cached · $0.00
Source file
frcd10/auditor-attest./reports/…
README badge
[![audit](https://auditor-attest.vercel.app      (troque pelo domínio que a Vercel te der)/badge/Kamino-Finance/klend.svg)](https://auditor-attest.vercel.app      (troque pelo domínio que a Vercel te der)/r/Kamino-Finance/klend)

On-chain attestation · Solana devnet

Transaction
3nxrLzf7YF4A8DyihpPgksDoNMuiRndWz1dqU4RcViV2VsUkZaRpbL1nshZNxug6akcSJhR3xYKUuWcyP6BJ2mAN
Account
E7Lypa73TGidwD6cQBWGBBQWQyZ78QDMuQY97gKr8sbS
Attester
4ZwnFVFENrXwLjdxz8g835TSQq4J8cVByH45vXPDenQf
Program
sXtvdoheTtFBukx8vCppJHhiiJHW2xa3xc5KaPAhzkC

Verify without us: the account address is the PDA of sha256(lowercased repo url) + the commit bytes under the program; its data holds the report sha256 above.

Security Audit Report — Kamino Lending (klend)

Repository: Kamino Lending (klend) Scope: PROGRAM — C:/Users/felip/auditor_validation/Kamino/klend/programs/klend Audit type: AI-assisted first-pass security review Date: 2026-06-19 Auditor: Automated security review agent (Claude)


1. Executive Summary

Kamino Lending is a Solana lending protocol (~27k LOC) deployed on mainnet with >$10M TVL. It consumes Pyth, Switchboard, and Scope oracles and supports Token-2022 mints. This first-pass review prioritized the highest-value paths: reserve math and accounting, the oracle ingestion pipeline, the liquidation/health-factor logic, and handler-level account validation.

Repository Risk Score: 4 / 10 (Medium)

The score equals the highest CONFIRMED finding severity. One genuine but conditional arithmetic/DoS issue was confirmed in the liquidation path. No confirmed loss-of-funds or unauthorized-access vulnerabilities were found in the reviewed paths. A number of additional candidates were investigated and either refuted, found to be by-design, or judged too low-confidence/edge-case to confirm in this pass (see Section 3).

Plain-language verdict: The core protocol invariants reviewed (vault-balance checks after transfers, cToken rounding direction, fee lifecycle net-zero accounting, flash-loan repayment enforcement, market-authority PDA signing, Token-2022 extension whitelisting, obligation ownership constraints) appear sound. The single confirmed issue is a panic-based denial of service that can block liquidation of dust-sized collateral slices on a reserve that has already been impaired by a socialized-loss event — a real but conditional edge case, not a direct theft vector.

Severity Distribution (Confirmed Findings Only)

SeverityCount
Critical (9-10)0
High (7-8)0
Medium (4-6)1
Low (1-3)0
Total1

2. Confirmed Findings

KL-01 — Protocol liquidation fee minimum-of-1 causes integer underflow when collateral redeems to zero liquidity

SeverityMedium (4 / 10)
CategoryArithmetic / DoS
Locationprograms/klend/src/lending_market/lending_operations.rs:2181
ConfidenceMedium

Description

In the liquidate-and-redeem path, after redeeming collateral to liquidity, the protocol computes a liquidation fee and subtracts it from the withdrawn liquidity amount using plain unchecked u64 subtraction:

let protocol_liquidation_fee = liquidation_operations::calculate_protocol_liquidation_fee(
    withdraw_liquidity_amount,
    liquidation_bonus_rate,
    protocol_liquidation_fee_pct,
);
let net_withdraw_liquidity_amount = withdraw_liquidity_amount - protocol_liquidation_fee; // line 2181

calculate_protocol_liquidation_fee enforces a minimum return value of 1 (max(protocol_fee, 1), liquidation_operations.rs:960). When the collateral being withdrawn redeems to a floor-rounded liquidity value of 0, the expression becomes 0 - 1. With overflow-checks = true (klend/Cargo.toml:6,12), this panics and aborts the transaction.

The path to a zero withdraw_liquidity_amount is:

  • post_liquidate_redeem (line ~2158) calls redeem_reserve_collateral, which returns withdraw_liquidity_amount = collateral_exchange_rate.collateral_to_liquidity(withdraw_collateral_amount) (reserve.rs:451), floor-rounded (reserve.rs:1404-1407).
  • The two reserve withdraw helpers (reserve.rs:887-898, 901-913) guard only on > available, not on == 0, so a 0 liquidity amount is accepted; redeem_collateral burns the non-zero ctoken amount and returns 0.
  • calculate_protocol_liquidation_fee(0, ...) then returns 1, producing 0 - 1 at line 2181.

The only validation guard (min_acceptable_received_liquidity_amount check, line 2189) runs AFTER the subtraction, so it cannot prevent the panic.

Reachability / preconditions

This requires the collateral exchange rate to be below 1, so that a non-zero ctoken amount floors to 0 liquidity. Under normal interest accrual the rate is monotonically >= 1 (INITIAL_COLLATERAL_RATE = ONE, consts.rs:42), so collateral_to_liquidity(c) >= c >= 1 and the bug cannot trigger.

The rate can only drop below 1 after a socialize_loss / forgive_debt event impairs the reserve: forgive_debt reduces borrowed_amount_sf (and thus total_supply, reserve.rs:1123-1128) without burning ctokens, driving total_supply below mint_total_supply. socialize_loss requires the losing obligation to be fully liquidated first (line 2317-2319), but the resulting rate drop is global to the reserve and affects other obligations holding it as collateral. The market_value guard (line 2034) uses the full non-floored deposited_amount, so it stays non-zero even when a small partial withdraw floors to 0.

This was confirmed against upstream Kamino master — the subtraction is unchecked there too, so this is genuine production code, not an injected bug.

Impact

A liquidation that targets a dust-sized collateral slice on an already-impaired reserve (post socialized-loss, exchange rate < 1) panics and aborts. The affected dust slices become unliquidatable through that path. The original "permanently unliquidatable / traps bad debt" framing is overstated: a liquidator can usually supply a larger liquidity_amount so that withdraw_collateral_amount floors above 0, avoiding the dust panic. The genuine, durable impact is blocking liquidation of dust-sized slices on an already-impaired reserve, plus a transaction-level DoS for the specific malformed call.

PoC sketch

  1. Reserve R suffers a socialized-loss event (an obligation is fully liquidated, then socialize_loss/forgive_debt reduces borrowed_amount_sf), dropping R's collateral exchange rate below 1.
  2. A separate obligation O holds a small amount of R as collateral and is liquidatable.
  3. Liquidator calls liquidate_obligation_and_redeem_reserve_collateral selecting a withdraw_collateral_amount small enough that collateral_to_liquidity(amount) floors to 0.
  4. withdraw_liquidity_amount = 0, calculate_protocol_liquidation_fee returns 1, line 2181 computes 0 - 1, the runtime panics with overflow, and the transaction aborts.

Recommendation

Replace the unchecked subtraction with a saturating or checked operation and explicitly handle the zero case, e.g.:

let net_withdraw_liquidity_amount = withdraw_liquidity_amount.saturating_sub(protocol_liquidation_fee);

and/or cap protocol_liquidation_fee to min(protocol_liquidation_fee, withdraw_liquidity_amount) before subtracting. Additionally, consider making calculate_protocol_liquidation_fee return 0 (not the enforced minimum of 1) when its input liquidity amount is 0, since there is no liquidity to take a fee from. Add an explicit guard rejecting (or short-circuiting) redemptions where withdraw_liquidity_amount == 0 so dust slices are handled deterministically rather than panicking.


3. Unconfirmed / Informational Candidates

The following candidates were investigated during the review and were refuted, found to be by-design, or judged too low-confidence / edge-case to confirm. They are listed for completeness and to guide deeper follow-up review. They are NOT confirmed vulnerabilities and should not be treated as such.

#TitleLocationRaw sevConfidenceDisposition
C-01Panic on negative Pyth i64 price — DoS of refresh_reserveprices/pyth.rs:75 (also :95)5highPlausible organic DoS during Pyth circuit-breaker events; account is admin-gated, not attacker-controlled. Worth fixing (use try_from with error, not unwrap). Not confirmed as exploitable in this pass.
C-02LIQUIDATION_CHECKS omits TWAP_CHECKED / HEURISTIC_CHECKEDstate/last_update.rs:464mediumRisk amplifier during genuine oracle instability; attacker cannot manufacture divergence without manipulating the feed. Needs design-intent confirmation.
C-03Auto-deleverage liquidations skip collateral-priority orderingstate/liquidation_operations.rs:229-2394highMatch arm omits deleverage reasons; likely partially by-design. Suboptimal ordering, not a direct theft. Needs design-intent confirmation.
C-04RepayObligationLiquidity: obligation owner not verifiedhandlers/handler_repay_obligation_liquidity.rs:114-1564highRepaying another's debt from your own funds is standard lending behavior. Enables liquidation griefing at attacker's own cost; no theft. Likely by-design.
C-05Switchboard staleness uses slot-based timestamp approximationprices/switchboard.rs:503mediumMatches Scope reference implementation's acknowledged limitation; last_update_timestamp field exists but unused. Marginal staleness window under congestion.
C-06Unchecked u64 sub in calculate_borrow_all_availablestate/reserve.rs:7673lowWorst-case micro-borrow + near-max-fee corner; panics revert atomically, no fund loss.
C-07Small-position full-liquidation: full debt settled vs partial collateralstate/liquidation_operations.rs:349-3633highIntentional socialized-loss behavior; concern is only that no bad-debt event is emitted (monitoring gap).
C-08Unchecked u128 add of pending_referrer_fees_sf in compound_intereststate/reserve.rs:12683lowRequires enormous accumulated fees over long time; not practically reachable.
C-09WithdrawProtocolFees: no signer required (permissionless sweep)handlers/handler_withdraw_protocol_fees.rs:47-933highDestination fixed to fee_collector ATA with no delegate; no theft, only loss of timing control.
C-10SeedDepositOnInitReserve: arbitrary signer can fund initial deposithandlers/handler_seed_deposit_on_init_reserve.rs:57-843highCaller donates funds; guarded by has_initial_deposit(); minimal practical impact.
C-11total_supply() unchecked Fraction subtraction wrapstate/reserve.rs:1123-11282lowOnly reachable if reserve already insolvent; not practically reachable.
C-12Combined deposit path skips is_ctoken_usage_blocked checklending_market/lending_checks.rs:194-2132mediumCircumvents admin intent on combined path; no direct economic harm. Worth aligning with standalone path.
C-13Pyth EMA/TWAP reuses spot publish_timeprices/pyth.rs:622highTWAP age parameter has no independent effect for Pyth reserves; config-clarity issue, not exploitable.
C-14Highest borrow-factor priority enforced only for LtvExceededstate/liquidation_operations.rs:223-2282mediumPartially by-design for auto-deleverage; suboptimal ordering.
C-15flash_repay_checks uses positional account index 3lending_market/flash_ixs.rs:632lowFull account-by-account match enforced on borrow side via flash_borrow_check_matching_repay.
C-16InitObligationFarmsForReserve: tag >= 4 allows arbitrary seed accountshandlers/handler_init_obligation.rs:102-1122mediumPDA seeds include owner + market; no direct exploit; may confuse off-chain tooling.

4. Coverage & Methodology

This is an AI-assisted FIRST-PASS review. It is NOT exhaustive. Deploy-time configuration, on-chain operational procedures, admin key management, and runtime/governance assumptions were NOT verified. Economic/game-theoretic simulation was not performed.

Areas read and traced

Area 1 — Reserve math & accounting (read fully): src/state/reserve.rs (CollateralExchangeRate math, total_supply, compound_interest, fee accounting, deposit/redeem/borrow logic), src/lending_market/lending_operations.rs (deposit, redeem, borrow, repay, liquidate_and_redeem, flash borrow/repay, socialize_loss, accumulate_referrer_fees, rollover), src/lending_market/lending_checks.rs (all pre/post transfer invariant checks), all deposit/redeem/flash/liquidate/socialize/withdraw-queue/cancel-ticket handlers, src/utils/fraction.rs, src/utils/consts.rs.

Verified safe: FIFO ticket ordering via PDA seed; initial-deposit anti-inflation guard (MIN_INITIAL_DEPOSIT_AMOUNT seeded 1:1); vault-balance invariant checks after every transfer; socialize_loss access control (lending_market_owner); flash-loan repayment enforcement via instruction sysvar; cToken rounding direction (protocol-favorable); fee lifecycle (pending->accumulated net-zero to total_supply); forgive_debt capped to borrowed_amount.

Area 2 — Oracle pipeline (read fully): prices/mod.rs, pyth.rs, switchboard.rs, scope.rs, checks.rs, types.rs, utils.rs. Traced full ingestion path from refresh_reserve -> get_price -> get_most_recent_price_and_twap -> get_validated_price into borrow/liquidation checks.

Verified: PriceStatusFlags definitions and all is_stale() call sites; Pyth account key validation (check_pyth_acc_matches); Switchboard owner/discriminator checks; Scope discriminator check; last_update_timestamp field existence in sbod-itf; Scope confidence-interval check absent by design; TWAP config validity via is_twap_config_valid(); oracle selection (most-recent timestamp wins); Pyth eager / Switchboard lazy confidence checks at 2% threshold; Scope chain multiplication overflow protected by checked_mul.

Area 3 — Liquidation / health-factor (read fully): liquidation_operations.rs (all 962 lines), lending_operations.rs (all 4528 lines), obligation.rs (all 1672 lines), reserve.rs (key sections), last_update.rs, lending_checks.rs, handler_liquidate_obligation_and_redeem_reserve_collateral.rs.

Verified safe: health-factor computation uses atomically-set stored sf values; borrow LTV guard via unhealthy_loan_to_value(); repay updates elevation-group trackers using settled (not repaid) amount; accrue_interest uses U256 BigFraction to avoid overflow; collateral_exchange_rate zero-supply guard; flash-loan repay amount validation; liquidation staleness requires LIQUIDATION_CHECKS; PriceStatusFlags intersect across reserves on refresh.

Area 4 — Handler / account validation (all 55 handlers read fully): borrow, repay, withdraw-collateral, liquidation, flash loan (both sides), init-reserve, init-obligation, init/refresh-farms, withdraw-protocol-fees, redeem-fees, withdraw-referrer-fees, socialize-loss, rollover, fill-borrow-order, queue handlers, recover/cancel-ticket, update/transfer-ownership handlers, seed-deposit, clone-reserve-config, topup-rewards, deposit handlers, mark-for-deleveraging. Plus flash_ixs.rs, farms_ixs.rs, kvault_ixs.rs, ix_utils.rs, constraints.rs, permissioning.rs, refresh_ix_utils.rs.

Verified safe: Token-2022 extension whitelist (TransferFeeConfig 0 bps, no TransferHook program, ConfidentialTransfer auto_approve=false, Pausable not paused); market-authority PDA seeds on all transfer-signing paths; obligation has_one = owner on all write-sensitive instructions (except repay, which is by-design open); farming CPI validates delegatee == obligation; flash-loan two-way account matching.


5. Disclaimer

This report was produced by an automated, AI-assisted security review process and represents a FIRST-PASS examination of the highest-value code paths. It does NOT constitute a complete or exhaustive audit, a guarantee of security, or professional financial/legal advice. Absence of a finding in a given area does not imply that area is free of vulnerabilities.

The single confirmed finding (KL-01) is conditional and edge-case in nature, with confidence rated medium. Its preconditions (a prior socialized-loss event impairing the reserve, plus a dust-sized withdraw) limit practical impact, and a competent liquidator can frequently avoid the panic by adjusting the withdraw amount.

All findings in this report — including the confirmed finding — MUST be independently re-verified against the current deployed source and reproduced (e.g., via a localnet/test harness) before any bug-bounty submission or remediation action. Severity ratings are advisory and should be re-assessed in the context of program-specific bounty rules and live economic conditions.