Skip to content

Commit

Permalink
feat: switch from loss liquidators to loss policies
Browse files Browse the repository at this point in the history
This commit also contains minor changes to allow migration from v3.0.x to v3.1.x:
- `CreditConfiguratorV3`'s constructor accepts `acl` as argument
- `CreditFacadeV3`'s constructor accepts `lossPolicy` as argument
- `lossPolicy` is no longer migrated during credit facade replacement
  • Loading branch information
lekhovitsky committed Jan 5, 2025
1 parent b8a2c07 commit 06d77c3
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 118 deletions.
25 changes: 10 additions & 15 deletions contracts/credit/CreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLTrait, SanityCheckTra
}

/// @notice Constructor
/// @param _acl ACL contract address
/// @param _creditManager Credit manager to connect to
/// @dev Copies allowed adaprters from the currently connected configurator
constructor(address _creditManager) ACLTrait(ACLTrait(CreditManagerV3(_creditManager).pool()).acl()) {
constructor(address _acl, address _creditManager) ACLTrait(_acl) {
creditManager = _creditManager; // I:[CC-1]
underlying = CreditManagerV3(_creditManager).underlying(); // I:[CC-1]

Expand Down Expand Up @@ -481,9 +482,6 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLTrait, SanityCheckTra
(uint128 minDebt, uint128 maxDebt) = prevCreditFacade.debtLimits();
_setLimits({minDebt: minDebt, maxDebt: maxDebt}); // I:[CC-22]

address lossLiquidator = prevCreditFacade.lossLiquidator();
if (lossLiquidator != address(0)) _setLossLiquidator(lossLiquidator); // I:[CC-22]

_migrateForbiddenTokens(prevCreditFacade.forbiddenTokenMask()); // I:[CC-22C]

if (prevCreditFacade.expirable() && CreditFacadeV3(newCreditFacade).expirable()) {
Expand Down Expand Up @@ -600,25 +598,22 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLTrait, SanityCheckTra
emit SetMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // I:[CC-24]
}

/// @notice Sets the new loss liquidator which can enforce policies on how liquidations with loss are performed
/// @param newLossLiquidator New loss liquidator, must be a contract
function setLossLiquidator(address newLossLiquidator)
/// @notice Sets the new loss policy which control which lossy liquidations should be allowed
/// @param newLossPolicy New loss policy, must be a contract
function setLossPolicy(address newLossPolicy)
external
override
configuratorOnly // I:[CC-2]
nonZeroAddress(newLossLiquidator) // I:[CC-26]
nonZeroAddress(newLossPolicy) // I:[CC-26]
{
_setLossLiquidator(newLossLiquidator); // I:[CC-26]
}
if (newLossPolicy.code.length == 0) revert AddressIsNotContractException(newLossPolicy); // I:[CC-26]

/// @dev `setLossLiquidator` implementation
function _setLossLiquidator(address newLossLiquidator) internal {
CreditFacadeV3 cf = CreditFacadeV3(creditFacade());

if (cf.lossLiquidator() == newLossLiquidator) return;
if (cf.lossPolicy() == newLossPolicy) return;

cf.setLossLiquidator(newLossLiquidator); // I:[CC-26]
emit SetLossLiquidator(newLossLiquidator); // I:[CC-26]
cf.setLossPolicy(newLossPolicy); // I:[CC-26]
emit SetLossPolicy(newLossPolicy); // I:[CC-26]
}

/// @notice Sets a new credit facade expiration date
Expand Down
77 changes: 44 additions & 33 deletions contracts/credit/CreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import "../interfaces/IExceptions.sol";
import {IPoolV3} from "../interfaces/IPoolV3.sol";
import {IPriceOracleV3, PriceUpdate} from "../interfaces/IPriceOracleV3.sol";
import {IDegenNFT} from "../interfaces/base/IDegenNFT.sol";
import {ILossPolicy} from "../interfaces/base/ILossPolicy.sol";
import {IPhantomToken, IPhantomTokenWithdrawer} from "../interfaces/base/IPhantomToken.sol";
import {IWETH} from "../interfaces/external/IWETH.sol";

Expand Down Expand Up @@ -117,7 +118,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
uint256 public override forbiddenTokenMask;

/// @notice Contract that enforces a policy on how liquidations with loss are performed
address public override lossLiquidator;
address public override lossPolicy;

/// @dev Ensures that function caller is credit configurator
modifier creditConfiguratorOnly() {
Expand All @@ -132,12 +133,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
}

/// @dev Ensures that function can't be called when the contract is paused, unless
/// caller is an approved emergency liquidator or the loss liquidator
/// caller is an approved emergency liquidator
modifier whenNotPausedOrEmergency() {
require(
!paused() || _hasRole("EMERGENCY_LIQUIDATOR", msg.sender) || msg.sender == lossLiquidator,
"Pausable: paused"
);
require(!paused() || _hasRole("EMERGENCY_LIQUIDATOR", msg.sender), "Pausable: paused");
_;
}

Expand All @@ -156,6 +154,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// @notice Constructor
/// @param _acl ACL contract address
/// @param _creditManager Credit manager to connect this facade to
/// @param _lossPolicy Loss policy address
/// @param _botList Bot list address
/// @param _weth WETH token address
/// @param _degenNFT Degen NFT address or `address(0)`
Expand All @@ -164,12 +163,14 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
constructor(
address _acl,
address _creditManager,
address _lossPolicy,
address _botList,
address _weth,
address _degenNFT,
bool _expirable
) ACLTrait(_acl) nonZeroAddress(_botList) {
) ACLTrait(_acl) nonZeroAddress(_lossPolicy) nonZeroAddress(_botList) {
creditManager = _creditManager; // U:[FA-1]
lossPolicy = _lossPolicy; // U:[FA-1]
botList = _botList; // U:[FA-1]
weth = _weth; // U:[FA-1]
degenNFT = _degenNFT; // U:[FA-1]
Expand Down Expand Up @@ -266,10 +267,11 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// @notice Liquidates a credit account
/// - Updates price feeds before running all computations if such call is present in the multicall
/// - Evaluates account's collateral and debt to determine whether liquidated account is unhealthy or expired
/// - If account has bad debt, liquidation is only allowed when it doesn't violate the loss policy,
/// further borrowing through the facade is forbidden in this case
/// - Performs a multicall (only `addCollateral`, `withdrawCollateral` and adapter calls are allowed)
/// - Liquidates a credit account in the credit manager, which repays debt to the pool, removes quotas, and
/// transfers underlying to the liquidator
/// - If pool incurs a loss on liquidation, further borrowing through the facade is forbidden
/// @notice The function computes account’s total value (oracle value of enabled tokens), discounts it by liquidator’s
/// premium, and uses this value to compute funds due to the pool and owner.
/// Debt to the pool must be repaid in underlying, while funds due to owner might be covered by underlying
Expand All @@ -282,21 +284,24 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// @param creditAccount Account to liquidate
/// @param to Address to transfer underlying left after liquidation
/// @param calls List of calls to perform before liquidating the account
/// @return reportedLoss Loss incurred on liquidation, if any
/// @dev If liquidation incurs loss, reverts if caller is not the loss liquidator
/// @dev If facade is paused, reverts if caller is not an approved emergency liquidator or the loss liquidator
/// @param lossPolicyData Additional data to pass to the loss policy contract
/// @dev If facade is paused, reverts if caller is not an approved emergency liquidator
/// @dev Reverts if `creditAccount` is not opened in connected credit manager
/// @dev Reverts if account has no debt or is neither unhealthy nor expired
/// @dev Reverts if remaining token balances increase during the multicall
/// @dev Liquidator can fully seize non-enabled tokens so it's highly recommended to avoid holding them.
/// Since adapter calls are allowed, unclaimed rewards from integrated protocols are also at risk;
/// bots can be used to claim and withdraw them.
function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls)
external
function liquidateCreditAccount(
address creditAccount,
address to,
MultiCall[] calldata calls,
bytes memory lossPolicyData
)
public
override
whenNotPausedOrEmergency // U:[FA-2,12]
nonReentrant // U:[FA-4]
returns (uint256 reportedLoss)
{
uint256 flags = LIQUIDATE_CREDIT_ACCOUNT_PERMISSIONS | SKIP_COLLATERAL_CHECK_FLAG;
if (
Expand All @@ -308,6 +313,12 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
}

(CollateralDebtData memory collateralDebtData, bool isUnhealthy) = _revertIfNotLiquidatable(creditAccount); // U:[FA-13,14]
if (isUnhealthy && _hasBadDebt(collateralDebtData)) {
if (!ILossPolicy(lossPolicy).isLiquidatable(creditAccount, msg.sender, lossPolicyData)) {
revert CreditAccountNotLiquidatableWithLossException(); // U:[FA-17]
}
maxDebtPerBlockMultiplier = 0; // U:[FA-17]
}

BalanceWithMask[] memory initialBalances = BalancesLogic.storeBalances({
creditAccount: creditAccount,
Expand All @@ -327,23 +338,19 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT

collateralDebtData.enabledTokensMask = collateralDebtData.enabledTokensMask.enable(UNDERLYING_TOKEN_MASK); // U:[FA-14]

uint256 remainingFunds;
(remainingFunds, reportedLoss) = ICreditManagerV3(creditManager).liquidateCreditAccount({
(uint256 remainingFunds,) = ICreditManagerV3(creditManager).liquidateCreditAccount({
creditAccount: creditAccount,
collateralDebtData: collateralDebtData,
to: to,
isExpired: !isUnhealthy
}); // U:[FA-14]

emit LiquidateCreditAccount(creditAccount, msg.sender, to, remainingFunds); // U:[FA-14]
}

if (reportedLoss != 0) {
maxDebtPerBlockMultiplier = 0; // U:[FA-17]

if (msg.sender != lossLiquidator) {
revert CallerNotLossLiquidatorException(); // U:[FA-17]
}
}
/// @dev Deprecated method that preserves liquidation signature from v3.0.x by using empty loss policy data
function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls) external override {
liquidateCreditAccount(creditAccount, to, calls, "");
}

/// @notice Partially liquidates credit account's debt in exchange for discounted collateral
Expand All @@ -361,7 +368,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// @param to Account to withdraw seized `token` to
/// @param priceUpdates On-demand price feed updates to apply before calculations, see `PriceUpdate` for details
/// @return seizedAmount Amount of `token` seized
/// @dev If facade is paused, reverts if caller is not an approved emergency liquidator or the loss liquidator
/// @dev If facade is paused, reverts if caller is not an approved emergency liquidator
/// @dev Reverts if `creditAccount` is not opened in connected credit manager
/// @dev Reverts if account has no debt or is neither unhealthy nor expired
/// @dev Reverts if `token` is underlying or if `token` is a phantom token and its `depositedToken` is underlying
Expand Down Expand Up @@ -847,19 +854,15 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
totalBorrowedInBlock = type(uint128).max; // U:[FA-49]
}

/// @notice Sets the new loss liquidator
/// @param newLossLiquidator New loss liquidator
/// @notice Sets the new loss policy
/// @param newLossPolicy New loss policy
/// @dev Reverts if caller is not credit configurator
/// @dev Reverts if `newLossLiquidator` is not a contract
function setLossLiquidator(address newLossLiquidator)
function setLossPolicy(address newLossPolicy)
external
override
creditConfiguratorOnly // U:[FA-6]
{
if (newLossLiquidator.code.length == 0) {
revert AddressIsNotContractException(newLossLiquidator); // U:[FA-51]
}
lossLiquidator = newLossLiquidator; // U:[FA-51]
lossPolicy = newLossPolicy; // U:[FA-51]
}

/// @notice Changes token's forbidden status
Expand All @@ -880,7 +883,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT

/// @notice Pauses contract, can only be called by an account with pausable admin role
/// @dev Pause blocks all user entrypoints to the contract.
/// Liquidations remain open only to emergency and loss liquidators.
/// Liquidations remain open only to emergency liquidators.
/// @dev Reverts if contract is already paused
function pause() external override pausableAdminsOnly {
_pause();
Expand Down Expand Up @@ -957,6 +960,14 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
if (cdd.debt == 0 || !isUnhealthy && !_isExpired()) revert CreditAccountNotLiquidatableException(); // U:[FA-13]
}

/// @dev Whether account's total value (minus liquidator's premium) is below its outstanding debt
function _hasBadDebt(CollateralDebtData memory cdd) internal view returns (bool) {
(,, uint16 liquidationDiscount,,) = ICreditManagerV3(creditManager).fees();
// NOTE: this formula does not account for transfer fees for simplicity, so there might be edge
// cases when liquidation bypasses the loss policy, however loss size is bounded by the fee
return cdd.totalValue * liquidationDiscount < (cdd.debt + cdd.accruedInterest) * PERCENTAGE_FACTOR;
}

/// @dev Calculates and returns partial liquidation payment amounts:
/// - amount of underlying that should go towards repaying debt
/// - amount of underlying that should go towards liquidation fees
Expand Down
6 changes: 3 additions & 3 deletions contracts/interfaces/ICreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ interface ICreditConfiguratorV3Events {
/// @notice Emitted when a new max debt per block multiplier is set
event SetMaxDebtPerBlockMultiplier(uint8 maxDebtPerBlockMultiplier);

/// @notice Emitted when new loss liquidator is set
event SetLossLiquidator(address indexed liquidator);
/// @notice Emitted when new loss policy is set
event SetLossPolicy(address indexed lossPolicy);

/// @notice Emitted when a new expiration timestamp is set in the credit facade
event SetExpirationDate(uint40 expirationDate);
Expand Down Expand Up @@ -154,7 +154,7 @@ interface ICreditConfiguratorV3 is IVersion, IACLTrait, ICreditConfiguratorV3Eve

function forbidBorrowing() external;

function setLossLiquidator(address newLossLiquidator) external;
function setLossPolicy(address newLossPolicy) external;

function setExpirationDate(uint40 newExpirationDate) external;
}
15 changes: 10 additions & 5 deletions contracts/interfaces/ICreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ interface ICreditFacadeV3 is IVersion, IACLTrait, ICreditFacadeV3Events {

function debtLimits() external view returns (uint128 minDebt, uint128 maxDebt);

function lossLiquidator() external view returns (address);
function lossPolicy() external view returns (address);

function forbiddenTokenMask() external view returns (uint256);

Expand All @@ -116,9 +116,14 @@ interface ICreditFacadeV3 is IVersion, IACLTrait, ICreditFacadeV3Events {

function closeCreditAccount(address creditAccount, MultiCall[] calldata calls) external payable;

function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls)
external
returns (uint256 reportedLoss);
function liquidateCreditAccount(
address creditAccount,
address to,
MultiCall[] calldata calls,
bytes memory lossPolicyData
) external;

function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls) external;

function partiallyLiquidateCreditAccount(
address creditAccount,
Expand All @@ -141,7 +146,7 @@ interface ICreditFacadeV3 is IVersion, IACLTrait, ICreditFacadeV3Events {

function setDebtLimits(uint128 newMinDebt, uint128 newMaxDebt, uint8 newMaxDebtPerBlockMultiplier) external;

function setLossLiquidator(address newLossLiquidator) external;
function setLossPolicy(address newLossPolicy) external;

function setTokenAllowance(address token, AllowanceAction allowance) external;

Expand Down
6 changes: 3 additions & 3 deletions contracts/interfaces/IExceptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ error UnknownMethodException(bytes4 selector);
/// @notice Thrown if a liquidator tries to liquidate an account with a health factor above 1
error CreditAccountNotLiquidatableException();

/// @notice Thrown if a liquidator tries to liquidate an account with loss but violates the loss policy
error CreditAccountNotLiquidatableWithLossException();

/// @notice Thrown if too much new debt was taken within a single block
error BorrowedBlockLimitException();

Expand Down Expand Up @@ -278,9 +281,6 @@ error CallerNotExecutorException();
/// @notice Thrown on attempting to call an access restricted function not as veto admin
error CallerNotVetoAdminException();

/// @notice Thrown on attempting to perform liquidation with loss not through the loss liquidator contract
error CallerNotLossLiquidatorException();

// -------- //
// BOT LIST //
// -------- //
Expand Down
22 changes: 22 additions & 0 deletions contracts/interfaces/base/ILossPolicy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2024.
pragma solidity ^0.8.17;

import {IVersion} from "./IVersion.sol";

/// @notice Loss policy dictates the conditions under which a liquidation with bad debt can proceed.
/// For example, it can restrict such liquidations to only be performed by whitelisted accounts that
/// can return premium to the DAO to recover part of the losses, or prevent liquidations of an asset
/// whose market price drops for a short period of time while its fundamental value doesn't change.
interface ILossPolicy is IVersion {
/// @notice Whether `creditAccount` can be liquidated with loss by `caller`, `data` is an optional field
/// that can be used to pass some off-chain data specific to the loss policy implementation
function isLiquidatable(address creditAccount, address caller, bytes calldata data) external returns (bool);

/// @notice Emergency function which forces `isLiquidatable` to always return `false`
function disable() external;

/// @notice Emergency function which forces `isLiquidatable` to always return `true`
function enable() external;
}
Loading

0 comments on commit 06d77c3

Please sign in to comment.