Powerful Yellow Bear
High
The matchOffersV3 function in the loan contract calculates the weightedAverageAPR
using integer division, leading to cumulative precision loss. This results in a discrepancy between the advertised maxApr
and the actual APR borrowers pay (maxAPR + 28). Borrowers unknowingly pay higher interest, violating the APR constraint defined in the borrow order.
This vulnerability occurs not only for "APR" but also for "Ratio".
-
Integer division precision loss:
- The
updatedLastApr
calculation uses integer division, truncating fractional values: - https://github.com/sherlock-audit/2024-11-debita-finance-v3/blob/main/Debita-V3-Contracts/contracts/DebitaV3Aggregator.sol#L477
uint updatedLastApr = (weightedAverageAPR[principleIndex] * amountPerPrinciple[principleIndex]) / (amountPerPrinciple[principleIndex] + lendAmountPerOrder[i]);
- Over multiple iterations, the truncation compounds, reducing the computed
weightedAverageAPR
.
- The
-
Small lend amounts:
- Small values in
lendAmountPerOrder
(e.g.,1
) exacerbate precision loss as their contribution is disproportionately affected by rounding.
- Small values in
-
Discrepancy between enforcement and charged APR:
- Borrowers expect
weightedAverageAPR
to adhere tomaxApr
, but the actual APR charged is derived from the lenders' APR (lendInfo.apr
), which remains unaffected by rounding.
- Borrowers expect
- Borrower's
maxApr
: 3,000. - Attack Plan: Exploit the precision loss in
weightedAverageAPR
calculation by creating multipleLendOffers
with highly skewedlendAmount
values and a consistentlendInfo.apr
of 3028. The goal is to manipulate the calculation so that theweightedAverageAPR
appears to be 3000, while the borrower effectively pays 3028 APR.
-
Borrower Prepares to Take Loan:
- The borrower creates a borrow order with a collateral that allows an
availableAmount
of 10,028e12 and specifies amaxApr
of 3000.(10,028e12 and 3000 are selected for simple calculation and any values are ok.)
- The borrower creates a borrow order with a collateral that allows an
-
Attacker Creates Lend Offers:
- The attacker creates 29 LendOffers, distributing the
lendAmount
as follows:- LendOffer 1:
lendAmount = 10000e12
,lendInfo.apr = 3028
. - LendOffers 2 to 29:
lendAmount = 1e12
,lendInfo.apr = 3028
.
- LendOffer 1:
This vulnerability occurs not only for "APR" but also for "Ratio".
- The attacker creates 29 LendOffers, distributing the
- Borrower Overpayment:
- Borrowers pay more interest than anticipated, leading to hidden costs.
What is more serious is when the borrower calculates the exact amount of token to pay the debt and then proceeds payDebt
, it will be reverted with unexpected reason. So the severity is HIGH.
The larger the amount of token borrowed, the more severe the consequences.
-
Protocol Reputation Damage:
- This discrepancy undermines trust, as the protocol does not transparently enforce the advertised
maxApr
.
- This discrepancy undermines trust, as the protocol does not transparently enforce the advertised
-
Legal and Regulatory Risks:
- Failure to enforce accurate APR disclosures could expose the protocol to legal challenges.
lendAmountPerOrder
:[10000e18, 1e18, 1e18, ..., 1e18]
(29 values - lendOrders.length < 30).lendInfo.apr
:3028
for all lenders.- Borrow order
maxApr
:3000
.
-
Calculated
weightedAverageAPR
:
weightedAverageAPR = (3028 * 10000e18) / 10000e18= 3027.
weightedAverageAPR = (3027 * 10000e18) / 10001e18 + (3028 * 1e18) / 10001e18 = 3026.
weightedAverageAPR = (3026 * 10001e18) / 10002e18 + (3028 * 1e18) / 10002e18 = 3025.
weightedAverageAPR = (3025 * 10002e18) / 10003e18 + (3028 * 1e18) / 10003e18 = 3024.
... After 100 iterations,weightedAverageAPR
is truncated to 3000 due to integer division. -
Actual APR Charged:
Borrower is charged an effective APR of 3028, derived directly from the lenders'lendInfo.apr
.
Length of borrow = 80day!
The borrow calculates the max interest with apr=3000:
interest = 10028e18 * 3000 / 10000 * 80days / 31536000 = 659e18
total = 10028e18 + 659e18 = 10,687e18
So the borrow prepares the amount of 10687e18 and tries to payDebt
on the last day of borrow.
If it's 12h before the borrow finish, the borrower thinks that amount is sufficient since there are 12 hours left..
But the real interest(79.day, 3000 apr) and the attacked interest(79.5day, 3028 apr) are:
real interest = 10028e18 * 3000 / 10000 * 79.5days / 31536000 = 654e18
attacked interest = 10028e18 * 3028 / 10000 * 79.5days / 31536000 = 661e18
loss = 7e18
This loss cannot be viewed as a simple precision loss, and the longer the loan period and loan amount, the more significant the loss.
-
Accumulate Weighted Contributions and Divide by Total Principle Amount:
- For each principle, calculate the weighted contribution of each lender’s APR (lendInfo.apr) based on their lent amount (lendAmountPerOrder[i]).:
weightedAverageRatio[principleIndex] += lendInfo.apr * lendAmountPerOrder[i]; amountPerPrinciple[principleIndex] += lendAmountPerOrder[i];
- After processing all lend orders, calculate the weighted average by dividing the accumulated value by the total amount lent for the given principle (amountPerPrinciple[principleIndex]).
weightedAverageRatio[principleIndex] /= amountPerPrinciple[principleIndex];
- For each principle, calculate the weighted contribution of each lender’s APR (lendInfo.apr) based on their lent amount (lendAmountPerOrder[i]).:
-
Use fixed-point arithmetic:
- Perform calculations with higher precision (e.g., 18 decimals) using fixed-point libraries like OpenZeppelin's
SafeMath
orPRBMath
:uint updatedLastApr = (weightedAverageAPR[principleIndex] * amountPerPrinciple[principleIndex] * 10**PRECISION) / ((amountPerPrinciple[principleIndex] + lendAmountPerOrder[i]) * 10**PRECISION);
- Perform calculations with higher precision (e.g., 18 decimals) using fixed-point libraries like OpenZeppelin's
-
Cap Small Contributions:
- Set a minimum threshold for
lendAmountPerOrder
to ensure meaningful contributions:require(lendAmountPerOrder[i] >= MINIMUM_AMOUNT, "Lend amount too small");
- Set a minimum threshold for