sherlock-audit / 2024-04-interest-rate-model-judging

9 stars 5 forks source link

elhaj - Precision Loss in `repayAtMaturity` can lead to system insolvency and loss of funds #215

Closed sherlock-admin3 closed 4 months ago

sherlock-admin3 commented 5 months ago

elhaj

high

Precision Loss in repayAtMaturity can lead to system insolvency and loss of funds

Summary

Vulnerability Detail

 function scaleProportionally(Position memory position, uint256 amount) internal pure returns (Position memory) {
   uint256 principal = amount.mulDivDown(position.principal, position.principal + position.fee);
   position.principal = principal;
   position.fee = amount - principal;
   return position;
 }

and then we reduce the amount floatingBackupBorrowed and pool.borrowed by principal that we get from scaleProportionally function :

floatingBackupBorrowed -= pool.repay(principalCovered);
function reduceProportionally(Position memory position, uint256 amount) internal pure returns (Position memory) {
    uint256 positionAssets = position.principal + position.fee;
    uint256 newPositionAssets = positionAssets - amount;
    position.principal = newPositionAssets.mulDivDown(position.principal, positionAssets);
    position.fee = newPositionAssets - position.principal;
    return position;
  }

User A decides to repay 100 wei:

  1. Calculate principalCovered:

    • Using scaleProportionally:
      • principalCovered = (100 wei * 1000 wei) / (1000 wei + 100 wei) = 90 wei (rounded down).
    • Update floatingBackupBorrowed and pool[1].borrowed:
      • floatingBackupBorrowed = 1000 wei - 90 wei = 910 wei.
      • pool[1].borrowed = 910 wei.
  2. Calculate User A's new position:

    • Using reduceProportionally:
      • New total position assets after repayment: 1100 wei - 100 wei = 1000 wei.
      • userPrincipal = (1000 wei * 1000 wei) / 1100 wei = 909 wei (rounded down).
      • userA[1].fee = 1000 wei - 909 wei = 91 wei.

Discrepancy:

POC :

add this test :

  function test_precisionLoss() external {
   market.deposit(12 ether, address(this));
   // deposit
   market.borrowAtMaturity(FixedLib.INTERVAL, 1 ether, 1.5 ether, address(this), address(this));

   // get values before maturity  before :
   (uint256 principalBefore, uint256 feesBefore) = market.fixedBorrowPositions(FixedLib.INTERVAL, address(this));
   (uint borrowedBefore,,,) = market.fixedPools(FixedLib.INTERVAL);
   uint backUpBefore = market.floatingBackupBorrowed();
   // audit : repaying in a loop with 1 wei each time will decrease the position principal , but not borrowedbackup :
   for (uint256 i; i < 1000; i++) {
     market.repayAtMaturity(FixedLib.INTERVAL, 1, 1, address(this));
   }
   uint backUpAfter = market.floatingBackupBorrowed();
   ( uint borrowedAfter,,,) = market.fixedPools(FixedLib.INTERVAL);
   // get user maturity :
   (uint256 principalAfter, uint256 feesAfter) = market.fixedBorrowPositions(FixedLib.INTERVAL, address(this));
   assertEq(backUpAfter,backUpBefore);// floatingBackupBorrowed didn't change
   assertEq(borrowedAfter,borrowedBefore);// pool.borrowed didn't change
   assertEq(principalBefore - principalAfter , 1000);// fixedBorrowPositions[address(this)].principal reduced by 1000 (we loop 1000 time)

 }

Impact

Code Snippet

Tool used

Manual Review

Recommendation

 function reduceProportionally(Position memory position, uint256 amount) internal pure returns (Position memory) {
-   position.principal = newPositionAssets.mulDivDown(position.principal, positionAssets);
+   position.principal = newPositionAssets.mulDivUp(position.principal, positionAssets);
    position.fee = newPositionAssets - position.principal;
    return position;
}
santipu03 commented 4 months ago

This issue is low (invalid) because it involves the loss of dust amounts of funds, which don't have any impact on the overall protocol behavior.

0xumarkhatab commented 4 months ago

The issue causes loss of funds . at least sounds clear to me.

Even the claim is backed by PoC

Accumulation of small dust can lead to a big value causing a lot of miscalculations in the rates .

santipu03 commented 4 months ago

From the Sherlock guidelines:

The losses must exceed small, finite amount of funds, and any amount relevant based on the precision or significance of the loss.

Your PoC shows a loss of 1e4 assets, that is 0.000000000001% of 1e18. Even if an attacker repeats for millions of times this attack, losing gas fees, it wouldn't even be significant.

elhajin commented 4 months ago
santipu03 commented 4 months ago

If we consider that for every repayment of a fixed loan, 1 wei of assets can become stuck in the pool, this may become a problem in the long run for the WBTC pool. A user could maybe cause a grief attack on the WBTC pool by executing this attack in a loop but it wouldn't make much sense because the gas fees would be high and there's no profit on doing it.

The main difference between this issue and 68 is that there's no profit in executing an attack like this, and the damage is more limited than 68. However, I admit that this issue can be triggered organically, even though the impact will be much more limited because it'd need a ton of fixed loan repayments.

After reconsidering all the facts, I believe this issue is borderline low/medium. The head of judging will have the final say on this.

cvetanovv commented 4 months ago

@elhajin This is a Low/Information severity issue. 1 wei is an extremely small value, even for 8 decimal tokens.

The attack vector you described in the issue is unrealistic, even for L2, where the gas is lower.

One transaction in optimism costs 0.1 gwei - https://tokentool.bitbond.com/gas-price/optimism

This is equal to 100000000 Wei. You can use this calculator to get an idea of how small 1 Wei is - https://eth-converter.com/