Platform: Code4rena
Start Date: 08/05/2023
Pot Size: $90,500 USDC
Total HM: 17
Participants: 102
Period: 7 days
Judge: 0xean
Total Solo HM: 4
Id: 236
League: ETH
Rank: 11/102
Findings: 1
Award: $1,409.77
🌟 Selected for report: 1
🚀 Solo Findings: 0
1409.77 USDC - $1,409.77
Users might face unjust liquidation of their assets even after exiting a particular market. This could lead to potential financial losses for users, and it might undermine the trust and reputation of the platform.
Consider a user with the following financial status:
$30,000
, and 10,000 USDT, worth $10,000
.The user decides to remove their risk from BTC volatility and exits the BTC market. As per the protocol's rules, exiting the market should remove BTC from their collateral.
Following the user's exit from the BTC market, a sharp rise in the ETH price occurs, and it surpasses $10,000.
Due to the increase in ETH price, the system identifies that the user's collateral (now only 10,000 USDT) is insufficient to cover their loan, leading to an insufficient collateralization rate.
Despite the user's exit from the BTC market, the system still triggers a liquidation process to liquidating BTC as collateral.
In reality, if the BTC was still part of the user's collateral, the total collateral value would have been $40,000 ($30,000 from BTC and $10,000 from USDT). This total value would be sufficient to cover the ETH loan even with the price surge of ETH. Therefore, the user should not have faced liquidation.
This can be traced back to the missing membership check in preLiquidateHook
function which does not consider if the user has exited the market or not.
function preLiquidateHook( address vTokenBorrowed, address vTokenCollateral, address borrower, uint256 repayAmount, bool skipLiquidityCheck ) external override { // Pause Action.LIQUIDATE on BORROWED TOKEN to prevent liquidating it. // If we want to pause liquidating to vTokenCollateral, we should pause // Action.SEIZE on it _checkActionPauseState(vTokenBorrowed, Action.LIQUIDATE); oracle.updatePrice(vTokenBorrowed); oracle.updatePrice(vTokenCollateral); if (!markets[vTokenBorrowed].isListed) { revert MarketNotListed(address(vTokenBorrowed)); } if (!markets[vTokenCollateral].isListed) { revert MarketNotListed(address(vTokenCollateral)); } uint256 borrowBalance = VToken(vTokenBorrowed).borrowBalanceStored(borrower); /* Allow accounts to be liquidated if the market is deprecated or it is a forced liquidation */ if (skipLiquidityCheck || isDeprecated(VToken(vTokenBorrowed))) { if (repayAmount > borrowBalance) { revert TooMuchRepay(); } return; } /* The borrower must have shortfall and collateral > threshold in order to be liquidatable */ AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); if (snapshot.totalCollateral <= minLiquidatableCollateral) { /* The liquidator should use either liquidateAccount or healAccount */ revert MinimalCollateralViolated(minLiquidatableCollateral, snapshot.totalCollateral); } if (snapshot.shortfall == 0) { revert InsufficientShortfall(); } /* The liquidator may not repay more than what is allowed by the closeFactor */ uint256 maxClose = mul_ScalarTruncate(Exp({ mantissa: closeFactorMantissa }), borrowBalance); if (repayAmount > maxClose) { revert TooMuchRepay(); } } /** * @notice Checks if the seizing of assets should be allowed to occur * @param vTokenCollateral Asset which was used as collateral and will be seized * @param seizerContract Contract that tries to seize the asset (either borrowed vToken or Comptroller) * @param liquidator The address repaying the borrow and seizing the collateral * @param borrower The address of the borrower * @custom:error ActionPaused error is thrown if seizing this type of collateral is paused * @custom:error MarketNotListed error is thrown if either collateral or borrowed token is not listed * @custom:error ComptrollerMismatch error is when seizer contract or seized asset belong to different pools * @custom:access Not restricted */ function preSeizeHook( address vTokenCollateral, address seizerContract, address liquidator, address borrower ) external override { // Pause Action.SEIZE on COLLATERAL to prevent seizing it. // If we want to pause liquidating vTokenBorrowed, we should pause // Action.LIQUIDATE on it _checkActionPauseState(vTokenCollateral, Action.SEIZE); if (!markets[vTokenCollateral].isListed) { revert MarketNotListed(vTokenCollateral); } if (seizerContract == address(this)) { // If Comptroller is the seizer, just check if collateral's comptroller // is equal to the current address if (address(VToken(vTokenCollateral).comptroller()) != address(this)) { revert ComptrollerMismatch(); } } else { // If the seizer is not the Comptroller, check that the seizer is a // listed market, and that the markets' comptrollers match if (!markets[seizerContract].isListed) { revert MarketNotListed(seizerContract); } if (VToken(vTokenCollateral).comptroller() != VToken(seizerContract).comptroller()) { revert ComptrollerMismatch(); } } // Keep the flywheel moving uint256 rewardDistributorsCount = rewardsDistributors.length; for (uint256 i; i < rewardDistributorsCount; ++i) { rewardsDistributors[i].updateRewardTokenSupplyIndex(vTokenCollateral); rewardsDistributors[i].distributeSupplierRewardToken(vTokenCollateral, borrower); rewardsDistributors[i].distributeSupplierRewardToken(vTokenCollateral, liquidator); } }
In essence, the user is punished for market volatility even after they have taken steps to protect themselves (by exiting the BTC market).
ChatGPT 4
Update the preLiquidateHook
function to check if a user has exited a market before proceeding with liquidation.
Invalid Validation
#0 - c4-sponsor
2023-05-23T20:48:55Z
chechu marked the issue as sponsor disputed
#1 - chechu
2023-05-23T20:49:01Z
This is the desired behaviour. Even if user exists an market we expect user to maintain healthy position to protect the protocol.
#2 - 0xean
2023-05-30T22:43:57Z
@chechu - By exiting the market, the user is no longer expecting those assets to be used as collateral
* @notice Removes asset from sender's account liquidity calculation; disabling them as collateral
So while the user is still expected to maintain a healthy position with the assets they have marked as being part of their collateral, do you agree that assets that are not being used as part of their collateral should not be seized?
#3 - c4-judge
2023-05-30T23:21:55Z
0xean marked the issue as primary issue
#4 - 0xean
2023-05-30T23:40:00Z
adding #67 to the conversation here as its really the same underlying concern.
#5 - chechu
2023-05-31T11:44:53Z
We have been reviewing this topic internally. Compound allows the seizing of tokens from markets not enabled as collateral. And our code does the same.
But, we added the sentence disabling them as collateral
in the Comptroller.exitMarket
function, so we think it's fair to forbid seizing if the borrower didn't enable the market as collateral.
So, I would say the issue is valid, and we'll work to mitigate it (we'll add the check in the preSeizeHook
, that is also used in the healAccount
flow)
#6 - 0xean
2023-05-31T15:56:16Z
thanks @chechu! Do you mind removing the Sponsor Disputed tag?
#7 - c4-sponsor
2023-06-01T20:38:43Z
chechu marked the issue as sponsor confirmed
#8 - c4-judge
2023-06-05T14:30:56Z
0xean marked the issue as satisfactory
#9 - c4-judge
2023-06-05T17:01:37Z
0xean marked the issue as selected for report