From d2f5cd0271748e2d8c539ebdc4d6e217eff602f5 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 7 Jul 2025 12:00:39 +0530 Subject: [PATCH 01/51] feat: add new vars in AccountLiquiditySnapshot --- contracts/ComptrollerStorage.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 4f698f0b0..ee8092fd4 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -27,6 +27,9 @@ contract ComptrollerStorage { uint256 effects; uint256 liquidity; uint256 shortfall; + uint256 weightavg; + uint256 healthFactor; + uint256 healthFactorThreshold; } struct RewardSpeeds { From 1f2d92bbf3d42b36d1dc4f3096d55f1f0a234cc9 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 7 Jul 2025 12:01:44 +0530 Subject: [PATCH 02/51] feat: modify comptroller interface --- contracts/ComptrollerInterface.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index 662a6aef9..00e16a118 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -95,6 +95,7 @@ interface ComptrollerInterface { /*** Liquidity/Liquidation Calculations ***/ function liquidateCalculateSeizeTokens( + address borrower, address vTokenBorrowed, address vTokenCollateral, uint256 repayAmount @@ -132,4 +133,6 @@ interface ComptrollerViewInterface { function supplyCaps(address) external view returns (uint256); function approvedDelegates(address user, address delegate) external view returns (bool); + + function getLiquidationIncentive(address borrower) external view returns (uint256); } From a2ac784f1e00e4aaf0ed4fe599f06daa41b6c65a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 7 Jul 2025 12:02:30 +0530 Subject: [PATCH 03/51] feat: add dynamic close factor and liquidation incentive --- contracts/Comptroller.sol | 50 +++++++++++++++++++++++++++++++++++---- contracts/VToken.sol | 7 ++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 8d25f5ad4..120289897 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -755,8 +755,22 @@ contract Comptroller is revert InsufficientShortfall(); } + uint256 closeFactor; + unchecked { + if (snapshot.healthFactor >= 1e18) revert InsufficientShortfall(); + uint256 wtAvg = snapshot.weightavg; + if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { + uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; + uint256 denominator = borrowBalance * (1e18 - ((wtAvg * (1e18 + liquidationIncentiveMantissa)) / 1e18)); + closeFactor = (numerator * 1e18) / denominator; + closeFactor = closeFactor > 1e18 ? 1e18 : closeFactor; + } else { + closeFactor = 1e18; + } + } + /* The liquidator may not repay more than what is allowed by the closeFactor */ - uint256 maxClose = mul_ScalarTruncate(Exp({ mantissa: closeFactorMantissa }), borrowBalance); + uint256 maxClose = mul_ScalarTruncate(Exp({ mantissa: closeFactor }), borrowBalance); if (repayAmount > maxClose) { revert TooMuchRepay(); } @@ -894,7 +908,7 @@ contract Comptroller is Exp memory collateral = Exp({ mantissa: snapshot.totalCollateral }); Exp memory scaledBorrows = mul_( Exp({ mantissa: snapshot.borrows }), - Exp({ mantissa: liquidationIncentiveMantissa }) + Exp({ mantissa: getLiquidationIncentive(user) }) ); Exp memory percentage = div_(collateral, scaledBorrows); @@ -943,7 +957,7 @@ contract Comptroller is } uint256 collateralToSeize = mul_ScalarTruncate( - Exp({ mantissa: liquidationIncentiveMantissa }), + Exp({ mantissa: getLiquidationIncentive(borrower) }), snapshot.borrows ); if (collateralToSeize >= snapshot.totalCollateral) { @@ -1375,6 +1389,7 @@ contract Comptroller is /** * @notice Calculate number of tokens of collateral asset to seize given an underlying amount * @dev Used in liquidation (called in vToken.liquidateBorrowFresh) + * @param borrower The address of the borrower * @param vTokenBorrowed The address of the borrowed vToken * @param vTokenCollateral The address of the collateral vToken * @param actualRepayAmount The amount of vTokenBorrowed underlying to convert into vTokenCollateral tokens @@ -1383,6 +1398,7 @@ contract Comptroller is * @custom:error PriceError if the oracle returns an invalid price */ function liquidateCalculateSeizeTokens( + address borrower, address vTokenBorrowed, address vTokenCollateral, uint256 actualRepayAmount @@ -1403,7 +1419,10 @@ contract Comptroller is Exp memory denominator; Exp memory ratio; - numerator = mul_(Exp({ mantissa: liquidationIncentiveMantissa }), Exp({ mantissa: priceBorrowedMantissa })); + numerator = mul_( + Exp({ mantissa: getLiquidationIncentive(borrower) }), + Exp({ mantissa: priceBorrowedMantissa }) + ); denominator = mul_(Exp({ mantissa: priceCollateralMantissa }), Exp({ mantissa: exchangeRateMantissa })); ratio = div_(numerator, denominator); @@ -1500,6 +1519,20 @@ contract Comptroller is return assetsIn; } + /// @notice Get the liquidation incentive for a borrower + /// @param borrower The address of the borrower + /// @return incentive The liquidation incentive for the borrower, scaled by 1e18 + function getLiquidationIncentive(address borrower) public view returns (uint256 incentive) { + AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); + + if (snapshot.healthFactor >= snapshot.healthFactorThreshold) return liquidationIncentiveMantissa; + + unchecked { + uint256 value = ((snapshot.healthFactor * 1e18) / snapshot.weightavg) - 1e18; + return value > liquidationIncentiveMantissa ? liquidationIncentiveMantissa : value; + } + } + /** * @notice Add the market to the borrower's "assets in" for liquidity calculations * @param vToken The market to enter @@ -1661,6 +1694,12 @@ contract Comptroller is // borrows += oraclePrice * borrowBalance snapshot.borrows = mul_ScalarTruncateAddUInt(oraclePrice, borrowBalance, snapshot.borrows); + Exp memory weightSum; + weightSum = add_(weightSum, weight(asset)); + if (i == assetsCount - 1) { + snapshot.weightavg = weightSum.mantissa / assetsCount; + } + // Calculate effects of interacting with vTokenModify if (asset == vTokenModify) { // redeem effect @@ -1674,6 +1713,9 @@ contract Comptroller is } uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; + snapshot.healthFactor = div_(snapshot.weightedCollateral, borrowPlusEffects); + snapshot.healthFactorThreshold = div_(snapshot.weightavg * (1e18 + liquidationIncentiveMantissa), 1e18); + // These are safe, as the underflow condition is checked first unchecked { if (snapshot.weightedCollateral > borrowPlusEffects) { diff --git a/contracts/VToken.sol b/contracts/VToken.sol index 0e12b02f7..e70f24ada 100644 --- a/contracts/VToken.sol +++ b/contracts/VToken.sol @@ -1221,6 +1221,7 @@ contract VToken is /* We calculate the number of collateral tokens that will be seized */ (uint256 amountSeizeError, uint256 seizeTokens) = comptroller.liquidateCalculateSeizeTokens( + borrower, address(this), address(vTokenCollateral), actualRepayAmount @@ -1274,8 +1275,10 @@ contract VToken is * borrowerTokensNew = accountTokens[borrower] - seizeTokens * liquidatorTokensNew = accountTokens[liquidator] + seizeTokens */ - uint256 liquidationIncentiveMantissa = ComptrollerViewInterface(address(comptroller)) - .liquidationIncentiveMantissa(); + + uint256 liquidationIncentiveMantissa = ComptrollerViewInterface(address(comptroller)).getLiquidationIncentive( + borrower + ); uint256 numerator = mul_(seizeTokens, Exp({ mantissa: protocolSeizeShareMantissa })); uint256 protocolSeizeTokens = div_(numerator, Exp({ mantissa: liquidationIncentiveMantissa })); uint256 liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens; From 39c970d81b41a1ac85d2c75fb300bd5e8cac936c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 8 Jul 2025 18:13:34 +0530 Subject: [PATCH 04/51] feat: add contract-sizer in hardhat config --- hardhat.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index 1af9cc4cf..e8ac62b29 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -7,6 +7,7 @@ import "@nomiclabs/hardhat-etherscan"; import "@openzeppelin/hardhat-upgrades"; import "@typechain/hardhat"; import * as dotenv from "dotenv"; +import "hardhat-contract-sizer"; import "hardhat-dependency-compiler"; import "hardhat-deploy"; import { DeployResult } from "hardhat-deploy/types"; From 5aac9d20c967e4c625566b1d0f095b10360aab14 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 8 Jul 2025 18:14:01 +0530 Subject: [PATCH 05/51] chore: update yarn.lock --- package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a8591fc0..27d0b7a56 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "eslint-plugin-prettier": "3.4.1", "eslint-plugin-promise": "^5.2.0", "hardhat": "^2.16.1", + "hardhat-contract-sizer": "^2.10.0", "hardhat-dependency-compiler": "^1.2.1", "hardhat-deploy": "^0.12.4", "hardhat-deploy-ethers": "^0.3.0-beta.13", diff --git a/yarn.lock b/yarn.lock index ff4b5742e..080ab4279 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,6 +3480,7 @@ __metadata: eslint-plugin-promise: ^5.2.0 ethers: ^5.7.0 hardhat: ^2.16.1 + hardhat-contract-sizer: ^2.10.0 hardhat-dependency-compiler: ^1.2.1 hardhat-deploy: ^0.12.4 hardhat-deploy-ethers: ^0.3.0-beta.13 @@ -7540,7 +7541,7 @@ __metadata: languageName: node linkType: hard -"hardhat-contract-sizer@npm:^2.1.1": +"hardhat-contract-sizer@npm:^2.1.1, hardhat-contract-sizer@npm:^2.10.0": version: 2.10.0 resolution: "hardhat-contract-sizer@npm:2.10.0" dependencies: From 083a1f715ab28b411bd473eb274fc9b760a6b2b8 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 8 Jul 2025 18:53:33 +0530 Subject: [PATCH 06/51] feat: move liquidation logic to Liquidation library --- contracts/lib/ExponentialNoError.sol | 131 ++++++++++++++++++++++++ contracts/lib/Liquidation.sol | 143 +++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 contracts/lib/ExponentialNoError.sol create mode 100644 contracts/lib/Liquidation.sol diff --git a/contracts/lib/ExponentialNoError.sol b/contracts/lib/ExponentialNoError.sol new file mode 100644 index 000000000..3dcccef0a --- /dev/null +++ b/contracts/lib/ExponentialNoError.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { EXP_SCALE as EXP_SCALE_, MANTISSA_ONE as MANTISSA_ONE_ } from "./constants.sol"; + +library ExponentialNoError { + struct Exp { + uint256 mantissa; + } + + struct Double { + uint256 mantissa; + } + + uint256 internal constant EXP_SCALE = EXP_SCALE_; + uint256 internal constant DOUBLE_SCALE = 1e36; + uint256 internal constant HALF_EXP_SCALE = EXP_SCALE / 2; + uint256 internal constant MANTISSA_ONE = MANTISSA_ONE_; + + function truncate(Exp memory exp) internal pure returns (uint256) { + return exp.mantissa / EXP_SCALE; + } + + function mul_ScalarTruncate(Exp memory a, uint256 scalar) internal pure returns (uint256) { + Exp memory product = mul_(a, scalar); + return truncate(product); + } + + function mul_ScalarTruncateAddUInt(Exp memory a, uint256 scalar, uint256 addend) internal pure returns (uint256) { + Exp memory product = mul_(a, scalar); + return add_(truncate(product), addend); + } + + function lessThanExp(Exp memory left, Exp memory right) internal pure returns (bool) { + return left.mantissa < right.mantissa; + } + + function safe224(uint256 n, string memory errorMessage) internal pure returns (uint224) { + require(n <= type(uint224).max, errorMessage); + return uint224(n); + } + + function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { + require(n <= type(uint32).max, errorMessage); + return uint32(n); + } + + function add_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { + return Exp(add_(a.mantissa, b.mantissa)); + } + + function add_(Double memory a, Double memory b) internal pure returns (Double memory) { + return Double(add_(a.mantissa, b.mantissa)); + } + + function add_(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + function sub_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { + return Exp(sub_(a.mantissa, b.mantissa)); + } + + function sub_(Double memory a, Double memory b) internal pure returns (Double memory) { + return Double(sub_(a.mantissa, b.mantissa)); + } + + function sub_(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + function mul_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { + return Exp(mul_(a.mantissa, b.mantissa) / EXP_SCALE); + } + + function mul_(Exp memory a, uint256 b) internal pure returns (Exp memory) { + return Exp(mul_(a.mantissa, b)); + } + + function mul_(uint256 a, Exp memory b) internal pure returns (uint256) { + return mul_(a, b.mantissa) / EXP_SCALE; + } + + function mul_(Double memory a, Double memory b) internal pure returns (Double memory) { + return Double(mul_(a.mantissa, b.mantissa) / DOUBLE_SCALE); + } + + function mul_(Double memory a, uint256 b) internal pure returns (Double memory) { + return Double(mul_(a.mantissa, b)); + } + + function mul_(uint256 a, Double memory b) internal pure returns (uint256) { + return mul_(a, b.mantissa) / DOUBLE_SCALE; + } + + function mul_(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + function div_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { + return Exp(div_(mul_(a.mantissa, EXP_SCALE), b.mantissa)); + } + + function div_(Exp memory a, uint256 b) internal pure returns (Exp memory) { + return Exp(div_(a.mantissa, b)); + } + + function div_(uint256 a, Exp memory b) internal pure returns (uint256) { + return div_(mul_(a, EXP_SCALE), b.mantissa); + } + + function div_(Double memory a, Double memory b) internal pure returns (Double memory) { + return Double(div_(mul_(a.mantissa, DOUBLE_SCALE), b.mantissa)); + } + + function div_(Double memory a, uint256 b) internal pure returns (Double memory) { + return Double(div_(a.mantissa, b)); + } + + function div_(uint256 a, Double memory b) internal pure returns (uint256) { + return div_(mul_(a, DOUBLE_SCALE), b.mantissa); + } + + function div_(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + function fraction(uint256 a, uint256 b) internal pure returns (Double memory) { + return Double(div_(mul_(a, DOUBLE_SCALE), b)); + } +} diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol new file mode 100644 index 000000000..a8c2746e2 --- /dev/null +++ b/contracts/lib/Liquidation.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { ExponentialNoError } from "./ExponentialNoError.sol"; +import { ComptrollerStorage } from "../ComptrollerStorage.sol"; +import { VToken } from "../VToken.sol"; + +library Liquidation { + struct AssetData { + uint256 vTokenBalance; + uint256 borrowBalance; + uint256 exchangeRateMantissa; + uint256 underlyingPrice; + uint256 assetWeight; + address vTokenAddress; + } + + struct EffectsParams { + VToken vTokenModify; + uint256 redeemTokens; + uint256 borrowAmount; + } + + /// @notice Thrown when a market is not listed in the comptroller. + /// @param market The address of the market that is not listed. + error MarketNotListed(address market); + + /** + * @notice Processes a batch of liquidation orders for a given borrower. + * @dev Iterates through the provided liquidation orders, validates that both the borrowed and collateral markets are listed, + * and executes the liquidation for each order using the `forceLiquidateBorrow` function. + * @param orders Array of liquidation orders to process. + * @param borrower The address of the borrower whose positions are being liquidated. + * @param liquidator The address performing the liquidation. + * @param markets Mapping of market addresses to their corresponding market data, used to validate market status. + * @custom:reverts MarketNotListed if either the borrowed or collateral market in an order is not listed. + */ + function processLiquidationOrders( + ComptrollerStorage.LiquidationOrder[] calldata orders, + address borrower, + address liquidator, + mapping(address => ComptrollerStorage.Market) storage markets + ) internal { + uint256 ordersCount = orders.length; + for (uint256 i; i < ordersCount; ++i) { + ComptrollerStorage.LiquidationOrder calldata order = orders[i]; + + // Validate markets are listed + if (!markets[address(order.vTokenBorrowed)].isListed) { + revert MarketNotListed(address(order.vTokenBorrowed)); + } + if (!markets[address(order.vTokenCollateral)].isListed) { + revert MarketNotListed(address(order.vTokenCollateral)); + } + + // Execute liquidation + order.vTokenBorrowed.forceLiquidateBorrow( + liquidator, + borrower, + order.repayAmount, + order.vTokenCollateral, + true + ); + } + } + + function calculateAssetValues( + AssetData memory asset, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, + EffectsParams memory effectsParams + ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + ExponentialNoError.Exp memory oraclePrice = ExponentialNoError.Exp({ mantissa: asset.underlyingPrice }); + ExponentialNoError.Exp memory vTokenPrice = ExponentialNoError.mul_( + ExponentialNoError.Exp({ mantissa: asset.exchangeRateMantissa }), + oraclePrice + ); + ExponentialNoError.Exp memory weightedVTokenPrice = ExponentialNoError.mul_( + ExponentialNoError.Exp({ mantissa: asset.assetWeight }), + vTokenPrice + ); + + // Core calculations + snapshot.weightedCollateral = ExponentialNoError.mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + asset.vTokenBalance, + snapshot.weightedCollateral + ); + snapshot.totalCollateral = ExponentialNoError.mul_ScalarTruncateAddUInt( + vTokenPrice, + asset.vTokenBalance, + snapshot.totalCollateral + ); + snapshot.borrows = ExponentialNoError.mul_ScalarTruncateAddUInt( + oraclePrice, + asset.borrowBalance, + snapshot.borrows + ); + snapshot.weightavg += asset.assetWeight; + + // Handle modified asset effects + if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { + snapshot.effects = ExponentialNoError.mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + effectsParams.redeemTokens, + snapshot.effects + ); + snapshot.effects = ExponentialNoError.mul_ScalarTruncateAddUInt( + oraclePrice, + effectsParams.borrowAmount, + snapshot.effects + ); + } + + return snapshot; + } + + function finalizeSnapshot( + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, + uint256 assetsCount, + uint256 liquidationIncentiveMantissa + ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + snapshot.weightavg = snapshot.weightavg / assetsCount; + uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; + + snapshot.healthFactor = ExponentialNoError.div_(snapshot.weightedCollateral, borrowPlusEffects); + snapshot.healthFactorThreshold = ExponentialNoError.div_( + snapshot.weightavg * (1e18 + liquidationIncentiveMantissa), + 1e18 + ); + + unchecked { + if (snapshot.weightedCollateral > borrowPlusEffects) { + snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; + snapshot.shortfall = 0; + } else { + snapshot.liquidity = 0; + snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; + } + } + + return snapshot; + } +} From 8c964fa2d7eab9ce94b2df86cf7fb470d5a2db03 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 8 Jul 2025 19:10:54 +0530 Subject: [PATCH 07/51] feat: update comptroller to use Liquidation library functions --- contracts/Comptroller.sol | 94 +++++++++------------------------------ 1 file changed, 22 insertions(+), 72 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 120289897..9b2ea1a8b 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -13,6 +13,7 @@ import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; +import { Liquidation } from "./lib/Liquidation.sol"; /** * @title Comptroller @@ -974,23 +975,7 @@ contract Comptroller is _ensureMaxLoops(ordersCount / 2); - for (uint256 i; i < ordersCount; ++i) { - if (!markets[address(orders[i].vTokenBorrowed)].isListed) { - revert MarketNotListed(address(orders[i].vTokenBorrowed)); - } - if (!markets[address(orders[i].vTokenCollateral)].isListed) { - revert MarketNotListed(address(orders[i].vTokenCollateral)); - } - - LiquidationOrder calldata order = orders[i]; - order.vTokenBorrowed.forceLiquidateBorrow( - msg.sender, - borrower, - order.repayAmount, - order.vTokenCollateral, - true - ); - } + Liquidation.processLiquidationOrders(orders, borrower, msg.sender, markets); VToken[] memory borrowMarkets = getAssetsIn(borrower); uint256 marketsCount = borrowMarkets.length; @@ -1417,16 +1402,14 @@ contract Comptroller is uint256 seizeTokens; Exp memory numerator; Exp memory denominator; - Exp memory ratio; numerator = mul_( Exp({ mantissa: getLiquidationIncentive(borrower) }), Exp({ mantissa: priceBorrowedMantissa }) ); denominator = mul_(Exp({ mantissa: priceCollateralMantissa }), Exp({ mantissa: exchangeRateMantissa })); - ratio = div_(numerator, denominator); - seizeTokens = mul_ScalarTruncate(ratio, actualRepayAmount); + seizeTokens = mul_ScalarTruncate(div_(numerator, denominator), actualRepayAmount); return (NO_ERROR, seizeTokens); } @@ -1665,68 +1648,35 @@ contract Comptroller is VToken[] memory assets = getAssetsIn(account); uint256 assetsCount = assets.length; - for (uint256 i; i < assetsCount; ++i) { - VToken asset = assets[i]; + Liquidation.EffectsParams memory effectsParams = Liquidation.EffectsParams({ + vTokenModify: vTokenModify, + redeemTokens: redeemTokens, + borrowAmount: borrowAmount + }); - // Read the balances and exchange rate from the vToken + for (uint256 i; i < assetsCount; ) { + VToken asset = assets[i]; (uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = _safeGetAccountSnapshot( asset, account ); - // Get the normalized price of the asset - Exp memory oraclePrice = Exp({ mantissa: _safeGetUnderlyingPrice(asset) }); - - // Pre-compute conversion factors from vTokens -> usd - Exp memory vTokenPrice = mul_(Exp({ mantissa: exchangeRateMantissa }), oraclePrice); - Exp memory weightedVTokenPrice = mul_(weight(asset), vTokenPrice); - - // weightedCollateral += weightedVTokenPrice * vTokenBalance - snapshot.weightedCollateral = mul_ScalarTruncateAddUInt( - weightedVTokenPrice, - vTokenBalance, - snapshot.weightedCollateral - ); - - // totalCollateral += vTokenPrice * vTokenBalance - snapshot.totalCollateral = mul_ScalarTruncateAddUInt(vTokenPrice, vTokenBalance, snapshot.totalCollateral); - - // borrows += oraclePrice * borrowBalance - snapshot.borrows = mul_ScalarTruncateAddUInt(oraclePrice, borrowBalance, snapshot.borrows); - - Exp memory weightSum; - weightSum = add_(weightSum, weight(asset)); - if (i == assetsCount - 1) { - snapshot.weightavg = weightSum.mantissa / assetsCount; - } - - // Calculate effects of interacting with vTokenModify - if (asset == vTokenModify) { - // redeem effect - // effects += tokensToDenom * redeemTokens - snapshot.effects = mul_ScalarTruncateAddUInt(weightedVTokenPrice, redeemTokens, snapshot.effects); - - // borrow effect - // effects += oraclePrice * borrowAmount - snapshot.effects = mul_ScalarTruncateAddUInt(oraclePrice, borrowAmount, snapshot.effects); - } - } - - uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; - snapshot.healthFactor = div_(snapshot.weightedCollateral, borrowPlusEffects); - snapshot.healthFactorThreshold = div_(snapshot.weightavg * (1e18 + liquidationIncentiveMantissa), 1e18); + Liquidation.AssetData memory assetData = Liquidation.AssetData({ + vTokenBalance: vTokenBalance, + borrowBalance: borrowBalance, + exchangeRateMantissa: exchangeRateMantissa, + underlyingPrice: _safeGetUnderlyingPrice(asset), + assetWeight: weight(asset).mantissa, + vTokenAddress: address(asset) + }); - // These are safe, as the underflow condition is checked first - unchecked { - if (snapshot.weightedCollateral > borrowPlusEffects) { - snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; - snapshot.shortfall = 0; - } else { - snapshot.liquidity = 0; - snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; + snapshot = Liquidation.calculateAssetValues(assetData, snapshot, effectsParams); + unchecked { + ++i; } } + snapshot = Liquidation.finalizeSnapshot(snapshot, assetsCount, liquidationIncentiveMantissa); return snapshot; } From 7c65f24c80f4481b43284b473565b4ab7074b433 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 9 Jul 2025 15:35:04 +0530 Subject: [PATCH 08/51] feat: add maximum liquidation incentive per asset --- contracts/Comptroller.sol | 115 ++++++++++++++++++++----------- contracts/ComptrollerStorage.sol | 13 ++-- 2 files changed, 80 insertions(+), 48 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 9b2ea1a8b..e929a1198 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -70,7 +70,11 @@ contract Comptroller is ); /// @notice Emitted when liquidation incentive is changed by admin - event NewLiquidationIncentive(uint256 oldLiquidationIncentiveMantissa, uint256 newLiquidationIncentiveMantissa); + event NewMarketLiquidationIncentive( + VToken indexed vToken, + uint256 oldLiquidationIncentiveMantissa, + uint256 newLiquidationIncentiveMantissa + ); /// @notice Emitted when price oracle is changed event NewPriceOracle(ResilientOracleInterface oldPriceOracle, ResilientOracleInterface newPriceOracle); @@ -762,7 +766,8 @@ contract Comptroller is uint256 wtAvg = snapshot.weightavg; if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; - uint256 denominator = borrowBalance * (1e18 - ((wtAvg * (1e18 + liquidationIncentiveMantissa)) / 1e18)); + uint256 denominator = borrowBalance * + (1e18 - ((wtAvg * (1e18 + marketLiquidationIncentiveMantissa[vTokenCollateral])) / 1e18)); closeFactor = (numerator * 1e18) / denominator; closeFactor = closeFactor > 1e18 ? 1e18 : closeFactor; } else { @@ -905,16 +910,27 @@ contract Comptroller is revert InsufficientShortfall(); } - // percentage = collateral / (borrows * liquidation incentive) - Exp memory collateral = Exp({ mantissa: snapshot.totalCollateral }); - Exp memory scaledBorrows = mul_( - Exp({ mantissa: snapshot.borrows }), - Exp({ mantissa: getLiquidationIncentive(user) }) - ); + Exp memory totalCollateral = Exp({ mantissa: snapshot.totalCollateral }); + Exp memory totalScaledBorrows = Exp({ mantissa: 0 }); + + for (uint256 i; i < userAssetsCount; ++i) { + VToken market = userAssets[i]; + (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(market, user); + + if (borrowBalance > 0) { + uint256 marketIncentive = getDynamicLiquidationIncentive(user, address(market)); + + Exp memory marketBorrow = Exp({ mantissa: borrowBalance }); + Exp memory scaledMarketBorrow = mul_(marketBorrow, Exp({ mantissa: marketIncentive })); - Exp memory percentage = div_(collateral, scaledBorrows); + totalScaledBorrows = add_(totalScaledBorrows, scaledMarketBorrow); + } + } + + // percentage = collateral / (borrows * liquidation incentive) + Exp memory percentage = div_(totalCollateral, totalScaledBorrows); if (lessThanExp(Exp({ mantissa: MANTISSA_ONE }), percentage)) { - revert CollateralExceedsThreshold(scaledBorrows.mantissa, collateral.mantissa); + revert CollateralExceedsThreshold(totalScaledBorrows.mantissa, totalCollateral.mantissa); } for (uint256 i; i < userAssetsCount; ++i) { @@ -957,10 +973,23 @@ contract Comptroller is revert CollateralExceedsThreshold(minLiquidatableCollateral, snapshot.totalCollateral); } - uint256 collateralToSeize = mul_ScalarTruncate( - Exp({ mantissa: getLiquidationIncentive(borrower) }), - snapshot.borrows - ); + uint256 collateralToSeize; + VToken[] memory userAssets = getAssetsIn(borrower); + uint256 userAssetsCount = userAssets.length; + + for (uint256 i; i < userAssetsCount; ++i) { + VToken market = userAssets[i]; + (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(market, borrower); + + if (borrowBalance > 0) { + uint256 marketIncentive = getDynamicLiquidationIncentive(borrower, address(market)); + + uint256 MarketCollateralToSeize = mul_ScalarTruncate(Exp({ mantissa: marketIncentive }), borrowBalance); + + collateralToSeize = add_(collateralToSeize, MarketCollateralToSeize); + } + } + if (collateralToSeize >= snapshot.totalCollateral) { // There is not enough collateral to seize. Use healBorrow to repay some part of the borrow // and record bad debt. @@ -1063,25 +1092,32 @@ contract Comptroller is } /** - * @notice Sets liquidationIncentive + * @notice Sets liquidationIncentive for a specific market * @dev This function is restricted by the AccessControlManager + * @param vToken The market to set the liquidation incentive on * @param newLiquidationIncentiveMantissa New liquidationIncentive scaled by 1e18 - * @custom:event Emits NewLiquidationIncentive on success + * @custom:event Emits NewMarketLiquidationIncentive on success * @custom:access Controlled by AccessControlManager */ - function setLiquidationIncentive(uint256 newLiquidationIncentiveMantissa) external { + function setMarketLiquidationIncentive(VToken vToken, uint256 newLiquidationIncentiveMantissa) external { require(newLiquidationIncentiveMantissa >= MANTISSA_ONE, "liquidation incentive should be greater than 1e18"); - _checkAccessAllowed("setLiquidationIncentive(uint256)"); + _checkAccessAllowed("setMarketLiquidationIncentive(address,uint256)"); - // Save current value for use in log - uint256 oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa; + Market storage market = markets[address(vToken)]; + if (!market.isListed) { + revert MarketNotListed(address(vToken)); + } - // Set liquidation incentive to new incentive - liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; + uint256 oldLiquidationIncentiveMantissa = market.liquidationIncentiveMantissa; + if (newLiquidationIncentiveMantissa == oldLiquidationIncentiveMantissa) { + return; // No change, no need to emit event + } + + market.liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; // Emit event with old incentive, new incentive - emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); + emit NewMarketLiquidationIncentive(vToken, oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); } /** @@ -1404,7 +1440,7 @@ contract Comptroller is Exp memory denominator; numerator = mul_( - Exp({ mantissa: getLiquidationIncentive(borrower) }), + Exp({ mantissa: getDynamicLiquidationIncentive(borrower, vTokenCollateral) }), Exp({ mantissa: priceBorrowedMantissa }) ); denominator = mul_(Exp({ mantissa: priceCollateralMantissa }), Exp({ mantissa: exchangeRateMantissa })); @@ -1505,9 +1541,10 @@ contract Comptroller is /// @notice Get the liquidation incentive for a borrower /// @param borrower The address of the borrower /// @return incentive The liquidation incentive for the borrower, scaled by 1e18 - function getLiquidationIncentive(address borrower) public view returns (uint256 incentive) { + function getDynamicLiquidationIncentive(address borrower, address market) public view returns (uint256 incentive) { AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); + uint256 liquidationIncentiveMantissa = marketLiquidationIncentiveMantissa[market]; if (snapshot.healthFactor >= snapshot.healthFactorThreshold) return liquidationIncentiveMantissa; unchecked { @@ -1644,11 +1681,11 @@ contract Comptroller is uint256 borrowAmount, function(VToken) internal view returns (Exp memory) weight ) internal view returns (AccountLiquiditySnapshot memory snapshot) { - // For each asset the account is in VToken[] memory assets = getAssetsIn(account); uint256 assetsCount = assets.length; + uint256 liquidationIncentiveMantissa; - Liquidation.EffectsParams memory effectsParams = Liquidation.EffectsParams({ + Liquidation.EffectsParams memory effects = Liquidation.EffectsParams({ vTokenModify: vTokenModify, redeemTokens: redeemTokens, borrowAmount: borrowAmount @@ -1656,28 +1693,22 @@ contract Comptroller is for (uint256 i; i < assetsCount; ) { VToken asset = assets[i]; - (uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = _safeGetAccountSnapshot( - asset, - account + snapshot = Liquidation.processAsset( + assets[i], + account, + effects, + weight(asset).mantissa, + _safeGetUnderlyingPrice, + _safeGetAccountSnapshot, + snapshot ); - Liquidation.AssetData memory assetData = Liquidation.AssetData({ - vTokenBalance: vTokenBalance, - borrowBalance: borrowBalance, - exchangeRateMantissa: exchangeRateMantissa, - underlyingPrice: _safeGetUnderlyingPrice(asset), - assetWeight: weight(asset).mantissa, - vTokenAddress: address(asset) - }); - - snapshot = Liquidation.calculateAssetValues(assetData, snapshot, effectsParams); unchecked { ++i; } } - snapshot = Liquidation.finalizeSnapshot(snapshot, assetsCount, liquidationIncentiveMantissa); - return snapshot; + return Liquidation.finalizeSnapshot(snapshot, assetsCount, div_(liquidationIncentiveMantissa, assetsCount)); } /** diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index ee8092fd4..9115e9ff9 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -51,6 +51,8 @@ contract ComptrollerStorage { uint256 liquidationThresholdMantissa; // Per-market mapping of "accounts in this asset" mapping(address => bool) accountMembership; + // discount on collateral that a liquidator receives when liquidating a borrow in this market + uint256 liquidationIncentiveMantissa; } /** @@ -63,11 +65,6 @@ contract ComptrollerStorage { */ uint256 public closeFactorMantissa; - /** - * @notice Multiplier representing the discount on collateral that a liquidator receives - */ - uint256 public liquidationIncentiveMantissa; - /** * @notice Per-account mapping of "assets you are in" */ @@ -121,10 +118,14 @@ contract ComptrollerStorage { //mapping(address user => mapping (address delegate => bool approved)) public approvedDelegates; mapping(address => mapping(address => bool)) public approvedDelegates; + /// @notice Mapping of liquidation incentives for each vToken + /// @dev This is used to determine the incentive for liquidators when they liquidate a borrow + mapping(address => uint256) public marketLiquidationIncentiveMantissa; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[47] private __gap; + uint256[46] private __gap; } From 32bf18799adf2e4f3c7fe1dec1f32f8196629238 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 9 Jul 2025 15:35:46 +0530 Subject: [PATCH 09/51] feat: update liquidation library --- contracts/lib/Liquidation.sol | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol index a8c2746e2..2c5ee6e9b 100644 --- a/contracts/lib/Liquidation.sol +++ b/contracts/lib/Liquidation.sol @@ -64,6 +64,55 @@ library Liquidation { } } + /** + * @notice Creates AssetData struct from raw inputs + */ + function createAssetData( + VToken asset, + address account, + uint256 assetWeight, + function(VToken) internal view returns (uint256) getUnderlyingPrice, + function(VToken, address) internal view returns (uint256, uint256, uint256) getAccountSnapshot + ) internal view returns (AssetData memory) { + (uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = getAccountSnapshot( + asset, + account + ); + + return + AssetData({ + vTokenBalance: vTokenBalance, + borrowBalance: borrowBalance, + exchangeRateMantissa: exchangeRateMantissa, + underlyingPrice: getUnderlyingPrice(asset), + assetWeight: assetWeight, + vTokenAddress: address(asset) + }); + } + + /** + * @notice Processes a single asset's liquidity impact + */ + function processAsset( + VToken asset, + address account, + EffectsParams memory effects, + uint256 assetWeight, + function(VToken) internal view returns (uint256) getUnderlyingPrice, + function(VToken, address) internal view returns (uint256, uint256, uint256) getAccountSnapshot, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) internal view returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + AssetData memory assetData = createAssetData( + asset, + account, + assetWeight, + getUnderlyingPrice, + getAccountSnapshot + ); + + return calculateAssetValues(assetData, snapshot, effects); + } + function calculateAssetValues( AssetData memory asset, ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, From 2716fec44d3775df1f4fe27e3f71a87d97b20124 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 9 Jul 2025 15:36:56 +0530 Subject: [PATCH 10/51] refactor: remove pool liquidation Incentive reference --- contracts/Pool/PoolRegistry.sol | 3 --- contracts/WUSDMLiquidator.sol | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/Pool/PoolRegistry.sol b/contracts/Pool/PoolRegistry.sol index fae17b8e1..5cc499d8f 100644 --- a/contracts/Pool/PoolRegistry.sol +++ b/contracts/Pool/PoolRegistry.sol @@ -124,7 +124,6 @@ contract PoolRegistry is Ownable2StepUpgradeable, AccessControlledV8, PoolRegist * @param name The name of the pool * @param comptroller Pool's Comptroller contract * @param closeFactor The pool's close factor (scaled by 1e18) - * @param liquidationIncentive The pool's liquidation incentive (scaled by 1e18) * @param minLiquidatableCollateral Minimal collateral for regular (non-batch) liquidations flow * @return index The index of the registered Venus pool * @custom:error ZeroAddressNotAllowed is thrown when Comptroller address is zero @@ -134,7 +133,6 @@ contract PoolRegistry is Ownable2StepUpgradeable, AccessControlledV8, PoolRegist string calldata name, Comptroller comptroller, uint256 closeFactor, - uint256 liquidationIncentive, uint256 minLiquidatableCollateral ) external virtual returns (uint256 index) { _checkAccessAllowed("addPool(string,address,uint256,uint256,uint256)"); @@ -146,7 +144,6 @@ contract PoolRegistry is Ownable2StepUpgradeable, AccessControlledV8, PoolRegist // Set Venus pool parameters comptroller.setCloseFactor(closeFactor); - comptroller.setLiquidationIncentive(liquidationIncentive); comptroller.setMinLiquidatableCollateral(minLiquidatableCollateral); return poolId; diff --git a/contracts/WUSDMLiquidator.sol b/contracts/WUSDMLiquidator.sol index b16090564..9b28c19f6 100644 --- a/contracts/WUSDMLiquidator.sol +++ b/contracts/WUSDMLiquidator.sol @@ -109,7 +109,7 @@ contract WUSDMLiquidator is Ownable2StepUpgradeable { } function _configureMarkets() internal { - (, uint256 wUSDMCollateralFactor, uint256 wUSDMLiquidationThreshold) = COMPTROLLER.markets(address(VWUSDM)); + (, uint256 wUSDMCollateralFactor, uint256 wUSDMLiquidationThreshold, ) = COMPTROLLER.markets(address(VWUSDM)); _originalConfig = OriginalConfig({ minLiquidatableCollateral: COMPTROLLER.minLiquidatableCollateral(), closeFactor: COMPTROLLER.closeFactorMantissa(), From e1f58da49dff7e03909490313797d70cb7ae5534 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 10 Jul 2025 16:58:29 +0530 Subject: [PATCH 11/51] feat: moved reward updates logic to Rewards library --- contracts/lib/Rewards.sol | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 contracts/lib/Rewards.sol diff --git a/contracts/lib/Rewards.sol b/contracts/lib/Rewards.sol new file mode 100644 index 000000000..5070a1766 --- /dev/null +++ b/contracts/lib/Rewards.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol"; +import { ExponentialNoError } from "../ExponentialNoError.sol"; +import { VToken } from "../VToken.sol"; + +library Rewards { + /** + * @dev Updates and distributes supply-side rewards for a market/user + */ + function updateAndDistributeSupplyRewards( + RewardsDistributor[] storage rewardsDistributors, + address vToken, + address user + ) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor distributor = rewardsDistributors[i]; + distributor.updateRewardTokenSupplyIndex(vToken); + distributor.distributeSupplierRewardToken(vToken, user); + } + } + + function updateAndDistributeSupplyRewardsMulti( + RewardsDistributor[] storage rewardsDistributors, + address vToken, + address user1, + address user2 + ) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor distributor = rewardsDistributors[i]; + distributor.updateRewardTokenSupplyIndex(vToken); + distributor.distributeSupplierRewardToken(vToken, user1); + distributor.distributeSupplierRewardToken(vToken, user2); + } + } + + /** + * @dev Updates and distributes borrow-side rewards + */ + function updateAndDistributeBorrowRewards( + RewardsDistributor[] storage rewardsDistributors, + address vToken, + address user + ) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + + ExponentialNoError.Exp memory borrowIndex = ExponentialNoError.Exp({ mantissa: VToken(vToken).borrowIndex() }); + + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor distributor = rewardsDistributors[i]; + distributor.updateRewardTokenBorrowIndex(vToken, borrowIndex); + distributor.distributeBorrowerRewardToken(vToken, user, borrowIndex); + } + } +} From 0921fcea463205bbc4f7fd5b260682d016878b5a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 10 Jul 2025 18:26:03 +0530 Subject: [PATCH 12/51] refactor: removed liquidation Incentive mapping --- contracts/ComptrollerInterface.sol | 4 +++- contracts/ComptrollerStorage.sol | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index 00e16a118..829f7d848 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -104,6 +104,8 @@ interface ComptrollerInterface { function getAllMarkets() external view returns (VToken[] memory); function actionPaused(address market, Action action) external view returns (bool); + + function getDynamicLiquidationIncentive(address borrower, address market) external view returns (uint256); } /** @@ -134,5 +136,5 @@ interface ComptrollerViewInterface { function approvedDelegates(address user, address delegate) external view returns (bool); - function getLiquidationIncentive(address borrower) external view returns (uint256); + function getDynamicLiquidationIncentive(address borrower, address market) external view returns (uint256); } diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 9115e9ff9..64095c14b 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -118,14 +118,10 @@ contract ComptrollerStorage { //mapping(address user => mapping (address delegate => bool approved)) public approvedDelegates; mapping(address => mapping(address => bool)) public approvedDelegates; - /// @notice Mapping of liquidation incentives for each vToken - /// @dev This is used to determine the incentive for liquidators when they liquidate a borrow - mapping(address => uint256) public marketLiquidationIncentiveMantissa; - /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[46] private __gap; + uint256[47] private __gap; } From dec2414a5bf19a68a671a803cde6db0389458bf8 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 10 Jul 2025 18:27:15 +0530 Subject: [PATCH 13/51] refactor: reduced comptroller size --- contracts/Comptroller.sol | 108 ++++++++-------------------------- contracts/lib/Liquidation.sol | 32 +++++++++- 2 files changed, 55 insertions(+), 85 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index e929a1198..2a929515d 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -14,6 +14,7 @@ import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; import { Liquidation } from "./lib/Liquidation.sol"; +import { Rewards } from "./lib/Rewards.sol"; /** * @title Comptroller @@ -448,13 +449,7 @@ contract Comptroller is } // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenSupplyIndex(vToken); - rewardsDistributor.distributeSupplierRewardToken(vToken, minter); - } + Rewards.updateAndDistributeSupplyRewards(rewardsDistributors, vToken, minter); } /** @@ -489,13 +484,7 @@ contract Comptroller is _checkRedeemAllowed(vToken, redeemer, redeemTokens); // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenSupplyIndex(vToken); - rewardsDistributor.distributeSupplierRewardToken(vToken, redeemer); - } + Rewards.updateAndDistributeSupplyRewards(rewardsDistributors, vToken, redeemer); } /** @@ -648,16 +637,8 @@ contract Comptroller is revert InsufficientLiquidity(); } - Exp memory borrowIndex = Exp({ mantissa: VToken(vToken).borrowIndex() }); - // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenBorrowIndex(vToken, borrowIndex); - rewardsDistributor.distributeBorrowerRewardToken(vToken, borrower, borrowIndex); - } + Rewards.updateAndDistributeBorrowRewards(rewardsDistributors, vToken, borrower); } /** @@ -691,14 +672,7 @@ contract Comptroller is } // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - Exp memory borrowIndex = Exp({ mantissa: VToken(vToken).borrowIndex() }); - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenBorrowIndex(vToken, borrowIndex); - rewardsDistributor.distributeBorrowerRewardToken(vToken, borrower, borrowIndex); - } + Rewards.updateAndDistributeBorrowRewards(rewardsDistributors, vToken, borrower); } /** @@ -760,6 +734,8 @@ contract Comptroller is revert InsufficientShortfall(); } + Market storage marketCollateral = markets[vTokenCollateral]; + uint256 closeFactor; unchecked { if (snapshot.healthFactor >= 1e18) revert InsufficientShortfall(); @@ -767,7 +743,7 @@ contract Comptroller is if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; uint256 denominator = borrowBalance * - (1e18 - ((wtAvg * (1e18 + marketLiquidationIncentiveMantissa[vTokenCollateral])) / 1e18)); + (1e18 - ((wtAvg * (1e18 + marketCollateral.liquidationIncentiveMantissa)) / 1e18)); closeFactor = (numerator * 1e18) / denominator; closeFactor = closeFactor > 1e18 ? 1e18 : closeFactor; } else { @@ -832,14 +808,7 @@ contract Comptroller is } // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenSupplyIndex(vTokenCollateral); - rewardsDistributor.distributeSupplierRewardToken(vTokenCollateral, borrower); - rewardsDistributor.distributeSupplierRewardToken(vTokenCollateral, liquidator); - } + Rewards.updateAndDistributeSupplyRewardsMulti(rewardsDistributors, vTokenCollateral, borrower, liquidator); } /** @@ -863,14 +832,7 @@ contract Comptroller is _checkRedeemAllowed(vToken, src, transferTokens); // Keep the flywheel moving - uint256 rewardDistributorsCount = rewardsDistributors.length; - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor rewardsDistributor = rewardsDistributors[i]; - rewardsDistributor.updateRewardTokenSupplyIndex(vToken); - rewardsDistributor.distributeSupplierRewardToken(vToken, src); - rewardsDistributor.distributeSupplierRewardToken(vToken, dst); - } + Rewards.updateAndDistributeSupplyRewardsMulti(rewardsDistributors, vToken, src, dst); } /*** Pool-level operations ***/ @@ -905,27 +867,14 @@ contract Comptroller is if (snapshot.totalCollateral > minLiquidatableCollateral) { revert CollateralExceedsThreshold(minLiquidatableCollateral, snapshot.totalCollateral); } - if (snapshot.shortfall == 0) { revert InsufficientShortfall(); } Exp memory totalCollateral = Exp({ mantissa: snapshot.totalCollateral }); - Exp memory totalScaledBorrows = Exp({ mantissa: 0 }); - - for (uint256 i; i < userAssetsCount; ++i) { - VToken market = userAssets[i]; - (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(market, user); - - if (borrowBalance > 0) { - uint256 marketIncentive = getDynamicLiquidationIncentive(user, address(market)); - - Exp memory marketBorrow = Exp({ mantissa: borrowBalance }); - Exp memory scaledMarketBorrow = mul_(marketBorrow, Exp({ mantissa: marketIncentive })); - - totalScaledBorrows = add_(totalScaledBorrows, scaledMarketBorrow); - } - } + Exp memory totalScaledBorrows = Exp({ + mantissa: Liquidation.calculateIncentiveAdjustedDebt(user, userAssets, ComptrollerInterface(address(this))) + }); // percentage = collateral / (borrows * liquidation incentive) Exp memory percentage = div_(totalCollateral, totalScaledBorrows); @@ -973,22 +922,14 @@ contract Comptroller is revert CollateralExceedsThreshold(minLiquidatableCollateral, snapshot.totalCollateral); } - uint256 collateralToSeize; - VToken[] memory userAssets = getAssetsIn(borrower); - uint256 userAssetsCount = userAssets.length; - - for (uint256 i; i < userAssetsCount; ++i) { - VToken market = userAssets[i]; - (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(market, borrower); - - if (borrowBalance > 0) { - uint256 marketIncentive = getDynamicLiquidationIncentive(borrower, address(market)); - - uint256 MarketCollateralToSeize = mul_ScalarTruncate(Exp({ mantissa: marketIncentive }), borrowBalance); + VToken[] memory borrowMarkets = getAssetsIn(borrower); + uint256 marketsCount = borrowMarkets.length; - collateralToSeize = add_(collateralToSeize, MarketCollateralToSeize); - } - } + uint256 collateralToSeize = Liquidation.calculateIncentiveAdjustedDebt( + borrower, + borrowMarkets, + ComptrollerInterface(address(this)) + ); if (collateralToSeize >= snapshot.totalCollateral) { // There is not enough collateral to seize. Use healBorrow to repay some part of the borrow @@ -1006,9 +947,6 @@ contract Comptroller is Liquidation.processLiquidationOrders(orders, borrower, msg.sender, markets); - VToken[] memory borrowMarkets = getAssetsIn(borrower); - uint256 marketsCount = borrowMarkets.length; - for (uint256 i; i < marketsCount; ++i) { (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(borrowMarkets[i], borrower); require(borrowBalance == 0, "Nonzero borrow balance after liquidation"); @@ -1541,10 +1479,12 @@ contract Comptroller is /// @notice Get the liquidation incentive for a borrower /// @param borrower The address of the borrower /// @return incentive The liquidation incentive for the borrower, scaled by 1e18 - function getDynamicLiquidationIncentive(address borrower, address market) public view returns (uint256 incentive) { + function getDynamicLiquidationIncentive(address borrower, address vToken) public view returns (uint256 incentive) { + Market storage market = markets[vToken]; + uint256 liquidationIncentiveMantissa = market.liquidationIncentiveMantissa; + AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); - uint256 liquidationIncentiveMantissa = marketLiquidationIncentiveMantissa[market]; if (snapshot.healthFactor >= snapshot.healthFactorThreshold) return liquidationIncentiveMantissa; unchecked { diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol index 2c5ee6e9b..23d06b48f 100644 --- a/contracts/lib/Liquidation.sol +++ b/contracts/lib/Liquidation.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.10; import { ExponentialNoError } from "./ExponentialNoError.sol"; import { ComptrollerStorage } from "../ComptrollerStorage.sol"; +import { ComptrollerInterface } from "../ComptrollerInterface.sol"; import { VToken } from "../VToken.sol"; library Liquidation { @@ -11,7 +12,7 @@ library Liquidation { uint256 borrowBalance; uint256 exchangeRateMantissa; uint256 underlyingPrice; - uint256 assetWeight; + uint256 assetWeight; // Weight of the asset in the context of liquidation address vTokenAddress; } @@ -64,6 +65,35 @@ library Liquidation { } } + /** + * @notice Calculates the sum of all borrow amounts weighted by their liquidation incentives + * @dev Returns Σ (borrowAmount × liquidationIncentive) for all markets + * @param borrower The account address + * @param markets Array of markets to check + * @param comptroller For incentive lookup + * @return weightedBorrowSum The incentive-adjusted total borrow value + */ + function calculateIncentiveAdjustedDebt( + address borrower, + VToken[] memory markets, + ComptrollerInterface comptroller + ) internal view returns (uint256 weightedBorrowSum) { + for (uint256 i; i < markets.length; ++i) { + VToken market = markets[i]; + (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); + + if (borrowBalance > 0) { + uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); + uint256 scaledBorrow = ExponentialNoError.mul_ScalarTruncate( + ExponentialNoError.Exp({ mantissa: marketIncentive }), + borrowBalance + ); + weightedBorrowSum = ExponentialNoError.add_(weightedBorrowSum, scaledBorrow); + } + } + return weightedBorrowSum; + } + /** * @notice Creates AssetData struct from raw inputs */ From f2ef6fdd1e5da9a7473f0a866a4db8c6b61afdd0 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 10 Jul 2025 18:27:40 +0530 Subject: [PATCH 14/51] fix: minor fix --- contracts/VToken.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/VToken.sol b/contracts/VToken.sol index e70f24ada..63f2d2ced 100644 --- a/contracts/VToken.sol +++ b/contracts/VToken.sol @@ -1276,9 +1276,8 @@ contract VToken is * liquidatorTokensNew = accountTokens[liquidator] + seizeTokens */ - uint256 liquidationIncentiveMantissa = ComptrollerViewInterface(address(comptroller)).getLiquidationIncentive( - borrower - ); + uint256 liquidationIncentiveMantissa = ComptrollerViewInterface(address(comptroller)) + .getDynamicLiquidationIncentive(borrower, address(this)); uint256 numerator = mul_(seizeTokens, Exp({ mantissa: protocolSeizeShareMantissa })); uint256 protocolSeizeTokens = div_(numerator, Exp({ mantissa: liquidationIncentiveMantissa })); uint256 liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens; From 27cf44cfa19a6b78f84145dc976adfec6065eac1 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 10 Jul 2025 18:29:20 +0530 Subject: [PATCH 15/51] fix: adjust heal account tests for dynamic factors --- tests/hardhat/Comptroller/healAccountTest.ts | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/hardhat/Comptroller/healAccountTest.ts b/tests/hardhat/Comptroller/healAccountTest.ts index 725ce685f..3482d292f 100644 --- a/tests/hardhat/Comptroller/healAccountTest.ts +++ b/tests/hardhat/Comptroller/healAccountTest.ts @@ -51,18 +51,22 @@ describe("healAccount", () => { accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); - await comptroller.setLiquidationIncentive(parseUnits("1.1", 18)); await comptroller.setMinLiquidatableCollateral(parseUnits("100", 18)); await setBalance(poolRegistry.address, parseEther("1")); const names = ["OMG", "ZRX", "BAT"]; + const [OMG, ZRX, BAT, SKT] = await Promise.all( names.map(async () => { const vToken = await smock.fake("VToken"); vToken.isVToken.returns(true); await comptroller.connect(poolRegistry.wallet).supportMarket(vToken.address); + await comptroller.setMarketLiquidationIncentive(vToken.address, parseUnits("1.1", 18)); + oracle.getUnderlyingPrice.whenCalledWith(vToken.address).returns(parseUnits("1", 18)); + await comptroller.setCollateralFactor(vToken.address, parseUnits("0.85", 18), parseUnits("0.9", 18)); return vToken; }), ); + const allTokens = [OMG, ZRX, BAT]; return { accessControl, comptroller, oracle, OMG, ZRX, BAT, SKT, allTokens, names }; } @@ -133,6 +137,23 @@ describe("healAccount", () => { expect(ZRX.healBorrow).to.have.been.calledWith(liquidator.address, user.address, zrxToRepay); expect(BAT.healBorrow).to.have.been.calledWith(liquidator.address, user.address, batToRepay); }); + + it("handles different liquidation incentives per market", async () => { + // Set different incentives + await comptroller.setMarketLiquidationIncentive(OMG.address, parseUnits("1.05", 18)); // 5% + await comptroller.setMarketLiquidationIncentive(ZRX.address, parseUnits("1.15", 18)); // 15% + await comptroller.setMarketLiquidationIncentive(BAT.address, parseUnits("1.25", 18)); // 25% + + await comptroller.connect(user).enterMarkets([OMG.address, ZRX.address, BAT.address]); + OMG.getAccountSnapshot.returns([0, parseUnits("1.05", 18), 0, parseUnits("1", 18)]); + ZRX.getAccountSnapshot.returns([0, parseUnits("1.15", 18), parseUnits("1", 18), parseUnits("1", 18)]); + BAT.getAccountSnapshot.returns([0, 0, parseUnits("1", 18), parseUnits("1", 18)]); + + await comptroller.connect(liquidator).healAccount(user.address); + + expect(OMG.seize).calledWith(liquidator.address, user.address, parseUnits("1.05", 18)); + expect(ZRX.seize).calledWith(liquidator.address, user.address, parseUnits("1.15", 18)); + }); }); describe("liquidation incentive * debt > collateral", async () => { @@ -175,6 +196,7 @@ describe("healAccount", () => { describe("failures", async () => { it("fails if liquidation incentive * debt < collateral", async () => { + await comptroller.setMinLiquidatableCollateral("2200000000000000000"); await comptroller.connect(user).enterMarkets([OMG.address, ZRX.address, BAT.address]); // Supply 5 OMG, borrow 0 OMG @@ -195,7 +217,7 @@ describe("healAccount", () => { it("fails if liquidation incentive * debt > collateral but there is no shortfall", async () => { // This could happen if liquidation incentive is too high - await comptroller.setLiquidationIncentive(parseUnits("100", 18)); + await comptroller.setMarketLiquidationIncentive(OMG.address, parseUnits("100", 18)); await comptroller.setCollateralFactor(OMG.address, parseUnits("0.9", 18), parseUnits("0.9", 18)); await comptroller.connect(user).enterMarkets([OMG.address]); From 47ba38e86f0858ca8ade05a39167533e0c3c940c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 14 Jul 2025 11:43:35 +0530 Subject: [PATCH 16/51] fix: adjust liquidate account tests for dynamic factors --- .../Comptroller/liquidateAccountTest.ts | 129 ++++++++++++------ 1 file changed, 86 insertions(+), 43 deletions(-) diff --git a/tests/hardhat/Comptroller/liquidateAccountTest.ts b/tests/hardhat/Comptroller/liquidateAccountTest.ts index 75fbd4f06..5ca30efec 100644 --- a/tests/hardhat/Comptroller/liquidateAccountTest.ts +++ b/tests/hardhat/Comptroller/liquidateAccountTest.ts @@ -38,11 +38,21 @@ const fakeSnapshotsAtCalls = (vToken: FakeContract, snapshots: AccountSn }); }; -const fakeSnaphotsForLiquidation = ( +const fakeSnapshotsForLiquidation = ( vToken: FakeContract, - { beforeLiquidation, afterLiquidation }: { beforeLiquidation: AccountSnapshot; afterLiquidation: AccountSnapshot }, + { + firstSnapshot, + secondSnapshot, + thirdSnapshot, + fourthSnapshot, + }: { + firstSnapshot: AccountSnapshot; + secondSnapshot: AccountSnapshot; + thirdSnapshot: AccountSnapshot; + fourthSnapshot: AccountSnapshot; + }, ) => { - fakeSnapshotsAtCalls(vToken, [beforeLiquidation, afterLiquidation]); + fakeSnapshotsAtCalls(vToken, [firstSnapshot, secondSnapshot, thirdSnapshot, fourthSnapshot]); }; describe("liquidateAccount", () => { @@ -79,7 +89,6 @@ describe("liquidateAccount", () => { accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); - await comptroller.setLiquidationIncentive(parseUnits("1.1", 18)); await comptroller.setMinLiquidatableCollateral(parseUnits("100", 18)); await setBalance(poolRegistry.address, parseEther("1")); const names = ["OMG", "ZRX", "BAT"]; @@ -92,6 +101,7 @@ describe("liquidateAccount", () => { await comptroller .connect(poolRegistry.wallet) .setCollateralFactor(vToken.address, parseUnits("0.8", 18), parseUnits("0.9", 18)); + await comptroller.setMarketLiquidationIncentive(vToken.address, parseUnits("1.1", 18)); return vToken; }), ); @@ -163,6 +173,17 @@ describe("liquidateAccount", () => { .withArgs(parseUnits("100", 18), parseUnits("110", 18)); }); + it("does not fails if collateral is equal to minLiquidatableCollateral", async () => { + await comptroller.connect(user).enterMarkets([OMG.address, ZRX.address, BAT.address]); + fakeUserSnapshot(OMG, user.address, { supply: parseUnits("50", 18), borrows: 0 }); + fakeUserSnapshot(ZRX, user.address, { supply: parseUnits("50", 18), borrows: parseUnits("50", 18) }); + fakeUserSnapshot(BAT, user.address, { supply: 0, borrows: parseUnits("50", 18) }); + + await expect( + comptroller.connect(liquidator).liquidateAccount(user.address, []), + ).to.not.be.revertedWithCustomError(comptroller, "CollateralExceedsThreshold"); + }); + it("fails if the vToken account snapshot returns an error", async () => { await comptroller.connect(user).enterMarkets([OMG.address, ZRX.address, BAT.address]); fakeUserSnapshot(OMG, user.address, { supply: parseUnits("1.11", 18), borrows: 0 }); @@ -228,56 +249,71 @@ describe("liquidateAccount", () => { }); it("succeeds and calls forceLiquidateBorrow for two orders", async () => { - fakeSnaphotsForLiquidation(OMG, { - beforeLiquidation: { supply: parseUnits("1.11", 18), borrows: 0 }, - afterLiquidation: { supply: parseUnits("0.01", 18), borrows: 0 }, + fakeSnapshotsForLiquidation(OMG, { + firstSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + secondSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + thirdSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + fourthSnapshot: { supply: parseUnits("0.01", 18), borrows: 0 }, }); - fakeSnaphotsForLiquidation(ZRX, { - beforeLiquidation: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: 0 }, + fakeSnapshotsForLiquidation(ZRX, { + firstSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + fourthSnapshot: { supply: 0, borrows: 0 }, }); - fakeSnaphotsForLiquidation(BAT, { - beforeLiquidation: { supply: 0, borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: 0 }, + fakeSnapshotsForLiquidation(BAT, { + firstSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + fourthSnapshot: { supply: 0, borrows: 0 }, }); const liquidationOrders = [ { vTokenCollateral: OMG.address, vTokenBorrowed: ZRX.address, repayAmount: parseUnits("1", 18) }, { vTokenCollateral: ZRX.address, vTokenBorrowed: BAT.address, repayAmount: parseUnits("1", 18) }, ]; + await comptroller.connect(liquidator).liquidateAccount(user.address, liquidationOrders); + // Verify forceLiquidateBorrow calls expect(ZRX.forceLiquidateBorrow).to.have.been.calledOnceWith( liquidator.address, user.address, parseUnits("1", 18), - OMG.address, // collateral - true, // whether to skip liquidity check + OMG.address, + true, ); expect(BAT.forceLiquidateBorrow).to.have.been.calledOnceWith( liquidator.address, user.address, parseUnits("1", 18), - ZRX.address, // collateral - true, // whether to skip liquidity check + ZRX.address, + true, ); - expect(OMG.getAccountSnapshot).to.have.been.calledTwice; - expect(ZRX.getAccountSnapshot).to.have.been.calledTwice; - expect(BAT.getAccountSnapshot).to.have.been.calledTwice; + + expect(OMG.getAccountSnapshot).to.have.callCount(5); + expect(ZRX.getAccountSnapshot).to.have.callCount(5); + expect(BAT.getAccountSnapshot).to.have.callCount(5); }); it("succeeds and calls forceLiquidateBorrow for three orders, including in-kind liquidation", async () => { - fakeSnaphotsForLiquidation(OMG, { - beforeLiquidation: { supply: parseUnits("1.11", 18), borrows: 0 }, - afterLiquidation: { supply: parseUnits("0.021", 18), borrows: 0 }, + fakeSnapshotsForLiquidation(OMG, { + firstSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + secondSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + thirdSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + fourthSnapshot: { supply: parseUnits("0.021", 18), borrows: 0 }, }); - fakeSnaphotsForLiquidation(ZRX, { - beforeLiquidation: { supply: parseUnits("1.11", 18), borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: 0 }, + fakeSnapshotsForLiquidation(ZRX, { + firstSnapshot: { supply: parseUnits("1.11", 18), borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: parseUnits("1.11", 18), borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: parseUnits("1.11", 18), borrows: parseUnits("1", 18) }, + fourthSnapshot: { supply: 0, borrows: 0 }, }); - fakeSnaphotsForLiquidation(BAT, { - beforeLiquidation: { supply: 0, borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: 0 }, + fakeSnapshotsForLiquidation(BAT, { + firstSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + fourthSnapshot: { supply: 0, borrows: 0 }, }); const liquidationOrders = [ @@ -301,7 +337,7 @@ describe("liquidateAccount", () => { // Two liquidations for BAT borrows: // 1. Liquidate BAT seizing ZRX - expect(BAT.forceLiquidateBorrow).to.have.been.calledTwice; + expect(BAT.forceLiquidateBorrow).to.have.callCount(2); expect(BAT.forceLiquidateBorrow.atCall(0)).to.have.been.calledWith( liquidator.address, user.address, @@ -317,26 +353,32 @@ describe("liquidateAccount", () => { OMG.address, // collateral true, // whether to skip liquidity check ); - expect(OMG.getAccountSnapshot).to.have.been.calledTwice; - expect(ZRX.getAccountSnapshot).to.have.been.calledTwice; - expect(BAT.getAccountSnapshot).to.have.been.calledTwice; + expect(OMG.getAccountSnapshot).to.have.been.callCount(5); + expect(ZRX.getAccountSnapshot).to.have.been.callCount(5); + expect(BAT.getAccountSnapshot).to.have.been.callCount(5); }); }); describe("post-liquidation check", async () => { it("fails if there's a borrow balance after liquidation", async () => { await comptroller.connect(user).enterMarkets([OMG.address, ZRX.address, BAT.address]); - fakeSnaphotsForLiquidation(OMG, { - beforeLiquidation: { supply: parseUnits("1.11", 18), borrows: 0 }, - afterLiquidation: { supply: parseUnits("0.01", 18), borrows: 0 }, + fakeSnapshotsForLiquidation(OMG, { + firstSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + secondSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + thirdSnapshot: { supply: parseUnits("1.11", 18), borrows: 0 }, + fourthSnapshot: { supply: parseUnits("0.01", 18), borrows: 0 }, }); - fakeSnaphotsForLiquidation(ZRX, { - beforeLiquidation: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: parseUnits("0.00000000000001", 18) }, + fakeSnapshotsForLiquidation(ZRX, { + firstSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: parseUnits("1.1", 18), borrows: parseUnits("1", 18) }, + fourthSnapshot: { supply: 0, borrows: parseUnits("0.00000000000001", 18) }, }); - fakeSnaphotsForLiquidation(BAT, { - beforeLiquidation: { supply: 0, borrows: parseUnits("1", 18) }, - afterLiquidation: { supply: 0, borrows: 0 }, + fakeSnapshotsForLiquidation(BAT, { + firstSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + secondSnapshot: { supply: 0, borrows: parseUnits("1", 18) }, + thirdSnapshot: { supply: 0, borrows: 0 }, + fourthSnapshot: { supply: 0, borrows: 0 }, }); await expect(comptroller.connect(liquidator).liquidateAccount(user.address, [])).to.be.revertedWith( "Nonzero borrow balance after liquidation", @@ -436,12 +478,13 @@ describe("liquidateAccount", () => { }); it("checks the shortfall if isForcedLiquidationEnabled is set back to false", async () => { + await comptroller.connect(user).enterMarkets([OMG.address]); await comptroller.setForcedLiquidation(OMG.address, false); OMG.borrowBalanceStored.returns(parseUnits("100", 18)); const tx = comptroller.callStatic.preLiquidateHook( OMG.address, OMG.address, - accounts[0].address, + user.address, parseUnits("1", 18), false, ); From 876e44e9d489fda891ee9bbea0aacfe931ff908d Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 14 Jul 2025 18:00:04 +0530 Subject: [PATCH 17/51] refactor: add zero checks in snapshot calculations --- contracts/Comptroller.sol | 7 +++- contracts/lib/Liquidation.sol | 65 +++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 2a929515d..74d39fe45 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -1648,7 +1648,12 @@ contract Comptroller is } } - return Liquidation.finalizeSnapshot(snapshot, assetsCount, div_(liquidationIncentiveMantissa, assetsCount)); + uint256 weightedAvg; + if (assetsCount > 0) { + weightedAvg = div_(liquidationIncentiveMantissa, assetsCount); + } + + return Liquidation.finalizeSnapshot(snapshot, assetsCount, weightedAvg); } /** diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol index 23d06b48f..c89ce2410 100644 --- a/contracts/lib/Liquidation.sol +++ b/contracts/lib/Liquidation.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.10; import { ExponentialNoError } from "./ExponentialNoError.sol"; import { ComptrollerStorage } from "../ComptrollerStorage.sol"; import { ComptrollerInterface } from "../ComptrollerInterface.sol"; +import { Comptroller } from "../Comptroller.sol"; import { VToken } from "../VToken.sol"; library Liquidation { @@ -12,7 +13,7 @@ library Liquidation { uint256 borrowBalance; uint256 exchangeRateMantissa; uint256 underlyingPrice; - uint256 assetWeight; // Weight of the asset in the context of liquidation + uint256 assetWeight; address vTokenAddress; } @@ -22,10 +23,6 @@ library Liquidation { uint256 borrowAmount; } - /// @notice Thrown when a market is not listed in the comptroller. - /// @param market The address of the market that is not listed. - error MarketNotListed(address market); - /** * @notice Processes a batch of liquidation orders for a given borrower. * @dev Iterates through the provided liquidation orders, validates that both the borrowed and collateral markets are listed, @@ -48,10 +45,10 @@ library Liquidation { // Validate markets are listed if (!markets[address(order.vTokenBorrowed)].isListed) { - revert MarketNotListed(address(order.vTokenBorrowed)); + revert Comptroller.MarketNotListed(address(order.vTokenBorrowed)); } if (!markets[address(order.vTokenCollateral)].isListed) { - revert MarketNotListed(address(order.vTokenCollateral)); + revert Comptroller.MarketNotListed(address(order.vTokenCollateral)); } // Execute liquidation @@ -95,7 +92,15 @@ library Liquidation { } /** - * @notice Creates AssetData struct from raw inputs + * @notice Constructs an AssetData struct for a given asset and account. + * @dev Fetches the account's vToken balance, borrow balance, and exchange rate for the asset, + * as well as the asset's underlying price and risk weight. + * @param asset The VToken asset to query. + * @param account The address of the account. + * @param assetWeight The risk weight of the asset. + * @param getUnderlyingPrice Function to fetch the asset's underlying price. + * @param getAccountSnapshot Function to fetch the account's balances for the asset. + * @return AssetData struct containing all relevant asset/account data. */ function createAssetData( VToken asset, @@ -121,7 +126,18 @@ library Liquidation { } /** - * @notice Processes a single asset's liquidity impact + * @notice Processes a single asset for a given account and updates the liquidity snapshot. + * @dev + * - Constructs AssetData for the asset and account. + * - Calculates and applies the asset's effect on the account's liquidity snapshot, including any modifications (redeem/borrow). + * @param asset The VToken asset to process. + * @param account The address of the account being evaluated. + * @param effects Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @param assetWeight The risk weight of the asset. + * @param getUnderlyingPrice Function to fetch the asset's underlying price. + * @param getAccountSnapshot Function to fetch the account's balances for the asset. + * @param snapshot The current account liquidity snapshot to update. + * @return The updated AccountLiquiditySnapshot struct. */ function processAsset( VToken asset, @@ -143,6 +159,15 @@ library Liquidation { return calculateAssetValues(assetData, snapshot, effects); } + /** + * @notice Calculates and updates the liquidity snapshot values for a given asset. + * @dev Computes weighted collateral, total collateral, and borrow values using asset data and price information. + * If the asset is being modified (redeemed or borrowed), applies the effects to the snapshot as well. + * @param asset The asset data struct containing balances, prices, and weights. + * @param snapshot The current account liquidity snapshot to update. + * @param effectsParams Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @return The updated AccountLiquiditySnapshot struct. + */ function calculateAssetValues( AssetData memory asset, ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, @@ -193,15 +218,33 @@ library Liquidation { return snapshot; } + /** + * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. + * @dev + * - Computes the average weight if there are assets. + * - Calculates the sum of borrows and effects. + * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. + * - Sets the health factor threshold using the weighted average and liquidation incentive. + * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. + * @param snapshot The account liquidity snapshot to be finalized. + * @param assetsCount The number of assets in the snapshot. + * @param liquidationIncentiveMantissa The liquidation incentive, scaled by 1e18. + * @return The finalized account liquidity snapshot with updated fields. + */ function finalizeSnapshot( ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, uint256 assetsCount, uint256 liquidationIncentiveMantissa ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - snapshot.weightavg = snapshot.weightavg / assetsCount; + if (assetsCount > 0) { + snapshot.weightavg = snapshot.weightavg / assetsCount; + } + uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; - snapshot.healthFactor = ExponentialNoError.div_(snapshot.weightedCollateral, borrowPlusEffects); + if (borrowPlusEffects > 0) { + snapshot.healthFactor = ExponentialNoError.div_(snapshot.weightedCollateral, borrowPlusEffects); + } snapshot.healthFactorThreshold = ExponentialNoError.div_( snapshot.weightavg * (1e18 + liquidationIncentiveMantissa), 1e18 From 5860efe70bd04afa769437ae222cf8452af446d0 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 14 Jul 2025 18:00:26 +0530 Subject: [PATCH 18/51] test: fix hooks and setters test --- tests/hardhat/Comptroller/hooks.ts | 6 +++--- tests/hardhat/Comptroller/setters.ts | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/hardhat/Comptroller/hooks.ts b/tests/hardhat/Comptroller/hooks.ts index 72a5ca3c6..95dfaf370 100644 --- a/tests/hardhat/Comptroller/hooks.ts +++ b/tests/hardhat/Comptroller/hooks.ts @@ -37,16 +37,16 @@ async function deploySimpleComptroller(): Promise { initializer: "initialize(uint256,address)", }); await comptroller.setPriceOracle(oracle.address); - await comptroller.setLiquidationIncentive(parseUnits("1", 18)); return { oracle, comptroller, accessControl, poolRegistry }; } -function configureVToken(vToken: FakeContract, comptroller: MockContract) { +async function configureVToken(vToken: FakeContract, comptroller: MockContract) { vToken.comptroller.returns(comptroller.address); vToken.isVToken.returns(true); vToken.exchangeRateStored.returns(parseUnits("2", 18)); vToken.totalSupply.returns(parseUnits("1000000", 18)); vToken.totalBorrows.returns(parseUnits("900000", 18)); + await comptroller.setMarketLiquidationIncentive(vToken.address, parseUnits("1.1", 18)); } describe("hooks", () => { @@ -70,7 +70,7 @@ describe("hooks", () => { beforeEach(async () => { [root] = await ethers.getSigners(); ({ comptroller, vToken } = await loadFixture(deploy)); - configureVToken(vToken, comptroller); + await configureVToken(vToken, comptroller); }); it("allows minting if cap is not reached", async () => { diff --git a/tests/hardhat/Comptroller/setters.ts b/tests/hardhat/Comptroller/setters.ts index e50da4007..6d9835c7b 100644 --- a/tests/hardhat/Comptroller/setters.ts +++ b/tests/hardhat/Comptroller/setters.ts @@ -60,6 +60,7 @@ describe("setters", async () => { accessControl.isAllowedToCall.returns(true); OMG = await smock.fake("VToken"); OMG.isVToken.returns(true); + OMG.comptroller.returns(comptroller.address); poolRegistrySigner = await ethers.getSigner(poolRegistry.address); // Sending transaction cost @@ -69,7 +70,7 @@ describe("setters", async () => { describe("setPriceOracle", async () => { let newPriceOracle: FakeContract; - before(async () => { + beforeEach(async () => { newPriceOracle = await smock.fake("ResilientOracleInterface"); }); @@ -111,13 +112,22 @@ describe("setters", async () => { }); }); - describe("setLiquidationIncentive", async () => { + describe("setMarketLiquidationIncentive", async () => { const newLiquidationIncentive = convertToUnit("1.2", 18); it("reverts if access control manager does not allow the call", async () => { - accessControl.isAllowedToCall.whenCalledWith(owner.address, "setLiquidationIncentive(uint256)").returns(false); - await expect(comptroller.setLiquidationIncentive(newLiquidationIncentive)) + await comptroller.connect(poolRegistry.wallet).supportMarket(OMG.address); + accessControl.isAllowedToCall + .whenCalledWith(owner.address, "setMarketLiquidationIncentive(address,uint256)") + .returns(false); + await expect(comptroller.setMarketLiquidationIncentive(OMG.address, newLiquidationIncentive)) .to.be.revertedWithCustomError(comptroller, "Unauthorized") - .withArgs(owner.address, comptroller.address, "setLiquidationIncentive(uint256)"); + .withArgs(owner.address, comptroller.address, "setMarketLiquidationIncentive(address,uint256)"); + }); + + it("reverts if market not listed", async () => { + await expect(comptroller.setMarketLiquidationIncentive(OMG.address, newLiquidationIncentive)) + .to.be.revertedWithCustomError(comptroller, "MarketNotListed") + .withArgs(OMG.address); }); }); From fc9c6a73d5bd5b800853d1ad23a97af97823fe2d Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 14 Jul 2025 18:01:09 +0530 Subject: [PATCH 19/51] test: fix seize tokens test --- .../liquidateCalculateAmountSeizeTest.ts | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts index f36fcca66..7d6518494 100644 --- a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts +++ b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts @@ -1,8 +1,10 @@ import { FakeContract, MockContract, smock } from "@defi-wonderland/smock"; import { PANIC_CODES } from "@nomicfoundation/hardhat-chai-matchers/panic"; -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { loadFixture, setBalance } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import chai from "chai"; import { BigNumberish, constants } from "ethers"; +import { parseEther } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; import { convertToUnit } from "../../../helpers/utils"; @@ -25,11 +27,17 @@ const repayAmount = convertToUnit(1, 18); async function calculateSeizeTokens( comptroller: MockContract, + borrower: string, vTokenBorrowed: FakeContract, vTokenCollateral: FakeContract, repayAmount: BigNumberish, ) { - return comptroller.liquidateCalculateSeizeTokens(vTokenBorrowed.address, vTokenCollateral.address, repayAmount); + return comptroller.liquidateCalculateSeizeTokens( + borrower, + vTokenBorrowed.address, + vTokenCollateral.address, + repayAmount, + ); } function rando(min: number, max: number): number { @@ -41,12 +49,18 @@ describe("Comptroller", () => { let oracle: FakeContract; let vTokenBorrowed: FakeContract; let vTokenCollateral: FakeContract; + let borrower: SignerWithAddress; const maxLoopsLimit = 150; + before(async () => { + await ethers.provider.getNetwork(); + }); + type LiquidateFixture = { accessControl: FakeContract; comptroller: MockContract; oracle: FakeContract; + poolRegistry: FakeContract; vTokenBorrowed: FakeContract; vTokenCollateral: FakeContract; }; @@ -64,22 +78,31 @@ describe("Comptroller", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); - await comptroller.setLiquidationIncentive(convertToUnit("1.1", 18)); const vTokenBorrowed = await smock.fake("VToken"); const vTokenCollateral = await smock.fake("VToken"); - return { accessControl, comptroller, oracle, vTokenBorrowed, vTokenCollateral }; + return { accessControl, comptroller, oracle, poolRegistry, vTokenBorrowed, vTokenCollateral }; } - async function configure({ accessControl, comptroller, vTokenCollateral, oracle, vTokenBorrowed }: LiquidateFixture) { + async function configure({ + accessControl, + comptroller, + vTokenCollateral, + oracle, + vTokenBorrowed, + poolRegistry, + }: LiquidateFixture) { oracle.getUnderlyingPrice.returns(0); + await setBalance(poolRegistry.address, parseEther("1")); + for (const vToken of [vTokenBorrowed, vTokenCollateral]) { vToken.comptroller.returns(comptroller.address); vToken.isVToken.returns(true); + await comptroller.connect(poolRegistry.wallet).supportMarket(vToken.address); + await comptroller.setMarketLiquidationIncentive(vToken.address, convertToUnit("1.1", 18)); } accessControl.isAllowedToCall.returns(true); @@ -89,6 +112,7 @@ describe("Comptroller", () => { } beforeEach(async () => { + [borrower] = await ethers.getSigners(); const contracts = await loadFixture(liquidateFixture); await configure(contracts); ({ comptroller, vTokenBorrowed, oracle, vTokenCollateral } = contracts); @@ -97,26 +121,26 @@ describe("Comptroller", () => { describe("liquidateCalculateAmountSeize", () => { it("fails if borrowed asset price is 0", async () => { await setOraclePrice(vTokenBorrowed, 0); - const call = calculateSeizeTokens(comptroller, vTokenBorrowed, vTokenCollateral, repayAmount); + const call = calculateSeizeTokens(comptroller, borrower.address, vTokenBorrowed, vTokenCollateral, repayAmount); await expect(call).to.be.revertedWithCustomError(comptroller, "PriceError").withArgs(vTokenBorrowed.address); }); it("fails if collateral asset price is 0", async () => { await setOraclePrice(vTokenCollateral, 0); - const call = calculateSeizeTokens(comptroller, vTokenBorrowed, vTokenCollateral, repayAmount); + const call = calculateSeizeTokens(comptroller, borrower.address, vTokenBorrowed, vTokenCollateral, repayAmount); await expect(call).to.be.revertedWithCustomError(comptroller, "PriceError").withArgs(vTokenCollateral.address); }); it("fails if the repayAmount causes overflow ", async () => { await expect( - calculateSeizeTokens(comptroller, vTokenBorrowed, vTokenCollateral, constants.MaxUint256), + calculateSeizeTokens(comptroller, borrower.address, vTokenBorrowed, vTokenCollateral, constants.MaxUint256), ).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); }); it("fails if the borrowed asset price causes overflow ", async () => { await setOraclePrice(vTokenBorrowed, constants.MaxUint256); await expect( - calculateSeizeTokens(comptroller, vTokenBorrowed, vTokenCollateral, repayAmount), + calculateSeizeTokens(comptroller, borrower.address, vTokenBorrowed, vTokenCollateral, repayAmount), ).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); }); @@ -125,7 +149,12 @@ describe("Comptroller", () => { await ethers.provider.getBlockNumber(); /// TODO: Somehow the error message does not get propagated into the resulting tx. Smock bug? await expect( - comptroller.liquidateCalculateSeizeTokens(vTokenBorrowed.address, vTokenCollateral.address, repayAmount), + comptroller.liquidateCalculateSeizeTokens( + borrower.address, + vTokenBorrowed.address, + vTokenCollateral.address, + repayAmount, + ), ).to.be.reverted; // revertedWith("exchangeRateStored: exchangeRateStoredInternal failed"); }); @@ -144,13 +173,20 @@ describe("Comptroller", () => { await setOraclePrice(vTokenCollateral, collateralPrice); await setOraclePrice(vTokenBorrowed, borrowedPrice); - await comptroller.setLiquidationIncentive(liquidationIncentive); + await comptroller.setMarketLiquidationIncentive(vTokenBorrowed.address, liquidationIncentive); + await comptroller.setMarketLiquidationIncentive(vTokenCollateral.address, liquidationIncentive); vTokenCollateral.exchangeRateStored.returns(exchangeRate); const seizeAmount = (repayAmount * liquidationIncentive * borrowedPrice) / collateralPrice; const seizeTokens = seizeAmount / exchangeRate; - const [err, result] = await calculateSeizeTokens(comptroller, vTokenBorrowed, vTokenCollateral, repayAmount); + const [err, result] = await calculateSeizeTokens( + comptroller, + borrower.address, + vTokenBorrowed, + vTokenCollateral, + repayAmount, + ); expect(err).to.equal(Error.NO_ERROR); expect(Number(result)).to.be.approximately(Number(seizeTokens), 1e7); }); From 691c424fbac2a9a39d53e1a1468524811f5a74be Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 11:40:22 +0530 Subject: [PATCH 20/51] refactor: moved rewards logic to internal functions --- contracts/Comptroller.sol | 46 ++++++++++++++++++++++++++----- contracts/lib/Rewards.sol | 58 --------------------------------------- 2 files changed, 39 insertions(+), 65 deletions(-) delete mode 100644 contracts/lib/Rewards.sol diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 74d39fe45..08477e3b4 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -14,7 +14,6 @@ import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; import { Liquidation } from "./lib/Liquidation.sol"; -import { Rewards } from "./lib/Rewards.sol"; /** * @title Comptroller @@ -449,7 +448,7 @@ contract Comptroller is } // Keep the flywheel moving - Rewards.updateAndDistributeSupplyRewards(rewardsDistributors, vToken, minter); + _updateAndDistributeSupplyRewards(vToken, minter); } /** @@ -484,7 +483,7 @@ contract Comptroller is _checkRedeemAllowed(vToken, redeemer, redeemTokens); // Keep the flywheel moving - Rewards.updateAndDistributeSupplyRewards(rewardsDistributors, vToken, redeemer); + _updateAndDistributeSupplyRewards(vToken, redeemer); } /** @@ -638,7 +637,7 @@ contract Comptroller is } // Keep the flywheel moving - Rewards.updateAndDistributeBorrowRewards(rewardsDistributors, vToken, borrower); + _updateAndDistributeBorrowRewards(vToken, borrower); } /** @@ -672,7 +671,7 @@ contract Comptroller is } // Keep the flywheel moving - Rewards.updateAndDistributeBorrowRewards(rewardsDistributors, vToken, borrower); + _updateAndDistributeBorrowRewards(vToken, borrower); } /** @@ -808,7 +807,7 @@ contract Comptroller is } // Keep the flywheel moving - Rewards.updateAndDistributeSupplyRewardsMulti(rewardsDistributors, vTokenCollateral, borrower, liquidator); + _updateAndDistributeSupplyRewardsMulti(vTokenCollateral, borrower, liquidator); } /** @@ -832,7 +831,7 @@ contract Comptroller is _checkRedeemAllowed(vToken, src, transferTokens); // Keep the flywheel moving - Rewards.updateAndDistributeSupplyRewardsMulti(rewardsDistributors, vToken, src, dst); + _updateAndDistributeSupplyRewardsMulti(vToken, src, dst); } /*** Pool-level operations ***/ @@ -1586,6 +1585,39 @@ contract Comptroller is } } + function _updateAndDistributeSupplyRewards(address vToken, address user) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor rewardsDistributor = rewardsDistributors[i]; + rewardsDistributor.updateRewardTokenSupplyIndex(vToken); + rewardsDistributor.distributeSupplierRewardToken(vToken, user); + } + } + + function _updateAndDistributeBorrowRewards(address vToken, address user) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + + Exp memory borrowIndex = Exp({ mantissa: VToken(vToken).borrowIndex() }); + + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor rewardsDistributor = rewardsDistributors[i]; + rewardsDistributor.updateRewardTokenBorrowIndex(vToken, borrowIndex); + rewardsDistributor.distributeBorrowerRewardToken(vToken, user, borrowIndex); + } + } + + function _updateAndDistributeSupplyRewardsMulti(address vToken, address user1, address user2) internal { + uint256 rewardDistributorsCount = rewardsDistributors.length; + + for (uint256 i; i < rewardDistributorsCount; ++i) { + RewardsDistributor rewardsDistributor = rewardsDistributors[i]; + rewardsDistributor.updateRewardTokenSupplyIndex(vToken); + rewardsDistributor.distributeSupplierRewardToken(vToken, user1); + rewardsDistributor.distributeSupplierRewardToken(vToken, user2); + } + } + /** * @notice Get the total collateral, weighted collateral, borrow balance, liquidity, shortfall * @param account The account to get the snapshot for diff --git a/contracts/lib/Rewards.sol b/contracts/lib/Rewards.sol deleted file mode 100644 index 5070a1766..000000000 --- a/contracts/lib/Rewards.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.10; - -import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol"; -import { ExponentialNoError } from "../ExponentialNoError.sol"; -import { VToken } from "../VToken.sol"; - -library Rewards { - /** - * @dev Updates and distributes supply-side rewards for a market/user - */ - function updateAndDistributeSupplyRewards( - RewardsDistributor[] storage rewardsDistributors, - address vToken, - address user - ) internal { - uint256 rewardDistributorsCount = rewardsDistributors.length; - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor distributor = rewardsDistributors[i]; - distributor.updateRewardTokenSupplyIndex(vToken); - distributor.distributeSupplierRewardToken(vToken, user); - } - } - - function updateAndDistributeSupplyRewardsMulti( - RewardsDistributor[] storage rewardsDistributors, - address vToken, - address user1, - address user2 - ) internal { - uint256 rewardDistributorsCount = rewardsDistributors.length; - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor distributor = rewardsDistributors[i]; - distributor.updateRewardTokenSupplyIndex(vToken); - distributor.distributeSupplierRewardToken(vToken, user1); - distributor.distributeSupplierRewardToken(vToken, user2); - } - } - - /** - * @dev Updates and distributes borrow-side rewards - */ - function updateAndDistributeBorrowRewards( - RewardsDistributor[] storage rewardsDistributors, - address vToken, - address user - ) internal { - uint256 rewardDistributorsCount = rewardsDistributors.length; - - ExponentialNoError.Exp memory borrowIndex = ExponentialNoError.Exp({ mantissa: VToken(vToken).borrowIndex() }); - - for (uint256 i; i < rewardDistributorsCount; ++i) { - RewardsDistributor distributor = rewardsDistributors[i]; - distributor.updateRewardTokenBorrowIndex(vToken, borrowIndex); - distributor.distributeBorrowerRewardToken(vToken, user, borrowIndex); - } - } -} From bebf45d34d57cc0bf9853b2fdd641c0b49f2cd41 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 18:50:19 +0530 Subject: [PATCH 21/51] test: fix pool lens test --- contracts/Lens/PoolLens.sol | 8 +++----- tests/hardhat/Lens/PoolLens.ts | 30 +++++++----------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/contracts/Lens/PoolLens.sol b/contracts/Lens/PoolLens.sol index fd414b8cf..97b2e8029 100644 --- a/contracts/Lens/PoolLens.sol +++ b/contracts/Lens/PoolLens.sol @@ -20,7 +20,7 @@ import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeM * for all pools within the lending protocol can be acquired through the function `getAllPools()`. Additionally, the following records can be * looked up for specific pools and markets: - the vToken balance of a given user; -- the pool data (oracle address, associated vToken, liquidation incentive, etc) of a pool via its associated comptroller address; +- the pool data (oracle address, associated vToken etc) of a pool via its associated comptroller address; - the vToken address in a pool for a given asset; - a list of all pools that support an asset; - the underlying asset price of a vToken; @@ -41,7 +41,6 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 { string description; address priceOracle; uint256 closeFactor; - uint256 liquidationIncentive; uint256 minLiquidatableCollateral; VTokenMetadata[] vTokens; } @@ -270,7 +269,7 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 { * * @param comptrollerAddress Address of the comptroller * - * @return badDebtSummary A struct with comptroller address, total bad debut denominated in usd, and + * @return badDebtSummary A struct with comptroller address, total bad debt denominated in usd, and * a break down of bad debt by market */ function getPoolBadDebt(address comptrollerAddress) external view returns (BadDebtSummary memory) { @@ -287,7 +286,7 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 { badDebtSummary.comptroller = comptrollerAddress; badDebtSummary.badDebts = badDebts; - // // Calculate the bad debt is USD per market + // Calculate the bad debt in USD per market for (uint256 i; i < markets.length; ++i) { BadDebt memory badDebt; badDebt.vTokenAddress = address(markets[i]); @@ -368,7 +367,6 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 { vTokens: vTokenMetadataItems, priceOracle: address(comptrollerViewInstance.oracle()), closeFactor: comptrollerViewInstance.closeFactorMantissa(), - liquidationIncentive: comptrollerViewInstance.liquidationIncentiveMantissa(), minLiquidatableCollateral: comptrollerViewInstance.minLiquidatableCollateral() }); diff --git a/tests/hardhat/Lens/PoolLens.ts b/tests/hardhat/Lens/PoolLens.ts index 00a0e7675..f17f70235 100644 --- a/tests/hardhat/Lens/PoolLens.ts +++ b/tests/hardhat/Lens/PoolLens.ts @@ -66,8 +66,6 @@ for (const isTimeBased of [false, true]) { let fakeAccessControlManager: FakeContract; let closeFactor1: BigNumberish; let closeFactor2: BigNumberish; - let liquidationIncentive1: BigNumberish; - let liquidationIncentive2: BigNumberish; const minLiquidatableCollateral = parseUnits("100", 18); const maxLoopsLimit = 150; const defaultBtcPrice = "21000.34"; @@ -106,7 +104,6 @@ for (const isTimeBased of [false, true]) { priceOracle = await MockPriceOracle.deploy(); closeFactor1 = parseUnits("0.05", 18); - liquidationIncentive1 = parseUnits("1", 18); const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); @@ -123,25 +120,12 @@ for (const isTimeBased of [false, true]) { ); // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptroller1Proxy.address, - closeFactor1, - liquidationIncentive1, - minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 1", comptroller1Proxy.address, closeFactor1, minLiquidatableCollateral); closeFactor2 = parseUnits("0.05", 18); - liquidationIncentive2 = parseUnits("1", 18); // Registering the second pool - await poolRegistry.addPool( - "Pool 2", - comptroller2Proxy.address, - closeFactor2, - liquidationIncentive2, - minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 2", comptroller2Proxy.address, closeFactor2, minLiquidatableCollateral); const MockToken = await ethers.getContractFactory("MockToken"); mockDAI = await MockToken.deploy("MakerDAO", "DAI", 18); await mockDAI.faucet(parseUnits("1000", 18)); @@ -258,7 +242,6 @@ for (const isTimeBased of [false, true]) { expect(venusPool1Actual.description).equal("Pool1 description"); expect(venusPool1Actual.priceOracle).equal(priceOracle.address); expect(venusPool1Actual.closeFactor).equal(closeFactor1); - expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive1); expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); const vTokensActual = venusPool1Actual.vTokens; @@ -286,7 +269,6 @@ for (const isTimeBased of [false, true]) { expect(venusPool2Actual.description).equal("Pool2 description"); expect(venusPool1Actual.priceOracle).equal(priceOracle.address); expect(venusPool1Actual.closeFactor).equal(closeFactor2); - expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive2); expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); }); @@ -301,7 +283,6 @@ for (const isTimeBased of [false, true]) { expect(poolData.description).equal("Pool1 description"); expect(poolData.priceOracle).equal(priceOracle.address); expect(poolData.closeFactor).equal(closeFactor1); - expect(poolData.liquidationIncentive).equal(liquidationIncentive1); expect(poolData.minLiquidatableCollateral).equal(minLiquidatableCollateral); const vTokensActual = poolData.vTokens; @@ -410,6 +391,9 @@ for (const isTimeBased of [false, true]) { [parseUnits("9000000000", 18), parseUnits("9000000000", 18)], ); + await comptroller1Proxy.setMarketLiquidationIncentive(vWBTC.address, parseUnits("1.1", 18)); + await comptroller1Proxy.setMarketLiquidationIncentive(vDAI.address, parseUnits("1.1", 18)); + await mockWBTC.connect(borrowerDai).faucet(parseUnits("20", 18)); await mockWBTC.connect(borrowerDai).approve(vWBTC.address, parseUnits("20", 18)); await vWBTC.connect(borrowerDai).mint(parseUnits("2", 18)); @@ -440,13 +424,13 @@ for (const isTimeBased of [false, true]) { const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); - expect(resp.totalBadDebtUsd).to.be.equal("210003400000000000000000005"); + expect(resp.totalBadDebtUsd).to.be.equal("192821303636611822200000005"); expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); expect(resp.badDebts[1][1].toString()).to.be.equal("5"); expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); - expect(resp.badDebts[0][1].toString()).to.be.equal("210003400000000000000000000"); + expect(resp.badDebts[0][1].toString()).to.be.equal("192821303636611822200000000"); // Cleanup await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); From 281125835c5d7f6695211b681788db4a3059728a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 18:52:16 +0530 Subject: [PATCH 22/51] test: fix tests --- tests/hardhat/MaxLoopsLimitHelper.ts | 5 +++ tests/hardhat/PoolRegistry.ts | 49 ++++------------------------ tests/hardhat/Prime.ts | 8 +---- tests/hardhat/Rewards.ts | 8 +---- tests/hardhat/UpgradedVToken.ts | 8 +---- 5 files changed, 14 insertions(+), 64 deletions(-) diff --git a/tests/hardhat/MaxLoopsLimitHelper.ts b/tests/hardhat/MaxLoopsLimitHelper.ts index dc0288daf..1e0dc4b9e 100644 --- a/tests/hardhat/MaxLoopsLimitHelper.ts +++ b/tests/hardhat/MaxLoopsLimitHelper.ts @@ -1,5 +1,6 @@ import { MockContract, MockContractFactory, smock } from "@defi-wonderland/smock"; import chai from "chai"; +import { ethers } from "hardhat"; import { HarnessMaxLoopsLimitHelper, HarnessMaxLoopsLimitHelper__factory } from "../../typechain"; @@ -7,6 +8,10 @@ const { expect } = chai; chai.use(smock.matchers); describe("MaxLoopsLimit: tests", () => { + before(async () => { + await ethers.provider.ready; // Ensure provider is initialized + }); + let maxLoopsLimitHelperFactory: MockContractFactory; let maxLoopsLimitHelper: MockContract; diff --git a/tests/hardhat/PoolRegistry.ts b/tests/hardhat/PoolRegistry.ts index 37f1bceae..18ddb9e4b 100644 --- a/tests/hardhat/PoolRegistry.ts +++ b/tests/hardhat/PoolRegistry.ts @@ -115,26 +115,13 @@ describe("PoolRegistry: Tests", function () { ); const _closeFactor = parseUnits("0.05", 18); - const _liquidationIncentive = parseUnits("1", 18); const _minLiquidatableCollateral = parseUnits("100", 18); // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptroller1Proxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 1", comptroller1Proxy.address, _closeFactor, _minLiquidatableCollateral); // Registering the second pool - await poolRegistry.addPool( - "Pool 2", - comptroller2Proxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 2", comptroller2Proxy.address, _closeFactor, _minLiquidatableCollateral); // Setup Proxies vWBTC = await makeVToken({ @@ -475,38 +462,20 @@ describe("PoolRegistry: Tests", function () { const addPoolSignature = "addPool(string,address,uint256,uint256,uint256)"; fakeAccessControlManager.isAllowedToCall.whenCalledWith(owner.address, addPoolSignature).returns(false); await expect( - poolRegistry.addPool( - "Pool 3", - comptroller3Proxy.address, - parseUnits("0.5", 18), - parseUnits("1.1", 18), - parseUnits("100", 18), - ), + poolRegistry.addPool("Pool 3", comptroller3Proxy.address, parseUnits("0.5", 18), parseUnits("100", 18)), ).to.be.revertedWithCustomError(poolRegistry, "Unauthorized"); }); it("reverts if pool name is too long", async () => { const longName = Array(101).fill("a").join(""); await expect( - poolRegistry.addPool( - longName, - comptroller3Proxy.address, - parseUnits("0.5", 18), - parseUnits("1.1", 18), - parseUnits("100", 18), - ), + poolRegistry.addPool(longName, comptroller3Proxy.address, parseUnits("0.5", 18), parseUnits("100", 18)), ).to.be.revertedWith("Pool's name is too large"); }); it("reverts if Comptroller address is zero", async () => { await expect( - poolRegistry.addPool( - "Pool 3", - constants.AddressZero, - parseUnits("0.5", 18), - parseUnits("1.1", 18), - parseUnits("100", 18), - ), + poolRegistry.addPool("Pool 3", constants.AddressZero, parseUnits("0.5", 18), parseUnits("100", 18)), ).to.be.revertedWithCustomError(poolRegistry, "ZeroAddressNotAllowed"); }); @@ -516,13 +485,7 @@ describe("PoolRegistry: Tests", function () { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptroller = await Comptroller.deploy(poolRegistry.address); await expect( - poolRegistry.addPool( - "Pool 3", - comptroller.address, - parseUnits("0.5", 18), - parseUnits("1.1", 18), - parseUnits("100", 18), - ), + poolRegistry.addPool("Pool 3", comptroller.address, parseUnits("0.5", 18), parseUnits("100", 18)), ).to.be.revertedWithCustomError(poolRegistry, "ZeroAddressNotAllowed"); }); }); diff --git a/tests/hardhat/Prime.ts b/tests/hardhat/Prime.ts index 755625706..e8ac80f7c 100644 --- a/tests/hardhat/Prime.ts +++ b/tests/hardhat/Prime.ts @@ -84,13 +84,7 @@ async function deployProtocol(): Promise { await comptrollerProxy.setPriceOracle(fakePriceOracle.address); // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptrollerProxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 1", comptrollerProxy.address, _closeFactor, _minLiquidatableCollateral); const vTokenBeacon = await deployVTokenBeacon(); const vUSDT = await makeVToken({ diff --git a/tests/hardhat/Rewards.ts b/tests/hardhat/Rewards.ts index 8782760d4..0027cf4d0 100644 --- a/tests/hardhat/Rewards.ts +++ b/tests/hardhat/Rewards.ts @@ -92,13 +92,7 @@ async function rewardsFixture(isTimeBased: boolean) { await comptrollerProxy.setPriceOracle(fakePriceOracle.address); // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptrollerProxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 1", comptrollerProxy.address, _closeFactor, _minLiquidatableCollateral); if (isTimeBased) { blocksPerYear = 0; diff --git a/tests/hardhat/UpgradedVToken.ts b/tests/hardhat/UpgradedVToken.ts index 46317ad24..539dc206a 100644 --- a/tests/hardhat/UpgradedVToken.ts +++ b/tests/hardhat/UpgradedVToken.ts @@ -70,13 +70,7 @@ for (const isTimeBased of [false, true]) { await comptroller1Proxy.setPriceOracle(priceOracle.address); // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptroller1Proxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); + await poolRegistry.addPool("Pool 1", comptroller1Proxy.address, _closeFactor, _minLiquidatableCollateral); vTokenBeacon = await deployVTokenBeacon(); const vWBTC = await makeVToken({ From 8876c2930162d17142ad3478833cedf969b8994f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 18:55:32 +0530 Subject: [PATCH 23/51] feat: getter for liquidation incentive per market --- contracts/Comptroller.sol | 16 ++++++++++++++++ contracts/ComptrollerInterface.sol | 2 ++ 2 files changed, 18 insertions(+) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 08477e3b4..4c8b7629e 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -1344,6 +1344,22 @@ contract Comptroller is return markets[address(vToken)].accountMembership[account]; } + /** + * @notice Returns the liquidation incentive mantissa for a given vToken market. + * @param vToken The address of the vToken market to query. + * @return liquidationIncentiveMantissa The liquidation incentive mantissa for the specified market. + * @custom:error MarketNotListed is thrown if the market is not listed + */ + function getMarketLiquidationIncentive( + address vToken + ) external view returns (uint256 liquidationIncentiveMantissa) { + Market storage market = markets[vToken]; + if (!market.isListed) { + revert MarketNotListed(vToken); + } + return market.liquidationIncentiveMantissa; + } + /** * @notice Calculate number of tokens of collateral asset to seize given an underlying amount * @dev Used in liquidation (called in vToken.liquidateBorrowFresh) diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index 829f7d848..fe9cac4b0 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -137,4 +137,6 @@ interface ComptrollerViewInterface { function approvedDelegates(address user, address delegate) external view returns (bool); function getDynamicLiquidationIncentive(address borrower, address market) external view returns (uint256); + + function getMarketLiquidationIncentive(address vToken) external view returns (uint256); } From e6e906feb955d5a1d9db69423e1898a22e95d315 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 18:56:01 +0530 Subject: [PATCH 24/51] fix: fixed vTokens test --- contracts/VToken.sol | 5 ++++- tests/hardhat/Tokens/liquidateTest.ts | 4 +++- tests/hardhat/Tokens/setters.ts | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/VToken.sol b/contracts/VToken.sol index 63f2d2ced..aa3cacf69 100644 --- a/contracts/VToken.sol +++ b/contracts/VToken.sol @@ -471,7 +471,10 @@ contract VToken is */ function setProtocolSeizeShare(uint256 newProtocolSeizeShareMantissa_) external { _checkAccessAllowed("setProtocolSeizeShare(uint256)"); - uint256 liquidationIncentive = ComptrollerViewInterface(address(comptroller)).liquidationIncentiveMantissa(); + + uint256 liquidationIncentive = ComptrollerViewInterface(address(comptroller)).getMarketLiquidationIncentive( + address(this) + ); if (newProtocolSeizeShareMantissa_ + MANTISSA_ONE > liquidationIncentive) { revert ProtocolSeizeShareTooBig(); } diff --git a/tests/hardhat/Tokens/liquidateTest.ts b/tests/hardhat/Tokens/liquidateTest.ts index 98d00500b..32d5c68a6 100644 --- a/tests/hardhat/Tokens/liquidateTest.ts +++ b/tests/hardhat/Tokens/liquidateTest.ts @@ -49,7 +49,6 @@ type LiquidateTestFixture = { async function liquidateTestFixture(): Promise { const comptroller = await fakeComptroller(); - comptroller.liquidationIncentiveMantissa.returns(parseUnits("1.1", 18)); const accessControlManager = await smock.fake("AccessControlManager"); accessControlManager.isAllowedToCall.returns(true); const [admin, liquidator, borrower] = await ethers.getSigners(); @@ -90,6 +89,7 @@ async function liquidateTestFixture(): Promise { const underlyingCollateral = await collateralVToken.underlying(); const collateralErc20 = ERC20Harness__factory.connect(underlyingCollateral, admin); await collateralErc20.harnessSetBalance(collateralVToken.address, cash); + return { accessControlManager, comptroller, @@ -117,6 +117,8 @@ function configure({ comptroller.liquidateCalculateSeizeTokens.reset(); comptroller.liquidateCalculateSeizeTokens.returns([Error.NO_ERROR, seizeTokens]); + comptroller.getDynamicLiquidationIncentive.reset(); + comptroller.getDynamicLiquidationIncentive.returns(parseUnits("1.1", 18)); borrowedUnderlying.transferFrom.reset(); diff --git a/tests/hardhat/Tokens/setters.ts b/tests/hardhat/Tokens/setters.ts index ef4967ef4..e7fcda49f 100644 --- a/tests/hardhat/Tokens/setters.ts +++ b/tests/hardhat/Tokens/setters.ts @@ -40,9 +40,10 @@ describe("VToken", function () { )); comptroller.isComptroller.returns(true); newComptroller.isComptroller.returns(true); - comptroller.liquidationIncentiveMantissa.returns(parseUnits("1.1", 18)); accessControlManager.isAllowedToCall.reset(); accessControlManager.isAllowedToCall.returns(true); + comptroller.getMarketLiquidationIncentive.reset(); + comptroller.getMarketLiquidationIncentive.returns(parseUnits("1.1", 18)); // 105% liquidation incentive }); describe("setProtocolSeizeShare", () => { From 749dbb66064f596ac6803efaa0cd72a4c7f054e6 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 15 Jul 2025 19:12:40 +0530 Subject: [PATCH 25/51] test: fix NativeToken gateway test --- tests/hardhat/Gateway/NativeTokenGateway.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/hardhat/Gateway/NativeTokenGateway.ts b/tests/hardhat/Gateway/NativeTokenGateway.ts index b88e23919..08ec236a9 100644 --- a/tests/hardhat/Gateway/NativeTokenGateway.ts +++ b/tests/hardhat/Gateway/NativeTokenGateway.ts @@ -46,7 +46,6 @@ async function deployGateway(): Promise { accessControl.isAllowedToCall.returns(true); const closeFactor = parseUnits("6", 17); - const liquidationIncentive = parseUnits("1", 18); const minLiquidatableCollateral = parseUnits("100", 18); const PoolRegistry = await ethers.getContractFactory("PoolRegistry"); @@ -66,15 +65,7 @@ async function deployGateway(): Promise { await comptrollerProxy.setPriceOracle(fakePriceOracle.address); // Registering the pool - await poolRegistry.addPool( - "Pool 1", - comptrollerProxy.address, - closeFactor, - liquidationIncentive, - minLiquidatableCollateral, - ); - - await comptrollerProxy.setPriceOracle(fakePriceOracle.address); + await poolRegistry.addPool("Pool 1", comptrollerProxy.address, closeFactor, minLiquidatableCollateral); const wethFactory = await ethers.getContractFactory("WrappedNative"); const weth = await wethFactory.deploy(); From e8ea51250597052025d0ca4695176b323265bac4 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 16 Jul 2025 15:40:04 +0530 Subject: [PATCH 26/51] fix: Average liquidation incentive calculation --- contracts/Comptroller.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 4c8b7629e..6ef5bc57d 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -1691,17 +1691,19 @@ contract Comptroller is snapshot ); + liquidationIncentiveMantissa += markets[address(asset)].liquidationIncentiveMantissa; + unchecked { ++i; } } - uint256 weightedAvg; + uint256 liquidationIncentiveAvg; if (assetsCount > 0) { - weightedAvg = div_(liquidationIncentiveMantissa, assetsCount); + liquidationIncentiveAvg = div_(liquidationIncentiveMantissa, assetsCount); } - return Liquidation.finalizeSnapshot(snapshot, assetsCount, weightedAvg); + return Liquidation.finalizeSnapshot(snapshot, assetsCount, liquidationIncentiveAvg); } /** From c4a57682fa990665a8d3da55a46be9a8068326de Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 16 Jul 2025 15:44:46 +0530 Subject: [PATCH 27/51] refactor: corrected addPool signature --- contracts/Pool/PoolRegistry.sol | 2 +- deploy/013-vip-based-config.ts | 12 +++--------- helpers/deploymentConfig.ts | 4 ++-- tests/hardhat/PoolRegistry.ts | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/contracts/Pool/PoolRegistry.sol b/contracts/Pool/PoolRegistry.sol index 5cc499d8f..214154081 100644 --- a/contracts/Pool/PoolRegistry.sol +++ b/contracts/Pool/PoolRegistry.sol @@ -135,7 +135,7 @@ contract PoolRegistry is Ownable2StepUpgradeable, AccessControlledV8, PoolRegist uint256 closeFactor, uint256 minLiquidatableCollateral ) external virtual returns (uint256 index) { - _checkAccessAllowed("addPool(string,address,uint256,uint256,uint256)"); + _checkAccessAllowed("addPool(string,address,uint256,uint256)"); // Input validation ensureNonzeroAddress(address(comptroller)); ensureNonzeroAddress(address(comptroller.oracle())); diff --git a/deploy/013-vip-based-config.ts b/deploy/013-vip-based-config.ts index 6204d25be..c6aad67d6 100644 --- a/deploy/013-vip-based-config.ts +++ b/deploy/013-vip-based-config.ts @@ -135,15 +135,9 @@ const addPool = (poolRegistry: PoolRegistry, comptroller: Comptroller, pool: Poo console.log(`Adding a command to add Comptroller_${pool.id} to PoolRegistry`); return { contract: poolRegistry.address, - signature: "addPool(string,address,uint256,uint256,uint256)", - argTypes: ["string", "address", "uint256", "uint256", "uint256"], - parameters: [ - pool.name, - comptroller.address, - pool.closeFactor, - pool.liquidationIncentive, - pool.minLiquidatableCollateral, - ], + signature: "addPool(string,address,uint256,uint256)", + argTypes: ["string", "address", "uint256", "uint256"], + parameters: [pool.name, comptroller.address, pool.closeFactor, pool.minLiquidatableCollateral], value: 0, }; }; diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 95964c287..356b5ccb3 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -364,7 +364,7 @@ const poolRegistryPermissions = (): AccessControlEntry[] => { const deployerPermissions = (): AccessControlEntry[] => { const methods = [ "swapPoolsAssets(address[],uint256[],address[][])", - "addPool(string,address,uint256,uint256,uint256)", + "addPool(string,address,uint256,uint256)", "addMarket(AddMarketInput)", "setRewardTokenSpeeds(address[],uint256[],uint256[])", "setReduceReservesBlockDelta(uint256)", @@ -386,7 +386,7 @@ const normalTimelockPermissions = (timelock: string): AccessControlEntry[] => { "setMarketSupplyCaps(address[],uint256[])", "setActionsPaused(address[],uint256[],bool)", "setMinLiquidatableCollateral(uint256)", - "addPool(string,address,uint256,uint256,uint256)", + "addPool(string,address,uint256,uint256)", "addMarket(AddMarketInput)", "setPoolName(address,string)", "updatePoolMetadata(address,VenusPoolMetaData)", diff --git a/tests/hardhat/PoolRegistry.ts b/tests/hardhat/PoolRegistry.ts index 18ddb9e4b..ce0b8c34b 100644 --- a/tests/hardhat/PoolRegistry.ts +++ b/tests/hardhat/PoolRegistry.ts @@ -459,7 +459,7 @@ describe("PoolRegistry: Tests", function () { describe("addPool", async () => { it("reverts if ACM denies the access", async () => { - const addPoolSignature = "addPool(string,address,uint256,uint256,uint256)"; + const addPoolSignature = "addPool(string,address,uint256,uint256)"; fakeAccessControlManager.isAllowedToCall.whenCalledWith(owner.address, addPoolSignature).returns(false); await expect( poolRegistry.addPool("Pool 3", comptroller3Proxy.address, parseUnits("0.5", 18), parseUnits("100", 18)), From fe24745b9baea788b408865818cb8642385a9b03 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 16 Jul 2025 19:06:53 +0530 Subject: [PATCH 28/51] refactor: adjust computation in calculateIncentiveAdjustedDebt --- contracts/Comptroller.sol | 10 ++++++++-- contracts/lib/Liquidation.sol | 27 +++++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 6ef5bc57d..469266c98 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -872,7 +872,12 @@ contract Comptroller is Exp memory totalCollateral = Exp({ mantissa: snapshot.totalCollateral }); Exp memory totalScaledBorrows = Exp({ - mantissa: Liquidation.calculateIncentiveAdjustedDebt(user, userAssets, ComptrollerInterface(address(this))) + mantissa: Liquidation.calculateIncentiveAdjustedDebt( + user, + userAssets, + ComptrollerInterface(address(this)), + _safeGetUnderlyingPrice + ) }); // percentage = collateral / (borrows * liquidation incentive) @@ -927,7 +932,8 @@ contract Comptroller is uint256 collateralToSeize = Liquidation.calculateIncentiveAdjustedDebt( borrower, borrowMarkets, - ComptrollerInterface(address(this)) + ComptrollerInterface(address(this)), + _safeGetUnderlyingPrice ); if (collateralToSeize >= snapshot.totalCollateral) { diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol index c89ce2410..3aca89edc 100644 --- a/contracts/lib/Liquidation.sol +++ b/contracts/lib/Liquidation.sol @@ -73,20 +73,31 @@ library Liquidation { function calculateIncentiveAdjustedDebt( address borrower, VToken[] memory markets, - ComptrollerInterface comptroller + ComptrollerInterface comptroller, + function(VToken) internal view returns (uint256) getUnderlyingPrice ) internal view returns (uint256 weightedBorrowSum) { for (uint256 i; i < markets.length; ++i) { VToken market = markets[i]; + (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); + if (borrowBalance == 0) continue; + + // Convert to USD value using oracle price + uint256 borrowPrice = getUnderlyingPrice(market); + uint256 borrowValueUSD = ExponentialNoError.mul_ScalarTruncate( + ExponentialNoError.Exp({ mantissa: borrowPrice }), + borrowBalance + ); - if (borrowBalance > 0) { - uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); - uint256 scaledBorrow = ExponentialNoError.mul_ScalarTruncate( + uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); + + weightedBorrowSum = ExponentialNoError.add_( + weightedBorrowSum, + ExponentialNoError.mul_ScalarTruncate( ExponentialNoError.Exp({ mantissa: marketIncentive }), - borrowBalance - ); - weightedBorrowSum = ExponentialNoError.add_(weightedBorrowSum, scaledBorrow); - } + borrowValueUSD + ) + ); } return weightedBorrowSum; } From bfd294f8ee2cf0079133d624c233bcd6a5a72e9c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 16 Jul 2025 19:07:22 +0530 Subject: [PATCH 29/51] test: fixed integration tests --- tests/integration/index.ts | 41 ++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/tests/integration/index.ts b/tests/integration/index.ts index aa8e75e0f..99cb1fc00 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -103,6 +103,12 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun deployer, ); + await AccessControlManager.giveCallPermission( + ethers.constants.AddressZero, + "setMarketLiquidationIncentive(address,uint256)", + deployer, + ); + // Set supply caps const supply = convertToUnit(10, 36); await Comptroller.setMarketSupplyCaps([vBNX.address, vBTCB.address], [supply, supply]); @@ -111,6 +117,9 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun const borrowCap = convertToUnit(10, 36); await Comptroller.setMarketBorrowCaps([vBNX.address, vBTCB.address], [borrowCap, borrowCap]); + await Comptroller.setMarketLiquidationIncentive(vBNX.address, convertToUnit(1, 18)); + await Comptroller.setMarketLiquidationIncentive(vBTCB.address, convertToUnit(1, 18)); + const vBNXPrice: BigNumber = new BigNumber( scaleDownBy((await priceOracle.getUnderlyingPrice(vBNX.address)).toString(), 18), ); @@ -624,26 +633,28 @@ describe("Straight Cases For Single User Liquidation and healing", function () { ); }); - it("Should revert when liquidation is called through vToken and trying to pay too much", async function () { - // Mint and Incrrease collateral of the user - udnerlyingMintAmount = convertToUnit("1", 18); - const VTokenMintAmount = convertToUnit("1", 8); + it("Should allow 100% repay when health factor is below threshold", async function () { + // 1. Setup - Mint collateral + const underlyingMintAmount = convertToUnit("1", 18); + const vTokenMintAmount = convertToUnit("1", 8); const expectedTotalBalance = Number(convertToUnit(1, 7)) + Number(convertToUnit(1, 8)); - await BNX.connect(acc2Signer).faucet(udnerlyingMintAmount); - await BNX.connect(acc2Signer).approve(vBNX.address, udnerlyingMintAmount); - await expect(vBNX.connect(acc2Signer).mint(udnerlyingMintAmount)) + await BNX.connect(acc2Signer).faucet(underlyingMintAmount); + await BNX.connect(acc2Signer).approve(vBNX.address, underlyingMintAmount); + + await expect(vBNX.connect(acc2Signer).mint(underlyingMintAmount)) .to.emit(vBNX, "Mint") - .withArgs(acc2, udnerlyingMintAmount, VTokenMintAmount, expectedTotalBalance); - // price manipulation and borrow to overcome insufficient shortfall + .withArgs(acc2, underlyingMintAmount, vTokenMintAmount, expectedTotalBalance); + + // 2. Manipulate prices to create severe shortfall (health factor 0) const dummyPriceOracle = await smock.fake("MockPriceOracle"); - dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBTCB.address).returns(convertToUnit("1", 20)); - dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 18)); + dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBTCB.address).returns(convertToUnit("1", 20)); // High BTCB price + dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 18)); // BNX price await Comptroller.setPriceOracle(dummyPriceOracle.address); - // Liquidation - await expect( - vBTCB.connect(acc1Signer).liquidateBorrow(acc2, convertToUnit("1", 18), vBNX.address), - ).to.be.revertedWithCustomError(Comptroller, "TooMuchRepay"); + + // 3. Attempt full liquidation (should succeed) + const repayAmount = convertToUnit("1", 18); + await expect(vBTCB.connect(acc1Signer).liquidateBorrow(acc2, repayAmount, vBNX.address)).to.not.be.reverted; }); it("Should success when liquidation is called through vToken", async function () { From 232421a4cc52303900e4b16aacd9d60eb365178e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 11:39:38 +0530 Subject: [PATCH 30/51] fix: fix Pool lens test --- tests/hardhat/Lens/PoolLens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hardhat/Lens/PoolLens.ts b/tests/hardhat/Lens/PoolLens.ts index f17f70235..83c9692da 100644 --- a/tests/hardhat/Lens/PoolLens.ts +++ b/tests/hardhat/Lens/PoolLens.ts @@ -424,13 +424,13 @@ for (const isTimeBased of [false, true]) { const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); - expect(resp.totalBadDebtUsd).to.be.equal("192821303636611822200000005"); + expect(resp.totalBadDebtUsd).to.be.equal("210003400000000000000000005"); expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); expect(resp.badDebts[1][1].toString()).to.be.equal("5"); expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); - expect(resp.badDebts[0][1].toString()).to.be.equal("192821303636611822200000000"); + expect(resp.badDebts[0][1].toString()).to.be.equal("210003400000000000000000000"); // Cleanup await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); From fbde27d82237d068d4d0551cdec3b25d64b79a1f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 18:43:34 +0530 Subject: [PATCH 31/51] fix: corrected averageLT calculation --- contracts/lib/Liquidation.sol | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol index 3aca89edc..3d9c1461c 100644 --- a/contracts/lib/Liquidation.sol +++ b/contracts/lib/Liquidation.sol @@ -210,7 +210,8 @@ library Liquidation { asset.borrowBalance, snapshot.borrows ); - snapshot.weightavg += asset.assetWeight; + uint256 vTokenBalanceUSD = ExponentialNoError.mul_ScalarTruncate(vTokenPrice, asset.vTokenBalance); + snapshot.averageLT += ExponentialNoError.mul_(asset.assetWeight, vTokenBalanceUSD); // Handle modified asset effects if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { @@ -232,32 +233,27 @@ library Liquidation { /** * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. * @dev - * - Computes the average weight if there are assets. + * - Computes the average weight. * - Calculates the sum of borrows and effects. * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. * - Sets the health factor threshold using the weighted average and liquidation incentive. * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. * @param snapshot The account liquidity snapshot to be finalized. - * @param assetsCount The number of assets in the snapshot. - * @param liquidationIncentiveMantissa The liquidation incentive, scaled by 1e18. * @return The finalized account liquidity snapshot with updated fields. */ function finalizeSnapshot( - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, - uint256 assetsCount, - uint256 liquidationIncentiveMantissa + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - if (assetsCount > 0) { - snapshot.weightavg = snapshot.weightavg / assetsCount; + if (snapshot.totalCollateral > 0) { + snapshot.averageLT = ExponentialNoError.div_(snapshot.averageLT, snapshot.totalCollateral); } - uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; if (borrowPlusEffects > 0) { snapshot.healthFactor = ExponentialNoError.div_(snapshot.weightedCollateral, borrowPlusEffects); } snapshot.healthFactorThreshold = ExponentialNoError.div_( - snapshot.weightavg * (1e18 + liquidationIncentiveMantissa), + snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg), 1e18 ); From 438695d5299e771817ea23a82c25e148b4070928 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 18:44:36 +0530 Subject: [PATCH 32/51] feat: add Toxic liquidation check --- contracts/Comptroller.sol | 12 +++++++++--- contracts/ComptrollerStorage.sol | 7 ++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 469266c98..1f411d837 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -204,6 +204,9 @@ contract Comptroller is /// @notice Thrown if delegate approval status is already set to the requested value error DelegationStatusUnchanged(); + /// @notice Thrown when the liquidation worsens the health factor of the borrower + error ToxicLiquidation(); + /// @param poolRegistry_ Pool registry address /// @custom:oz-upgrades-unsafe-allow constructor /// @custom:error ZeroAddressNotAllowed is thrown when pool registry address is zero @@ -733,6 +736,10 @@ contract Comptroller is revert InsufficientShortfall(); } + if ((snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg)) > snapshot.healthFactor) { + revert ToxicLiquidation(); + } + Market storage marketCollateral = markets[vTokenCollateral]; uint256 closeFactor; @@ -1704,12 +1711,11 @@ contract Comptroller is } } - uint256 liquidationIncentiveAvg; if (assetsCount > 0) { - liquidationIncentiveAvg = div_(liquidationIncentiveMantissa, assetsCount); + snapshot.liquidationIncentiveAvg = div_(liquidationIncentiveMantissa, assetsCount); } - return Liquidation.finalizeSnapshot(snapshot, assetsCount, liquidationIncentiveAvg); + return Liquidation.finalizeSnapshot(snapshot); } /** diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 64095c14b..1006d8678 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -27,9 +27,10 @@ contract ComptrollerStorage { uint256 effects; uint256 liquidity; uint256 shortfall; - uint256 weightavg; - uint256 healthFactor; - uint256 healthFactorThreshold; + uint256 averageLT; // Average liquidation threshold of all assets in the snapshot + uint256 healthFactor; // Health factor of the account, calculated as (weightedCollateral / borrows) + uint256 healthFactorThreshold; // Health factor threshold for liquidation, calculated as (averageLT * (1e18 + LiquidationIncentiveAvg) / 1e18) + uint256 liquidationIncentiveAvg; // Average liquidation incentive of all assets in the snapshot } struct RewardSpeeds { From 58e2678d50dfc1114ca70dbcd4b5f2708e7c557e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 18:45:01 +0530 Subject: [PATCH 33/51] feat: update comptroller interface --- contracts/ComptrollerInterface.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index fe9cac4b0..6f5b04dc0 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -106,6 +106,8 @@ interface ComptrollerInterface { function actionPaused(address market, Action action) external view returns (bool); function getDynamicLiquidationIncentive(address borrower, address market) external view returns (uint256); + + function getMarketLiquidationIncentive(address vToken) external view returns (uint256); } /** From 2413962f02f42adaa8e4822975c929bc241f2292 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 18:45:31 +0530 Subject: [PATCH 34/51] test: refactored integration tests --- tests/integration/index.ts | 52 ++++++-------------------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/tests/integration/index.ts b/tests/integration/index.ts index 99cb1fc00..001c869eb 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -610,7 +610,7 @@ describe("Straight Cases For Single User Liquidation and healing", function () { ).to.be.revertedWithCustomError(Comptroller, "InsufficientShortfall"); }); - it("Should revert when liquidation is called through vToken and trying to seize more tokens", async function () { + it("Should revert when liquidation worsens the health of the borrower", async function () { // Mint and Incrrease collateral of the user udnerlyingMintAmount = convertToUnit("1", 18); const VTokenMintAmount = convertToUnit(1, 8); @@ -628,36 +628,13 @@ describe("Straight Cases For Single User Liquidation and healing", function () { dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 18)); await Comptroller.setPriceOracle(dummyPriceOracle.address); // Liquidation - await expect(vBTCB.connect(acc1Signer).liquidateBorrow(acc2, 201, vBNX.address)).to.be.revertedWith( - "LIQUIDATE_SEIZE_TOO_MUCH", + await expect(vBTCB.connect(acc1Signer).liquidateBorrow(acc2, 201, vBNX.address)).to.be.revertedWithCustomError( + Comptroller, + "ToxicLiquidation", ); }); - it("Should allow 100% repay when health factor is below threshold", async function () { - // 1. Setup - Mint collateral - const underlyingMintAmount = convertToUnit("1", 18); - const vTokenMintAmount = convertToUnit("1", 8); - const expectedTotalBalance = Number(convertToUnit(1, 7)) + Number(convertToUnit(1, 8)); - - await BNX.connect(acc2Signer).faucet(underlyingMintAmount); - await BNX.connect(acc2Signer).approve(vBNX.address, underlyingMintAmount); - - await expect(vBNX.connect(acc2Signer).mint(underlyingMintAmount)) - .to.emit(vBNX, "Mint") - .withArgs(acc2, underlyingMintAmount, vTokenMintAmount, expectedTotalBalance); - - // 2. Manipulate prices to create severe shortfall (health factor 0) - const dummyPriceOracle = await smock.fake("MockPriceOracle"); - dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBTCB.address).returns(convertToUnit("1", 20)); // High BTCB price - dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 18)); // BNX price - await Comptroller.setPriceOracle(dummyPriceOracle.address); - - // 3. Attempt full liquidation (should succeed) - const repayAmount = convertToUnit("1", 18); - await expect(vBTCB.connect(acc1Signer).liquidateBorrow(acc2, repayAmount, vBNX.address)).to.not.be.reverted; - }); - - it("Should success when liquidation is called through vToken", async function () { + it("Should revert for ToxicLiquidation", async function () { // Mint and Incrrease collateral of the user mintAmount = convertToUnit("1", 18); vTokenMintAmount = convertToUnit("1", 8); @@ -674,23 +651,10 @@ describe("Straight Cases For Single User Liquidation and healing", function () { dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBTCB.address).returns(convertToUnit("100", 18)); dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 18)); await Comptroller.setPriceOracle(dummyPriceOracle.address); - const borrowBalance = await vBTCB.borrowBalanceStored(acc2); - const closeFactor = await Comptroller.closeFactorMantissa(); - const maxClose = (borrowBalance * closeFactor) / 1e18; - const seizeAmount = EXPONENT_SCALE * maxClose * (convertToUnit("100", 18) / convertToUnit("100", 18)); - const exchangeRateStored = await vBNX.exchangeRateStored(); - const seizeTokensOverall = seizeAmount / exchangeRateStored; - const reserveMantissa = await vBTCB.protocolSeizeShareMantissa(); - const seizeTokens = (seizeTokensOverall - (seizeTokensOverall * reserveMantissa) / EXPONENT_SCALE).toFixed(0); - const protocolSeizeToken = seizeTokensOverall - seizeTokens; - const protocolSeizeAmount = (exchangeRateStored * protocolSeizeToken) / EXPONENT_SCALE; - await expect(vBTCB.connect(acc1Signer).liquidateBorrow(acc2, maxClose.toString(), vBNX.address)) - .to.emit(vBTCB, "LiquidateBorrow") - .withArgs(acc1, acc2, maxClose.toString(), vBNX.address, seizeTokensOverall.toFixed(0)); - expect(protocolSeizeAmount).equal(await BNX.balanceOf(ProtocolShareReserve.address)); - const liquidatorBalance = await vBNX.connect(acc1Signer).balanceOf(acc1); - expect(liquidatorBalance).to.equal(seizeTokens); + await expect( + vBTCB.connect(acc1Signer).liquidateBorrow(acc2, convertToUnit(1, 18), vBNX.address), + ).to.be.revertedWithCustomError(Comptroller, "ToxicLiquidation"); }); }); From c5564d9c8488c782419437c7843da25379eab4ca Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 17 Jul 2025 18:54:51 +0530 Subject: [PATCH 35/51] fix: fixed references for averageLT --- contracts/Comptroller.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 1f411d837..174a4ebf1 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -745,7 +745,7 @@ contract Comptroller is uint256 closeFactor; unchecked { if (snapshot.healthFactor >= 1e18) revert InsufficientShortfall(); - uint256 wtAvg = snapshot.weightavg; + uint256 wtAvg = snapshot.averageLT; if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; uint256 denominator = borrowBalance * @@ -1516,7 +1516,7 @@ contract Comptroller is if (snapshot.healthFactor >= snapshot.healthFactorThreshold) return liquidationIncentiveMantissa; unchecked { - uint256 value = ((snapshot.healthFactor * 1e18) / snapshot.weightavg) - 1e18; + uint256 value = ((snapshot.healthFactor * 1e18) / snapshot.averageLT) - 1e18; return value > liquidationIncentiveMantissa ? liquidationIncentiveMantissa : value; } } From 525f1cff467a44fc1c312554d6fe3d780694d72c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 17:30:17 +0530 Subject: [PATCH 36/51] fix: fixed storage layout --- contracts/ComptrollerStorage.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 1006d8678..0a4ac3638 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -53,7 +53,7 @@ contract ComptrollerStorage { // Per-market mapping of "accounts in this asset" mapping(address => bool) accountMembership; // discount on collateral that a liquidator receives when liquidating a borrow in this market - uint256 liquidationIncentiveMantissa; + uint256 maxLiquidationIncentiveMantissa; } /** @@ -66,6 +66,11 @@ contract ComptrollerStorage { */ uint256 public closeFactorMantissa; + /** + * @notice Multiplier representing the discount on collateral that a liquidator receives + */ + uint256 public deprecatedLiquidationIncentiveMantissa; + /** * @notice Per-account mapping of "assets you are in" */ From c825a8ec7c573578568d482e90b81e74ebf1ca59 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 17:42:26 +0530 Subject: [PATCH 37/51] feat: external Liquidaiton Manager contract instead of library --- contracts/LiquidationManager.sol | 183 ++++++++++++++++++ contracts/lib/ExponentialNoError.sol | 131 ------------- contracts/lib/Liquidation.sol | 272 --------------------------- 3 files changed, 183 insertions(+), 403 deletions(-) create mode 100644 contracts/LiquidationManager.sol delete mode 100644 contracts/lib/ExponentialNoError.sol delete mode 100644 contracts/lib/Liquidation.sol diff --git a/contracts/LiquidationManager.sol b/contracts/LiquidationManager.sol new file mode 100644 index 000000000..b219041a2 --- /dev/null +++ b/contracts/LiquidationManager.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { VToken } from "./VToken.sol"; +import { ComptrollerStorage } from "./ComptrollerStorage.sol"; +import { ComptrollerInterface } from "./ComptrollerInterface.sol"; +import { Comptroller } from "./Comptroller.sol"; +import { ExponentialNoError } from "./ExponentialNoError.sol"; +import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; +import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; + +contract LiquidationManager is ILiquidationManager, ExponentialNoError { + /** + * @notice Processes a batch of liquidation orders for a given borrower. + * @dev Iterates through the provided liquidation orders and executes the liquidation for each order using the `forceLiquidateBorrow` function. + * @param order Aliquidation orders to process. + * @param borrower The address of the borrower whose positions are being liquidated. + * @param liquidator The address performing the liquidation. + * @custom:reverts MarketNotListed if either the borrowed or collateral market in an order is not listed. + */ + function processLiquidationOrder( + ComptrollerStorage.LiquidationOrder calldata order, + address borrower, + address liquidator + ) external { + // Execute liquidation + order.vTokenBorrowed.forceLiquidateBorrow( + liquidator, + borrower, + order.repayAmount, + order.vTokenCollateral, + true + ); + } + + /** + * @notice Calculates incentive-adjusted debt + */ + function calculateIncentiveAdjustedDebt( + address borrower, + VToken[] memory markets, + ComptrollerInterface comptroller + ) external view returns (uint256 weightedBorrowSum) { + for (uint256 i; i < markets.length; ++i) { + VToken market = markets[i]; + (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); + if (borrowBalance == 0) continue; + + ResilientOracleInterface oracle = comptroller.getOracle(); + uint256 borrowPrice = oracle.getUnderlyingPrice(address(market)); + uint256 borrowValueUSD = mul_ScalarTruncate(Exp({ mantissa: borrowPrice }), borrowBalance); + + uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); + + weightedBorrowSum = ExponentialNoError.add_( + weightedBorrowSum, + ExponentialNoError.mul_ScalarTruncate( + ExponentialNoError.Exp({ mantissa: marketIncentive }), + borrowValueUSD + ) + ); + } + } + + /** + * @notice Processes a single asset for a given account and updates the liquidity snapshot. + * @dev + * - Constructs AssetData for the asset and account. + * - Calculates and applies the asset's effect on the account's liquidity snapshot, including any modifications (redeem/borrow). + * @param asset The VToken asset to process. + * @param account The address of the account being evaluated. + * @param effects Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @param assetWeight The risk weight of the asset. + * @param snapshot The current account liquidity snapshot to update. + * @return The updated AccountLiquiditySnapshot struct. + */ + function processAsset( + VToken asset, + address account, + EffectsParams memory effects, + uint256 assetWeight, + uint256 underlyingPrice, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external view returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + (, uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = asset.getAccountSnapshot( + account + ); + + AssetData memory assetData = AssetData({ + vTokenBalance: vTokenBalance, + borrowBalance: borrowBalance, + exchangeRateMantissa: exchangeRateMantissa, + underlyingPrice: underlyingPrice, + assetWeight: assetWeight, + vTokenAddress: address(asset) + }); + + return _calculateAssetValues(assetData, snapshot, effects); + } + + /** + * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. + * @dev + * - Computes the average weight. + * - Calculates the sum of borrows and effects. + * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. + * - Sets the health factor threshold using the weighted average and liquidation incentive. + * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. + * @param snapshot The account liquidity snapshot to be finalized. + * @return The finalized account liquidity snapshot with updated fields. + */ + function finalizeSnapshot( + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + if (snapshot.totalCollateral > 0) { + snapshot.averageLT = div_(snapshot.averageLT, snapshot.totalCollateral); + } + uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; + + if (borrowPlusEffects > 0) { + snapshot.healthFactor = div_(snapshot.weightedCollateral, borrowPlusEffects); + } + snapshot.healthFactorThreshold = div_(snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg), 1e18); + + unchecked { + if (snapshot.weightedCollateral > borrowPlusEffects) { + snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; + snapshot.shortfall = 0; + } else { + snapshot.liquidity = 0; + snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; + } + } + + return snapshot; + } + + /** + * @notice Calculates and updates the liquidity snapshot values for a given asset. + * @dev Computes weighted collateral, total collateral, and borrow values using asset data and price information. + * If the asset is being modified (redeemed or borrowed), applies the effects to the snapshot as well. + * @param asset The asset data struct containing balances, prices, and weights. + * @param snapshot The current account liquidity snapshot to update. + * @param effectsParams Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @return The updated AccountLiquiditySnapshot struct. + */ + function _calculateAssetValues( + AssetData memory asset, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, + EffectsParams memory effectsParams + ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + Exp memory oraclePrice = Exp({ mantissa: asset.underlyingPrice }); + Exp memory vTokenPrice = mul_(Exp({ mantissa: asset.exchangeRateMantissa }), oraclePrice); + Exp memory weightedVTokenPrice = mul_(Exp({ mantissa: asset.assetWeight }), vTokenPrice); + + // Core calculations + snapshot.weightedCollateral = mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + asset.vTokenBalance, + snapshot.weightedCollateral + ); + snapshot.totalCollateral = mul_ScalarTruncateAddUInt( + vTokenPrice, + asset.vTokenBalance, + snapshot.totalCollateral + ); + snapshot.borrows = mul_ScalarTruncateAddUInt(oraclePrice, asset.borrowBalance, snapshot.borrows); + uint256 vTokenBalanceUSD = mul_ScalarTruncate(vTokenPrice, asset.vTokenBalance); + snapshot.averageLT += mul_(asset.assetWeight, vTokenBalanceUSD); + + // Handle modified asset effects + if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { + snapshot.effects = mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + effectsParams.redeemTokens, + snapshot.effects + ); + snapshot.effects = mul_ScalarTruncateAddUInt(oraclePrice, effectsParams.borrowAmount, snapshot.effects); + } + + return snapshot; + } +} diff --git a/contracts/lib/ExponentialNoError.sol b/contracts/lib/ExponentialNoError.sol deleted file mode 100644 index 3dcccef0a..000000000 --- a/contracts/lib/ExponentialNoError.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.25; - -import { EXP_SCALE as EXP_SCALE_, MANTISSA_ONE as MANTISSA_ONE_ } from "./constants.sol"; - -library ExponentialNoError { - struct Exp { - uint256 mantissa; - } - - struct Double { - uint256 mantissa; - } - - uint256 internal constant EXP_SCALE = EXP_SCALE_; - uint256 internal constant DOUBLE_SCALE = 1e36; - uint256 internal constant HALF_EXP_SCALE = EXP_SCALE / 2; - uint256 internal constant MANTISSA_ONE = MANTISSA_ONE_; - - function truncate(Exp memory exp) internal pure returns (uint256) { - return exp.mantissa / EXP_SCALE; - } - - function mul_ScalarTruncate(Exp memory a, uint256 scalar) internal pure returns (uint256) { - Exp memory product = mul_(a, scalar); - return truncate(product); - } - - function mul_ScalarTruncateAddUInt(Exp memory a, uint256 scalar, uint256 addend) internal pure returns (uint256) { - Exp memory product = mul_(a, scalar); - return add_(truncate(product), addend); - } - - function lessThanExp(Exp memory left, Exp memory right) internal pure returns (bool) { - return left.mantissa < right.mantissa; - } - - function safe224(uint256 n, string memory errorMessage) internal pure returns (uint224) { - require(n <= type(uint224).max, errorMessage); - return uint224(n); - } - - function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { - require(n <= type(uint32).max, errorMessage); - return uint32(n); - } - - function add_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { - return Exp(add_(a.mantissa, b.mantissa)); - } - - function add_(Double memory a, Double memory b) internal pure returns (Double memory) { - return Double(add_(a.mantissa, b.mantissa)); - } - - function add_(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } - - function sub_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { - return Exp(sub_(a.mantissa, b.mantissa)); - } - - function sub_(Double memory a, Double memory b) internal pure returns (Double memory) { - return Double(sub_(a.mantissa, b.mantissa)); - } - - function sub_(uint256 a, uint256 b) internal pure returns (uint256) { - return a - b; - } - - function mul_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { - return Exp(mul_(a.mantissa, b.mantissa) / EXP_SCALE); - } - - function mul_(Exp memory a, uint256 b) internal pure returns (Exp memory) { - return Exp(mul_(a.mantissa, b)); - } - - function mul_(uint256 a, Exp memory b) internal pure returns (uint256) { - return mul_(a, b.mantissa) / EXP_SCALE; - } - - function mul_(Double memory a, Double memory b) internal pure returns (Double memory) { - return Double(mul_(a.mantissa, b.mantissa) / DOUBLE_SCALE); - } - - function mul_(Double memory a, uint256 b) internal pure returns (Double memory) { - return Double(mul_(a.mantissa, b)); - } - - function mul_(uint256 a, Double memory b) internal pure returns (uint256) { - return mul_(a, b.mantissa) / DOUBLE_SCALE; - } - - function mul_(uint256 a, uint256 b) internal pure returns (uint256) { - return a * b; - } - - function div_(Exp memory a, Exp memory b) internal pure returns (Exp memory) { - return Exp(div_(mul_(a.mantissa, EXP_SCALE), b.mantissa)); - } - - function div_(Exp memory a, uint256 b) internal pure returns (Exp memory) { - return Exp(div_(a.mantissa, b)); - } - - function div_(uint256 a, Exp memory b) internal pure returns (uint256) { - return div_(mul_(a, EXP_SCALE), b.mantissa); - } - - function div_(Double memory a, Double memory b) internal pure returns (Double memory) { - return Double(div_(mul_(a.mantissa, DOUBLE_SCALE), b.mantissa)); - } - - function div_(Double memory a, uint256 b) internal pure returns (Double memory) { - return Double(div_(a.mantissa, b)); - } - - function div_(uint256 a, Double memory b) internal pure returns (uint256) { - return div_(mul_(a, DOUBLE_SCALE), b.mantissa); - } - - function div_(uint256 a, uint256 b) internal pure returns (uint256) { - return a / b; - } - - function fraction(uint256 a, uint256 b) internal pure returns (Double memory) { - return Double(div_(mul_(a, DOUBLE_SCALE), b)); - } -} diff --git a/contracts/lib/Liquidation.sol b/contracts/lib/Liquidation.sol deleted file mode 100644 index 3d9c1461c..000000000 --- a/contracts/lib/Liquidation.sol +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.10; - -import { ExponentialNoError } from "./ExponentialNoError.sol"; -import { ComptrollerStorage } from "../ComptrollerStorage.sol"; -import { ComptrollerInterface } from "../ComptrollerInterface.sol"; -import { Comptroller } from "../Comptroller.sol"; -import { VToken } from "../VToken.sol"; - -library Liquidation { - struct AssetData { - uint256 vTokenBalance; - uint256 borrowBalance; - uint256 exchangeRateMantissa; - uint256 underlyingPrice; - uint256 assetWeight; - address vTokenAddress; - } - - struct EffectsParams { - VToken vTokenModify; - uint256 redeemTokens; - uint256 borrowAmount; - } - - /** - * @notice Processes a batch of liquidation orders for a given borrower. - * @dev Iterates through the provided liquidation orders, validates that both the borrowed and collateral markets are listed, - * and executes the liquidation for each order using the `forceLiquidateBorrow` function. - * @param orders Array of liquidation orders to process. - * @param borrower The address of the borrower whose positions are being liquidated. - * @param liquidator The address performing the liquidation. - * @param markets Mapping of market addresses to their corresponding market data, used to validate market status. - * @custom:reverts MarketNotListed if either the borrowed or collateral market in an order is not listed. - */ - function processLiquidationOrders( - ComptrollerStorage.LiquidationOrder[] calldata orders, - address borrower, - address liquidator, - mapping(address => ComptrollerStorage.Market) storage markets - ) internal { - uint256 ordersCount = orders.length; - for (uint256 i; i < ordersCount; ++i) { - ComptrollerStorage.LiquidationOrder calldata order = orders[i]; - - // Validate markets are listed - if (!markets[address(order.vTokenBorrowed)].isListed) { - revert Comptroller.MarketNotListed(address(order.vTokenBorrowed)); - } - if (!markets[address(order.vTokenCollateral)].isListed) { - revert Comptroller.MarketNotListed(address(order.vTokenCollateral)); - } - - // Execute liquidation - order.vTokenBorrowed.forceLiquidateBorrow( - liquidator, - borrower, - order.repayAmount, - order.vTokenCollateral, - true - ); - } - } - - /** - * @notice Calculates the sum of all borrow amounts weighted by their liquidation incentives - * @dev Returns Σ (borrowAmount × liquidationIncentive) for all markets - * @param borrower The account address - * @param markets Array of markets to check - * @param comptroller For incentive lookup - * @return weightedBorrowSum The incentive-adjusted total borrow value - */ - function calculateIncentiveAdjustedDebt( - address borrower, - VToken[] memory markets, - ComptrollerInterface comptroller, - function(VToken) internal view returns (uint256) getUnderlyingPrice - ) internal view returns (uint256 weightedBorrowSum) { - for (uint256 i; i < markets.length; ++i) { - VToken market = markets[i]; - - (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); - if (borrowBalance == 0) continue; - - // Convert to USD value using oracle price - uint256 borrowPrice = getUnderlyingPrice(market); - uint256 borrowValueUSD = ExponentialNoError.mul_ScalarTruncate( - ExponentialNoError.Exp({ mantissa: borrowPrice }), - borrowBalance - ); - - uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); - - weightedBorrowSum = ExponentialNoError.add_( - weightedBorrowSum, - ExponentialNoError.mul_ScalarTruncate( - ExponentialNoError.Exp({ mantissa: marketIncentive }), - borrowValueUSD - ) - ); - } - return weightedBorrowSum; - } - - /** - * @notice Constructs an AssetData struct for a given asset and account. - * @dev Fetches the account's vToken balance, borrow balance, and exchange rate for the asset, - * as well as the asset's underlying price and risk weight. - * @param asset The VToken asset to query. - * @param account The address of the account. - * @param assetWeight The risk weight of the asset. - * @param getUnderlyingPrice Function to fetch the asset's underlying price. - * @param getAccountSnapshot Function to fetch the account's balances for the asset. - * @return AssetData struct containing all relevant asset/account data. - */ - function createAssetData( - VToken asset, - address account, - uint256 assetWeight, - function(VToken) internal view returns (uint256) getUnderlyingPrice, - function(VToken, address) internal view returns (uint256, uint256, uint256) getAccountSnapshot - ) internal view returns (AssetData memory) { - (uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = getAccountSnapshot( - asset, - account - ); - - return - AssetData({ - vTokenBalance: vTokenBalance, - borrowBalance: borrowBalance, - exchangeRateMantissa: exchangeRateMantissa, - underlyingPrice: getUnderlyingPrice(asset), - assetWeight: assetWeight, - vTokenAddress: address(asset) - }); - } - - /** - * @notice Processes a single asset for a given account and updates the liquidity snapshot. - * @dev - * - Constructs AssetData for the asset and account. - * - Calculates and applies the asset's effect on the account's liquidity snapshot, including any modifications (redeem/borrow). - * @param asset The VToken asset to process. - * @param account The address of the account being evaluated. - * @param effects Parameters describing any modifications (redeem/borrow) to apply for this asset. - * @param assetWeight The risk weight of the asset. - * @param getUnderlyingPrice Function to fetch the asset's underlying price. - * @param getAccountSnapshot Function to fetch the account's balances for the asset. - * @param snapshot The current account liquidity snapshot to update. - * @return The updated AccountLiquiditySnapshot struct. - */ - function processAsset( - VToken asset, - address account, - EffectsParams memory effects, - uint256 assetWeight, - function(VToken) internal view returns (uint256) getUnderlyingPrice, - function(VToken, address) internal view returns (uint256, uint256, uint256) getAccountSnapshot, - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) internal view returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - AssetData memory assetData = createAssetData( - asset, - account, - assetWeight, - getUnderlyingPrice, - getAccountSnapshot - ); - - return calculateAssetValues(assetData, snapshot, effects); - } - - /** - * @notice Calculates and updates the liquidity snapshot values for a given asset. - * @dev Computes weighted collateral, total collateral, and borrow values using asset data and price information. - * If the asset is being modified (redeemed or borrowed), applies the effects to the snapshot as well. - * @param asset The asset data struct containing balances, prices, and weights. - * @param snapshot The current account liquidity snapshot to update. - * @param effectsParams Parameters describing any modifications (redeem/borrow) to apply for this asset. - * @return The updated AccountLiquiditySnapshot struct. - */ - function calculateAssetValues( - AssetData memory asset, - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, - EffectsParams memory effectsParams - ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - ExponentialNoError.Exp memory oraclePrice = ExponentialNoError.Exp({ mantissa: asset.underlyingPrice }); - ExponentialNoError.Exp memory vTokenPrice = ExponentialNoError.mul_( - ExponentialNoError.Exp({ mantissa: asset.exchangeRateMantissa }), - oraclePrice - ); - ExponentialNoError.Exp memory weightedVTokenPrice = ExponentialNoError.mul_( - ExponentialNoError.Exp({ mantissa: asset.assetWeight }), - vTokenPrice - ); - - // Core calculations - snapshot.weightedCollateral = ExponentialNoError.mul_ScalarTruncateAddUInt( - weightedVTokenPrice, - asset.vTokenBalance, - snapshot.weightedCollateral - ); - snapshot.totalCollateral = ExponentialNoError.mul_ScalarTruncateAddUInt( - vTokenPrice, - asset.vTokenBalance, - snapshot.totalCollateral - ); - snapshot.borrows = ExponentialNoError.mul_ScalarTruncateAddUInt( - oraclePrice, - asset.borrowBalance, - snapshot.borrows - ); - uint256 vTokenBalanceUSD = ExponentialNoError.mul_ScalarTruncate(vTokenPrice, asset.vTokenBalance); - snapshot.averageLT += ExponentialNoError.mul_(asset.assetWeight, vTokenBalanceUSD); - - // Handle modified asset effects - if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { - snapshot.effects = ExponentialNoError.mul_ScalarTruncateAddUInt( - weightedVTokenPrice, - effectsParams.redeemTokens, - snapshot.effects - ); - snapshot.effects = ExponentialNoError.mul_ScalarTruncateAddUInt( - oraclePrice, - effectsParams.borrowAmount, - snapshot.effects - ); - } - - return snapshot; - } - - /** - * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. - * @dev - * - Computes the average weight. - * - Calculates the sum of borrows and effects. - * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. - * - Sets the health factor threshold using the weighted average and liquidation incentive. - * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. - * @param snapshot The account liquidity snapshot to be finalized. - * @return The finalized account liquidity snapshot with updated fields. - */ - function finalizeSnapshot( - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - if (snapshot.totalCollateral > 0) { - snapshot.averageLT = ExponentialNoError.div_(snapshot.averageLT, snapshot.totalCollateral); - } - uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; - - if (borrowPlusEffects > 0) { - snapshot.healthFactor = ExponentialNoError.div_(snapshot.weightedCollateral, borrowPlusEffects); - } - snapshot.healthFactorThreshold = ExponentialNoError.div_( - snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg), - 1e18 - ); - - unchecked { - if (snapshot.weightedCollateral > borrowPlusEffects) { - snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; - snapshot.shortfall = 0; - } else { - snapshot.liquidity = 0; - snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; - } - } - - return snapshot; - } -} From c907123e4ad51e1a26cd70e4c3af9c1bc19a7ef1 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 17:43:11 +0530 Subject: [PATCH 38/51] feat: Liquidation Manager interface --- contracts/LiquidationManagerInterface.sol | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 contracts/LiquidationManagerInterface.sol diff --git a/contracts/LiquidationManagerInterface.sol b/contracts/LiquidationManagerInterface.sol new file mode 100644 index 000000000..4851fc728 --- /dev/null +++ b/contracts/LiquidationManagerInterface.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { VToken } from "./VToken.sol"; +import { ComptrollerStorage } from "./ComptrollerStorage.sol"; +import { ComptrollerInterface } from "./ComptrollerInterface.sol"; + +interface ILiquidationManager { + struct AssetData { + uint256 vTokenBalance; + uint256 borrowBalance; + uint256 exchangeRateMantissa; + uint256 underlyingPrice; + uint256 assetWeight; + address vTokenAddress; + } + + struct EffectsParams { + VToken vTokenModify; + uint256 redeemTokens; + uint256 borrowAmount; + } + + function processLiquidationOrder( + ComptrollerStorage.LiquidationOrder calldata order, + address borrower, + address liquidator + ) external; + + function calculateIncentiveAdjustedDebt( + address borrower, + VToken[] memory markets, + ComptrollerInterface comptroller + ) external view returns (uint256 weightedBorrowSum); + + function processAsset( + VToken asset, + address account, + EffectsParams memory effects, + uint256 assetWeight, + uint256 underlyingPrice, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external view returns (ComptrollerStorage.AccountLiquiditySnapshot memory); + + function finalizeSnapshot( + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory); +} From b6f0e5e741a0d4cef514f625b1fb4c11f9c5802c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 17:43:45 +0530 Subject: [PATCH 39/51] feat: update comptroller interface --- contracts/ComptrollerInterface.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index 6f5b04dc0..a9fa669b7 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -108,6 +108,8 @@ interface ComptrollerInterface { function getDynamicLiquidationIncentive(address borrower, address market) external view returns (uint256); function getMarketLiquidationIncentive(address vToken) external view returns (uint256); + + function getOracle() external view returns (ResilientOracleInterface); } /** From d072b0ac477f0575b3385fea5ea9e23e7269df91 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 19:01:17 +0530 Subject: [PATCH 40/51] feat: add liquidation manager setter and refactor dependencies --- contracts/Comptroller.sol | 71 +++++++++++++++++++++++--------- contracts/ComptrollerStorage.sol | 7 +++- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 174a4ebf1..0ad898ef8 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -13,7 +13,7 @@ import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; -import { Liquidation } from "./lib/Liquidation.sol"; +import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; /** * @title Comptroller @@ -105,9 +105,13 @@ contract Comptroller is /// @notice Emitted when a market is unlisted event MarketUnlisted(address indexed vToken); + /// @notice Emitted when the borrowing or redeeming delegate rights are updated for an account event DelegateUpdated(address indexed approver, address indexed delegate, bool approved); + /// @notice Emitted when the liquidation manager is set + event LiquidationModuleSet(address indexed ILiquidationManager); + /// @notice Thrown when collateral factor exceeds the upper bound error InvalidCollateralFactor(); @@ -749,7 +753,7 @@ contract Comptroller is if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; uint256 denominator = borrowBalance * - (1e18 - ((wtAvg * (1e18 + marketCollateral.liquidationIncentiveMantissa)) / 1e18)); + (1e18 - ((wtAvg * (1e18 + marketCollateral.maxLiquidationIncentiveMantissa)) / 1e18)); closeFactor = (numerator * 1e18) / denominator; closeFactor = closeFactor > 1e18 ? 1e18 : closeFactor; } else { @@ -879,11 +883,10 @@ contract Comptroller is Exp memory totalCollateral = Exp({ mantissa: snapshot.totalCollateral }); Exp memory totalScaledBorrows = Exp({ - mantissa: Liquidation.calculateIncentiveAdjustedDebt( + mantissa: liquidationManager.calculateIncentiveAdjustedDebt( user, userAssets, - ComptrollerInterface(address(this)), - _safeGetUnderlyingPrice + ComptrollerInterface(address(this)) ) }); @@ -936,11 +939,10 @@ contract Comptroller is VToken[] memory borrowMarkets = getAssetsIn(borrower); uint256 marketsCount = borrowMarkets.length; - uint256 collateralToSeize = Liquidation.calculateIncentiveAdjustedDebt( + uint256 collateralToSeize = liquidationManager.calculateIncentiveAdjustedDebt( borrower, borrowMarkets, - ComptrollerInterface(address(this)), - _safeGetUnderlyingPrice + ComptrollerInterface(address(this)) ); if (collateralToSeize >= snapshot.totalCollateral) { @@ -957,7 +959,16 @@ contract Comptroller is _ensureMaxLoops(ordersCount / 2); - Liquidation.processLiquidationOrders(orders, borrower, msg.sender, markets); + for (uint256 i; i < ordersCount; ++i) { + if (!markets[address(orders[i].vTokenBorrowed)].isListed) { + revert MarketNotListed(address(orders[i].vTokenBorrowed)); + } + if (!markets[address(orders[i].vTokenCollateral)].isListed) { + revert MarketNotListed(address(orders[i].vTokenCollateral)); + } + + liquidationManager.processLiquidationOrder(orders[i], borrower, msg.sender); + } for (uint256 i; i < marketsCount; ++i) { (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(borrowMarkets[i], borrower); @@ -1059,17 +1070,32 @@ contract Comptroller is revert MarketNotListed(address(vToken)); } - uint256 oldLiquidationIncentiveMantissa = market.liquidationIncentiveMantissa; + uint256 oldLiquidationIncentiveMantissa = market.maxLiquidationIncentiveMantissa; if (newLiquidationIncentiveMantissa == oldLiquidationIncentiveMantissa) { return; // No change, no need to emit event } - market.liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; + market.maxLiquidationIncentiveMantissa = newLiquidationIncentiveMantissa; // Emit event with old incentive, new incentive emit NewMarketLiquidationIncentive(vToken, oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); } + /** + * @notice Sets the address of the liquidation manager module. + * @dev Restricted by AccessControlManager. Ensures the address is non-zero. + * @param liquidationManager_ Address of the new liquidation manager contract. + * @custom:event Emits LiquidationModuleSet on success + * @custom:error ZeroAddressNotAllowed is thrown when the address is zero + * @custom:access Controlled by AccessControlManager + */ + function setLiquidationModule(address liquidationManager_) external { + _checkAccessAllowed("setLiquidationModule(address)"); + ensureNonzeroAddress(liquidationManager_); + liquidationManager = ILiquidationManager(liquidationManager_); + emit LiquidationModuleSet(liquidationManager_); + } + /** * @notice Add the market to the markets mapping and set it as listed * @dev Only callable by the PoolRegistry @@ -1370,7 +1396,7 @@ contract Comptroller is if (!market.isListed) { revert MarketNotListed(vToken); } - return market.liquidationIncentiveMantissa; + return market.maxLiquidationIncentiveMantissa; } /** @@ -1444,6 +1470,14 @@ contract Comptroller is return rewardsDistributors; } + /** + * @notice Returns the current oracle contract used by the Comptroller. + * @return The address of the ResilientOracleInterface contract. + */ + function getOracle() external view returns (ResilientOracleInterface) { + return oracle; + } + /** * @notice A marker method that returns true for a valid Comptroller contract * @return Always true @@ -1509,7 +1543,7 @@ contract Comptroller is /// @return incentive The liquidation incentive for the borrower, scaled by 1e18 function getDynamicLiquidationIncentive(address borrower, address vToken) public view returns (uint256 incentive) { Market storage market = markets[vToken]; - uint256 liquidationIncentiveMantissa = market.liquidationIncentiveMantissa; + uint256 liquidationIncentiveMantissa = market.maxLiquidationIncentiveMantissa; AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); @@ -1686,7 +1720,7 @@ contract Comptroller is uint256 assetsCount = assets.length; uint256 liquidationIncentiveMantissa; - Liquidation.EffectsParams memory effects = Liquidation.EffectsParams({ + ILiquidationManager.EffectsParams memory effects = ILiquidationManager.EffectsParams({ vTokenModify: vTokenModify, redeemTokens: redeemTokens, borrowAmount: borrowAmount @@ -1694,17 +1728,16 @@ contract Comptroller is for (uint256 i; i < assetsCount; ) { VToken asset = assets[i]; - snapshot = Liquidation.processAsset( + snapshot = liquidationManager.processAsset( assets[i], account, effects, weight(asset).mantissa, - _safeGetUnderlyingPrice, - _safeGetAccountSnapshot, + _safeGetUnderlyingPrice(asset), snapshot ); - liquidationIncentiveMantissa += markets[address(asset)].liquidationIncentiveMantissa; + liquidationIncentiveMantissa += markets[address(asset)].maxLiquidationIncentiveMantissa; unchecked { ++i; @@ -1715,7 +1748,7 @@ contract Comptroller is snapshot.liquidationIncentiveAvg = div_(liquidationIncentiveMantissa, assetsCount); } - return Liquidation.finalizeSnapshot(snapshot); + return liquidationManager.finalizeSnapshot(snapshot); } /** diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 0a4ac3638..d9aa005dd 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -7,6 +7,7 @@ import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { IPrime } from "@venusprotocol/venus-protocol/contracts/Tokens/Prime/Interfaces/IPrime.sol"; import { Action } from "./ComptrollerInterface.sol"; +import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; /** * @title ComptrollerStorage @@ -124,10 +125,14 @@ contract ComptrollerStorage { //mapping(address user => mapping (address delegate => bool approved)) public approvedDelegates; mapping(address => mapping(address => bool)) public approvedDelegates; + /// @notice The liquidation manager contract that handles liquidation logic + // This is an interface to allow for different liquidation strategies + ILiquidationManager public liquidationManager; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[47] private __gap; + uint256[46] private __gap; } From d86ccaaa3bb5ddf0efe455072d0f0c2ec41d3df5 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 19:53:06 +0530 Subject: [PATCH 41/51] refactor: add MarketListed internal function to reduce comptroller size --- contracts/Comptroller.sol | 76 ++++++++++++++------------------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 0ad898ef8..20ea6159c 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -277,9 +277,7 @@ contract Comptroller is Market storage _market = markets[market]; - if (!_market.isListed) { - revert MarketNotListed(market); - } + _checkMarketListed(market); if (!actionPaused(market, Action.BORROW)) { revert BorrowActionNotPaused(); @@ -439,9 +437,7 @@ contract Comptroller is function preMintHook(address vToken, address minter, uint256 mintAmount) external override { _checkActionPauseState(vToken, Action.MINT); - if (!markets[vToken].isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(vToken); uint256 supplyCap = supplyCaps[vToken]; // Skipping the cap check for uncapped coins to save some gas @@ -601,9 +597,7 @@ contract Comptroller is function preBorrowHook(address vToken, address borrower, uint256 borrowAmount) external override { _checkActionPauseState(vToken, Action.BORROW); - if (!markets[vToken].isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(vToken); if (!markets[vToken].accountMembership[borrower]) { // only vTokens may call borrowAllowed if borrower not in market @@ -672,10 +666,7 @@ contract Comptroller is _checkActionPauseState(vToken, Action.REPAY); oracle.updatePrice(vToken); - - if (!markets[vToken].isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(vToken); // Keep the flywheel moving _updateAndDistributeBorrowRewards(vToken, borrower); @@ -711,12 +702,8 @@ contract Comptroller is // Update the prices of tokens updatePrices(borrower); - if (!markets[vTokenBorrowed].isListed) { - revert MarketNotListed(address(vTokenBorrowed)); - } - if (!markets[vTokenCollateral].isListed) { - revert MarketNotListed(address(vTokenCollateral)); - } + _checkMarketListed(vTokenBorrowed); + _checkMarketListed(vTokenCollateral); uint256 borrowBalance = VToken(vTokenBorrowed).borrowBalanceStored(borrower); @@ -792,9 +779,7 @@ contract Comptroller is Market storage market = markets[vTokenCollateral]; - if (!market.isListed) { - revert MarketNotListed(vTokenCollateral); - } + _checkMarketListed(vTokenCollateral); if (seizerContract == address(this)) { // If Comptroller is the seizer, just check if collateral's comptroller @@ -805,9 +790,7 @@ contract Comptroller is } 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); - } + _checkMarketListed(seizerContract); if (VToken(vTokenCollateral).comptroller() != VToken(seizerContract).comptroller()) { revert ComptrollerMismatch(); } @@ -960,12 +943,8 @@ contract Comptroller is _ensureMaxLoops(ordersCount / 2); for (uint256 i; i < ordersCount; ++i) { - if (!markets[address(orders[i].vTokenBorrowed)].isListed) { - revert MarketNotListed(address(orders[i].vTokenBorrowed)); - } - if (!markets[address(orders[i].vTokenCollateral)].isListed) { - revert MarketNotListed(address(orders[i].vTokenCollateral)); - } + _checkMarketListed(address(orders[i].vTokenBorrowed)); + _checkMarketListed(address(orders[i].vTokenCollateral)); liquidationManager.processLiquidationOrder(orders[i], borrower, msg.sender); } @@ -1015,9 +994,7 @@ contract Comptroller is // Verify market is listed Market storage market = markets[address(vToken)]; - if (!market.isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(address(vToken)); // Check collateral factor <= 0.9 if (newCollateralFactorMantissa > MAX_COLLATERAL_FACTOR_MANTISSA) { @@ -1066,9 +1043,7 @@ contract Comptroller is _checkAccessAllowed("setMarketLiquidationIncentive(address,uint256)"); Market storage market = markets[address(vToken)]; - if (!market.isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(address(vToken)); uint256 oldLiquidationIncentiveMantissa = market.maxLiquidationIncentiveMantissa; if (newLiquidationIncentiveMantissa == oldLiquidationIncentiveMantissa) { @@ -1288,9 +1263,7 @@ contract Comptroller is _checkAccessAllowed("setForcedLiquidation(address,bool)"); ensureNonzeroAddress(vTokenBorrowed); - if (!markets[vTokenBorrowed].isListed) { - revert MarketNotListed(vTokenBorrowed); - } + _checkMarketListed(vTokenBorrowed); isForcedLiquidationEnabled[vTokenBorrowed] = enable; emit IsForcedLiquidationEnabledUpdated(vTokenBorrowed, enable); @@ -1393,9 +1366,7 @@ contract Comptroller is address vToken ) external view returns (uint256 liquidationIncentiveMantissa) { Market storage market = markets[vToken]; - if (!market.isListed) { - revert MarketNotListed(vToken); - } + _checkMarketListed(vToken); return market.maxLiquidationIncentiveMantissa; } @@ -1564,9 +1535,7 @@ contract Comptroller is _checkActionPauseState(address(vToken), Action.ENTER_MARKET); Market storage marketToJoin = markets[address(vToken)]; - if (!marketToJoin.isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(address(vToken)); if (marketToJoin.accountMembership[borrower]) { // already joined @@ -1623,9 +1592,7 @@ contract Comptroller is function _checkRedeemAllowed(address vToken, address redeemer, uint256 redeemTokens) internal { Market storage market = markets[vToken]; - if (!market.isListed) { - revert MarketNotListed(address(vToken)); - } + _checkMarketListed(vToken); /* If the redeemer is not 'in' the market, then we can bypass the liquidity check */ if (!market.accountMembership[redeemer]) { @@ -1681,6 +1648,17 @@ contract Comptroller is } } + /** + * @notice Checks whether a given market (vToken) is listed. + * @param vToken The address of the vToken to check. + * @custom:error MarketNotListed error is thrown if the market is not listed + */ + function _checkMarketListed(address vToken) internal view { + if (!markets[vToken].isListed) { + revert MarketNotListed(vToken); + } + } + /** * @notice Get the total collateral, weighted collateral, borrow balance, liquidity, shortfall * @param account The account to get the snapshot for From ebcd3e4976b3a2f7a5e4b20f9516ecb19e854363 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 22 Jul 2025 19:57:50 +0530 Subject: [PATCH 42/51] feat: add natspec comments for reward functions --- contracts/Comptroller.sol | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 20ea6159c..54fd12571 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -1615,6 +1615,12 @@ contract Comptroller is } } + /** + * @notice Updates and distributes supply rewards for a specific user and vToken. + * @dev Iterates through all reward distributors, updating the supply index and distributing rewards. + * @param vToken The address of the vToken for which rewards are being updated and distributed. + * @param user The address of the supplier to receive the distributed rewards. + */ function _updateAndDistributeSupplyRewards(address vToken, address user) internal { uint256 rewardDistributorsCount = rewardsDistributors.length; @@ -1625,6 +1631,12 @@ contract Comptroller is } } + /** + * @notice Updates and distributes borrow rewards for a specific user and vToken. + * @dev Iterates through all reward distributors, updating the borrow index and distributing rewards. + * @param vToken The address of the vToken for which rewards are being updated and distributed. + * @param user The address of the user to receive the borrow rewards. + */ function _updateAndDistributeBorrowRewards(address vToken, address user) internal { uint256 rewardDistributorsCount = rewardsDistributors.length; @@ -1637,6 +1649,14 @@ contract Comptroller is } } + /** + * @dev Updates the supply reward index and distributes supplier reward tokens for multiple users. + * Iterates through all registered rewards distributors, updating the supply index for the given vToken, + * and distributing reward tokens to the specified users. + * @param vToken The address of the vToken for which rewards are being updated and distributed. + * @param user1 The address of the first user to receive supplier reward tokens. + * @param user2 The address of the second user to receive supplier reward tokens. + */ function _updateAndDistributeSupplyRewardsMulti(address vToken, address user1, address user2) internal { uint256 rewardDistributorsCount = rewardsDistributors.length; From 845137341f0c413ac29dab7fcd403ca97f80aeed Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 23 Jul 2025 17:29:12 +0530 Subject: [PATCH 43/51] fix: fix tests --- tests/hardhat/Comptroller/accountLiquidityTest.ts | 3 +++ tests/hardhat/Comptroller/assetsListTest.ts | 3 +++ tests/hardhat/Comptroller/healAccountTest.ts | 3 +++ tests/hardhat/Comptroller/liquidateAccountTest.ts | 3 +++ .../hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts | 3 +++ tests/hardhat/Gateway/NativeTokenGateway.ts | 4 ++++ tests/hardhat/Lens/PoolLens.ts | 4 ++++ tests/hardhat/Prime.ts | 4 ++++ tests/hardhat/Rewards.ts | 4 ++++ 9 files changed, 31 insertions(+) diff --git a/tests/hardhat/Comptroller/accountLiquidityTest.ts b/tests/hardhat/Comptroller/accountLiquidityTest.ts index 77df1ef65..63bdd2ac6 100644 --- a/tests/hardhat/Comptroller/accountLiquidityTest.ts +++ b/tests/hardhat/Comptroller/accountLiquidityTest.ts @@ -35,9 +35,12 @@ async function makeComptroller(): Promise { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); return { accessControl, comptroller, oracle, poolRegistry }; } diff --git a/tests/hardhat/Comptroller/assetsListTest.ts b/tests/hardhat/Comptroller/assetsListTest.ts index b9d48e18e..a3cb15fc8 100644 --- a/tests/hardhat/Comptroller/assetsListTest.ts +++ b/tests/hardhat/Comptroller/assetsListTest.ts @@ -62,9 +62,12 @@ describe("assetListTest", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); const names = ["OMG", "ZRX", "BAT", "sketch"]; const [OMG, ZRX, BAT, SKT] = await Promise.all( names.map(async name => { diff --git a/tests/hardhat/Comptroller/healAccountTest.ts b/tests/hardhat/Comptroller/healAccountTest.ts index 3482d292f..a452009c1 100644 --- a/tests/hardhat/Comptroller/healAccountTest.ts +++ b/tests/hardhat/Comptroller/healAccountTest.ts @@ -48,9 +48,12 @@ describe("healAccount", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); await comptroller.setMinLiquidatableCollateral(parseUnits("100", 18)); await setBalance(poolRegistry.address, parseEther("1")); const names = ["OMG", "ZRX", "BAT"]; diff --git a/tests/hardhat/Comptroller/liquidateAccountTest.ts b/tests/hardhat/Comptroller/liquidateAccountTest.ts index 5ca30efec..4474e08c0 100644 --- a/tests/hardhat/Comptroller/liquidateAccountTest.ts +++ b/tests/hardhat/Comptroller/liquidateAccountTest.ts @@ -86,9 +86,12 @@ describe("liquidateAccount", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); await comptroller.setMinLiquidatableCollateral(parseUnits("100", 18)); await setBalance(poolRegistry.address, parseEther("1")); const names = ["OMG", "ZRX", "BAT"]; diff --git a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts index 7d6518494..e07693be3 100644 --- a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts +++ b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts @@ -78,8 +78,11 @@ describe("Comptroller", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); const vTokenBorrowed = await smock.fake("VToken"); const vTokenCollateral = await smock.fake("VToken"); diff --git a/tests/hardhat/Gateway/NativeTokenGateway.ts b/tests/hardhat/Gateway/NativeTokenGateway.ts index 08ec236a9..42f59a9bd 100644 --- a/tests/hardhat/Gateway/NativeTokenGateway.ts +++ b/tests/hardhat/Gateway/NativeTokenGateway.ts @@ -54,6 +54,9 @@ async function deployGateway(): Promise { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); + const maxLoopsLimit = 150; const fakePriceOracle = await smock.fake(MockPriceOracle__factory.abi); @@ -63,6 +66,7 @@ async function deployGateway(): Promise { ])) as Comptroller; await comptrollerProxy.setPriceOracle(fakePriceOracle.address); + await comptrollerProxy.setLiquidationModule(liquidationManager.address); // Registering the pool await poolRegistry.addPool("Pool 1", comptrollerProxy.address, closeFactor, minLiquidatableCollateral); diff --git a/tests/hardhat/Lens/PoolLens.ts b/tests/hardhat/Lens/PoolLens.ts index 83c9692da..710101871 100644 --- a/tests/hardhat/Lens/PoolLens.ts +++ b/tests/hardhat/Lens/PoolLens.ts @@ -108,6 +108,9 @@ for (const isTimeBased of [false, true]) { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); + [comptroller1Proxy, comptroller2Proxy] = await Promise.all( [...Array(3)].map(async () => { const comptroller = await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ @@ -115,6 +118,7 @@ for (const isTimeBased of [false, true]) { fakeAccessControlManager.address, ]); await comptroller.setPriceOracle(priceOracle.address); + await comptroller.setLiquidationModule(liquidationManager.address); return comptroller as Comptroller; }), ); diff --git a/tests/hardhat/Prime.ts b/tests/hardhat/Prime.ts index e8ac80f7c..297aad5ae 100644 --- a/tests/hardhat/Prime.ts +++ b/tests/hardhat/Prime.ts @@ -74,6 +74,9 @@ async function deployProtocol(): Promise { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); + const maxLoopsLimit = 150; const fakePriceOracle = await smock.fake(MockPriceOracle__factory.abi); @@ -82,6 +85,7 @@ async function deployProtocol(): Promise { accessControl.address, ])) as Comptroller; await comptrollerProxy.setPriceOracle(fakePriceOracle.address); + await comptrollerProxy.setLiquidationModule(liquidationManager.address); // Registering the first pool await poolRegistry.addPool("Pool 1", comptrollerProxy.address, _closeFactor, _minLiquidatableCollateral); diff --git a/tests/hardhat/Rewards.ts b/tests/hardhat/Rewards.ts index 0027cf4d0..b3d3f4439 100644 --- a/tests/hardhat/Rewards.ts +++ b/tests/hardhat/Rewards.ts @@ -85,11 +85,15 @@ async function rewardsFixture(isTimeBased: boolean) { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); + comptrollerProxy = (await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ maxLoopsLimit, fakeAccessControlManager.address, ])) as Comptroller; await comptrollerProxy.setPriceOracle(fakePriceOracle.address); + await comptrollerProxy.setLiquidationModule(liquidationManager.address); // Registering the first pool await poolRegistry.addPool("Pool 1", comptrollerProxy.address, _closeFactor, _minLiquidatableCollateral); From d8bc0cef87b3ab7023839fb205d13116f8116f3f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 23 Jul 2025 17:31:58 +0530 Subject: [PATCH 44/51] refactor: move order processing back to comptroller --- contracts/Comptroller.sol | 11 +++++++++-- contracts/LiquidationManager.sol | 23 ----------------------- contracts/LiquidationManagerInterface.sol | 6 ------ 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 54fd12571..a336be66b 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -946,7 +946,14 @@ contract Comptroller is _checkMarketListed(address(orders[i].vTokenBorrowed)); _checkMarketListed(address(orders[i].vTokenCollateral)); - liquidationManager.processLiquidationOrder(orders[i], borrower, msg.sender); + LiquidationOrder calldata order = orders[i]; + order.vTokenBorrowed.forceLiquidateBorrow( + msg.sender, + borrower, + order.repayAmount, + order.vTokenCollateral, + true + ); } for (uint256 i; i < marketsCount; ++i) { @@ -1727,7 +1734,7 @@ contract Comptroller is for (uint256 i; i < assetsCount; ) { VToken asset = assets[i]; snapshot = liquidationManager.processAsset( - assets[i], + asset, account, effects, weight(asset).mantissa, diff --git a/contracts/LiquidationManager.sol b/contracts/LiquidationManager.sol index b219041a2..a2aeb8de8 100644 --- a/contracts/LiquidationManager.sol +++ b/contracts/LiquidationManager.sol @@ -10,29 +10,6 @@ import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; contract LiquidationManager is ILiquidationManager, ExponentialNoError { - /** - * @notice Processes a batch of liquidation orders for a given borrower. - * @dev Iterates through the provided liquidation orders and executes the liquidation for each order using the `forceLiquidateBorrow` function. - * @param order Aliquidation orders to process. - * @param borrower The address of the borrower whose positions are being liquidated. - * @param liquidator The address performing the liquidation. - * @custom:reverts MarketNotListed if either the borrowed or collateral market in an order is not listed. - */ - function processLiquidationOrder( - ComptrollerStorage.LiquidationOrder calldata order, - address borrower, - address liquidator - ) external { - // Execute liquidation - order.vTokenBorrowed.forceLiquidateBorrow( - liquidator, - borrower, - order.repayAmount, - order.vTokenCollateral, - true - ); - } - /** * @notice Calculates incentive-adjusted debt */ diff --git a/contracts/LiquidationManagerInterface.sol b/contracts/LiquidationManagerInterface.sol index 4851fc728..b419627a8 100644 --- a/contracts/LiquidationManagerInterface.sol +++ b/contracts/LiquidationManagerInterface.sol @@ -21,12 +21,6 @@ interface ILiquidationManager { uint256 borrowAmount; } - function processLiquidationOrder( - ComptrollerStorage.LiquidationOrder calldata order, - address borrower, - address liquidator - ) external; - function calculateIncentiveAdjustedDebt( address borrower, VToken[] memory markets, From dbf57390e90db3ebc9617a4ce52b188e5a99c93a Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 23 Jul 2025 17:32:17 +0530 Subject: [PATCH 45/51] fix: integration test --- tests/integration/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/index.ts b/tests/integration/index.ts index 001c869eb..f2e6cabc8 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -52,6 +52,8 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun const pools = await PoolRegistry.callStatic.getAllPools(); const Comptroller = await ethers.getContractAt("Comptroller", pools[0].comptroller); + const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const liquidationManager = await LiquidationManager.deploy(); const BNX = await ethers.getContract("MockBNX"); const BTCB = await ethers.getContract("MockBTCB"); @@ -109,6 +111,12 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun deployer, ); + await AccessControlManager.giveCallPermission( + ethers.constants.AddressZero, + "setLiquidationModule(address)", + deployer, + ); + // Set supply caps const supply = convertToUnit(10, 36); await Comptroller.setMarketSupplyCaps([vBNX.address, vBTCB.address], [supply, supply]); @@ -119,6 +127,7 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun await Comptroller.setMarketLiquidationIncentive(vBNX.address, convertToUnit(1, 18)); await Comptroller.setMarketLiquidationIncentive(vBTCB.address, convertToUnit(1, 18)); + await Comptroller.setLiquidationModule(liquidationManager.address); const vBNXPrice: BigNumber = new BigNumber( scaleDownBy((await priceOracle.getUnderlyingPrice(vBNX.address)).toString(), 18), From 3177bf889be6367ddf5861803214f5f55c5b57b3 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 24 Jul 2025 20:13:14 +0530 Subject: [PATCH 46/51] test: fixed fork tests --- tests/hardhat/Fork/borrowAndRepayTest.ts | 52 +++++++++++- tests/hardhat/Fork/liquidation.ts | 100 +++++++++++++++++++---- 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/tests/hardhat/Fork/borrowAndRepayTest.ts b/tests/hardhat/Fork/borrowAndRepayTest.ts index 69f791b02..77402dec0 100644 --- a/tests/hardhat/Fork/borrowAndRepayTest.ts +++ b/tests/hardhat/Fork/borrowAndRepayTest.ts @@ -4,8 +4,11 @@ import chai from "chai"; import { BigNumber, BigNumberish, Signer } from "ethers"; import { ethers } from "hardhat"; +import { DEFAULT_BLOCKS_PER_YEAR } from "../../../helpers/deploymentConfig"; import { convertToUnit } from "../../../helpers/utils"; import { + AccessControlManager, + AccessControlManager__factory, BinanceOracle, BinanceOracle__factory, ChainlinkOracle__factory, @@ -13,6 +16,7 @@ import { Comptroller__factory, IERC20, IERC20__factory, + LiquidationManager, ResilientOracleInterface, ResilientOracleInterface__factory, VToken, @@ -31,6 +35,7 @@ const FORKED_NETWORK = process.env.FORKED_NETWORK || "bscmainnet"; if (FORK) console.log(`fork tests are running on: ${FORKED_NETWORK}`); const { + ACM, ACC1, ACC2, ADMIN, @@ -44,13 +49,19 @@ const { TOKEN1_HOLDER, TOKEN2_HOLDER, BLOCK_NUMBER, + POOL_REGISTRY, } = getContractAddresses(FORKED_NETWORK as string); +const COMPTROLLER_BEACON = "0x38B4Efab9ea1bAcD19dC81f19c4D1C2F9DeAe1B2"; +const VTOKEN_BEACON = "0x2b8A1C539ABaC89CbF7E2Bc6987A0A38A5e660D4"; +const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); + let token1: IERC20 | WrappedNative; let token2: IERC20; let vTOKEN1: VToken; let vTOKEN2: VToken; let comptroller: Comptroller; +let liquidationManager: LiquidationManager; let acc1Signer: Signer; let acc2Signer: Signer; let token2Holder: Signer; @@ -60,6 +71,7 @@ let mintAmount: BigNumber; let TOKEN2BorrowAmount: BigNumberish; let binanceOracle: BinanceOracle; let priceOracle: ResilientOracleInterface; +let accessControlManager: AccessControlManager; async function configureTimelock() { impersonatedTimelock = await initMainnetUser(ADMIN, ethers.utils.parseUnits("2")); @@ -78,6 +90,42 @@ if (FORK) { await setForkBlock(BLOCK_NUMBER); await configureTimelock(); + + comptroller = Comptroller__factory.connect(COMPTROLLER, impersonatedTimelock); + vTOKEN2 = await configureVToken(VTOKEN2); + vTOKEN1 = await configureVToken(VTOKEN1); + + // --- Upgrade Comptroller Implementation --- + const ComptrollerFactory = await ethers.getContractFactory("Comptroller"); + const newComptrollerImpl = await ComptrollerFactory.deploy(POOL_REGISTRY); + await newComptrollerImpl.deployed(); + + const comptrollerBeacon = await ethers.getContractAt( + "UpgradeableBeacon", + COMPTROLLER_BEACON, + impersonatedTimelock, + ); + await comptrollerBeacon.upgradeTo(newComptrollerImpl.address); + + // --- Upgrade VToken Implementation --- + const VTokenFactory = await ethers.getContractFactory("VToken"); + const newVTokenImpl = await VTokenFactory.deploy(false, DEFAULT_BLOCKS_PER_YEAR, maxBorrowRateMantissa); + await newVTokenImpl.deployed(); + + const vTokenBeacon = await ethers.getContractAt("UpgradeableBeacon", VTOKEN_BEACON, impersonatedTimelock); + await vTokenBeacon.upgradeTo(newVTokenImpl.address); + + // --- Deploy and Set New LiquidationManager --- + const LiquidationManagerFactory = await ethers.getContractFactory("LiquidationManager"); + liquidationManager = await LiquidationManagerFactory.deploy(); + await liquidationManager.deployed(); + + accessControlManager = AccessControlManager__factory.connect(ACM, impersonatedTimelock); + await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(comptroller.address, "setLiquidationModule(address)", ADMIN); + await comptroller.setLiquidationModule(liquidationManager.address); + acc1Signer = await initMainnetUser(ACC1, ethers.utils.parseUnits("2")); acc2Signer = await initMainnetUser(ACC2, ethers.utils.parseUnits("2")); // it will be the depositor @@ -116,10 +164,6 @@ if (FORK) { await token1.connect(token1Holder).deposit({ value: convertToUnit("200000", 18) }); } - vTOKEN2 = await configureVToken(VTOKEN2); - vTOKEN1 = await configureVToken(VTOKEN1); - comptroller = Comptroller__factory.connect(COMPTROLLER, impersonatedTimelock); - const oracle = await comptroller.oracle(); priceOracle = ResilientOracleInterface__factory.connect(oracle, impersonatedTimelock); diff --git a/tests/hardhat/Fork/liquidation.ts b/tests/hardhat/Fork/liquidation.ts index 3841ac91c..e003f2e67 100644 --- a/tests/hardhat/Fork/liquidation.ts +++ b/tests/hardhat/Fork/liquidation.ts @@ -4,6 +4,7 @@ import { BigNumberish, Signer } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; +import { DEFAULT_BLOCKS_PER_YEAR } from "../../../helpers/deploymentConfig"; import { convertToUnit } from "../../../helpers/utils"; import { AccessControlManager, @@ -14,6 +15,7 @@ import { Comptroller__factory, IERC20, IERC20__factory, + LiquidationManager, MockPriceOracle, MockPriceOracle__factory, VToken, @@ -45,15 +47,20 @@ const { RESILIENT_ORACLE, CHAINLINK_ORACLE, BLOCK_NUMBER, + POOL_REGISTRY, } = getContractAddresses(FORKED_NETWORK as string); const AddressZero = "0x0000000000000000000000000000000000000000"; +const COMPTROLLER_BEACON = "0x38B4Efab9ea1bAcD19dC81f19c4D1C2F9DeAe1B2"; +const VTOKEN_BEACON = "0x2b8A1C539ABaC89CbF7E2Bc6987A0A38A5e660D4"; +const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); let token1: IERC20 | WrappedNative; let token2: IERC20; let vTOKEN1: VToken; let vTOKEN2: VToken; let comptroller: Comptroller; +let liquidationManager: LiquidationManager; let token1Holder: Signer; let token2Holder: Signer; let acc1Signer: Signer; @@ -105,14 +112,46 @@ if (FORK) { await setForkBlock(BLOCK_NUMBER); await configureTimelock(); + comptroller = Comptroller__factory.connect(COMPTROLLER, impersonatedTimelock); + vTOKEN2 = await configureVToken(VTOKEN2); + vTOKEN1 = await configureVToken(VTOKEN1); + + // --- Upgrade Comptroller Implementation --- + const ComptrollerFactory = await ethers.getContractFactory("Comptroller"); + const newComptrollerImpl = await ComptrollerFactory.deploy(POOL_REGISTRY); + await newComptrollerImpl.deployed(); + + const comptrollerBeacon = await ethers.getContractAt( + "UpgradeableBeacon", + COMPTROLLER_BEACON, + impersonatedTimelock, + ); + await comptrollerBeacon.upgradeTo(newComptrollerImpl.address); + + // --- Upgrade VToken Implementation --- + const VTokenFactory = await ethers.getContractFactory("VToken"); + const newVTokenImpl = await VTokenFactory.deploy(false, DEFAULT_BLOCKS_PER_YEAR, maxBorrowRateMantissa); + await newVTokenImpl.deployed(); + + const vTokenBeacon = await ethers.getContractAt("UpgradeableBeacon", VTOKEN_BEACON, impersonatedTimelock); + await vTokenBeacon.upgradeTo(newVTokenImpl.address); + + // --- Deploy and Set New LiquidationManager --- + const LiquidationManagerFactory = await ethers.getContractFactory("LiquidationManager"); + liquidationManager = await LiquidationManagerFactory.deploy(); + await liquidationManager.deployed(); + + accessControlManager = AccessControlManager__factory.connect(ACM, impersonatedTimelock); + await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(comptroller.address, "setLiquidationModule(address)", ADMIN); + await comptroller.setLiquidationModule(liquidationManager.address); + acc1Signer = await initMainnetUser(ACC1, ethers.utils.parseUnits("2")); acc2Signer = await initMainnetUser(ACC2, ethers.utils.parseUnits("2")); token2Holder = await initMainnetUser(TOKEN2_HOLDER, ethers.utils.parseUnits("2")); token1Holder = await initMainnetUser(TOKEN1_HOLDER, ethers.utils.parseUnits("2000000")); - vTOKEN2 = await configureVToken(VTOKEN2); - vTOKEN1 = await configureVToken(VTOKEN1); - comptroller = Comptroller__factory.connect(COMPTROLLER, impersonatedTimelock); token2 = IERC20__factory.connect(TOKEN2, impersonatedTimelock); token1 = IERC20__factory.connect(TOKEN1, impersonatedTimelock); if (FORKED_NETWORK == "arbitrumsepolia" || FORKED_NETWORK == "arbitrumone") { @@ -141,6 +180,7 @@ if (FORK) { }; resilientOracle = MockPriceOracle__factory.connect(RESILIENT_ORACLE, impersonatedTimelock); + await resilientOracle.setTokenConfig(tupleForToken1); await resilientOracle.setTokenConfig(tupleForToken2); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(token1.address, convertToUnit("1", 18)); @@ -156,6 +196,12 @@ if (FORK) { ); await comptroller.connect(acc1Signer).enterMarkets([vTOKEN2.address]); await comptroller.connect(acc2Signer).enterMarkets([vTOKEN1.address]); + + await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(comptroller.address, "setMarketLiquidationIncentive(address,uint256)", ADMIN); + await comptroller.setMarketLiquidationIncentive(vTOKEN1.address, parseUnits("1", 18)); + await comptroller.setMarketLiquidationIncentive(vTOKEN2.address, parseUnits("1", 18)); } describe("Liquidate from VToken", async () => { @@ -200,6 +246,7 @@ if (FORK) { it("Should revert when liquidation is called through vToken and trying to seize more tokens", async function () { await comptroller.setMinLiquidatableCollateral(0); + await comptroller.setForcedLiquidation(vTOKEN2.address, true); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(token1.address, convertToUnit("1", 5)); const borrowBalance = await vTOKEN2.borrowBalanceStored(ACC2); @@ -215,6 +262,7 @@ if (FORK) { it("Should revert when liquidation is called through vToken and trying to pay too much", async function () { // Mint and Incrrease collateral of the user await comptroller.setMinLiquidatableCollateral(0); + await comptroller.setForcedLiquidation(vTOKEN2.address, true); const underlyingMintAmount = convertToUnit("1", 18); await token1.connect(token1Holder).transfer(ACC2, underlyingMintAmount); await token1.connect(acc2Signer).approve(vTOKEN1.address, underlyingMintAmount); @@ -237,6 +285,7 @@ if (FORK) { it("liquidate user", async () => { await comptroller.setMinLiquidatableCollateral(0); + await comptroller.setForcedLiquidation(vTOKEN2.address, true); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(token1.address, convertToUnit("1", 6)); const borrowBalance = await vTOKEN2.borrowBalanceStored(ACC2); @@ -257,7 +306,7 @@ if (FORK) { const priceBorrowed = await chainlinkOracle.getPrice(TOKEN2); const priceCollateral = await chainlinkOracle.getPrice(TOKEN1); - const liquidationIncentive = await comptroller.liquidationIncentiveMantissa(); + const liquidationIncentive = await comptroller.getMarketLiquidationIncentive(vTOKEN1.address); const exchangeRateCollateralPrev = await vTOKEN1.callStatic.exchangeRateCurrent(); const num = (liquidationIncentive * priceBorrowed) / 1e18; const den = (priceCollateral * exchangeRateCollateralPrev) / 1e18; @@ -321,8 +370,7 @@ if (FORK) { it("Should success on liquidation when repay amount is equal to borrowing", async function () { await comptroller .connect(impersonatedTimelock) - .setCollateralFactor(vTOKEN1.address, convertToUnit(7, 17), convertToUnit(8, 17)); - await comptroller.connect(impersonatedTimelock).setLiquidationIncentive(convertToUnit(1, 18)); + .setCollateralFactor(vTOKEN1.address, convertToUnit(6, 17), convertToUnit(8, 17)); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(token1.address, convertToUnit("1", 12)); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(token2.address, convertToUnit("1", 12)); @@ -335,39 +383,46 @@ if (FORK) { const totalReservesToken1Prev = await vTOKEN1.totalReserves(); const vTOKEN1BalAcc1Prev = await vTOKEN1.balanceOf(ACC1); const vTOKEN1BalAcc2Prev = await vTOKEN1.balanceOf(ACC2); - const priceBorrowed = await chainlinkOracle.getPrice(TOKEN2); + const priceCollateral = await chainlinkOracle.getPrice(TOKEN1); - const liquidationIncentive = await comptroller.liquidationIncentiveMantissa(); const exchangeRateCollateralPrev = await vTOKEN1.callStatic.exchangeRateCurrent(); - const num = (liquidationIncentive * priceBorrowed) / 1e18; - const den = (priceCollateral * exchangeRateCollateralPrev) / 1e18; - const ratio = num / den; await token1.connect(token1Holder).transfer(ACC2, convertToUnit(1, 12)); await token1.connect(acc2Signer).approve(vTOKEN1.address, convertToUnit(1, 12)); await vTOKEN1.connect(acc2Signer).mint(convertToUnit(1, 12)); - // repayAmount will be calculated after accruing interest and then using borrowBalanceStored to get the repayAmount. const NetworkRespectiveRepayAmounts = { bsctestnet: 1000000048189326, sepolia: 1000000138102911, - bscmainnet: 1000000020807824, + bscmainnet: 1000000018727042, ethereum: 1000000262400450, opbnbtestnet: 1000000000288189, opbnbmainnet: 1000000008986559, arbitrumsepolia: 1000000000046406, arbitrumone: 1000000032216389, }; - const repayAmount = NetworkRespectiveRepayAmounts[FORKED_NETWORK]; - const seizeTokens = ratio * repayAmount; + + const borrowMarkets = await comptroller.getAssetsIn(ACC2); + const collateralToSeize = await liquidationManager.calculateIncentiveAdjustedDebt( + ACC2, + borrowMarkets, + comptroller.address, + ); + + // Convert collateralToSeize (USD value) to actual vTokens to seize + const collateralValue = (priceCollateral * exchangeRateCollateralPrev) / 1e18; + const seizeTokens = (collateralToSeize * 1e18) / collateralValue; + const param = { vTokenCollateral: vTOKEN1.address, vTokenBorrowed: vTOKEN2.address, repayAmount: repayAmount, }; + const result = comptroller.connect(acc1Signer).liquidateAccount(ACC2, [param]); await expect(result).to.emit(vTOKEN2, "LiquidateBorrow"); + expect(await vTOKEN2.borrowBalanceStored(ACC2)).equals(0); const vTOKEN1BalAcc1New = await vTOKEN1.balanceOf(ACC1); @@ -375,15 +430,24 @@ if (FORK) { const totalReservesToken1New = await vTOKEN1.totalReserves(); const exchangeRateCollateralNew = await vTOKEN1.exchangeRateStored(); + // 10. Verify token distribution (95% liquidator, 5% protocol) const liquidatorSeizeTokens = Math.floor((seizeTokens * 95) / 100); const protocolSeizeTokens = Math.floor((seizeTokens * 5) / 100); const reserveIncrease = (protocolSeizeTokens * exchangeRateCollateralNew) / 1e18; - expect(vTOKEN1BalAcc2Prev.sub(vTOKEN1BalAcc2New)).to.closeTo(Math.floor(seizeTokens), 100); - expect(vTOKEN1BalAcc1New.sub(vTOKEN1BalAcc1Prev)).to.closeTo(liquidatorSeizeTokens, 1); + expect(vTOKEN1BalAcc2Prev.sub(vTOKEN1BalAcc2New)).to.closeTo( + Math.floor(seizeTokens), + 100, // tolerance for rounding + ); + + expect(vTOKEN1BalAcc1New.sub(vTOKEN1BalAcc1Prev)).to.closeTo( + liquidatorSeizeTokens, + 1, // tolerance + ); + expect(totalReservesToken1New.sub(totalReservesToken1Prev)).to.closeTo( Math.round(reserveIncrease), - parseUnits("1", 17), + parseUnits("1", 17), // tolerance ); }); }); From ec585aff7498e95cde1926e7e44fad700a32361e Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 28 Jul 2025 20:40:01 +0530 Subject: [PATCH 47/51] feat: liquidation Manager for common functionalities of core and IL --- contracts/LiquidationManager.sol | 194 +++++++++---------------------- 1 file changed, 55 insertions(+), 139 deletions(-) diff --git a/contracts/LiquidationManager.sol b/contracts/LiquidationManager.sol index a2aeb8de8..22b715c70 100644 --- a/contracts/LiquidationManager.sol +++ b/contracts/LiquidationManager.sol @@ -1,160 +1,76 @@ // SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.10; +pragma solidity 0.8.25; -import { VToken } from "./VToken.sol"; -import { ComptrollerStorage } from "./ComptrollerStorage.sol"; -import { ComptrollerInterface } from "./ComptrollerInterface.sol"; -import { Comptroller } from "./Comptroller.sol"; import { ExponentialNoError } from "./ExponentialNoError.sol"; -import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; -import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; -contract LiquidationManager is ILiquidationManager, ExponentialNoError { - /** - * @notice Calculates incentive-adjusted debt - */ - function calculateIncentiveAdjustedDebt( - address borrower, - VToken[] memory markets, - ComptrollerInterface comptroller - ) external view returns (uint256 weightedBorrowSum) { - for (uint256 i; i < markets.length; ++i) { - VToken market = markets[i]; - (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); - if (borrowBalance == 0) continue; - - ResilientOracleInterface oracle = comptroller.getOracle(); - uint256 borrowPrice = oracle.getUnderlyingPrice(address(market)); - uint256 borrowValueUSD = mul_ScalarTruncate(Exp({ mantissa: borrowPrice }), borrowBalance); - - uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); - - weightedBorrowSum = ExponentialNoError.add_( - weightedBorrowSum, - ExponentialNoError.mul_ScalarTruncate( - ExponentialNoError.Exp({ mantissa: marketIncentive }), - borrowValueUSD - ) +contract LiquidationManager is ExponentialNoError { + function calculateCloseFactor( + uint256 borrowBalance, + uint256 wtAvg, + uint256 totalCollateral, + uint256 healthFactor, + uint256 healthFactorThreshold, + uint256 maxLiquidationIncentive + ) external pure returns (uint256 closeFactor) { + if (healthFactor >= healthFactorThreshold) { + // Prevent underflow + require( + wtAvg * totalCollateral <= borrowBalance * MANTISSA_ONE, + "LiquidationManager: Collateral exceeds borrow capacity" ); - } - } - - /** - * @notice Processes a single asset for a given account and updates the liquidity snapshot. - * @dev - * - Constructs AssetData for the asset and account. - * - Calculates and applies the asset's effect on the account's liquidity snapshot, including any modifications (redeem/borrow). - * @param asset The VToken asset to process. - * @param account The address of the account being evaluated. - * @param effects Parameters describing any modifications (redeem/borrow) to apply for this asset. - * @param assetWeight The risk weight of the asset. - * @param snapshot The current account liquidity snapshot to update. - * @return The updated AccountLiquiditySnapshot struct. - */ - function processAsset( - VToken asset, - address account, - EffectsParams memory effects, - uint256 assetWeight, - uint256 underlyingPrice, - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) external view returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - (, uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = asset.getAccountSnapshot( - account - ); - AssetData memory assetData = AssetData({ - vTokenBalance: vTokenBalance, - borrowBalance: borrowBalance, - exchangeRateMantissa: exchangeRateMantissa, - underlyingPrice: underlyingPrice, - assetWeight: assetWeight, - vTokenAddress: address(asset) - }); + uint256 numerator = borrowBalance * MANTISSA_ONE - wtAvg * totalCollateral; + uint256 denominator = borrowBalance * + (MANTISSA_ONE - ((wtAvg * (MANTISSA_ONE + maxLiquidationIncentive)) / MANTISSA_ONE)); - return _calculateAssetValues(assetData, snapshot, effects); - } - - /** - * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. - * @dev - * - Computes the average weight. - * - Calculates the sum of borrows and effects. - * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. - * - Sets the health factor threshold using the weighted average and liquidation incentive. - * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. - * @param snapshot The account liquidity snapshot to be finalized. - * @return The finalized account liquidity snapshot with updated fields. - */ - function finalizeSnapshot( - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) external pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - if (snapshot.totalCollateral > 0) { - snapshot.averageLT = div_(snapshot.averageLT, snapshot.totalCollateral); + closeFactor = (numerator * MANTISSA_ONE) / denominator; + closeFactor = closeFactor > MANTISSA_ONE ? MANTISSA_ONE : closeFactor; + } else { + closeFactor = MANTISSA_ONE; // Liquidate 100% if unhealthy } - uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; + } - if (borrowPlusEffects > 0) { - snapshot.healthFactor = div_(snapshot.weightedCollateral, borrowPlusEffects); + function calculateDynamicLiquidationIncentive( + uint256 healthFactor, + uint256 healthFactorThreshold, + uint256 averageLT, + uint256 maxLiquidationIncentiveMantissa + ) external pure returns (uint256 incentive) { + if (healthFactor >= healthFactorThreshold) { + return maxLiquidationIncentiveMantissa; } - snapshot.healthFactorThreshold = div_(snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg), 1e18); unchecked { - if (snapshot.weightedCollateral > borrowPlusEffects) { - snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; - snapshot.shortfall = 0; - } else { - snapshot.liquidity = 0; - snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; - } + uint256 value = ((healthFactor * 1e18) / averageLT) - 1e18; + return value > maxLiquidationIncentiveMantissa ? maxLiquidationIncentiveMantissa : value; } - - return snapshot; } - /** - * @notice Calculates and updates the liquidity snapshot values for a given asset. - * @dev Computes weighted collateral, total collateral, and borrow values using asset data and price information. - * If the asset is being modified (redeemed or borrowed), applies the effects to the snapshot as well. - * @param asset The asset data struct containing balances, prices, and weights. - * @param snapshot The current account liquidity snapshot to update. - * @param effectsParams Parameters describing any modifications (redeem/borrow) to apply for this asset. - * @return The updated AccountLiquiditySnapshot struct. - */ - function _calculateAssetValues( - AssetData memory asset, - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, - EffectsParams memory effectsParams - ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { - Exp memory oraclePrice = Exp({ mantissa: asset.underlyingPrice }); - Exp memory vTokenPrice = mul_(Exp({ mantissa: asset.exchangeRateMantissa }), oraclePrice); - Exp memory weightedVTokenPrice = mul_(Exp({ mantissa: asset.assetWeight }), vTokenPrice); - - // Core calculations - snapshot.weightedCollateral = mul_ScalarTruncateAddUInt( - weightedVTokenPrice, - asset.vTokenBalance, - snapshot.weightedCollateral + function calculateSeizeTokens( + uint256 actualRepayAmount, + uint256 liquidationIncentiveMantissa, + uint256 priceBorrowedMantissa, + uint256 priceCollateralMantissa, + uint256 exchangeRateMantissa + ) external pure returns (uint256 seizeTokens) { + Exp memory numerator = mul_( + Exp({ mantissa: liquidationIncentiveMantissa }), + Exp({ mantissa: priceBorrowedMantissa }) ); - snapshot.totalCollateral = mul_ScalarTruncateAddUInt( - vTokenPrice, - asset.vTokenBalance, - snapshot.totalCollateral + Exp memory denominator = mul_( + Exp({ mantissa: priceCollateralMantissa }), + Exp({ mantissa: exchangeRateMantissa }) ); - snapshot.borrows = mul_ScalarTruncateAddUInt(oraclePrice, asset.borrowBalance, snapshot.borrows); - uint256 vTokenBalanceUSD = mul_ScalarTruncate(vTokenPrice, asset.vTokenBalance); - snapshot.averageLT += mul_(asset.assetWeight, vTokenBalanceUSD); + seizeTokens = mul_ScalarTruncate(div_(numerator, denominator), actualRepayAmount); - // Handle modified asset effects - if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { - snapshot.effects = mul_ScalarTruncateAddUInt( - weightedVTokenPrice, - effectsParams.redeemTokens, - snapshot.effects - ); - snapshot.effects = mul_ScalarTruncateAddUInt(oraclePrice, effectsParams.borrowAmount, snapshot.effects); - } + return (seizeTokens); + } - return snapshot; + function isToxicLiquidation( + uint256 averageLT, + uint256 liquidationIncentiveAvg, + uint256 healthFactor + ) external pure returns (bool) { + return ((averageLT * (1e18 + liquidationIncentiveAvg)) > healthFactor); } } From 1dc088cb88e01711634d85c69688d3f4faef8a14 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 28 Jul 2025 20:43:37 +0530 Subject: [PATCH 48/51] feat: IL specific liquidation manager --- contracts/ILLiquidationManager.sol | 171 ++++++++++++++++++++++ contracts/LiquidationManagerInterface.sol | 42 ------ 2 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 contracts/ILLiquidationManager.sol delete mode 100644 contracts/LiquidationManagerInterface.sol diff --git a/contracts/ILLiquidationManager.sol b/contracts/ILLiquidationManager.sol new file mode 100644 index 000000000..e17d05bf6 --- /dev/null +++ b/contracts/ILLiquidationManager.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VToken } from "./VToken.sol"; +import { ComptrollerStorage } from "./ComptrollerStorage.sol"; +import { ComptrollerInterface } from "./ComptrollerInterface.sol"; +import { Comptroller } from "./Comptroller.sol"; +import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; +import { LiquidationManager } from "./LiquidationManager.sol"; + +contract ILLiquidationManager is LiquidationManager { + struct AssetData { + uint256 vTokenBalance; + uint256 borrowBalance; + uint256 exchangeRateMantissa; + uint256 underlyingPrice; + uint256 assetWeight; + address vTokenAddress; + } + + struct EffectsParams { + VToken vTokenModify; + uint256 redeemTokens; + uint256 borrowAmount; + } + + /** + * @notice Calculates incentive-adjusted debt + */ + function calculateIncentiveAdjustedDebt( + address borrower, + VToken[] memory markets, + ComptrollerInterface comptroller + ) external view returns (uint256 weightedBorrowSum) { + for (uint256 i; i < markets.length; ++i) { + VToken market = markets[i]; + (, , uint256 borrowBalance, ) = market.getAccountSnapshot(borrower); + if (borrowBalance == 0) continue; + + ResilientOracleInterface oracle = comptroller.getOracle(); + uint256 borrowPrice = oracle.getUnderlyingPrice(address(market)); + uint256 borrowValueUSD = mul_ScalarTruncate(Exp({ mantissa: borrowPrice }), borrowBalance); + + uint256 marketIncentive = comptroller.getDynamicLiquidationIncentive(borrower, address(market)); + + weightedBorrowSum = add_( + weightedBorrowSum, + mul_ScalarTruncate(Exp({ mantissa: marketIncentive }), borrowValueUSD) + ); + } + } + + /** + * @notice Processes a single asset for a given account and updates the liquidity snapshot. + * @dev + * - Constructs AssetData for the asset and account. + * - Calculates and applies the asset's effect on the account's liquidity snapshot, including any modifications (redeem/borrow). + * @param asset The VToken asset to process. + * @param account The address of the account being evaluated. + * @param effects Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @param assetWeight The risk weight of the asset. + * @param snapshot The current account liquidity snapshot to update. + * @return The updated AccountLiquiditySnapshot struct. + */ + function processAsset( + VToken asset, + address account, + EffectsParams memory effects, + uint256 assetWeight, + uint256 underlyingPrice, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external view returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + (, uint256 vTokenBalance, uint256 borrowBalance, uint256 exchangeRateMantissa) = asset.getAccountSnapshot( + account + ); + + AssetData memory assetData = AssetData({ + vTokenBalance: vTokenBalance, + borrowBalance: borrowBalance, + exchangeRateMantissa: exchangeRateMantissa, + underlyingPrice: underlyingPrice, + assetWeight: assetWeight, + vTokenAddress: address(asset) + }); + + return _calculateAssetValues(assetData, snapshot, effects); + } + + /** + * @notice Finalizes the account liquidity snapshot by calculating weighted averages, health factors, and liquidity/shortfall. + * @dev + * - Computes the average weight. + * - Calculates the sum of borrows and effects. + * - Determines the health factor as the ratio of weighted collateral to total borrow plus effects. + * - Sets the health factor threshold using the weighted average and liquidation incentive. + * - Calculates liquidity and shortfall based on the comparison of weighted collateral and borrow plus effects. + * @param snapshot The account liquidity snapshot to be finalized. + * @return The finalized account liquidity snapshot with updated fields. + */ + function finalizeSnapshot( + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot + ) external pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + if (snapshot.totalCollateral > 0) { + snapshot.averageLT = div_(snapshot.averageLT, snapshot.totalCollateral); + } + uint256 borrowPlusEffects = snapshot.borrows + snapshot.effects; + + if (borrowPlusEffects > 0) { + snapshot.healthFactor = div_(snapshot.weightedCollateral, borrowPlusEffects); + } + snapshot.healthFactorThreshold = div_(snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg), 1e18); + + unchecked { + if (snapshot.weightedCollateral > borrowPlusEffects) { + snapshot.liquidity = snapshot.weightedCollateral - borrowPlusEffects; + snapshot.shortfall = 0; + } else { + snapshot.liquidity = 0; + snapshot.shortfall = borrowPlusEffects - snapshot.weightedCollateral; + } + } + + return snapshot; + } + + /** + * @notice Calculates and updates the liquidity snapshot values for a given asset. + * @dev Computes weighted collateral, total collateral, and borrow values using asset data and price information. + * If the asset is being modified (redeemed or borrowed), applies the effects to the snapshot as well. + * @param asset The asset data struct containing balances, prices, and weights. + * @param snapshot The current account liquidity snapshot to update. + * @param effectsParams Parameters describing any modifications (redeem/borrow) to apply for this asset. + * @return The updated AccountLiquiditySnapshot struct. + */ + function _calculateAssetValues( + AssetData memory asset, + ComptrollerStorage.AccountLiquiditySnapshot memory snapshot, + EffectsParams memory effectsParams + ) internal pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory) { + Exp memory oraclePrice = Exp({ mantissa: asset.underlyingPrice }); + Exp memory vTokenPrice = mul_(Exp({ mantissa: asset.exchangeRateMantissa }), oraclePrice); + Exp memory weightedVTokenPrice = mul_(Exp({ mantissa: asset.assetWeight }), vTokenPrice); + + // Core calculations + snapshot.weightedCollateral = mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + asset.vTokenBalance, + snapshot.weightedCollateral + ); + snapshot.totalCollateral = mul_ScalarTruncateAddUInt( + vTokenPrice, + asset.vTokenBalance, + snapshot.totalCollateral + ); + snapshot.borrows = mul_ScalarTruncateAddUInt(oraclePrice, asset.borrowBalance, snapshot.borrows); + uint256 vTokenBalanceUSD = mul_ScalarTruncate(vTokenPrice, asset.vTokenBalance); + snapshot.averageLT += mul_(asset.assetWeight, vTokenBalanceUSD); + + // Handle modified asset effects + if (address(asset.vTokenAddress) == address(effectsParams.vTokenModify)) { + snapshot.effects = mul_ScalarTruncateAddUInt( + weightedVTokenPrice, + effectsParams.redeemTokens, + snapshot.effects + ); + snapshot.effects = mul_ScalarTruncateAddUInt(oraclePrice, effectsParams.borrowAmount, snapshot.effects); + } + + return snapshot; + } +} diff --git a/contracts/LiquidationManagerInterface.sol b/contracts/LiquidationManagerInterface.sol deleted file mode 100644 index b419627a8..000000000 --- a/contracts/LiquidationManagerInterface.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity ^0.8.10; - -import { VToken } from "./VToken.sol"; -import { ComptrollerStorage } from "./ComptrollerStorage.sol"; -import { ComptrollerInterface } from "./ComptrollerInterface.sol"; - -interface ILiquidationManager { - struct AssetData { - uint256 vTokenBalance; - uint256 borrowBalance; - uint256 exchangeRateMantissa; - uint256 underlyingPrice; - uint256 assetWeight; - address vTokenAddress; - } - - struct EffectsParams { - VToken vTokenModify; - uint256 redeemTokens; - uint256 borrowAmount; - } - - function calculateIncentiveAdjustedDebt( - address borrower, - VToken[] memory markets, - ComptrollerInterface comptroller - ) external view returns (uint256 weightedBorrowSum); - - function processAsset( - VToken asset, - address account, - EffectsParams memory effects, - uint256 assetWeight, - uint256 underlyingPrice, - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) external view returns (ComptrollerStorage.AccountLiquiditySnapshot memory); - - function finalizeSnapshot( - ComptrollerStorage.AccountLiquiditySnapshot memory snapshot - ) external pure returns (ComptrollerStorage.AccountLiquiditySnapshot memory); -} From 91c22c377517d07603bc6f970413b7fd2db9555c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 28 Jul 2025 20:44:17 +0530 Subject: [PATCH 49/51] refactor: using ILLiquidation manager in comptroller --- contracts/ComptrollerStorage.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index d9aa005dd..295d28fbf 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -7,7 +7,7 @@ import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { IPrime } from "@venusprotocol/venus-protocol/contracts/Tokens/Prime/Interfaces/IPrime.sol"; import { Action } from "./ComptrollerInterface.sol"; -import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; +import { ILLiquidationManager } from "./ILLiquidationManager.sol"; /** * @title ComptrollerStorage @@ -127,7 +127,8 @@ contract ComptrollerStorage { /// @notice The liquidation manager contract that handles liquidation logic // This is an interface to allow for different liquidation strategies - ILiquidationManager public liquidationManager; + // ILLiquidationManagerInterface public liquidationManager; + ILLiquidationManager public liquidationManager; /** * @dev This empty reserved space is put in place to allow future versions to add new From b7ad5ae0e68392571d3e9c00b3a5d37e79bba127 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 28 Jul 2025 20:45:23 +0530 Subject: [PATCH 50/51] refactor: moved some logic to Liquidation Manager --- contracts/Comptroller.sol | 77 ++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index a336be66b..a653e0bc0 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -13,7 +13,7 @@ import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; -import { ILiquidationManager } from "./LiquidationManagerInterface.sol"; +import { ILLiquidationManager } from "./ILLiquidationManager.sol"; /** * @title Comptroller @@ -110,7 +110,10 @@ contract Comptroller is event DelegateUpdated(address indexed approver, address indexed delegate, bool approved); /// @notice Emitted when the liquidation manager is set - event LiquidationModuleSet(address indexed ILiquidationManager); + event NewLiquidationManager( + ILLiquidationManager indexed oldLiquidationManager, + ILLiquidationManager indexed newLiquidationManager + ); /// @notice Thrown when collateral factor exceeds the upper bound error InvalidCollateralFactor(); @@ -727,26 +730,26 @@ contract Comptroller is revert InsufficientShortfall(); } - if ((snapshot.averageLT * (1e18 + snapshot.liquidationIncentiveAvg)) > snapshot.healthFactor) { + if ( + liquidationManager.isToxicLiquidation( + snapshot.averageLT, + snapshot.liquidationIncentiveAvg, + snapshot.healthFactor + ) + ) { revert ToxicLiquidation(); } Market storage marketCollateral = markets[vTokenCollateral]; - uint256 closeFactor; - unchecked { - if (snapshot.healthFactor >= 1e18) revert InsufficientShortfall(); - uint256 wtAvg = snapshot.averageLT; - if (snapshot.healthFactor >= snapshot.healthFactorThreshold) { - uint256 numerator = borrowBalance * 1e18 - wtAvg * snapshot.totalCollateral; - uint256 denominator = borrowBalance * - (1e18 - ((wtAvg * (1e18 + marketCollateral.maxLiquidationIncentiveMantissa)) / 1e18)); - closeFactor = (numerator * 1e18) / denominator; - closeFactor = closeFactor > 1e18 ? 1e18 : closeFactor; - } else { - closeFactor = 1e18; - } - } + uint256 closeFactor = liquidationManager.calculateCloseFactor( + borrowBalance, + snapshot.averageLT, + snapshot.totalCollateral, + snapshot.healthFactor, + snapshot.healthFactorThreshold, + marketCollateral.maxLiquidationIncentiveMantissa + ); /* The liquidator may not repay more than what is allowed by the closeFactor */ uint256 maxClose = mul_ScalarTruncate(Exp({ mantissa: closeFactor }), borrowBalance); @@ -1074,8 +1077,9 @@ contract Comptroller is function setLiquidationModule(address liquidationManager_) external { _checkAccessAllowed("setLiquidationModule(address)"); ensureNonzeroAddress(liquidationManager_); - liquidationManager = ILiquidationManager(liquidationManager_); - emit LiquidationModuleSet(liquidationManager_); + ILLiquidationManager oldLiquidationManager = liquidationManager; + liquidationManager = ILLiquidationManager(liquidationManager_); + emit NewLiquidationManager(oldLiquidationManager, liquidationManager); } /** @@ -1405,17 +1409,15 @@ contract Comptroller is * = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate) */ uint256 exchangeRateMantissa = VToken(vTokenCollateral).exchangeRateStored(); // Note: reverts on error - uint256 seizeTokens; - Exp memory numerator; - Exp memory denominator; - - numerator = mul_( - Exp({ mantissa: getDynamicLiquidationIncentive(borrower, vTokenCollateral) }), - Exp({ mantissa: priceBorrowedMantissa }) + uint256 liquidationIncentiveMantissa = getDynamicLiquidationIncentive(borrower, vTokenCollateral); + + uint256 seizeTokens = liquidationManager.calculateSeizeTokens( + actualRepayAmount, + liquidationIncentiveMantissa, + priceBorrowedMantissa, + priceCollateralMantissa, + exchangeRateMantissa ); - denominator = mul_(Exp({ mantissa: priceCollateralMantissa }), Exp({ mantissa: exchangeRateMantissa })); - - seizeTokens = mul_ScalarTruncate(div_(numerator, denominator), actualRepayAmount); return (NO_ERROR, seizeTokens); } @@ -1521,16 +1523,15 @@ contract Comptroller is /// @return incentive The liquidation incentive for the borrower, scaled by 1e18 function getDynamicLiquidationIncentive(address borrower, address vToken) public view returns (uint256 incentive) { Market storage market = markets[vToken]; - uint256 liquidationIncentiveMantissa = market.maxLiquidationIncentiveMantissa; - AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold); - if (snapshot.healthFactor >= snapshot.healthFactorThreshold) return liquidationIncentiveMantissa; - - unchecked { - uint256 value = ((snapshot.healthFactor * 1e18) / snapshot.averageLT) - 1e18; - return value > liquidationIncentiveMantissa ? liquidationIncentiveMantissa : value; - } + return + liquidationManager.calculateDynamicLiquidationIncentive( + snapshot.healthFactor, + snapshot.healthFactorThreshold, + snapshot.averageLT, + market.maxLiquidationIncentiveMantissa + ); } /** @@ -1725,7 +1726,7 @@ contract Comptroller is uint256 assetsCount = assets.length; uint256 liquidationIncentiveMantissa; - ILiquidationManager.EffectsParams memory effects = ILiquidationManager.EffectsParams({ + ILLiquidationManager.EffectsParams memory effects = ILLiquidationManager.EffectsParams({ vTokenModify: vTokenModify, redeemTokens: redeemTokens, borrowAmount: borrowAmount From e7285d647e3bb4692da9927ed766f83ed1707f5c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 28 Jul 2025 20:45:45 +0530 Subject: [PATCH 51/51] test: refactor tests --- .../Comptroller/accountLiquidityTest.ts | 2 +- tests/hardhat/Comptroller/assetsListTest.ts | 2 +- tests/hardhat/Comptroller/healAccountTest.ts | 2 +- .../Comptroller/liquidateAccountTest.ts | 2 +- .../liquidateCalculateAmountSeizeTest.ts | 2 +- tests/hardhat/Comptroller/setters.ts | 29 +++++++++++++++++++ tests/hardhat/Fork/borrowAndRepayTest.ts | 2 +- tests/hardhat/Fork/liquidation.ts | 2 +- tests/hardhat/Gateway/NativeTokenGateway.ts | 2 +- tests/hardhat/Lens/PoolLens.ts | 2 +- tests/hardhat/Prime.ts | 2 +- tests/hardhat/Rewards.ts | 2 +- tests/integration/index.ts | 2 +- 13 files changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/hardhat/Comptroller/accountLiquidityTest.ts b/tests/hardhat/Comptroller/accountLiquidityTest.ts index 63bdd2ac6..e6b2ce8f5 100644 --- a/tests/hardhat/Comptroller/accountLiquidityTest.ts +++ b/tests/hardhat/Comptroller/accountLiquidityTest.ts @@ -35,7 +35,7 @@ async function makeComptroller(): Promise { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); diff --git a/tests/hardhat/Comptroller/assetsListTest.ts b/tests/hardhat/Comptroller/assetsListTest.ts index a3cb15fc8..99d5484c5 100644 --- a/tests/hardhat/Comptroller/assetsListTest.ts +++ b/tests/hardhat/Comptroller/assetsListTest.ts @@ -62,7 +62,7 @@ describe("assetListTest", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); diff --git a/tests/hardhat/Comptroller/healAccountTest.ts b/tests/hardhat/Comptroller/healAccountTest.ts index a452009c1..804645e96 100644 --- a/tests/hardhat/Comptroller/healAccountTest.ts +++ b/tests/hardhat/Comptroller/healAccountTest.ts @@ -48,7 +48,7 @@ describe("healAccount", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); diff --git a/tests/hardhat/Comptroller/liquidateAccountTest.ts b/tests/hardhat/Comptroller/liquidateAccountTest.ts index 4474e08c0..e841eecb0 100644 --- a/tests/hardhat/Comptroller/liquidateAccountTest.ts +++ b/tests/hardhat/Comptroller/liquidateAccountTest.ts @@ -86,7 +86,7 @@ describe("liquidateAccount", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); diff --git a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts index e07693be3..c9e8def52 100644 --- a/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts +++ b/tests/hardhat/Comptroller/liquidateCalculateAmountSeizeTest.ts @@ -78,7 +78,7 @@ describe("Comptroller", () => { initializer: "initialize(uint256,address)", }); const oracle = await smock.fake("ResilientOracleInterface"); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); accessControl.isAllowedToCall.returns(true); await comptroller.setPriceOracle(oracle.address); diff --git a/tests/hardhat/Comptroller/setters.ts b/tests/hardhat/Comptroller/setters.ts index 6d9835c7b..d82a0f678 100644 --- a/tests/hardhat/Comptroller/setters.ts +++ b/tests/hardhat/Comptroller/setters.ts @@ -10,6 +10,7 @@ import { AccessControlManager, Comptroller, Comptroller__factory, + ILLiquidationManager, PoolRegistry, ResilientOracleInterface, RewardsDistributor, @@ -112,6 +113,34 @@ describe("setters", async () => { }); }); + describe("setLiquidationModule", async () => { + let newLiquidationManager: FakeContract; + + beforeEach(async () => { + newLiquidationManager = await smock.fake("ILLiquidationManager"); + }); + + it("reverts if access control manager does not allow the call", async () => { + accessControl.isAllowedToCall.whenCalledWith(owner.address, "setLiquidationModule(address)").returns(false); + await expect(comptroller.setLiquidationModule(newLiquidationManager.address)) + .to.be.revertedWithCustomError(comptroller, "Unauthorized") + .withArgs(owner.address, comptroller.address, "setLiquidationModule(address)"); + }); + + it("reverts if zero address is passed", async () => { + await expect(comptroller.setLiquidationModule(ethers.constants.AddressZero)).to.be.revertedWithCustomError( + comptroller, + "ZeroAddressNotAllowed", + ); + }); + + it("sets the liquidation manager and emits event", async () => { + await expect(comptroller.setLiquidationModule(newLiquidationManager.address)) + .to.emit(comptroller, "NewLiquidationManager") + .withArgs(ethers.constants.AddressZero, newLiquidationManager.address); + }); + }); + describe("setMarketLiquidationIncentive", async () => { const newLiquidationIncentive = convertToUnit("1.2", 18); it("reverts if access control manager does not allow the call", async () => { diff --git a/tests/hardhat/Fork/borrowAndRepayTest.ts b/tests/hardhat/Fork/borrowAndRepayTest.ts index 77402dec0..d3ef915f3 100644 --- a/tests/hardhat/Fork/borrowAndRepayTest.ts +++ b/tests/hardhat/Fork/borrowAndRepayTest.ts @@ -116,7 +116,7 @@ if (FORK) { await vTokenBeacon.upgradeTo(newVTokenImpl.address); // --- Deploy and Set New LiquidationManager --- - const LiquidationManagerFactory = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManagerFactory = await ethers.getContractFactory("ILLiquidationManager"); liquidationManager = await LiquidationManagerFactory.deploy(); await liquidationManager.deployed(); diff --git a/tests/hardhat/Fork/liquidation.ts b/tests/hardhat/Fork/liquidation.ts index e003f2e67..f4193a34d 100644 --- a/tests/hardhat/Fork/liquidation.ts +++ b/tests/hardhat/Fork/liquidation.ts @@ -137,7 +137,7 @@ if (FORK) { await vTokenBeacon.upgradeTo(newVTokenImpl.address); // --- Deploy and Set New LiquidationManager --- - const LiquidationManagerFactory = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManagerFactory = await ethers.getContractFactory("ILLiquidationManager"); liquidationManager = await LiquidationManagerFactory.deploy(); await liquidationManager.deployed(); diff --git a/tests/hardhat/Gateway/NativeTokenGateway.ts b/tests/hardhat/Gateway/NativeTokenGateway.ts index 42f59a9bd..578b99ac6 100644 --- a/tests/hardhat/Gateway/NativeTokenGateway.ts +++ b/tests/hardhat/Gateway/NativeTokenGateway.ts @@ -54,7 +54,7 @@ async function deployGateway(): Promise { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); const maxLoopsLimit = 150; diff --git a/tests/hardhat/Lens/PoolLens.ts b/tests/hardhat/Lens/PoolLens.ts index 710101871..6751e9064 100644 --- a/tests/hardhat/Lens/PoolLens.ts +++ b/tests/hardhat/Lens/PoolLens.ts @@ -108,7 +108,7 @@ for (const isTimeBased of [false, true]) { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); [comptroller1Proxy, comptroller2Proxy] = await Promise.all( diff --git a/tests/hardhat/Prime.ts b/tests/hardhat/Prime.ts index 297aad5ae..d4f268569 100644 --- a/tests/hardhat/Prime.ts +++ b/tests/hardhat/Prime.ts @@ -74,7 +74,7 @@ async function deployProtocol(): Promise { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); const maxLoopsLimit = 150; diff --git a/tests/hardhat/Rewards.ts b/tests/hardhat/Rewards.ts index b3d3f4439..01daf73f4 100644 --- a/tests/hardhat/Rewards.ts +++ b/tests/hardhat/Rewards.ts @@ -85,7 +85,7 @@ async function rewardsFixture(isTimeBased: boolean) { const Comptroller = await ethers.getContractFactory("Comptroller"); const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); comptrollerProxy = (await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ diff --git a/tests/integration/index.ts b/tests/integration/index.ts index f2e6cabc8..8a1af908d 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -52,7 +52,7 @@ const setupTest = deployments.createFixture(async ({ deployments, getNamedAccoun const pools = await PoolRegistry.callStatic.getAllPools(); const Comptroller = await ethers.getContractAt("Comptroller", pools[0].comptroller); - const LiquidationManager = await ethers.getContractFactory("LiquidationManager"); + const LiquidationManager = await ethers.getContractFactory("ILLiquidationManager"); const liquidationManager = await LiquidationManager.deploy(); const BNX = await ethers.getContract("MockBNX");