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

feat: PartialLiquidationV3 bot upgrade #6

Merged
merged 4 commits into from
Jul 21, 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
96 changes: 53 additions & 43 deletions contracts/bots/PartialLiquidationBotV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
pragma solidity ^0.8.23;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";

import {ICreditAccountV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditAccountV3.sol";
import {ICreditFacadeV3, MultiCall} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol";
Expand All @@ -24,6 +24,7 @@ import {
} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
import {IPriceOracleV3, PriceUpdate} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol";
import {IBot} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IBot.sol";
import {IPhantomToken} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPhantomToken.sol";
import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol";
import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol";
import {ReentrancyGuardTrait} from "@gearbox-protocol/core-v3/contracts/traits/ReentrancyGuardTrait.sol";
Expand All @@ -44,8 +45,7 @@ import {IPartialLiquidationBotV3} from "../interfaces/IPartialLiquidationBotV3.s
/// - health factor range check is made using normal prices, which, under certain circumstances, may be
/// mutually exclusive with the former;
/// - liquidator premium and DAO fee are the same as for the full liquidation in a given credit manager
/// (although fees are sent to the treasury instead of being deposited into pools);
/// - this implementation can't handle fee-on-transfer underlyings.
/// (although fees are sent to the treasury instead of being deposited into pools).
/// The bot can also be used for deleverage to prevent liquidations by triggering earlier, limiting
/// operation size and/or charging less in premium and fees.
contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTrait, SanityCheckTrait {
Expand All @@ -57,6 +57,7 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
address creditFacade;
address priceOracle;
address underlying;
address receivedToken;
uint256 feeLiquidation;
uint256 liquidationDiscount;
}
Expand Down Expand Up @@ -91,6 +92,7 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
/// @param maxHealthFactor_ Maximum health factor to allow after the liquidation
/// @param premiumScaleFactor_ Factor to scale credit manager's liquidation premium by
/// @param feeScaleFactor_ Factor to scale credit manager's liquidation fee by
/// @dev Reverts if `maxHealthFactor` is below 100% or below `minHealthFactor_`
/// @dev Reverts if `treasury_` is zero address
constructor(
address treasury_,
Expand All @@ -109,49 +111,38 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
feeScaleFactor = feeScaleFactor_;
}

/// @notice Returns serialized bot's parameters
function serialize() external view returns (bytes memory) {
return abi.encode(treasury, minHealthFactor, maxHealthFactor, premiumScaleFactor, feeScaleFactor);
}

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

/// @inheritdoc IPartialLiquidationBotV3
function liquidateExactDebt(
function partiallyLiquidate(
address creditAccount,
address token,
uint256 repaidAmount,
uint256 minSeizedAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external override nonReentrant returns (uint256 seizedAmount) {
LiquidationVars memory vars = _initVars(creditAccount);
IPriceOracleV3(vars.priceOracle).updatePrices(priceUpdates);
_validateLiquidation(vars, creditAccount, token);

seizedAmount = IPriceOracleV3(vars.priceOracle).convert(repaidAmount, vars.underlying, token)
* PERCENTAGE_FACTOR / vars.liquidationDiscount;
if (seizedAmount < minSeizedAmount) revert SeizedLessThanRequiredException();
LiquidationVars memory vars = _initVars(creditAccount, token);
if (priceUpdates.length != 0) IPriceOracleV3(vars.priceOracle).updatePrices(priceUpdates);
_validateLiquidation(vars, creditAccount);

_executeLiquidation(vars, creditAccount, token, repaidAmount, seizedAmount, to);
_checkHealthFactor(vars, creditAccount);
}
uint256 balanceBefore = IERC20(vars.underlying).safeBalanceOf(creditAccount);
IERC20(vars.underlying).safeTransferFrom(msg.sender, creditAccount, repaidAmount);
repaidAmount = IERC20(vars.underlying).safeBalanceOf(creditAccount) - balanceBefore;

/// @inheritdoc IPartialLiquidationBotV3
function liquidateExactCollateral(
address creditAccount,
address token,
uint256 seizedAmount,
uint256 maxRepaidAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external override nonReentrant returns (uint256 repaidAmount) {
LiquidationVars memory vars = _initVars(creditAccount);
IPriceOracleV3(vars.priceOracle).updatePrices(priceUpdates);
_validateLiquidation(vars, creditAccount, token);
uint256 fee;
(repaidAmount, fee, seizedAmount) = _calcPartialLiquidationPayments(vars, repaidAmount, token);

repaidAmount = IPriceOracleV3(vars.priceOracle).convert(seizedAmount, token, vars.underlying)
* vars.liquidationDiscount / PERCENTAGE_FACTOR;
if (repaidAmount > maxRepaidAmount) revert RepaidMoreThanAllowedException();
seizedAmount = _executeLiquidation(vars, creditAccount, token, repaidAmount, seizedAmount, fee, to);
if (seizedAmount < minSeizedAmount) revert SeizedLessThanRequiredException();

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

Expand All @@ -160,7 +151,7 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
// --------- //

/// @dev Loads state variables used in `creditAccount` liquidation
function _initVars(address creditAccount) internal view returns (LiquidationVars memory vars) {
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();
Expand All @@ -169,32 +160,47 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
vars.liquidationDiscount =
PERCENTAGE_FACTOR - (PERCENTAGE_FACTOR - liquidationDiscount) * premiumScaleFactor / PERCENTAGE_FACTOR;
vars.feeLiquidation = feeLiquidation * feeScaleFactor / PERCENTAGE_FACTOR;
try IPhantomToken(token).getPhantomTokenInfo() returns (address, address depositedToken) {
vars.receivedToken = depositedToken;
} catch {
vars.receivedToken = token;
}
}

/// @dev Ensures that `creditAccount` is liquidatable and `token` is not underlying
function _validateLiquidation(LiquidationVars memory vars, address creditAccount, address token) internal view {
if (token == vars.underlying) revert UnderlyingNotLiquidatableException();
function _validateLiquidation(LiquidationVars memory vars, address creditAccount) internal view {
if (vars.receivedToken == vars.underlying) revert UnderlyingNotLiquidatableException();
if (!_isLiquidatable(_calcDebtAndCollateral(vars.creditManager, creditAccount), minHealthFactor)) {
revert CreditAccountNotLiquidatableException();
}
}

/// @dev Executes partial liquidation:
/// - transfers `repaidAmount` of underlying from the caller to `creditAccount`
/// - performs a multicall on `creditAccount` that repays debt, withdraws fee to the treasury,
/// and withdraws `seizedAmount` of `token` to `to`
/// @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
/// - amount of collateral that should be withdrawn to the liquidator
function _calcPartialLiquidationPayments(LiquidationVars memory vars, uint256 amount, address token)
internal
view
returns (uint256 repaidAmount, uint256 fee, uint256 seizedAmount)
{
seizedAmount = IPriceOracleV3(vars.priceOracle).convert(amount, vars.underlying, token) * PERCENTAGE_FACTOR
/ vars.liquidationDiscount;
fee = amount * vars.feeLiquidation / PERCENTAGE_FACTOR;
repaidAmount = amount - fee;
}

/// @dev Executes partial liquidation by performing a multicall on `creditAccount` that repays debt,
/// withdraws fee to the treasury and withdraws `token` to `to`
function _executeLiquidation(
LiquidationVars memory vars,
address creditAccount,
address token,
uint256 repaidAmount,
uint256 seizedAmount,
uint256 fee,
address to
) internal {
IERC20(vars.underlying).safeTransferFrom(msg.sender, creditAccount, repaidAmount);
uint256 fee = repaidAmount * vars.feeLiquidation / PERCENTAGE_FACTOR;
repaidAmount -= fee;

) internal returns (uint256 receivedAmount) {
MultiCall[] memory calls = new MultiCall[](3);
calls[0] = MultiCall({
target: vars.creditFacade,
Expand All @@ -208,9 +214,13 @@ contract PartialLiquidationBotV3 is IPartialLiquidationBotV3, ReentrancyGuardTra
target: vars.creditFacade,
callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (token, seizedAmount, to))
});
uint256 balanceBefore = IERC20(vars.receivedToken).safeBalanceOf(to);
ICreditFacadeV3(vars.creditFacade).botMulticall(creditAccount, calls);
receivedAmount = IERC20(vars.receivedToken).safeBalanceOf(to) - balanceBefore;

emit LiquidatePartial(vars.creditManager, creditAccount, token, repaidAmount, seizedAmount, fee);
emit PartiallyLiquidate(
vars.creditManager, creditAccount, vars.receivedToken, repaidAmount, receivedAmount, fee
);
}

/// @dev Ensures that `creditAccount`'s health factor is within allowed range after partial liquidation
Expand Down
33 changes: 5 additions & 28 deletions contracts/interfaces/IPartialLiquidationBotV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface IPartialLiquidationBotV3 is IBot {
/// @param repaidDebt Amount of `creditAccount`'s debt repaid
/// @param seizedCollateral Amount of `token` seized from `creditAccount`
/// @param fee Amount of underlying sent to the treasury as liqudiation fee
event LiquidatePartial(
event PartiallyLiquidate(
address indexed creditManager,
address indexed creditAccount,
address indexed token,
Expand All @@ -40,9 +40,6 @@ interface IPartialLiquidationBotV3 is IBot {
/// @notice Thrown when health factor after liquidation is greater than maximum allowed
error LiquidatedMoreThanNeededException();

/// @notice Thrown when amount of underlying repaid is greater than allowed
error RepaidMoreThanAllowedException();

/// @notice Thrown when amount of collateral seized is less than required
error SeizedLessThanRequiredException();

Expand Down Expand Up @@ -77,38 +74,18 @@ interface IPartialLiquidationBotV3 is IBot {
/// @param priceUpdates On-demand price feed updates to apply before calculations
/// @return seizedAmount Amount of `token` seized
/// @dev Requires underlying token approval from caller to this contract
/// @dev Reverts if `token` is underlying
/// @dev Reverts if `token` is underlying or if `token` is a phantom token and its `depositedToken` is underlying
/// @dev Reverts if `creditAccount`'s health factor is not less than `minHealthFactor` before liquidation
/// @dev Reverts if amount of `token` to be seized is less than `minSeizedAmount`
/// @dev Reverts if `creditAccount`'s health factor is not within allowed range after liquidation
function liquidateExactDebt(
/// @dev If `token` is a phantom token, it's withdrawn first, and its `depositedToken` is then sent to the liquidator.
/// Both `seizedAmount` and `minSeizedAmount` refer to `depositedToken` in this case.
function partiallyLiquidate(
address creditAccount,
address token,
uint256 repaidAmount,
uint256 minSeizedAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external returns (uint256 seizedAmount);

/// @notice Liquidates credit account by repaying its debt in exchange for the given amount of discounted collateral
/// @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 underlying to repay
/// @param to Address to send seized `token` to
/// @param priceUpdates On-demand price feed updates to apply before calculations
/// @return repaidAmount Amount of underlying repaid
/// @dev Requires underlying token approval from caller to this contract
/// @dev Reverts if `token` is underlying
/// @dev Reverts if `creditAccount`'s health factor is not less than `minHealthFactor` before liquidation
/// @dev Reverts if amount of underlying to be repaid is greater than `maxRepaidAmount`
/// @dev Reverts if `creditAccount`'s health factor is not within allowed range after liquidation
function liquidateExactCollateral(
address creditAccount,
address token,
uint256 seizedAmount,
uint256 maxRepaidAmount,
address to,
PriceUpdate[] calldata priceUpdates
) external returns (uint256 repaidAmount);
}
Loading