Boost _ Folks Finance 34179 - [Smart Contract - High] Incorrect Updates to pooldepositDatatotalAmount and loancollateralUsed During Repayment with Collateral
Submitted on Tue Aug 06 2024 06:41:08 GMT-0400 (Atlantic Standard Time) by @alix_40 for Boost | Folks Finance
Smart contract unable to operate due to lack of token funds
Description
This report is intended to be submitted under my team account "A2Security" but I reached the report submission rate limit on the last day. Please count this report as though it were from "A2Security".
Description
The protocol's repayment with collateral mechanism contains a mathematical flaw in its accounting logic, specifically affecting the updates of pool.depositData.totalAmount during repayment operations.
In a properly functioning system, depositData.totalAmount should never exceed the sum of the pool's actual balance plus the total borrowed amount. This invariant ensures accurate representation of the pool's financial state and is crucial for various protocol operations.
The repayment with collateral process involves users repaying their loans using their deposited collateral instead of external funds.However, the current implementation in LoanManagerLogic.sol incorrectly updates the totalAmount
function updateWithRepayWithCollateral(HubPoolState.PoolData storage pool, uint256 principalPaid, uint256 interestPaid, uint256 loanStableRate)
externalreturns (DataTypes.RepayWithCollateralPoolParamsmemory repayWithCollateralPoolParams){// ... other code ... pool.depositData.totalAmount -= principalPaid - interestPaid; // Incorrect update// ... rest of the function}
It subtracts principalPaid - interestPaid from totalAmount, which effectively reduces the total amount by the principal and increases it by the interest. This is incorrect because the interest should not be added to totalAmount.
The issue arises because when a user repays with collateral, the interest they're paying is already accounted in pool.depositData.totalAmount. This amount was included when the user initially deposited their collateral. By adding the interest again during repayment, we're double-counting this value, leading to an artificial inflation of totalAmount.
This mishandling leads to several issues:
pool.depositData.totalAmount becomes inflated, exceeding the actual balance held in the pool.
The discrepancy between totalAmount and the actual pool balance grows over time, compounding with each repayment using collateral.
The inflated totalAmount affects critical calculations such as utilization ratios, interest rates, and liquidity assessments.
As an example lets take the following scenario:
A user deposits 1000 USDC into the pool.
loan.collateralUsed = 1000 USDC(in terms of FUSDC)
pool.depositData.totalAmount = 1000 USDC
loan.borrowUsed = 0
actual poolHub balance = 1000 USDC
The user borrows 900 USDC against this deposit. A after some time the user repays the loan with his collateral with interest of 60 USDC, the state after the repayment will be :
This shows a mismatch where pool.depositData.totalAmount is higher than the actual pool balance, which should not happen.
Impact
The inflated totalDepositAmount in the protocol leads to several significant effects:
Utilization Ratio: The utilization ratio calculated in HubPoolLogic.sol is artificially lowered, as it uses totalDeposits in the denominator. This makes it appear that a smaller portion of available funds is being utilized.
Interest Rates: Both borrowing and lending interest rates, derived from the utilization ratio, are affected. The lower perceived utilization results in the protocol setting lower interest rates than it should, potentially underpricing risk.
Deposit Indexes: Deposit interest index calculations are impacted, leading to an underestimation of depositors' share growth over time. T Due to the undervalued deposits index resulting from the false utilization ratio (lower UT means lower Ftoken value ), a portion of user funds become locked in the protocol, as the calculated value of their Ftoken would be less than the actual value.
Caps and Limits: Functions like isDepositCapReached or isBorrowCapReached operate with incorrect values.
These effects compound over time, leading to increasing discrepancies between the protocol's perceived state and its actual financial position. which will result in significant financial inaccuracies .
Tools Used
Manual Review
Recomandation :
In the updateWithRepayWithCollateral function in LoanManagerLogic.sol we should only subtract the principal from totalAmount:
function updateWithRepayWithCollateral(HubPoolState.PoolData storage pool, uint256 principalPaid, uint256 interestPaid, uint256 loanStableRate)
external
returns (DataTypes.RepayWithCollateralPoolParams memory repayWithCollateralPoolParams)
{
// ... other code ...
- uint256 fAmountRepaid = principalPaid - interestPaid
+ pool.depositData.totalAmount -= principalPaid;
// ... rest of the function
}
Proof of concept
Proof of Concept
here a coded poc shows how repaying with collateral will lead to an inflated totalDepositAmount that is more than the actual pool balance + total borrowed amount ,
please run test with : forge test --mt test_poc_01 -vvv --via-ir
[PASS] test_poc_01() (gas: 1454096)Logs: amount to repay 4327641473 actual balance before : 2715350070 totalDeposited minus borrowed before : 2714348991 actual balance after : 2872986419 totalDeposited minus borrowed after : 3199626813
to run test please first add the following file to test/pocs/base_test.sol
then please add the poc test to test/pocs/forktest.t.sol
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.23;import"./base_test.sol";import"contracts/oracle/storage/NodeDefinition.sol";contractforktestisbaseTest {////////////////////////////////////////////////////////////////////////////////////////////////////// poc incorrect calculation for repayWithCollateral /////////////////////////////////////////////////////////////////////////////////////////////////////// @note : you should run this test with --via-ir flag :functiontest_poc_01() public {// get the prestate vars :uint256 totalDepositBefore = hubPoolUsdc.getDepositData().totalAmount; uint256 totalBorrowBefore = hubPoolUsdc.getVariableBorrowData().totalAmount + hubPoolUsdc.getStableBorrowData().totalAmount;
uint256 poolActualBalance =IERC20(USDC_TOKEN).balanceOf(address(hubPoolUsdc));// update caps : vm.startPrank(LISTING_ROLE); loanManager.updateLoanPoolCaps(2, hubPoolUsdc.getPoolId(),1e12,1e11); hubPoolUsdc.updateCapsData(HubPoolState.CapsData(type(uint64).max, type(uint64).max,1e18)); vm.stopPrank();// bob deposit some token as collateral in a pool iduint256 bobDeposit =1e10; // 10000 usdc_approveUsdc(bob,address(spokeUsdc), bobDeposit);_deposit(bob, bobAccountId, bobLoanIds[0], bobDeposit, spokeUsdc);// borrow same token given that collateral factor is 0.5 :uint256 borrowAmount =0.4e10; // 4000usdc_borrowVariable(bob, bobAccountId, bobLoanIds[0], hubPoolUsdc.poolId(), borrowAmount);// skip some time and repay your loan with collateralskip(730days); // two years// update intrestIndexes : hubPoolUsdc.updateInterestIndexes();// get the user loan after some time : (,, uint8[] memory colPools, uint8[] memory borPools, LoanManagerState.UserLoanCollateral[] memory coll, LoanManagerState.UserLoanBorrow[] memory borr) =
loanManager.getUserLoan(bobLoanIds[0]);assertTrue(colPools.length == borPools.length && colPools.length ==1,"not 1 length");assertTrue(coll.length == borr.length && coll.length ==1,"not 1 length structs ");// get the dpositData.totalAmount , and check that is more then the actual funds in the pooluint256 poolVariableInterestIndex = hubPoolUsdc.getVariableBorrowData().interestIndex; uint256 amountToRepay = MathUtils.calcBorrowBalance(borr[0].balance, poolVariableInterestIndex, borr[0].lastInterestIndex);
console.log("amount to repay ", amountToRepay);_repayWithCollateral(bob, bobAccountId, bobLoanIds[0], amountToRepay, hubPoolUsdc.poolId());// bob withdraws remaining collateral : (uint8 pId,uint256 collBall) =getPoolAndColl(bobLoanIds[0]);_withdraw(bob, bobAccountId, bobLoanIds[0], pId, collBall,true);// get the post state vars :uint256 totalDepositAfter = hubPoolUsdc.getDepositData().totalAmount; uint256 totalBorrowAfter = hubPoolUsdc.getVariableBorrowData().totalAmount + hubPoolUsdc.getStableBorrowData().totalAmount;
uint256 poolActualBalanceAfter =IERC20(USDC_TOKEN).balanceOf(address(hubPoolUsdc));// logs :// now notice that : totalDeposit in this case is inflated and it's actually more then the actual balance :uint256 availableBalanceBefore = (totalDepositBefore - totalBorrowBefore);uint256 availableBalanceAfter = totalDepositAfter - totalBorrowAfter; console.log("actual balance before : ", poolActualBalance); console.log("totalDeposited minus borrowed before : ", availableBalanceBefore); console.log("actual balance after : ", poolActualBalanceAfter); console.log("totalDeposited minus borrowed after : ", availableBalanceAfter); } }