Skip to content

Commit

Permalink
feat: CreditFacadeV3 improvements (#207)
Browse files Browse the repository at this point in the history
In this PR:
* move `setBotPermissions` inside multicall
* overall code simplification and improvements
* add params in facade exceptions to make them slightly more useful
* confusing `IncreaseDebt` and `DecreaseDebt` events are removed in favour of pool's `Borrow` and `Repay`
  • Loading branch information
lekhovitsky authored May 8, 2024
1 parent 5478414 commit 31fc139
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 632 deletions.
418 changes: 180 additions & 238 deletions contracts/credit/CreditFacadeV3.sol

Large diffs are not rendered by default.

16 changes: 2 additions & 14 deletions contracts/interfaces/ICreditFacadeV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,9 @@ struct CumulativeLossParams {
/// @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
/// @param minHealthFactor Min account's health factor in bps in order not to revert
/// @param enabledTokensMask Bitmask of account's enabled collateral tokens after the multicall
/// @param revertOnForbiddenTokens Whether to revert on enabled forbidden tokens after the multicall
/// @param useSafePrices Whether to use safe pricing (min of main and reserve feeds) when evaluating collateral
struct FullCheckParams {
uint256[] collateralHints;
uint16 minHealthFactor;
uint256 enabledTokensMask;
bool revertOnForbiddenTokens;
bool useSafePrices;
}

interface ICreditFacadeV3Events {
Expand All @@ -54,12 +48,6 @@ interface ICreditFacadeV3Events {
address indexed creditAccount, address indexed liquidator, address to, uint256 remainingFunds
);

/// @notice Emitted when account's debt is increased
event IncreaseDebt(address indexed creditAccount, uint256 amount);

/// @notice Emitted when account's debt is decreased
event DecreaseDebt(address indexed creditAccount, uint256 amount);

/// @notice Emitted when collateral is added to account
event AddCollateral(address indexed creditAccount, address indexed token, uint256 amount);

Expand All @@ -80,6 +68,8 @@ interface ICreditFacadeV3Events {
interface ICreditFacadeV3 is IVersion, ICreditFacadeV3Events {
function creditManager() external view returns (address);

function underlying() external view returns (address);

function degenNFT() external view returns (address);

function weth() external view returns (address);
Expand Down Expand Up @@ -119,8 +109,6 @@ interface ICreditFacadeV3 is IVersion, ICreditFacadeV3Events {

function botMulticall(address creditAccount, MultiCall[] calldata calls) external;

function setBotPermissions(address creditAccount, address bot, uint192 permissions) external;

// ------------- //
// CONFIGURATION //
// ------------- //
Expand Down
37 changes: 34 additions & 3 deletions contracts/interfaces/ICreditFacadeV3Multicall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,46 @@ import {PriceUpdate} from "./IPriceOracleV3.sol";
// PERMISSIONS //
// ----------- //

// NOTE: permissions 1 << 3, 1 << 4 and 1 << 7 were used by now deprecated methods, thus non-consecutive values

uint192 constant ADD_COLLATERAL_PERMISSION = 1 << 0;
uint192 constant INCREASE_DEBT_PERMISSION = 1 << 1;
uint192 constant DECREASE_DEBT_PERMISSION = 1 << 2;
uint192 constant WITHDRAW_COLLATERAL_PERMISSION = 1 << 5;
uint192 constant UPDATE_QUOTA_PERMISSION = 1 << 6;
uint192 constant SET_BOT_PERMISSIONS_PERMISSION = 1 << 8;
uint192 constant EXTERNAL_CALLS_PERMISSION = 1 << 16;

uint256 constant ALL_PERMISSIONS = ADD_COLLATERAL_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION | INCREASE_DEBT_PERMISSION
| DECREASE_DEBT_PERMISSION | UPDATE_QUOTA_PERMISSION | EXTERNAL_CALLS_PERMISSION;
uint192 constant ALL_PERMISSIONS = ADD_COLLATERAL_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION | UPDATE_QUOTA_PERMISSION
| INCREASE_DEBT_PERMISSION | DECREASE_DEBT_PERMISSION | SET_BOT_PERMISSIONS_PERMISSION | EXTERNAL_CALLS_PERMISSION;
uint192 constant OPEN_CREDIT_ACCOUNT_PERMISSIONS = ALL_PERMISSIONS & ~DECREASE_DEBT_PERMISSION;
uint192 constant CLOSE_CREDIT_ACCOUNT_PERMISSIONS = ALL_PERMISSIONS & ~INCREASE_DEBT_PERMISSION;
uint192 constant LIQUIDATE_CREDIT_ACCOUNT_PERMISSIONS =
EXTERNAL_CALLS_PERMISSION | ADD_COLLATERAL_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION;

// ----- //
// FLAGS //
// ----- //

/// @dev Indicates that collateral check after the multicall can be skipped, set to true on account closure or liquidation
uint256 constant SKIP_COLLATERAL_CHECK = 1 << 192;

/// @dev Indicates that external calls from credit account to adapters were made during multicall,
/// set to true on the first call to the adapter
uint256 constant EXTERNAL_CONTRACT_WAS_CALLED = 1 << 193;

/// @dev Indicates that the price updates call should be skipped, set to true on liquidation when the first call
/// of the multicall is `onDemandPriceUpdates`
uint256 constant SKIP_PRICE_UPDATES_CALL = 1 << 194;

/// @dev Indicates that collateral check must revert if any forbidden token is encountered on the account,
/// set to true after risky operations, such as `increaseDebt` or `withdrawCollateral`
uint256 constant REVERT_ON_FORBIDDEN_TOKENS = 1 << 195;

/// @dev Indicates that collateral check must be performed using safe prices, set to true on `withdrawCollateral`
/// or if account has enabled forbidden tokens
uint256 constant USE_SAFE_PRICES = 1 << 196;

/// @title Credit facade V3 multicall interface
/// @dev Unless specified otherwise, all these methods are only available in `openCreditAccount`,
/// `closeCreditAccount`, `multicall`, and, with account owner's permission, `botMulticall`
Expand Down Expand Up @@ -111,6 +133,15 @@ interface ICreditFacadeV3Multicall {
/// when known subset of account's collateral tokens covers all the debt. Underlying token is always
/// checked last so it's forbidden to pass its mask.
/// @param minHealthFactor Min account's health factor in bps in order not to revert, must be at least 10000
/// @dev This method is available in all kinds of multicalls
/// @dev This method can't be called during closure or liquidation
function setFullCheckParams(uint256[] calldata collateralHints, uint16 minHealthFactor) external;

/// @notice Sets `bot`'s permissions to manage account to `permissions`
/// @param bot Bot to set permissions for
/// @param permissions A bitmask encoding bot permissions
/// @dev Reverts if `permissions` has unexpected bits enabled (including `SET_BOT_PERMISSIONS_PERMISSION`,
/// that would have been way too tricky) or some bits required by `bot` disabled
/// @dev Reverts if account has more active bots than allowed after changing permissions
/// @dev Changes account's `BOT_PERMISSIONS_SET_FLAG` in the credit manager if needed
function setBotPermissions(address bot, uint192 permissions) external;
}
18 changes: 9 additions & 9 deletions contracts/interfaces/IExceptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ error ForbiddenInWhitelistedModeException();
error NotAllowedWhenNotExpirableException();

/// @notice Thrown if a selector that doesn't match any allowed function is passed to the credit facade in a multicall
error UnknownMethodException();
error UnknownMethodException(bytes4 selector);

/// @notice Thrown if a liquidator tries to liquidate an account with a health factor above 1
error CreditAccountNotLiquidatableException();
Expand All @@ -182,34 +182,34 @@ error ExpectedBalancesAlreadySetException();
error ExpectedBalancesNotSetException();

/// @notice Thrown if balance of at least one token is less than expected during a slippage check
error BalanceLessThanExpectedException();
error BalanceLessThanExpectedException(address token);

/// @notice Thrown when trying to perform an action that is forbidden when credit account has enabled forbidden tokens
error ForbiddenTokensException();
error ForbiddenTokensException(uint256 forbiddenTokensMask);

/// @notice Thrown when forbidden token quota is increased during the multicall
error ForbiddenTokenQuotaIncreasedException();
error ForbiddenTokenQuotaIncreasedException(address token);

/// @notice Thrown when enabled forbidden token balance is increased during the multicall
error ForbiddenTokenBalanceIncreasedException();
error ForbiddenTokenBalanceIncreasedException(address token);

/// @notice Thrown when the remaining token balance is increased during the liquidation
error RemainingTokenBalanceIncreasedException();
error RemainingTokenBalanceIncreasedException(address token);

/// @notice Thrown if `botMulticall` is called by an address that is not approved by account owner or is forbidden
error NotApprovedBotException();
error NotApprovedBotException(address bot);

/// @notice Thrown when attempting to perform a multicall action with no permission for it
error NoPermissionException(uint256 permission);

/// @notice Thrown when attempting to give a bot unexpected permissions
error UnexpectedPermissionsException();
error UnexpectedPermissionsException(uint256 permissions);

/// @notice Thrown when a custom HF parameter lower than 10000 is passed into the full collateral check
error CustomHealthFactorTooLowException();

/// @notice Thrown when submitted collateral hint is not a valid token mask
error InvalidCollateralHintException();
error InvalidCollateralHintException(uint256 mask);

// ------ //
// ACCESS //
Expand Down
29 changes: 13 additions & 16 deletions contracts/libraries/BalancesLogic.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2023.
// (c) Gearbox Foundation, 2024.
pragma solidity ^0.8.17;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Expand Down Expand Up @@ -72,21 +72,20 @@ library BalancesLogic {
/// @param creditAccount Credit account to compare balances for
/// @param balances Array of previously stored balances
/// @param comparison Whether current balances must be greater/less than or equal to stored ones
/// @return success True if condition specified by `comparison` holds for all tokens, false otherwise
/// @return failedToken The first token for which the condition specified by `comparison` fails, if any
function compareBalances(address creditAccount, Balance[] memory balances, Comparison comparison)
internal
view
returns (bool success)
returns (address failedToken)
{
uint256 len = balances.length;
unchecked {
for (uint256 i = 0; i < len; ++i) {
uint256 len = balances.length;
for (uint256 i; i < len; ++i) {
if (!BalancesLogic.checkBalance(creditAccount, balances[i].token, balances[i].balance, comparison)) {
return false; // U:[BLL-3]
return balances[i].token; // U:[BLL-3]
}
}
}
return true; // U:[BLL-3]
}

/// @dev Returns balances of specified tokens on the credit account
Expand Down Expand Up @@ -123,26 +122,24 @@ library BalancesLogic {
/// @param tokensMask Bit mask of tokens to compare balances for
/// @param balances Array of previously stored balances
/// @param comparison Whether current balances must be greater/less than or equal to stored ones
/// @return success True if condition specified by `comparison` holds for all tokens, false otherwise
/// @return failedToken The first token for which the condition specified by `comparison` fails, if any
/// @dev This function assumes that `tokensMask` encodes a subset of tokens from `balances`
function compareBalances(
address creditAccount,
uint256 tokensMask,
BalanceWithMask[] memory balances,
Comparison comparison
) internal view returns (bool) {
if (tokensMask == 0) return true;
) internal view returns (address failedToken) {
if (tokensMask == 0) return address(0);

unchecked {
uint256 len = balances.length;
for (uint256 i; i < len; ++i) {
if (tokensMask & balances[i].tokenMask != 0) {
if (!BalancesLogic.checkBalance(creditAccount, balances[i].token, balances[i].balance, comparison))
{
return false; // U:[BLL-5]
}
if (tokensMask & balances[i].tokenMask == 0) continue;
if (!BalancesLogic.checkBalance(creditAccount, balances[i].token, balances[i].balance, comparison)) {
return balances[i].token; // U:[BLL-5]
}
}
}
return true; // U:[BLL-5]
}
}
32 changes: 19 additions & 13 deletions contracts/test/integration/credit/Bots.int.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events {
bytes memory DUMB_CALLDATA = adapterMock.dumbCallData();

vm.prank(address(creditFacade));
botList.setBotPermissions(bot, creditAccount, uint192(ALL_PERMISSIONS));
botList.setBotPermissions(bot, creditAccount, ALL_PERMISSIONS & ~SET_BOT_PERMISSIONS_PERMISSION);

vm.expectRevert(NotApprovedBotException.selector);
vm.expectRevert(abi.encodeWithSelector(NotApprovedBotException.selector, (address(this))));
creditFacade.botMulticall(
creditAccount, MultiCallBuilder.build(MultiCall({target: address(adapterMock), callData: DUMB_CALLDATA}))
);
Expand All @@ -66,8 +66,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events {
MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))})
);

vm.prank(USER);
creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS));
_setBotPermissions(USER, creditAccount, bot, ALL_PERMISSIONS & ~SET_BOT_PERMISSIONS_PERMISSION);

botList.getBotStatus({creditAccount: creditAccount, bot: bot});

Expand Down Expand Up @@ -103,7 +102,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events {
vm.prank(CONFIGURATOR);
botList.setBotForbiddenStatus(bot, true);

vm.expectRevert(NotApprovedBotException.selector);
vm.expectRevert(abi.encodeWithSelector(NotApprovedBotException.selector, (bot)));
vm.prank(bot);
creditFacade.botMulticall(creditAccount, calls);
}
Expand All @@ -114,17 +113,12 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events {

address bot = address(new BotMock());

vm.expectRevert(CallerNotCreditAccountOwnerException.selector);
vm.prank(FRIEND);
creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS));

vm.expectCall(
address(creditManager),
abi.encodeCall(CreditManagerV3.setFlagFor, (creditAccount, BOT_PERMISSIONS_SET_FLAG, true))
);

vm.prank(USER);
creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS));
_setBotPermissions(USER, creditAccount, bot, ALL_PERMISSIONS & ~SET_BOT_PERMISSIONS_PERMISSION);

assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG > 0, "Flag was not set");

Expand All @@ -133,9 +127,21 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events {
abi.encodeCall(CreditManagerV3.setFlagFor, (creditAccount, BOT_PERMISSIONS_SET_FLAG, false))
);

vm.prank(USER);
creditFacade.setBotPermissions(creditAccount, bot, 0);
_setBotPermissions(USER, creditAccount, bot, 0);

assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG == 0, "Flag was not set");
}

function _setBotPermissions(address user, address creditAccount, address bot, uint192 permissions) internal {
vm.prank(user);
creditFacade.multicall(
creditAccount,
MultiCallBuilder.build(
MultiCall(
address(creditFacade),
abi.encodeCall(ICreditFacadeV3Multicall.setBotPermissions, (bot, permissions))
)
)
);
}
}
14 changes: 9 additions & 5 deletions contracts/test/integration/credit/CloseCreditAccount.int.sol
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,15 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca
address bot = address(new BotMock());

vm.prank(USER);
creditFacade.setBotPermissions({
creditAccount: creditAccount,
bot: bot,
permissions: uint192(ADD_COLLATERAL_PERMISSION)
});
creditFacade.multicall(
creditAccount,
MultiCallBuilder.build(
MultiCall(
address(creditFacade),
abi.encodeCall(ICreditFacadeV3Multicall.setBotPermissions, (bot, ADD_COLLATERAL_PERMISSION))
)
)
);

// LIST OF EXPECTED CALLS

Expand Down
Loading

0 comments on commit 31fc139

Please sign in to comment.