Skip to content

Commit

Permalink
feat: PartialLiquidationV3 bot upgrade (#6)
Browse files Browse the repository at this point in the history
In this PR:
* remove `liquidateExactCollateral`, rename `liquidateExactDebt` into `partiallyLiquidate`
* support fee-on-transfer underlyings and phantom collaterals
* add serialization
  • Loading branch information
lekhovitsky authored Jul 21, 2024
1 parent 2875a62 commit 807ccc7
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 162 deletions.
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

0 comments on commit 807ccc7

Please sign in to comment.