Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: debt limit changes no longer break partial liquidations #250

Merged
merged 5 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 17 additions & 26 deletions contracts/credit/CreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";

// LIBRARIES & CONSTANTS
import {BitMask} from "../libraries/BitMask.sol";
import {CreditLogic} from "../libraries/CreditLogic.sol";
import {PERCENTAGE_FACTOR, UNDERLYING_TOKEN_MASK, WAD} from "../libraries/Constants.sol";

// CONTRACTS
Expand Down Expand Up @@ -89,6 +90,8 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLNonReentrantTrait {
/// @dev Reverts if `token` is underlying
/// @dev Reverts if `token` is not quoted in the quota keeper
/// @dev Reverts if `liquidationThreshold` is greater than underlying's LT
/// @dev `liquidationThreshold` can be zero to allow users to deposit connector tokens to credit accounts and swap
/// them into actual collateral and to withdraw reward tokens sent to credit accounts by integrated protocols
function addCollateralToken(address token, uint16 liquidationThreshold)
external
override
Expand Down Expand Up @@ -332,33 +335,15 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLNonReentrantTrait {
// CREDIT MANAGER //
// -------------- //

/// @notice Sets the maximum number of tokens enabled as collateral on a credit account
/// @param newMaxEnabledTokens New maximum number of enabled tokens
/// @dev Reverts if `newMaxEnabledTokens` is zero
function setMaxEnabledTokens(uint8 newMaxEnabledTokens)
external
override
configuratorOnly // I:[CC-2]
{
CreditManagerV3 cm = CreditManagerV3(creditManager);

if (newMaxEnabledTokens == 0) revert IncorrectParameterException(); // I:[CC-26]

if (newMaxEnabledTokens == cm.maxEnabledTokens()) return;

cm.setMaxEnabledTokens(newMaxEnabledTokens); // I:[CC-26]
emit SetMaxEnabledTokens(newMaxEnabledTokens); // I:[CC-26]
}

/// @notice Sets new fees params in the credit manager (all fields in bps)
/// @notice Sets underlying token's liquidation threshold to 1 - liquidation fee - liquidation premium and
/// upper-bounds all other tokens' LTs with this number, which interrupts ongoing LT rampings
/// @notice Sets underlying token's liquidation threshold to 1 - liquidation fee - liquidation premium
/// @param feeLiquidation Percentage of liquidated account value taken by the protocol as profit
/// @param liquidationPremium Percentage of liquidated account value that can be taken by liquidator
/// @param feeLiquidationExpired Percentage of liquidated expired account value taken by the protocol as profit
/// @param liquidationPremiumExpired Percentage of liquidated expired account value that can be taken by liquidator
/// @dev Reverts if `liquidationPremium + feeLiquidation` is above 100%
/// @dev Reverts if `liquidationPremiumExpired + feeLiquidationExpired` is above 100%
/// @dev Reverts if new underlying's LT is below some collateral token's LT, accounting for ramps
function setFees(
uint16 feeLiquidation,
uint16 liquidationPremium,
Expand Down Expand Up @@ -432,15 +417,21 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLNonReentrantTrait {
ltFinal: ltUnderlying,
timestampRampStart: type(uint40).max,
rampDuration: 0
}); // I:[CC-25]
}); // I:[CC-18]

uint256 len = CreditManagerV3(creditManager).collateralTokensCount();
unchecked {
for (uint256 i = 1; i < len; ++i) {
(address token, uint16 lt) = CreditManagerV3(creditManager).collateralTokenByMask({tokenMask: 1 << i});
if (lt > ltUnderlying) {
_setLiquidationThreshold({token: token, liquidationThreshold: ltUnderlying}); // I:[CC-25]
}
address token = CreditManagerV3(creditManager).getTokenByMask(1 << i);
(uint16 ltInitial, uint16 ltFinal, uint40 timestampRampStart, uint24 rampDuration) =
CreditManagerV3(creditManager).ltParams(token);
uint16 lt = CreditLogic.getLiquidationThreshold({
ltInitial: ltInitial,
ltFinal: ltFinal,
timestampRampStart: timestampRampStart,
rampDuration: rampDuration
});
if (lt > ltUnderlying || ltFinal > ltUnderlying) revert IncorrectLiquidationThresholdException(); // I:[CC-18]
}
}
}
Expand Down Expand Up @@ -503,7 +494,7 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLNonReentrantTrait {
_setLimits({minDebt: minDebt, maxDebt: maxDebt}); // I:[CC-22]

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

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

Expand Down
17 changes: 12 additions & 5 deletions contracts/credit/CreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
/// @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
override
Expand Down Expand Up @@ -367,6 +370,8 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
/// @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
/// @dev Like in full liquidations, liquidator can seize non-enabled tokens from the credit account, altough
/// here they are actually used to repay debt; unclaimed rewards are also safe in this case
function partiallyLiquidateCreditAccount(
address creditAccount,
address token,
Expand Down Expand Up @@ -689,7 +694,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
(uint256 newDebt,,) =
ICreditManagerV3(creditManager).manageDebt(creditAccount, amount, enabledTokensMask, action); // U:[FA-27,31]

_revertIfOutOfDebtLimits(newDebt); // U:[FA-28,32,33]
_revertIfOutOfDebtLimits(newDebt, action); // U:[FA-28,32,33]
}

/// @dev `ICreditFacadeV3Multicall.updateQuota` implementation
Expand Down Expand Up @@ -876,9 +881,11 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
totalBorrowedInBlock = uint128(newDebtInCurrentBlock); // U:[FA-43]
}

/// @dev Ensures that account's debt principal is within allowed range or is zero
function _revertIfOutOfDebtLimits(uint256 debt) internal view {
if (debt == 0) return;
/// @dev Ensures that account's debt principal takes allowed values:
/// - for borrowing, new debt must be within allowed limits
/// - for repayment, new debt must be above allowed minimum or zero
function _revertIfOutOfDebtLimits(uint256 debt, ManageDebtAction action) internal view {
if (debt == 0 && action == ManageDebtAction.DECREASE_DEBT) return;
uint256 minDebt;
uint256 maxDebt;

Expand All @@ -890,7 +897,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
minDebt := and(data, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
}

if (debt < minDebt || debt > maxDebt) {
if (debt < minDebt || debt > maxDebt && action == ManageDebtAction.INCREASE_DEBT) {
revert BorrowAmountOutOfLimitsException(); // U:[FA-44]
}
}
Expand Down
18 changes: 6 additions & 12 deletions contracts/credit/CreditManagerV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {IPoolQuotaKeeperV3} from "../interfaces/IPoolQuotaKeeperV3.sol";

// LIBRARIES
import {
DEFAULT_MAX_ENABLED_TOKENS,
INACTIVE_CREDIT_ACCOUNT_ADDRESS,
PERCENTAGE_FACTOR,
UNDERLYING_TOKEN_MASK,
Expand Down Expand Up @@ -83,7 +82,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT
address public override priceOracle;

/// @notice Maximum number of tokens that a credit account can have enabled as collateral
uint8 public override maxEnabledTokens = DEFAULT_MAX_ENABLED_TOKENS;
uint8 public immutable override maxEnabledTokens;

/// @notice Number of known collateral tokens
uint8 public override collateralTokensCount;
Expand Down Expand Up @@ -149,6 +148,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT
/// @param _pool Address of the lending pool to connect this credit manager to
/// @param _accountFactory Account factory address
/// @param _priceOracle Price oracle address
/// @param _maxEnabledTokens Maximum number of tokens that a credit account can have enabled as collateral
/// @param _feeInterest Percentage of accrued interest in bps to take by the protocol as profit
/// @param _name Credit manager name
/// @dev Adds pool's underlying as collateral token with LT = 0
Expand All @@ -157,12 +157,16 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT
address _pool,
address _accountFactory,
address _priceOracle,
uint8 _maxEnabledTokens,
uint16 _feeInterest,
string memory _name
) {
if (_maxEnabledTokens == 0) revert IncorrectParameterException(); // U:[CM-1]

pool = _pool; // U:[CM-1]
accountFactory = _accountFactory; // U:[CM-1]
priceOracle = _priceOracle; // U:[CM-1]
maxEnabledTokens = _maxEnabledTokens; // U:[CM-1]
feeInterest = _feeInterest; // U:[CM-1]
name = _name; // U:[CM-1]

Expand Down Expand Up @@ -1197,16 +1201,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT
}
}

/// @notice Sets a new max number of enabled tokens
/// @param _maxEnabledTokens The new max number of enabled tokens
function setMaxEnabledTokens(uint8 _maxEnabledTokens)
external
override
creditConfiguratorOnly // U:[CM-4]
{
maxEnabledTokens = _maxEnabledTokens; // U:[CM-44]
}

/// @notice Sets the link between the adapter and the target contract
/// @param adapter Address of the adapter contract to use to access the third-party contract,
/// passing `address(0)` will forbid accessing `targetContract`
Expand Down
11 changes: 9 additions & 2 deletions contracts/credit/CreditManagerV3_USDT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ import {IPoolV3} from "../interfaces/IPoolV3.sol";
/// @title Credit manager V3 USDT
/// @notice Credit manager variation for USDT underlying with enabled transfer fees
contract CreditManagerV3_USDT is CreditManagerV3, USDT_Transfer {
constructor(address _pool, address _accountFactory, address _priceOracle, uint16 _feeInterest, string memory _name)
CreditManagerV3(_pool, _accountFactory, _priceOracle, _feeInterest, _name)
constructor(
address _pool,
address _accountFactory,
address _priceOracle,
uint8 _maxEnabledTokens,
uint16 _feeInterest,
string memory _name
)
CreditManagerV3(_pool, _accountFactory, _priceOracle, _maxEnabledTokens, _feeInterest, _name)
USDT_Transfer(IPoolV3(_pool).asset())
{}

Expand Down
5 changes: 0 additions & 5 deletions contracts/interfaces/ICreditConfiguratorV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ interface ICreditConfiguratorV3Events {
// CREDIT MANAGER //
// -------------- //

/// @notice Emitted when a new maximum number of enabled tokens is set in the credit manager
event SetMaxEnabledTokens(uint8 maxEnabledTokens);

/// @notice Emitted when new fee parameters are set in the credit manager
event UpdateFees(
uint16 feeLiquidation, uint16 liquidationPremium, uint16 feeLiquidationExpired, uint16 liquidationPremiumExpired
Expand Down Expand Up @@ -148,8 +145,6 @@ interface ICreditConfiguratorV3 is IVersion, ICreditConfiguratorV3Events {
uint16 liquidationPremiumExpired
) external;

function setMaxEnabledTokens(uint8 newMaxEnabledTokens) external;

// -------- //
// UPGRADES //
// -------- //
Expand Down
17 changes: 13 additions & 4 deletions contracts/interfaces/ICreditFacadeV3Multicall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,22 @@ interface ICreditFacadeV3Multicall {
/// @dev This method is available in all kinds of multicalls
function compareBalances() external;

/// @notice Adds collateral to account
/// @notice Adds collateral to account.
/// Only the underlying token counts towards account's collateral value by default, while all other tokens
/// must be enabled as collateral by "purchasing" quota for it. Holding non-enabled token on account with
/// non-zero debt poses a risk of losing it entirely to the liquidator. Adding non-enabled tokens is still
/// supported to allow users to later swap them into enabled ones in the same multicall.
/// @param token Token to add
/// @param amount Amount to add
/// @dev Requires token approval from caller to the credit manager
/// @dev This method can also be called during liquidation
function addCollateral(address token, uint256 amount) external;

/// @notice Adds collateral to account using signed EIP-2612 permit message
/// @notice Adds collateral to account using signed EIP-2612 permit message.
/// Only the underlying token counts towards account's collateral value by default, while all other tokens
/// must be enabled as collateral by "purchasing" quota for it. Holding non-enabled token on account with
/// non-zero debt poses a risk of losing it entirely to the liquidator. Adding non-enabled tokens is still
/// supported to allow users to later swap them into enabled ones in the same multicall.
/// @param token Token to add
/// @param amount Amount to add
/// @param deadline Permit deadline
Expand All @@ -94,7 +102,7 @@ interface ICreditFacadeV3Multicall {
/// @param amount Underlying amount to borrow
/// @dev Increasing debt is prohibited when closing an account
/// @dev Increasing debt is prohibited if it was previously updated in the same block
/// @dev The resulting debt amount must be within allowed range
/// @dev The resulting debt amount must be within allowed limits
/// @dev Increasing debt is prohibited if there are forbidden tokens enabled as collateral on the account
/// @dev After debt increase, total amount borrowed by the credit manager in the current block must not exceed
/// the limit defined in the facade
Expand All @@ -104,7 +112,8 @@ interface ICreditFacadeV3Multicall {
/// @param amount Underlying amount to repay, value above account's total debt indicates full repayment
/// @dev Decreasing debt is prohibited when opening an account
/// @dev Decreasing debt is prohibited if it was previously updated in the same block
/// @dev The resulting debt amount must be within allowed range or zero
/// @dev The resulting debt amount must be above allowed minimum or zero (maximum is not checked here
/// to allow small repayments and partial liquidations in case configurator lowers it)
/// @dev Full repayment brings account into a special mode that skips collateral checks and thus requires
/// an account to have no potential debt sources, e.g., all quotas must be disabled
function decreaseDebt(uint256 amount) external;
Expand Down
2 changes: 0 additions & 2 deletions contracts/interfaces/ICreditManagerV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,6 @@ interface ICreditManagerV3 is IVersion, ICreditManagerV3Events {
uint16 liquidationDiscountExpired
) external;

function setMaxEnabledTokens(uint8 maxEnabledTokens) external;

function setContractAllowance(address adapter, address targetContract) external;

function setCreditFacade(address creditFacade) external;
Expand Down
2 changes: 0 additions & 2 deletions contracts/libraries/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ uint16 constant DEFAULT_FEE_LIQUIDATION_EXPIRED = 1_00;
uint16 constant DEFAULT_LIQUIDATION_PREMIUM_EXPIRED = 2_00;
uint8 constant DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER = 2;

uint8 constant DEFAULT_MAX_ENABLED_TOKENS = 4;

uint8 constant BOT_PERMISSIONS_SET_FLAG = 1;

uint256 constant UNDERLYING_TOKEN_MASK = 1;
Expand Down
9 changes: 9 additions & 0 deletions contracts/pool/PoolV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,15 @@ contract PoolV3 is ERC4626, ERC20Permit, ACLNonReentrantTrait, ContractsRegister
// INTERNALS //
// --------- //

/// @dev Same as `ERC20._transfer` but reverts if contract is paused
function _transfer(address from, address to, uint256 amount)
internal
override
whenNotPaused // U:[LP-2A]
{
super._transfer(from, to, amount);
}

/// @dev Returns amount of token that should be transferred to receive `amount`
/// Pools with fee-on-transfer underlying should override this method
function _amountWithFee(uint256 amount) internal view virtual returns (uint256) {
Expand Down
1 change: 1 addition & 0 deletions contracts/test/config/MockCreditConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ contract MockCreditConfig is Test, IPoolV3DeployConfig {

cp.minDebt = uint128(getAccountAmount / 2);
cp.maxDebt = uint128(10 * getAccountAmount);
cp.maxEnabledTokens = DEFAULT_MAX_ENABLED_TOKENS;
cp.feeInterest = DEFAULT_FEE_INTEREST;
cp.feeLiquidation = DEFAULT_FEE_LIQUIDATION;
cp.liquidationPremium = DEFAULT_LIQUIDATION_PREMIUM;
Expand Down
36 changes: 25 additions & 11 deletions contracts/test/helpers/IntegrationTestHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager {
pool: address(pool),
degenNFT: (whitelisted) ? address(degenNFT) : address(0),
expirable: (anyExpirable) ? cmParams.expirable : expirable,
maxEnabledTokens: cmParams.maxEnabledTokens,
feeInterest: cmParams.feeInterest,
name: cmParams.name
});
Expand Down Expand Up @@ -494,16 +495,23 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager {

return creditFacade.openCreditAccount(
onBehalfOf,
MultiCallBuilder.build(
MultiCall({
target: address(creditFacade),
callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (debt))
}),
MultiCall({
target: address(creditFacade),
callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (underlying, amount))
})
),
debt == 0
? MultiCallBuilder.build(
MultiCall({
target: address(creditFacade),
callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (underlying, amount))
})
)
: MultiCallBuilder.build(
MultiCall({
target: address(creditFacade),
callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (debt))
}),
MultiCall({
target: address(creditFacade),
callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (underlying, amount))
})
),
referralCode
);
}
Expand Down Expand Up @@ -547,8 +555,14 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager {
}

function _makeAccountsLiquitable() internal {
vm.prank(CONFIGURATOR);
vm.startPrank(CONFIGURATOR);
uint256 idx = creditManager.collateralTokensCount() - 1;
while (idx != 0) {
address token = creditManager.getTokenByMask(1 << (idx--));
creditConfigurator.setLiquidationThreshold(token, 0);
}
creditConfigurator.setFees(200, 9000, 100, 9500);
vm.stopPrank();

// switch to new block to be able to close account
vm.roll(block.number + 1);
Expand Down
Loading
Loading