Skip to content

Commit

Permalink
feat: loss liquidator (#285)
Browse files Browse the repository at this point in the history
Lossy liquidations no longer pause credit facade.
Instead, they must go through a contract that can enforce various policies on them.
For more context, an early implementation can be found in the governance repo.

`liquidateCreditAccount` now returns reported loss.

Access modifiers of some functions are revised.
  • Loading branch information
lekhovitsky authored Sep 1, 2024
1 parent 603b94f commit 28d3790
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 244 deletions.
42 changes: 14 additions & 28 deletions contracts/credit/CreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,7 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ControlledTrait, SanityC
(uint128 minDebt, uint128 maxDebt) = prevCreditFacade.debtLimits();
_setLimits({minDebt: minDebt, maxDebt: maxDebt}); // I:[CC-22]

(, uint128 maxCumulativeLoss) = prevCreditFacade.lossParams();
_setMaxCumulativeLoss(maxCumulativeLoss); // I:[CC-22]
_setLossLiquidator(prevCreditFacade.lossLiquidator()); // I:[CC-22]

_migrateEmergencyLiquidators(prevCreditFacade); // I:[CC-22C]

Expand Down Expand Up @@ -638,37 +637,24 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ControlledTrait, SanityC
emit SetMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // I:[CC-1A,24]
}

/// @notice Sets the new maximum cumulative loss from bad debt liquidations
/// @param newMaxCumulativeLoss New max cumulative lossd
function setMaxCumulativeLoss(uint128 newMaxCumulativeLoss)
/// @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 or zero address
function setLossLiquidator(address newLossLiquidator)
external
override
controllerOrConfiguratorOnly // I:[CC-2B]
configuratorOnly // I:[CC-2]
{
_setMaxCumulativeLoss(newMaxCumulativeLoss); // I:[CC-31]
_setLossLiquidator(newLossLiquidator); // I:[CC-26]
}

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

(, uint128 maxCumulativeLossCurrent) = cf.lossParams(); // I:[CC-31]
if (_maxCumulativeLoss == maxCumulativeLossCurrent) return;
if (cf.lossLiquidator() == newLossLiquidator) return;

cf.setCumulativeLossParams(_maxCumulativeLoss, false); // I:[CC-31]
emit SetMaxCumulativeLoss(_maxCumulativeLoss); // I:[CC-31]
}

/// @notice Resets the current cumulative loss from bad debt liquidations to zero
function resetCumulativeLoss()
external
override
controllerOrConfiguratorOnly // I:[CC-2B]
{
CreditFacadeV3 cf = CreditFacadeV3(creditFacade());
(, uint128 maxCumulativeLossCurrent) = cf.lossParams(); // I:[CC-32]
cf.setCumulativeLossParams(maxCumulativeLossCurrent, true); // I:[CC-32]
emit ResetCumulativeLoss(); // I:[CC-32]
cf.setLossLiquidator(newLossLiquidator); // I:[CC-26]
emit SetLossLiquidator(newLossLiquidator); // I:[CC-26]
}

/// @notice Sets a new credit facade expiration timestamp
Expand Down Expand Up @@ -710,7 +696,7 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ControlledTrait, SanityC
function _addEmergencyLiquidator(address liquidator) internal {
CreditFacadeV3 cf = CreditFacadeV3(creditFacade());

if (cf.canLiquidateWhilePaused(liquidator)) return;
if (cf.isEmergencyLiquidator(liquidator)) return;

cf.setEmergencyLiquidator(liquidator, AllowanceAction.ALLOW); // I:[CC-27]
emit AddEmergencyLiquidator(liquidator); // I:[CC-27]
Expand All @@ -721,11 +707,11 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ControlledTrait, SanityC
function removeEmergencyLiquidator(address liquidator)
external
override
configuratorOnly // I:[CC-2]
controllerOrConfiguratorOnly // I:[CC-2B]
{
CreditFacadeV3 cf = CreditFacadeV3(creditFacade());

if (cf.canLiquidateWhilePaused(liquidator)) {
if (cf.isEmergencyLiquidator(liquidator)) {
cf.setEmergencyLiquidator(liquidator, AllowanceAction.FORBID); // I:[CC-28]
emit RemoveEmergencyLiquidator(liquidator); // I:[CC-28]
}
Expand Down
64 changes: 32 additions & 32 deletions contracts/credit/CreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@ import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
// INTERFACES
import {IBotListV3} from "../interfaces/IBotListV3.sol";
import {AllowanceAction} from "../interfaces/ICreditConfiguratorV3.sol";
import {
CumulativeLossParams,
DebtLimits,
FullCheckParams,
ICreditFacadeV3,
MultiCall
} from "../interfaces/ICreditFacadeV3.sol";
import {DebtLimits, FullCheckParams, ICreditFacadeV3, MultiCall} from "../interfaces/ICreditFacadeV3.sol";
import "../interfaces/ICreditFacadeV3Multicall.sol";
import {
CollateralCalcTask,
Expand Down Expand Up @@ -61,10 +55,12 @@ import {SanityCheckTrait} from "../traits/SanityCheckTrait.sol";
/// @notice Users can also let external bots manage their accounts via `botMulticall`. Bots can be relatively general,
/// the facade only ensures that they can do no harm to the protocol by running the collateral check after the
/// multicall and checking the permissions given to them by users. See `BotListV3` for additional details.
/// @notice Credit facade implements a few safeguards on top of those present in the credit manager, including debt and
/// quota size validation, pausing on large protocol losses, Degen NFT whitelist mode, and forbidden tokens
/// (they count towards account value, but having them enabled as collateral restricts available actions and
/// activates a safer version of collateral check).
/// @notice Credit facade implements a few safeguards on top of those present in the credit manager, including
/// - debt and quota size validation
/// - degen NFT whitelist mode
/// - policies on how liquidations with loss are performed
/// - forbidden tokens (they count towards account value, but having them enabled as collateral restricts allowed
/// actions and triggers a safer version of collateral check, incentivizing users to decrease exposure to them).
contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardTrait, SanityCheckTrait {
using Address for address;
using BitMask for uint256;
Expand Down Expand Up @@ -119,8 +115,8 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// @notice Bit mask encoding a set of forbidden tokens
uint256 public override forbiddenTokenMask;

/// @notice Info on bad debt liquidation losses packed into a single slot
CumulativeLossParams public override lossParams;
/// @notice Contract that enforces a policy on how liquidations with loss are performed
address public override lossLiquidator;

/// @dev Set of emergency liquidators
EnumerableSet.AddressSet internal _emergencyLiquidatorsSet;
Expand All @@ -137,9 +133,13 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
_;
}

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

Expand Down Expand Up @@ -177,7 +177,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
}

/// @notice Whether `addr` is an approved emergency liquidator
function canLiquidateWhilePaused(address addr) public view override returns (bool) {
function isEmergencyLiquidator(address addr) public view override returns (bool) {
return _emergencyLiquidatorsSet.contains(addr);
}

Expand Down Expand Up @@ -277,7 +277,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
/// - 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
/// - If cumulative loss from bad debt liquidations exceeds the threshold, the facade is paused
/// @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 @@ -290,7 +289,9 @@ 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
/// @dev When the credit facade is paused, reverts if caller is not an approved emergency liquidator
/// @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
/// @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
Expand All @@ -302,6 +303,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
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 Down Expand Up @@ -332,7 +334,8 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT

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

(uint256 remainingFunds, uint256 reportedLoss) = ICreditManagerV3(creditManager).liquidateCreditAccount({
uint256 remainingFunds;
(remainingFunds, reportedLoss) = ICreditManagerV3(creditManager).liquidateCreditAccount({
creditAccount: creditAccount,
collateralDebtData: collateralDebtData,
to: to,
Expand All @@ -344,12 +347,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
if (reportedLoss != 0) {
maxDebtPerBlockMultiplier = 0; // U:[FA-17]

// both cast and addition are safe because amounts are of much smaller scale
lossParams.currentCumulativeLoss += uint128(reportedLoss); // U:[FA-17]

// can't pause an already paused contract
if (!paused() && lossParams.currentCumulativeLoss > lossParams.maxCumulativeLoss) {
_pause(); // U:[FA-17]
address lossLiquidator_ = lossLiquidator;
if (lossLiquidator_ != address(0) && msg.sender != lossLiquidator_) {
revert CallerNotLossLiquidatorException(); // U:[FA-17]
}
}
}
Expand Down Expand Up @@ -804,19 +804,19 @@ contract CreditFacadeV3 is ICreditFacadeV3, Pausable, ACLTrait, ReentrancyGuardT
totalBorrowedInBlock = type(uint128).max; // U:[FA-49]
}

/// @notice Sets the new max cumulative loss
/// @param newMaxCumulativeLoss New max cumulative loss
/// @param resetCumulativeLoss Whether to reset the current cumulative loss to zero
/// @notice Sets the new loss liquidator
/// @param newLossLiquidator New loss liquidator
/// @dev Reverts if caller is not credit configurator
function setCumulativeLossParams(uint128 newMaxCumulativeLoss, bool resetCumulativeLoss)
/// @dev Reverts if `newLossLiquidator` is not a contract, unless it's zero address
function setLossLiquidator(address newLossLiquidator)
external
override
creditConfiguratorOnly // U:[FA-6]
{
lossParams.maxCumulativeLoss = newMaxCumulativeLoss; // U:[FA-51]
if (resetCumulativeLoss) {
lossParams.currentCumulativeLoss = 0; // U:[FA-51]
if (newLossLiquidator != address(0) && newLossLiquidator.code.length == 0) {
revert AddressIsNotContractException(newLossLiquidator); // U:[FA-51]
}
lossLiquidator = newLossLiquidator; // U:[FA-51]
}

/// @notice Changes token's forbidden status
Expand Down
11 changes: 3 additions & 8 deletions contracts/interfaces/ICreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,8 @@ interface ICreditConfiguratorV3Events {
/// @notice Emitted when a new max debt per block multiplier is set
event SetMaxDebtPerBlockMultiplier(uint8 maxDebtPerBlockMultiplier);

/// @notice Emitted when a new max cumulative loss is set
event SetMaxCumulativeLoss(uint128 maxCumulativeLoss);

/// @notice Emitted when cumulative loss is reset to zero in the credit facade
event ResetCumulativeLoss();
/// @notice Emitted when new loss liquidator is set
event SetLossLiquidator(address indexed liquidator);

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

function forbidBorrowing() external;

function setMaxCumulativeLoss(uint128 newMaxCumulativeLoss) external;

function resetCumulativeLoss() external;
function setLossLiquidator(address newLossLiquidator) external;

function setExpirationDate(uint40 newExpirationDate) external;

Expand Down
18 changes: 6 additions & 12 deletions contracts/interfaces/ICreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ struct DebtLimits {
uint128 maxDebt;
}

/// @notice Info on bad debt liquidation losses packed into a single slot
/// @param currentCumulativeLoss Current cumulative loss from bad debt liquidations
/// @param maxCumulativeLoss Max cumulative loss incurred before the facade gets paused
struct CumulativeLossParams {
uint128 currentCumulativeLoss;
uint128 maxCumulativeLoss;
}

/// @notice Collateral check params
/// @param collateralHints Optional array of token masks to check first to reduce the amount of computation
/// when known subset of account's collateral tokens covers all the debt
Expand Down Expand Up @@ -106,13 +98,13 @@ interface ICreditFacadeV3 is IVersion, IACLTrait, ICreditFacadeV3Events {

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

function lossParams() external view returns (uint128 currentCumulativeLoss, uint128 maxCumulativeLoss);
function lossLiquidator() external view returns (address);

function forbiddenTokenMask() external view returns (uint256);

function emergencyLiquidators() external view returns (address[] memory);

function canLiquidateWhilePaused(address) external view returns (bool);
function isEmergencyLiquidator(address) external view returns (bool);

// ------------------ //
// ACCOUNT MANAGEMENT //
Expand All @@ -125,7 +117,9 @@ 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;
function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls)
external
returns (uint256 reportedLoss);

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

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

function setCumulativeLossParams(uint128 newMaxCumulativeLoss, bool resetCumulativeLoss) external;
function setLossLiquidator(address newLossLiquidator) external;

function setTokenAllowance(address token, AllowanceAction allowance) external;

Expand Down
20 changes: 2 additions & 18 deletions contracts/interfaces/IExceptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -272,24 +272,8 @@ error CallerNotExecutorException();
/// @notice Thrown on attempting to call an access restricted function not as veto admin
error CallerNotVetoAdminException();

// ------------------- //
// CONTROLLER TIMELOCK //
// ------------------- //

/// @notice Thrown when the new parameter values do not satisfy required conditions
error ParameterChecksFailedException();

/// @notice Thrown when attempting to execute a non-queued transaction
error TxNotQueuedException();

/// @notice Thrown when attempting to execute a transaction that is either immature or stale
error TxExecutedOutsideTimeWindowException();

/// @notice Thrown when execution of a transaction fails
error TxExecutionRevertedException();

/// @notice Thrown when the value of a parameter on execution is different from the value on queue
error ParameterChangedAfterQueuedTxException();
/// @notice Thrown on attempting to perform liquidation with loss not through the loss liquidator contract
error CallerNotLossLiquidatorException();

// -------- //
// BOT LIST //
Expand Down
2 changes: 0 additions & 2 deletions contracts/pool/PoolV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,6 @@ contract PoolV3 is
function lendCreditAccount(uint256 borrowedAmount, address creditAccount)
external
override
whenNotPaused // U:[LP-2A]
nonReentrant // U:[LP-2B]
{
uint128 borrowedAmountU128 = borrowedAmount.toUint128();
Expand Down Expand Up @@ -485,7 +484,6 @@ contract PoolV3 is
function repayCreditAccount(uint256 repaidAmount, uint256 profit, uint256 loss)
external
override
whenNotPaused // U:[LP-2A]
nonReentrant // U:[LP-2B]
{
uint128 repaidAmountU128 = repaidAmount.toUint128();
Expand Down
Loading

0 comments on commit 28d3790

Please sign in to comment.