Skip to content

Commit

Permalink
Merge pull request #305 from VenusProtocol/VEN-1930
Browse files Browse the repository at this point in the history
[VEN-1930]: forced liquidation
  • Loading branch information
chechu authored Oct 18, 2023
2 parents f608b06 + 0973a15 commit 66d1ea6
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 17 deletions.
38 changes: 23 additions & 15 deletions contracts/Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ contract Comptroller is
/// @notice Emitted when a market is supported
event MarketSupported(VToken vToken);

/// @notice Emitted when forced liquidation is enabled or disabled for a market
event IsForcedLiquidationEnabledUpdated(address indexed vToken, bool enable);

/// @notice Thrown when collateral factor exceeds the upper bound
error InvalidCollateralFactor();

Expand Down Expand Up @@ -462,8 +465,8 @@ contract Comptroller is

uint256 borrowBalance = VToken(vTokenBorrowed).borrowBalanceStored(borrower);

/* Allow accounts to be liquidated if the market is deprecated or it is a forced liquidation */
if (skipLiquidityCheck || isDeprecated(VToken(vTokenBorrowed))) {
/* Allow accounts to be liquidated if it is a forced liquidation */
if (skipLiquidityCheck || isForcedLiquidationEnabled[vTokenBorrowed]) {
if (repayAmount > borrowBalance) {
revert TooMuchRepay();
}
Expand Down Expand Up @@ -983,6 +986,24 @@ contract Comptroller is
_setMaxLoopsLimit(limit);
}

/**
* @notice Enables forced liquidations for a market. If forced liquidation is enabled,
* borrows in the market may be liquidated regardless of the account liquidity
* @param vTokenBorrowed Borrowed vToken
* @param enable Whether to enable forced liquidations
*/
function setForcedLiquidation(address vTokenBorrowed, bool enable) external {
_checkAccessAllowed("setForcedLiquidation(address,bool)");
ensureNonzeroAddress(vTokenBorrowed);

if (!markets[vTokenBorrowed].isListed) {
revert MarketNotListed(vTokenBorrowed);
}

isForcedLiquidationEnabled[vTokenBorrowed] = enable;
emit IsForcedLiquidationEnabledUpdated(vTokenBorrowed, enable);
}

/**
* @notice Determine the current account liquidity with respect to liquidation threshold requirements
* @dev The interface of this function is intentionally kept compatible with Compound and Venus Core
Expand Down Expand Up @@ -1182,19 +1203,6 @@ contract Comptroller is
return _actionPaused[market][action];
}

/**
* @notice Check if a vToken market has been deprecated
* @dev All borrows in a deprecated vToken market can be immediately liquidated
* @param vToken The market to check if deprecated
* @return deprecated True if the given vToken market has been deprecated
*/
function isDeprecated(VToken vToken) public view returns (bool) {
return
markets[address(vToken)].collateralFactorMantissa == 0 &&
actionPaused(address(vToken), Action.BORROW) &&
vToken.reserveFactorMantissa() == MANTISSA_ONE;
}

/**
* @notice Add the market to the borrower's "assets in" for liquidity calculations
* @param vToken The market to enter
Expand Down
5 changes: 4 additions & 1 deletion contracts/ComptrollerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ contract ComptrollerStorage {
// Used to check if rewards distributor is added
mapping(address => bool) internal rewardsDistributorExists;

/// @notice Flag indicating whether forced liquidation enabled for a market
mapping(address => bool) public isForcedLiquidationEnabled;

uint256 internal constant NO_ERROR = 0;

// closeFactorMantissa must be strictly greater than this value
Expand All @@ -123,5 +126,5 @@ contract ComptrollerStorage {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
uint256[49] private __gap;
}
108 changes: 107 additions & 1 deletion tests/hardhat/Comptroller/liquidateAccountTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe("liquidateAccount", () => {
let OMG: FakeContract<VToken>;
let ZRX: FakeContract<VToken>;
let BAT: FakeContract<VToken>;
let accessControl: FakeContract<AccessControlManager>;
const maxLoopsLimit = 150;

type LiquidateAccountFixture = {
Expand Down Expand Up @@ -113,7 +114,7 @@ describe("liquidateAccount", () => {
[, liquidator, user] = await ethers.getSigners();
const contracts = await loadFixture(liquidateAccountFixture);
configure(contracts);
({ comptroller, OMG, ZRX, BAT } = contracts);
({ comptroller, OMG, ZRX, BAT, accessControl } = contracts);
});

describe("collateral to borrows ratio requirements", async () => {
Expand Down Expand Up @@ -342,4 +343,109 @@ describe("liquidateAccount", () => {
);
});
});

describe("setForcedLiquidation", async () => {
it("fails if asset is not listed", async () => {
const someVToken = await smock.fake<VToken>("VToken");
await expect(comptroller.setForcedLiquidation(someVToken.address, true)).to.be.revertedWithCustomError(
comptroller,
"MarketNotListed",
);
});

it("fails if ACM does not allow the call", async () => {
accessControl.isAllowedToCall.returns(false);
await expect(comptroller.setForcedLiquidation(OMG.address, true)).to.be.revertedWithCustomError(
comptroller,
"Unauthorized",
);
accessControl.isAllowedToCall.returns(true);
});

it("sets forced liquidation", async () => {
await comptroller.setForcedLiquidation(OMG.address, true);
expect(await comptroller.isForcedLiquidationEnabled(OMG.address)).to.be.true;

await comptroller.setForcedLiquidation(OMG.address, false);
expect(await comptroller.isForcedLiquidationEnabled(OMG.address)).to.be.false;
});

it("emits IsForcedLiquidationEnabledUpdated event", async () => {
const tx1 = await comptroller.setForcedLiquidation(OMG.address, true);
await expect(tx1).to.emit(comptroller, "IsForcedLiquidationEnabledUpdated").withArgs(OMG.address, true);

const tx2 = await comptroller.setForcedLiquidation(OMG.address, false);
await expect(tx2).to.emit(comptroller, "IsForcedLiquidationEnabledUpdated").withArgs(OMG.address, false);
});
});

describe("preLiquidateHook", async () => {
let accounts: SignerWithAddress[];

beforeEach(async () => {
accounts = await ethers.getSigners();
await comptroller.setForcedLiquidation(OMG.address, true);
});

it("reverts if borrowed market is not listed", async () => {
const someVToken = await smock.fake<VToken>("VToken");
await expect(
comptroller.preLiquidateHook(someVToken.address, OMG.address, accounts[0].address, parseUnits("1", 18), false),
).to.be.revertedWithCustomError(comptroller, "MarketNotListed");
});

it("reverts if collateral market is not listed", async () => {
const someVToken = await smock.fake<VToken>("VToken");
await expect(
comptroller.preLiquidateHook(OMG.address, someVToken.address, accounts[0].address, parseUnits("1", 18), false),
).to.be.revertedWithCustomError(comptroller, "MarketNotListed");
});

it("allows liquidations without shortfall", async () => {
OMG.borrowBalanceStored.returns(parseUnits("100", 18));
await comptroller.callStatic.preLiquidateHook(
OMG.address,
OMG.address,
accounts[0].address,
parseUnits("1", 18),
true,
);
});

it("allows to repay 100% of the borrow", async () => {
OMG.borrowBalanceStored.returns(parseUnits("1", 18));
await comptroller.callStatic.preLiquidateHook(
OMG.address,
OMG.address,
accounts[0].address,
parseUnits("1", 18),
false,
);
});

it("fails with TOO_MUCH_REPAY if trying to repay > borrowed amount", async () => {
OMG.borrowBalanceStored.returns(parseUnits("0.99", 18));
const tx = comptroller.callStatic.preLiquidateHook(
OMG.address,
OMG.address,
accounts[0].address,
parseUnits("1", 18),
false,
);
await expect(tx).to.be.revertedWithCustomError(comptroller, "TooMuchRepay");
});

it("checks the shortfall if isForcedLiquidationEnabled is set back to false", async () => {
await comptroller.setForcedLiquidation(OMG.address, false);
OMG.borrowBalanceStored.returns(parseUnits("100", 18));
const tx = comptroller.callStatic.preLiquidateHook(
OMG.address,
OMG.address,
accounts[0].address,
parseUnits("1", 18),
false,
);
await expect(tx).to.be.revertedWithCustomError(comptroller, "MinimalCollateralViolated");
});
});
});

0 comments on commit 66d1ea6

Please sign in to comment.