In the LendingPool contract, there is a logical inconsistency between normal token amounts and scaled debt amounts tracked by the usageIndex
. In practice, it allows a user to repay (or liquidate) less than their true debt balance if the index is updated in ways that create a mismatch between the nominal repayment/liquidation amount and the internally calculated scaled repayment. As a result, the protocol may believe the user has covered their debt, while receiving fewer tokens than expected, potentially creating a shortfall in the lending pool.
Further testing reveals this flaw persists not only in direct repayments (_repay
) but also during finalizeLiquidation
. If DebtToken.burn
expects a scaled value while receiving a normal one (or vice versa), the system can burn more (or less) debt than is actually covered by transferred tokens. Consequently, liquidations can fail to collect sufficient funds, adding the same under-repayment risk as in the normal repayment flow.
Note: This issue is not the same known issue "LendingPool: Interest calculations use different methods for deposits (linear) and borrows (compound), this generates dust" . That known issue only leads to minor leftover amounts and does not allow underpayment or create a shortfall. The vulnerability described above is distinct and significantly more severe.
The mismatch between normal token amounts (actual transfer values) and scaled amounts (used internally to represent debt via the usageIndex
). By comparing a normal amount to a scaled debt balance and subsequently transferring tokens based on the scaled logic, the protocol risks under-collecting the real value required to repay the debt.
Specifically, the contract uses amount
(in normal units) when invoking the burn
function, yet calculates the actual token transfer using scaledAmount = amount / usageIndex
. If usageIndex > 1
, a smaller quantity of tokens is sent than the nominal amount. Consequently, the DebtToken contract might remove a greater debt balance than the tokens actually transferred, allowing a borrower to settle an obligation with fewer tokens than expected. This discrepancy can create a shortfall in the lending pool, undermine its solvency, and adversely affect other participants’ capital.
If exploited, this logic allows borrowers (and, in the case of liquidation, liquidators or the Stability Pool) to effectively repay or settle less debt than the actual amount owed, leading to a deficit in the lending pool. This deficit arises not only from normal _repay
operations but also from finalizeLiquidation
, where the under-collection of tokens further compromises the protocol’s solvency. As a result, other depositors and lenders may bear the financial risk if insufficient funds remain to cover outstanding obligations. The recurring shortfalls undermine the protocol’s integrity and can cause systemic issues across the entire platform if attackers repeatedly exploit the mismatch.
The proof of concept explains how the mismatch between “normal” token amounts and “scaled” balances can lead to under-repayment, jeopardizing the pool’s solvency:
By comparing a “normal” repay amount to a “scaled” debt balance and then transferring only the scaled portion, the protocol can mistakenly register a full repayment even though fewer tokens are actually sent. This test forces usageIndex
to double after the borrower’s initial debt is established, so that when 60 tokens are “repaid,” only 30 tokens get transferred in reality. Consequently, the pool records a 60-token debt reduction despite receiving half that amount, highlighting the logical vulnerability.
Below is a summarized version of the relevant code snippet, with comments highlighting the vulnerability. Marked with (!)
are the logical conflicts leading to inconsistent debt repayment:
Inconsistent Comparison
amount
(the actual token amount intended for repayment) is compared with userScaledDebt
(the user’s debt in “scaled” units), producing incorrect results for actualRepayAmount
.
Insufficient or Excessive Token Transfer
Later, safeTransferFrom
uses amountScaled = actualRepayAmount / usageIndex
. If usageIndex > 1
, fewer real tokens are transferred than needed; if usageIndex < 1
, more tokens than necessary could be transferred.
Double Inconsistency with the burn
Function
burn
receives amount
(the original normal value) instead of actualRepayAmount
. This breaks the coherence between what the user “claims” to repay versus what is actually removed from the DebtToken
.
The system might over-burn or under-burn debt compared to the real tokens supplied, creating accounting mismatches and potential deficits for other lenders.
Collectively, these issues can result in more or less debt being canceled than the actual tokens transferred, causing deficits or accounting inconsistencies that undermine the protocol’s overall solvency.
Initial Debt of 60 Tokens (usageIndex = 1)
The user borrows 60 tokens with usageIndex = 1
, creating a “normal” debt of 60 tokens.
Forcing Usage Index Up to 2
Another token is minted at usageIndex = 2
, effectively doubling the user’s debt from 60 to 120 in normal terms (scaled debt × 2).
User Tries to Repay 60 “Normal” Tokens
Due to the flawed logic, repayAmount = 60
is compared to the scaled debt (60).
The contract calculates scaledAmount = 60 / 2 = 30
, so only 30 real tokens are effectively transferred.
60 Debt Burned but Only 30 Tokens Received
The function “burns” 60 tokens of debt, reducing it from 120 down to 60.
However, the protocol only obtains 30 actual tokens, leaving a 30-token deficit that compromises the pool’s solvency.
Logs confirm the mismatch:
debtBefore
: 60
debtAfter small mint
: 120
debtAfter repay
: 60
Debt burned (normal units)
: 60
Tokens actually transferred
: 30
Thus, the protocol mistakenly believes a full 60-token repayment has occurred, when only 30 tokens were actually settled.
This test reproduces a scenario where the usage index doubles from 1 to 2 after the user initially borrows 60 tokens. The user then repays 60 tokens “normally,” but only transfers 30 tokens in practice due to the underlying scaled math.
The test confirms that the protocol recognizes a 60-token repayment while only receiving 30 tokens, illustrating the logic mismatch between “scaled” and “normal” amounts.
MockDebtToken
emulates a debt-tracking mechanism using a “scaled” balance model, where each mint/burn operation adjusts both the user’s scaled balance and a global usageIndex
. By allowing direct manipulation of the usageIndex
, the mock highlights how a user’s normal debt can diverge from the actual tokens transferred. This setup is ideal for testing the repayment logic vulnerability without the complexity of a full lending pool contract.
Add the following Mock to contracts/mocks/MockDebtToken.sol
Add the following test to test/unit/core/pools/LendingPool/LendingPool.test.js
This scenario demonstrates how discrepancies in debt calculation and actual token transfer allow borrowers to repay less than their real obligations, producing a shortfall in the pool and risking the protocol’s solvency.
Manual Code Review
Performed a thorough manual examination of the contract logic, focusing on the repayment workflow and comparing scaled debt values versus actual token transfers. This facilitated the discovery and validation of the logical mismatch in the repayment process.
Ensure all comparisons and transfers occur in the same (normal) unit. Pass actualRepayAmount
(in normal tokens) both to DebtToken.burn
and safeTransferFrom
, removing the mismatched scaling.
This ensures the contract consistently uses “normal” amounts for both the debt and token transfers, preventing any under- or over-repayment caused by mismatched scaling.
Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.
Interest IS applied through the balanceOf() mechanism. The separate balanceIncrease calculation is redundant/wrong. Users pay full debt including interest via userBalance capping.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.