Skip to content

Commit

Permalink
feat: minor PartialLiquidationBotV3 improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
lekhovitsky committed Feb 17, 2024
1 parent 02e0437 commit 6b3e805
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 70 deletions.
115 changes: 61 additions & 54 deletions contracts/bots/PartialLiquidationBotV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.
import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol";
import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol";

import {
AP_TREASURY,
IAddressProviderV3,
NO_VERSION_CONTROL
} from "@gearbox-protocol/core-v3/contracts/interfaces/IAddressProviderV3.sol";
import {ICreditAccountV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditAccountV3.sol";
import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol";
import {ICreditFacadeV3Multicall} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3Multicall.sol";
import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol";
Expand All @@ -16,7 +22,7 @@ import {
PriceFeedDoesNotExistException
} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
import {IPriceOracleV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol";
import {ACLNonReentrantTrait} from "@gearbox-protocol/core-v3/contracts/traits/ACLNonReentrantTrait.sol";
import {ACLTrait} from "@gearbox-protocol/core-v3/contracts/traits/ACLTrait.sol";
import {ContractsRegisterTrait} from "@gearbox-protocol/core-v3/contracts/traits/ContractsRegisterTrait.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Expand All @@ -38,7 +44,7 @@ import {IPartialLiquidationBotV3} from "../interfaces/IPartialLiquidationBotV3.s
/// - liquidator premium and DAO fee are the same as for the full liquidation in a given credit manager
/// (although fees are accumulated in this contract instead of being deposited into pools)
/// - this implementation can't handle fee-on-transfer underlyings
contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLNonReentrantTrait, ContractsRegisterTrait {
contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLTrait, ContractsRegisterTrait {
using EnumerableSet for EnumerableSet.AddressSet;

/// @dev Internal liquidation variables
Expand All @@ -47,68 +53,64 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLNonReentrantTra
address creditFacade;
address priceOracle;
address underlying;
uint256 feeRate;
uint256 discountRate;
uint256 feeLiquidation;
uint256 liquidationDiscount;
}

/// @inheritdoc IVersion
uint256 public constant override version = 3_00;

/// @inheritdoc IPartialLiquidationBotV3
address public immutable override treasury;

/// @dev Set of allowed credit managers
EnumerableSet.AddressSet internal _creditManagersSet;

/// @dev Ensures that `creditManager` is an allowed credit manager
modifier allowedCreditManagersOnly(address creditManager) {
if (!_creditManagersSet.contains(creditManager)) revert CreditManagerIsNotAllowedException();
_;
}

/// @notice Constructor
/// @param addressProvider Address provider contract address
constructor(address addressProvider)
ACLNonReentrantTrait(addressProvider)
ContractsRegisterTrait(addressProvider)
{}
constructor(address addressProvider) ACLTrait(addressProvider) ContractsRegisterTrait(addressProvider) {
treasury = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL);
}

// ----------- //
// LIQUIDATION //
// ----------- //

/// @inheritdoc IPartialLiquidationBotV3
function liquidateExactDebt(
address creditManager,
address creditAccount,
address token,
uint256 repaidAmount,
uint256 minSeizedAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external override nonReentrant allowedCreditManagersOnly(creditManager) returns (uint256 seizedAmount) {
LiquidationVars memory vars = _initVars(creditManager);
_checkLiquidation(vars, creditAccount, token, priceUpdates);
) external override returns (uint256 seizedAmount) {
LiquidationVars memory vars = _initVars(creditAccount, token);
_applyOnDemandPriceUpdates(vars, priceUpdates);
_revertIfNotLiquidatable(vars, creditAccount);

seizedAmount = IPriceOracleV3(vars.priceOracle).convert(repaidAmount, vars.underlying, token)
* PERCENTAGE_FACTOR / vars.discountRate;
* PERCENTAGE_FACTOR / vars.liquidationDiscount;
if (seizedAmount < minSeizedAmount) revert SeizedLessThanRequiredException();

_executeLiquidation(vars, creditAccount, token, repaidAmount, seizedAmount, to);
}

/// @inheritdoc IPartialLiquidationBotV3
function liquidateExactCollateral(
address creditManager,
address creditAccount,
address token,
uint256 seizedAmount,
uint256 maxRepaidAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external override nonReentrant allowedCreditManagersOnly(creditManager) returns (uint256 repaidAmount) {
LiquidationVars memory vars = _initVars(creditManager);
_checkLiquidation(vars, creditAccount, token, priceUpdates);
) external override returns (uint256 repaidAmount) {
LiquidationVars memory vars = _initVars(creditAccount, token);
_applyOnDemandPriceUpdates(vars, priceUpdates);
_revertIfNotLiquidatable(vars, creditAccount);

repaidAmount = IPriceOracleV3(vars.priceOracle).convert(seizedAmount, token, vars.underlying)
* vars.discountRate / PERCENTAGE_FACTOR;
* vars.liquidationDiscount / PERCENTAGE_FACTOR;
if (repaidAmount > maxRepaidAmount) revert RepaidMoreThanAllowedException();

_executeLiquidation(vars, creditAccount, token, repaidAmount, seizedAmount, to);
Expand Down Expand Up @@ -138,51 +140,56 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLNonReentrantTra
IERC20(underlying).approve(creditManager, type(uint256).max);
}

// ---- //
// FEES //
// ---- //

/// @inheritdoc IPartialLiquidationBotV3
function withdrawFees(address token, uint256 amount, address to) external override configuratorOnly {
if (amount == type(uint256).max) amount = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(to, amount);
function collectFees() external override {
uint256 numManagers = _creditManagersSet.length();
for (uint256 i; i < numManagers; ++i) {
address token = ICreditManagerV3(_creditManagersSet.at(i)).underlying();
uint256 amount = IERC20(token).balanceOf(address(this));
if (amount > 0) IERC20(token).transfer(treasury, amount);
}
}

// --------- //
// INTERNALS //
// --------- //

/// @dev Loads `creditManager`'s state variables used in liquidation
function _initVars(address creditManager) internal view returns (LiquidationVars memory vars) {
vars.creditManager = creditManager;
vars.creditFacade = ICreditManagerV3(creditManager).creditFacade();
vars.priceOracle = ICreditManagerV3(creditManager).priceOracle();
vars.underlying = ICreditManagerV3(creditManager).underlying();
(, vars.feeRate, vars.discountRate,,) = ICreditManagerV3(creditManager).fees();
}
/// @dev Loads state variables used in `creditAccount` liquidation and sanitizes inputs, i.e.,
/// `creditAccount`'s credit manager is allowed and `token` is not its underlying
function _initVars(address creditAccount, address token) internal view returns (LiquidationVars memory vars) {
vars.creditManager = ICreditAccountV3(creditAccount).creditManager();
vars.creditFacade = ICreditManagerV3(vars.creditManager).creditFacade();
vars.priceOracle = ICreditManagerV3(vars.creditManager).priceOracle();
vars.underlying = ICreditManagerV3(vars.creditManager).underlying();
(, vars.feeLiquidation, vars.liquidationDiscount,,) = ICreditManagerV3(vars.creditManager).fees();

/// @dev Internal function that checks liquidation validity:
/// - `token` is not underlying
/// - `creditAccount` is liquidatable after applying `priceUpdates`
function _checkLiquidation(
LiquidationVars memory vars,
address creditAccount,
address token,
PriceUpdate[] calldata priceUpdates
) internal {
if (!_creditManagersSet.contains(vars.creditManager)) revert CreditManagerIsNotAllowedException();
if (token == vars.underlying) revert UnderlyingNotLiquidatableException();
}

/// @dev Applies on-demand price feed updates, reverts if trying to update unknown price feeds
function _applyOnDemandPriceUpdates(LiquidationVars memory vars, PriceUpdate[] calldata priceUpdates) internal {
uint256 len = priceUpdates.length;
unchecked {
for (uint256 i; i < len; ++i) {
PriceUpdate calldata update = priceUpdates[i];
address priceFeed = IPriceOracleV3(vars.priceOracle).priceFeedsRaw(update.token, update.reserve);
if (priceFeed == address(0)) revert PriceFeedDoesNotExistException();
IUpdatablePriceFeed(priceFeed).updatePrice(update.data);
}
for (uint256 i; i < len; ++i) {
PriceUpdate calldata update = priceUpdates[i];
address priceFeed = IPriceOracleV3(vars.priceOracle).priceFeedsRaw(update.token, update.reserve);
if (priceFeed == address(0)) revert PriceFeedDoesNotExistException();
IUpdatablePriceFeed(priceFeed).updatePrice(update.data);
}
}

/// @dev Ensures that `creditAccount` is liquidatable
function _revertIfNotLiquidatable(LiquidationVars memory vars, address creditAccount) internal view {
if (!ICreditManagerV3(vars.creditManager).isLiquidatable(creditAccount, PERCENTAGE_FACTOR)) {
revert CreditAccountNotLiquidatableException();
}
}

/// @dev Internal function that executes liquidation:
/// @dev Executes partial liquidation:
/// - transfers `repaidAmount` of underlying from the caller
/// - performs a multicall on `creditAccount` that repays debt and withdraws `seizedAmount` of `token` to `to`
function _executeLiquidation(
Expand All @@ -194,7 +201,7 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLNonReentrantTra
address to
) internal {
IERC20(vars.underlying).transferFrom(msg.sender, address(this), repaidAmount);
uint256 fee = repaidAmount * vars.feeRate / PERCENTAGE_FACTOR;
uint256 fee = repaidAmount * vars.feeLiquidation / PERCENTAGE_FACTOR;
repaidAmount -= fee;

MultiCall[] memory calls = new MultiCall[](3);
Expand All @@ -212,6 +219,6 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ACLNonReentrantTra
});
ICreditFacadeV3(vars.creditFacade).botMulticall(creditAccount, calls);

emit Liquidate(vars.creditManager, creditAccount, token, repaidAmount, seizedAmount, fee);
emit LiquidatePartial(vars.creditManager, creditAccount, token, repaidAmount, seizedAmount, fee);
}
}
33 changes: 17 additions & 16 deletions contracts/interfaces/IPartialLiquidationBotV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface IPartialLiquidationBotV3 is IVersion {
/// @param repaidDebt Amount of `creditAccount`'s debt repaid
/// @param seizedCollateral Amount of `token` seized from `creditAccount`
/// @param fee Amount of underlying withheld on the bot as liqudiation fee
event Liquidate(
event LiquidatePartial(
address indexed creditManager,
address indexed creditAccount,
address indexed token,
Expand Down Expand Up @@ -66,21 +66,19 @@ interface IPartialLiquidationBotV3 is IVersion {
// ----------- //

/// @notice Liquidates credit account by repaying the given amount of its debt in exchange for discounted collateral
/// @param creditManager Credit manager to liquidate an account in
/// @param creditAccount Credit account to liquidate
/// @param token Collateral token to seize
/// @param repaidAmount Amount of `creditManager`'s underlying to repay
/// @param repaidAmount Amount of underlying to repay
/// @param minSeizedAmount Minimum amount of `token` to seize from `creditAccount`
/// @param to Address to send 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 Reverts if `creditManager` is not an allowed credit manager
/// @dev Reverts if `token` is `creditManager`'s underlying
/// @dev Reverts if `creditAccount`'s credit manager is not allowed
/// @dev Reverts if `token` is underlying
/// @dev Reverts if `priceUpdates` contains updates of unknown feeds
/// @dev Reverts if `creditAccount` is not liquidatable after applying `priceUpdates`
/// @dev Reverts if amount of `token` to be seized is less than `minSeizedAmount`
function liquidateExactDebt(
address creditManager,
address creditAccount,
address token,
uint256 repaidAmount,
Expand All @@ -90,21 +88,19 @@ interface IPartialLiquidationBotV3 is IVersion {
) external returns (uint256 seizedAmount);

/// @notice Liquidates credit account by repaying its debt in exchange for the given amount of discounted collateral
/// @param creditManager Credit manager to liquidate an account in
/// @param creditAccount Credit account to liquidate
/// @param token Collateral token to seize
/// @param seizedAmount Amount of `token` to seize from `creditAccount`
/// @param maxRepaidAmount Maxiumum amount of `creditManager`'s underlying to repay
/// @param maxRepaidAmount Maxiumum amount of underlying to repay
/// @param to Address to send seized `token` to
/// @param priceUpdates On-demand price feed updates to apply before calculations, see `PriceUpdate` for details
/// @return repaidAmount Amount of `creditManager`'s underlying repaid
/// @dev Reverts if `creditManager` is not an allowed credit manager
/// @dev Reverts if `token` is `creditManager`'s underlying
/// @return repaidAmount Amount of underlying repaid
/// @dev Reverts if `creditAccount`'s credit manager is not allowed
/// @dev Reverts if `token` is underlying
/// @dev Reverts if `priceUpdates` contains updates of unknown feeds
/// @dev Reverts if `creditAccount` is not liquidatable after applying `priceUpdates`
/// @dev Reverts if amount of underlying to be repaid is greater than `maxRepaidAmount`
function liquidateExactCollateral(
address creditManager,
address creditAccount,
address token,
uint256 seizedAmount,
Expand All @@ -128,8 +124,13 @@ interface IPartialLiquidationBotV3 is IVersion {
/// @dev Reverts if caller is not configurator
function addCreditManager(address creditManager) external;

/// @notice Withdraws `amount` of accumulated fees in `token` to `to`
/// @dev If `amount` is `type(uint256).max`, withdraws full balance
/// @dev Reverts if caller is not configurator
function withdrawFees(address token, uint256 amount, address to) external;
// ---- //
// FEES //
// ---- //

/// @notice Treasury to send collected fees to
function treasury() external view returns (address);

/// @notice Sends accumulated fees to the treasury
function collectFees() external;
}

0 comments on commit 6b3e805

Please sign in to comment.