DEV Community

Saravana kumar for Cryip

Posted on • Originally published at cryip.co

Keom Protocol Exploit : Deep Dive Analysis Report

Keom Finance is a decentralized lending and borrowing protocol deployed on Polygon zkEVM. It is a direct fork of Compound Finance, one of the most widely-deployed DeFi lending protocols. In the Compound/Keom model, users deposit underlying tokens (e.g., USDC, ETH) and receive cTokens (called KTokens in Keom) representing their share of the pool. These cTokens accrue interest and can later be redeemed for the underlying asset plus earned yield. The redeem mechanism involves two primary functions: redeemUnderlying(uint redeemAmount) where the user specifies how much underlying they want back and redeemFresh(), the internal function that performs the actual accounting and transfer. The vulnerability resided entirely within redeemFresh().

The Vulnerability Deep Dive

The purpose of redeemFresh() is to validate a redemption request and execute the transfer. The function receives two core values: redeemTokens (the number of cTokens to burn) and redeemAmount (the amount of underlying tokens to return). In a standard redemption, these two values are mathematically linked by the current exchange rate:

redeemAmount = redeemTokens * exchangeRate / 1e18
// i.e., if you burn X cTokens, you receive X * exchangeRate underlying tokens

The function must also ensure the user cannot redeem more cTokens than they actually hold, a basic balance check. The critical requirement is: if redeemTokens is modified, redeemAmount must be recalculated to match.

The Buggy Code (Lines 992–993)

The vulnerable sequence of operations in the actual deployed code was as follows:
/ STEP 1: Initialize from passed parameters
uint redeemTokens = vars.redeemTokens; // e.g., 999,999,999,999 (large)
uint redeemAmount = vars.redeemAmount; // e.g., full market cash = $94,000
// STEP 2 (line 992): Calculate new total supply
// BUG: uses redeemTokens BEFORE it has been capped
vars.totalSupplyNew = totalSupply - redeemTokens;
// STEP 3 (line 993): Cap redeemTokens to user's actual balance
if (redeemTokens > balanceOf(msg.sender)) {
redeemTokens = balanceOf(msg.sender); // e.g., capped to 1 wei
}
// STEP 4: Transfer — redeemAmount NEVER recalculated!
// Still equals $94,000 even though redeemTokens is now 1 wei
doTransferOut(msg.sender, redeemAmount); // TRANSFERS FULL MARKET CASH

Why This Is a Critical Bug

After line 993, the contract correctly knows the user only has a tiny amount of cTokens.
However, redeemAmount, the value that controls how much underlying is actually sent, was set in Step 1 and is never revisited.
The contract burned 1 wei of cTokens but transferred the full $94,000 market balance.
There is no additional check between the capping and the transfer to detect this inconsistency.

The Correct Implementation

The fix requires a single additional step: recalculate redeemAmount after capping redeemTokens. The corrected logic should be:

// CORRECT ORDER OF OPERATIONS
uint redeemTokens = vars.redeemTokens;
uint redeemAmount = vars.redeemAmount;
// STEP 1: Cap redeemTokens FIRST
if (redeemTokens > balanceOf(msg.sender)) {
redeemTokens = balanceOf(msg.sender);
}
// STEP 2: Recalculate redeemAmount based on capped redeemTokens
redeemAmount = redeemTokens * exchangeRateCurrent() / 1e18; // <-- MISSING LINE
// STEP 3: Now safe to calculate new totals
vars.totalSupplyNew = totalSupply - redeemTokens;
// STEP 4: Transfer the correctly bounded amount
doTransferOut(msg.sender, redeemAmount);

Compound Finance Comparison

In Compound Finance's original codebase, the redemption logic correctly sequences these operations.
The bug in Keom was introduced during the fork and customisation process — the ordering of operations was altered without maintaining the invariant that redeemTokens and redeemAmount must always be consistent with each other.
This is a textbook example of how a fork can introduce critical vulnerabilities that do not exist in the upstream protocol.

Attack Walkthrough

Step-by-Step Execution
The attack was elegant in its simplicity. No flash loans, no price manipulation, no multi-step reentrancy — just a single transaction exploiting the accounting flaw:
Attacker deployed a malicious contract at 0x5a2f...f16f with 0.002 ETH as initial capital.
The contract called KToken.mint() with a tiny amount of underlying tokens, receiving a minimal amount of KTokens (cTokens) in return.
The contract then called redeemUnderlying(fullMarketCash), passing in the entire cash balance of the lending market as the redemption amount.
Inside redeemFresh(), redeemAmount was set to the full market cash. redeemTokens was computed as the equivalent cTokens required (a huge number). totalSupplyNew was calculated using this huge uncapped value.
redeemTokens was then capped to the attacker's tiny cToken balance, but redeemAmount remained at the full market cash figure.
doTransferOut() transferred the full market cash balance to the attacker.
Total profit: approximately $94,000 in a single transaction.
Transaction Details

  • Transaction Hash: 0x4ccde7fc6b240397...603d9dfd8
  • Block Number: 30488585
  • Timestamp: 2026-03-17 18:54:33 UTC

Gas Information

  • Gas Limit: 5,000,000
  • Gas Price: 0.01 Gwei

Addresses Involved

  • From (Attacker EOA): 0xb343fe12f86f785a88918599b29b690c4a5da6d5
  • To (Attack Contract): 0x5a2f4151ea961d3dfc4ddf116ca95bfa5865f16f
    Transaction Value

  • ETH Sent: 0.002 ETH (initial capital)
    Additional Info

  • Nonce: 0 (first transaction from this address)

Financial Impact

Total Estimated Loss: ~$94,000 USD
The attacker drained the full cash balance of the targeted KToken market in a single transaction.
All liquidity providers in the affected market suffered a pro-rata loss on their deposits.
Given Polygon zkEVM's scheduled deprecation in 2026, recovery options for affected users are extremely limited.

Broader Implications

• Users who had deposited funds into the affected market lost their entire position with no mechanism for recovery.
• The protocol was paused following the incident, preventing further exploitation but also preventing normal withdrawals.
• Keom's reputation as a safe lending venue on zkEVM was severely damaged at a time when the L2 ecosystem was already contracting.
• The exploit reinforced concerns about the security of Compound Finance forks, which have been a recurring source of DeFi losses in 2024–2026.

Root Cause Classification

Vulnerability Details
Vulnerability Category: Business Logic Error
Classification

  • CWE Category: CWE-840 – Business Logic Errors
  • DeFi Category: Incorrect Variable Ordering / Stale State
  • Compound Classification: Fork Divergence Bug

Risk Assessment

  • Exploitability: Trivial (no special tools required)
  • Detectability: High (visible in a careful code review)
  • CVSS Estimate: 9.8 (Critical) Why Code Audits Miss These This class of bug is particularly dangerous because each individual line of code appears correct in isolation. The cap on line 993 looks like a proper safety check. The totalSupplyNew calculation on line 992 looks like standard accounting. The doTransferOut call looks like a normal transfer. The bug only becomes visible when you trace the data flow between variables, specifically, that redeemAmount is set before the cap and never updated afterward. Automated tools like Slither can detect some variable-ordering issues, but this specific pattern, where a downstream variable depends on an upstream value that is subsequently modified, requires understanding the semantic intent of the variables, not just their syntactic usage. This is precisely where formal verification tooling and manual security review provide value that automated scanners miss.

Recommendations

Add the missing redeemAmount recalculation immediately after any capping of redeemTokens.
Redeploy the KToken contracts with the corrected logic after a full re-audit.
Add an invariant check before doTransferOut: assert(redeemAmount == redeemTokens * exchangeRate / 1e18).
Process Improvements for Compound Forks
Maintain a diff log of every change made from the upstream Compound codebase. Each departure from the original should be explicitly justified and reviewed.
Add unit tests that specifically verify the relationship between redeemTokens and redeemAmount after every code path through redeemFresh().
Implement fuzz testing against redemption functions — a fuzzer would likely have caught this by generating inputs where redeemAmount far exceeds what redeemTokens can cover.
Engage a DeFi-specialized audit firm (e.g., Trail of Bits, Spearbit, Sherlock) with explicit experience auditing Compound forks before any mainnet deployment.
Consider formal verification of the core accounting functions using tools like Certora Prover or Halmos.

Protocol-Level Safeguards

Implement TVL-based circuit breakers: if a single transaction attempts to redeem more than X% of market liquidity, revert automatically.
Add monitoring and alerting for abnormally large single-transaction redemptions relative to the depositor's balance.
Consider timelocks on large withdrawals to allow community review of suspicious redemption activity.

Top comments (0)