Skip to content

Conversation

@Debugger022
Copy link
Contributor

@Debugger022 Debugger022 commented Jul 9, 2025

Description

This PR implements the following features:-

[VEN-3321] - Add the concept of Liquidation Threshold to the core Pool on BNB.
[VEN-3322] - Dynamic Liquidation Incentive and Close Factor in core Pool on BNB.
[VEN-3323] - Define maximum Liquidation Incentive per seized asset in the core pool on BNB.
[VEN-233] - Repay logic improvisation

This pull request introduces significant enhancements and refactoring to the contracts, primarily to support dynamic liquidation incentives, improved health factor calculations, and integration with a new LiquidationManager contract. The changes include new interfaces and storage structures, updated function signatures, and expanded account health and liquidation logic.

Liquidation and Account Health Enhancements

  • Added new functions and updated existing ones in ComptrollerInterface.sol to support dynamic liquidation incentives and close factor, health factor calculations, and detailed account snapshots, including overloaded methods for seize token calculations.

LiquidationManager Integration

  • Added references to the new LiquidationManager contract in both storage and interface files, and exposed its address via the Comptroller contract.

Storage and Contract Refactoring

  • Deprecated old close factor and liquidation incentive variables, replacing them with new ones (__oldCloseFactorMantissaSlot, maxLiquidationIncentiveMantissa) and moved to ComptrollerV19Storage. Updated all relevant contract inheritance to use the new storage version. Also, removed the old setCloseFactor and setLiquidationIncentive functions as they will now be calculated dynamically using the liquidationManager.

Diamond Facet and Functionality Updates

  • Updated FacetBase and MarketFacet to use new health snapshot logic and ComptrollerLens integration, including new internal and external functions for health factor calculations and liquidation seize token calculations.

Repay logic Improvisation

  • Updated the repayAmount calculation in the VToken from this to this:
    vars.repayAmount = repayAmount >= vars.accountBorrows ? vars.accountBorrows : repayAmount;

@Debugger022 Debugger022 self-assigned this Jul 9, 2025
@Debugger022 Debugger022 changed the title [VEN-3321]: Liquidation Threshold and Improvements in Core Pool on BNB [VEN-3321]: Liquidation Threshold and repay Improvements in Core Pool on BNB Nov 27, 2025
* @notice Multiplier used to calculate the maximum repayAmount when liquidating a borrow (deprecated)
*/
uint256 public closeFactorMantissa;
uint256 public __oldCloseFactorMantissaSlot;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could make it private then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 181 to 187
function getHypotheticalHealthSnapshot(
address account,
VToken vTokenModify,
uint256 redeemTokens,
uint256 borrowAmount,
WeightFunction weightingStrategy
) external view returns (uint256 err, ComptrollerLensInterface.AccountSnapshot memory snapshot) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this external function to the policy facets to reduce the contract size across the other facets. The internal version can remain here in the FacetBase so it's still accessible to all facets that need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(
vars.oraclePrice,
// borrow effect: oraclePrice * borrowAmount
snapshot.borrows = mul_ScalarTruncateAddUInt(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required, but we could call this totalBorrows to keep the naming consistent with totalCollateral.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
function _finalizeSnapshot(AccountSnapshot memory snapshot) internal pure {
if (snapshot.totalCollateral > 0) {
snapshot.liquidationThresholdAvg = div_(snapshot.weightedCollateral * 1e18, snapshot.totalCollateral);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value could represent either the LT or the CF, depending on the WeightFunction being applied right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes !

Comment on lines +118 to 126
* @param liquidationIncentiveMantissa The liquidation incentive to apply
* @return (errorCode, number of vTokenCollateral tokens to be seized in a liquidation)
*/
function liquidateCalculateSeizeTokens(
address borrower,
address vTokenBorrowed,
address vTokenCollateral,
uint256 actualRepayAmount
uint256 actualRepayAmount,
uint256 liquidationIncentiveMantissa
) external view returns (uint256, uint256) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This liquidateCalculateSeizeTokens variant (without the borrower parameter) is intentionally kept for vBNB, since vBNB is non-upgradeable and cannot adopt the updated signature. I missed adding the explanatory comment earlier, please avoid making changes to this version and add a note similar to the one in the corresponding ComptrollerLens function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @param newMaxLiquidationIncentive The new max liquidation incentive, scaled by 1e18
* @return uint256 0=success, otherwise reverted
*/
function __setMarketMaxLiquidationIncentive(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the internal functions below the external ones to keep the function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SET_COLLATERAL_FACTOR_NO_EXISTS,
SET_COLLATERAL_FACTOR_VALIDATION,
SET_COLLATERAL_FACTOR_WITHOUT_PRICE,
SET_COLLATERAL_FACTOR_VALIDATION_LIQUIDATION_THRESHOLD,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add the new error code at the end of the enum to avoid breaking existing error-code ordering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 165 to +171
function liquidateBorrow(
address borrower,
uint repayAmount,
VTokenInterface vTokenCollateral
VTokenInterface vTokenCollateral,
ComptrollerLensInterface.AccountSnapshot memory snapshot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the liquidator provide the snapshot? They could pass an invalid or manipulated snapshot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The liquidateBorrow function can only be called from the liquidator contract by the liquidator. In this function, we calculate the snapshot internally, preventing the liquidator from passing an invalid snapshot.

* @param snapshot The account snapshot of the borrower
* @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount.
*/
function liquidateBorrowInternal(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need two variants of this function ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One variant keeps vBNB compatibility, while the other implements the dynamic liquidation logic.

}

// liquidateBorrowFresh emits borrow-specific logs on errors, so we don't need to
return liquidateBorrowFresh(msg.sender, borrower, repayAmount, vTokenCollateral, snapshot);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need two variants of this function ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One variant keeps vBNB compatibility, while the other implements the dynamic liquidation logic.

* @param actualRepayAmount The amount of vTokenBorrowed underlying to convert into vTokenCollateral tokens
* @return (errorCode, number of vTokenCollateral tokens to be seized in a liquidation)
*/
function liquidateVAICalculateSeizeTokens(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this calculation is only needed by the VAIController, why do we maintain two variants of liquidateVAICalculateSeizeTokens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @param healthFactor The health factor of the borrower
* @return incentive The dynamic liquidation incentive for the borrower, scaled by 1e18
*/
function getDynamicLiquidationIncentive(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variant of getDynamicLiquidationIncentive (the one that doesn’t take the borrower address) is currently used only in the Liquidator contract, but we need to account for eMode groups there as well. The only cases where eMode can safely be ignored are vBNB and VAI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Read the balances and exchange rate from the vToken
(oErr, vars.vTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(
(vars.err, vars.vTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the returned errorCode directly instead of storing it in vars. In that case, we can remove err from AccountSnapshotLocal entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/* We calculate the number of collateral tokens that will be seized */
(uint amountSeizeError, uint seizeTokens) = comptroller.liquidateCalculateSeizeTokens(
borrower,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider avoiding changes to legacy contracts so we can preserve their ABI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fred-venus fred-venus force-pushed the develop branch 6 times, most recently from f8786ac to d1378e1 Compare December 12, 2025 09:03
@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
contracts 73% 59%
contracts.Admin 88% 41%
contracts.Comptroller 100% 90%
contracts.Comptroller.Diamond 95% 61%
contracts.Comptroller.Diamond.facets 81% 68%
contracts.Comptroller.Diamond.interfaces 100% 100%
contracts.Comptroller.Types 100% 100%
contracts.Comptroller.legacy 100% 100%
contracts.Comptroller.legacy.Diamond 0% 0%
contracts.Comptroller.legacy.Diamond.facets 0% 0%
contracts.Comptroller.legacy.Diamond.interfaces 100% 100%
contracts.DelegateBorrowers 100% 89%
contracts.FlashLoan.interfaces 100% 100%
contracts.Governance 68% 45%
contracts.InterestRateModels 74% 59%
contracts.Lens 43% 38%
contracts.Liquidator 84% 59%
contracts.Oracle 100% 100%
contracts.PegStability 88% 84%
contracts.Swap 88% 57%
contracts.Swap.interfaces 100% 100%
contracts.Swap.lib 81% 53%
contracts.Tokens 100% 100%
contracts.Tokens.Prime 96% 72%
contracts.Tokens.Prime.Interfaces 100% 100%
contracts.Tokens.Prime.libs 90% 76%
contracts.Tokens.VAI 82% 53%
contracts.Tokens.VRT 20% 9%
contracts.Tokens.VTokens 69% 51%
contracts.Tokens.VTokens.legacy 0% 0%
contracts.Tokens.VTokens.legacy.Utils 0% 0%
contracts.Tokens.XVS 19% 8%
contracts.Tokens.test 100% 100%
contracts.Utils 52% 31%
contracts.VAIVault 50% 45%
contracts.VRTVault 49% 36%
contracts.XVSVault 63% 50%
contracts.external 100% 100%
contracts.lib 89% 71%
Summary 56% (3725 / 6596) 42% (1407 / 3322)

address borrower,
address vToken
) external view returns (uint256 incentive) {
uint256 effectiveLiquidationIncentive = this.getEffectiveLiquidationIncentive(borrower, vToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make getEffectiveLiquidationIncentive an public function so that we can avoid the use of this. We don't follow this pattern in our contracts. Use of this is more expensive.


/* The borrower must have shortfall in order to be liquidatable */
(Error err, , uint256 shortfall) = getHypotheticalAccountLiquidityInternal(
(uint256 err, ComptrollerLensInterface.AccountSnapshot memory snapshot) = this.getHypotheticalHealthSnapshot(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here we can avoid the use of this

* @dev Calculates incentive based on provided health factor and liquidation threshold average,
* without reading borrower's account state from storage
* @param vToken The address of the vToken to be seized
* @param borrower The address of the borrower
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can expand the comment to include why the borrower parameter is needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants