Boost _ Folks Finance 33947 - [Smart Contract - Low] During liquidations when borrowToRepay collateral the liquidator pays more borrowAmount than they should and receives no bonus
Submitted on Fri Aug 02 2024 09:38:03 GMT-0400 (Atlantic Standard Time) by @iamandreiski for Boost | Folks Finance
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
When a user is undercollateralized and eligible for liquidation, a liquidator can initiate a liquidation process in which the borrowed amount(either partial or whole) of the token-in-question + interest will be transferred to the liquidator for later repayment as well as the collateral in the amount of the borrowed token + 10% liquidation bonus. The problem arises when the borrow amount that should be repaid is greater than the collateral amount to-be-received by the liquidator. In those cases, due to invalid calculations, the liquidator will actually receive 10% more borrow amount to repay (or 10% less collateral than the borrow amount), effectively damaging the liquidator.
Vulnerability Details
There are more than one outcome in which this can result:
Violator will pay 10% less collateral for the borrowed amount, effectively "incentivizing" users with underwater loans.
This will disincentivize liquidators to liquidate these kind of loans effectively leading to the accumulation of more bad debt and essentially undercollateralized loans / protocol/pool insolvency.
When a user initiates a liquidation, eventually calcLiquidationAmounts() will be called in order to calculate the collateral and borrow amounts based on amounts owed, user input, etc.
It will take the smaller amount between a user-input value of the amount that they'd want to liquidate OR either the balance of the loan OR the maxRepayBorrowAmount (which is a calculation based upon the amount that needs to be liquidated to make the loan healthy).
For the sake of this situation let's say that the violatorLoanBorrow.balance was picked as the "smallest" to be liquidated, which is the total balance of the loan of that pool.
After the above-mentioned amount was determined, it will be converted to its equivalent collateral value:
For the sake of this example, let's say that the borrowed amount to be repaid is 1 WETH, and the collateral is USDC, with the WETH/USDC price at 3000 USDC.
In underlying collateral amount, this would be 3300 USDC (Taking in consideration the liquidation bonus(if it's 10%)).
The problem arises if the collateral that the user has in the pool is less than this, let's say 2500 USDC.
This would result in converting 2500 USDC to WETH, or 0.83 WETH, the problem is that the liquidation bonus would be added to this value:
0.833 * (1e4 + 0.1e4) / 1e4 = 0.916
This would cause the liquidator to receive only 2500 USDC collateral, but pay the equivalent of 2750 USDC in borrow amount, due to the incorrect calculation.
It should be : assetAmount * 1e4 / (1e4 + liquidationBonus)
Even if liquidators intentionally input lower-than-collateral maxAmountToRepay values, after some time this will also result in bad debt accumulation as the collateral would be transferred to liquidators, but there would be "residual" borrow amounts.
Impact Details
Loans in which the collateral is less than the amount-to-be-repayed will result in the liquidator paying 10% more borrowAmount, while receiving 10% less collateral amount. This is effectively damaging the liquidator in the benefit of the borrower, and could potentially lead to a pool insolvency if enough bad debt is accumulated.
References
Below PoC is in Foundry, the only thing needed to run it is the importation of MathUtils in the test suite.
Proof of concept
Proof of Concept
function testLiquidationCalcs() public {
//WETH/USDC Price: 3000 USDC per WETH
//Collateral in USDC pool: 2500 USDC
//Outstanding borrow amount: 1 WETH
uint256 repayBorrowAmount = 1e18;
uint256 collateralInPool = 2500e6;
//First we turn the the borrow token into collateral amount:
uint256 seizeUnderlyingCollateralAmount = repayBorrowAmount.convToSeizedCollateralAmount(
1e18,
6,
3000e18,
18,
0.1e4
);
console.log("Seized Collateral (USDC)", seizeUnderlyingCollateralAmount);
assertEq(seizeUnderlyingCollateralAmount, 3300e6);
if (seizeUnderlyingCollateralAmount > collateralInPool) {
seizeUnderlyingCollateralAmount = collateralInPool;
repayBorrowAmount = seizeUnderlyingCollateralAmount.convToRepayBorrowAmount(
1e18,
6,
3000e18,
18,
0.1e4
);
}
console.log("Repay Borrow Amount: (WETH)", repayBorrowAmount);
assertEq(repayBorrowAmount, 916666666666666666); // OR 0.916e18
//We'll also convert the new repayBorrowAmount to its collateral counterpart just to compare it:
uint256 newRepayCollateralCounterpart = repayBorrowAmount.convToSeizedCollateralAmount(
1e18,
6,
3000e18,
18,
0 // Here we're assigning the bonus as 0 not to mess up calculations
);
console.log("New Repay Borrow Amount in Collateral (USDC):", newRepayCollateralCounterpart);
assertEq(newRepayCollateralCounterpart, 2749999999); // OR 2749e6
assert(newRepayCollateralCounterpart > collateralInPool);
}