Skip to content

Commit

Permalink
feat: BotListV3 improvements (#199)
Browse files Browse the repository at this point in the history
In this PR:
* new `IBotV3` interface that defines bot's required permissions which are checked in the bot list
* special permission bots are deprecated
* `BotListV3` interface changes:
  * redundant `creditManager` parameter removed from most functions that operate on `creditAccount`
  * `EraseBot` event removed in favor of `SetBotPermissions(..., 0)`
* minor implementation improvements that make code more consistent
* increase `BotListV3` version to `3_10`
  • Loading branch information
lekhovitsky authored Apr 23, 2024
1 parent 832fe64 commit 97a88db
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 306 deletions.
128 changes: 43 additions & 85 deletions contracts/core/BotListV3.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// 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 {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {IBotListV3, BotInfo} from "../interfaces/IBotListV3.sol";
import {IBotV3} from "../interfaces/IBotV3.sol";
import {ICreditAccountBase} from "../interfaces/ICreditAccountV3.sol";
import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol";
import {
AddressIsNotContractException,
CallerNotCreditFacadeException,
InsufficientBotPermissionsException,
InvalidBotException
} from "../interfaces/IExceptions.sol";

Expand All @@ -19,15 +21,11 @@ import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol";

/// @title Bot list V3
/// @notice Stores bot permissions (bit masks dictating which actions can be performed with credit accounts in multicall).
/// Besides normal per-account permissions, there are special per-manager permissions that apply to all accounts
/// in a given credit manager and can be used to extend the core system or enforce additional safety measures
/// with special DAO-approved bots.
contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
using Address for address;
using EnumerableSet for EnumerableSet.AddressSet;

/// @notice Contract version
uint256 public constant override version = 3_00;
uint256 public constant override version = 3_10;

/// @notice Credit manager's approved status
mapping(address => bool) public override approvedCreditManager;
Expand All @@ -38,12 +36,6 @@ contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
/// @dev Mapping credit manager => credit account => set of bots with non-zero permissions
mapping(address => mapping(address => EnumerableSet.AddressSet)) internal _activeBots;

/// @dev Ensures that function can only be called by a facade connected to approved `creditManager`
modifier onlyValidCreditFacade(address creditManager) {
_revertIfCallerNotValidCreditFacade(creditManager);
_;
}

/// @notice Constructor
/// @param addressProvider Address provider contract address
constructor(address addressProvider)
Expand All @@ -55,88 +47,78 @@ contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
// PERMISSIONS //
// ----------- //

/// @notice Returns `bot`'s permissions for `creditAccount` in `creditManager`
function botPermissions(address bot, address creditManager, address creditAccount)
external
view
override
returns (uint192)
{
/// @notice Returns `bot`'s permissions for `creditAccount` in its credit manager
function botPermissions(address bot, address creditAccount) external view override returns (uint192) {
address creditManager = ICreditAccountBase(creditAccount).creditManager();
return _botInfo[bot].permissions[creditManager][creditAccount];
}

/// @notice Returns all bots with non-zero permissions for `creditAccount` in `creditManager`
function activeBots(address creditManager, address creditAccount)
external
view
override
returns (address[] memory)
{
/// @notice Returns all bots with non-zero permissions for `creditAccount` in its credit manager
function activeBots(address creditAccount) external view override returns (address[] memory) {
address creditManager = ICreditAccountBase(creditAccount).creditManager();
return _activeBots[creditManager][creditAccount].values();
}

/// @notice Returns `bot`'s permissions for `creditAccount` in `creditManager`, including information
/// on whether bot is forbidden or has special permissions in the credit manager
function getBotStatus(address bot, address creditManager, address creditAccount)
/// @notice Returns `bot`'s permissions for `creditAccount` in its credit manager and whether it is forbidden
function getBotStatus(address bot, address creditAccount)
external
view
override
returns (uint192 permissions, bool forbidden, bool hasSpecialPermissions)
returns (uint192 permissions, bool forbidden)
{
BotInfo storage info = _botInfo[bot];
if (info.forbidden) return (0, true, false);
if (info.forbidden) return (0, true);

uint192 specialPermissions = info.specialPermissions[creditManager];
if (specialPermissions != 0) return (specialPermissions, false, true);

return (info.permissions[creditManager][creditAccount], false, false);
address creditManager = ICreditAccountBase(creditAccount).creditManager();
return (info.permissions[creditManager][creditAccount], false);
}

/// @notice Sets `bot`'s permissions for `creditAccount` in `creditManager` to `permissions`
/// @notice Sets `bot`'s permissions for `creditAccount` in its credit manager to `permissions`
/// @return activeBotsRemaining Number of bots with non-zero permissions remaining after the update
/// @dev Reverts if caller is not a facade connected to approved `creditManager`
/// @dev Reverts if `bot` is zero address or not a contract
/// @dev Reverts if trying to set non-zero permissions for a forbidden bot or for a bot with special permissions
function setBotPermissions(address bot, address creditManager, address creditAccount, uint192 permissions)
/// @dev Reverts if `creditAccount`'s credit manager is not approved or caller is not a facade connected to it
/// @dev Reverts if trying to set non-zero permissions that don't meet bot's requirements
/// @dev Reverts if trying to set non-zero permissions for a forbidden bot
/// @custom:tests U:[BL-1]
function setBotPermissions(address bot, address creditAccount, uint192 permissions)
external
override
nonZeroAddress(bot)
onlyValidCreditFacade(creditManager)
returns (uint256 activeBotsRemaining)
{
if (!bot.isContract()) revert AddressIsNotContractException(bot);
address creditManager = ICreditAccountBase(creditAccount).creditManager();
_revertIfCallerNotValidCreditFacade(creditManager);

BotInfo storage info = _botInfo[bot];
EnumerableSet.AddressSet storage accountBots = _activeBots[creditManager][creditAccount];

if (permissions != 0) {
BotInfo storage info = _botInfo[bot];
if (info.forbidden || info.specialPermissions[creditManager] != 0) {
revert InvalidBotException();
}

if (IBotV3(bot).requiredPermissions() & ~permissions != 0) revert InsufficientBotPermissionsException();
if (info.forbidden) revert InvalidBotException();
accountBots.add(bot);
info.permissions[creditManager][creditAccount] = permissions;
emit SetBotPermissions(bot, creditManager, creditAccount, permissions);
} else {
_eraseBot(bot, creditManager, creditAccount);
accountBots.remove(bot);
}

activeBotsRemaining = accountBots.length();

if (info.permissions[creditManager][creditAccount] != permissions) {
info.permissions[creditManager][creditAccount] = permissions;
emit SetBotPermissions(bot, creditManager, creditAccount, permissions);
}
}

/// @notice Removes all bots' permissions for `creditAccount` in `creditManager`
function eraseAllBotPermissions(address creditManager, address creditAccount)
external
override
onlyValidCreditFacade(creditManager)
{
/// @notice Removes all bots' permissions for `creditAccount` in its credit manager
/// @dev Reverts if `creditAccount`'s credit manager is not approved or caller is not a facade connected to it
/// @custom:tests U:[BL-2]
function eraseAllBotPermissions(address creditAccount) external override {
address creditManager = ICreditAccountBase(creditAccount).creditManager();
_revertIfCallerNotValidCreditFacade(creditManager);

EnumerableSet.AddressSet storage accountBots = _activeBots[creditManager][creditAccount];
unchecked {
for (uint256 i = accountBots.length(); i != 0; --i) {
address bot = accountBots.at(i - 1);
_eraseBot(bot, creditManager, creditAccount);
accountBots.remove(bot);
_botInfo[bot].permissions[creditManager][creditAccount] = 0;
emit SetBotPermissions(bot, creditManager, creditAccount, 0);
}
}
}
Expand All @@ -150,11 +132,6 @@ contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
return _botInfo[bot].forbidden;
}

/// @notice Returns `bot`'s special permissions in `creditManager`
function botSpecialPermissions(address bot, address creditManager) external view override returns (uint192) {
return _botInfo[bot].specialPermissions[creditManager];
}

/// @notice Sets `bot`'s status to `forbidden`
function setBotForbiddenStatus(address bot, bool forbidden) external override configuratorOnly {
BotInfo storage info = _botInfo[bot];
Expand All @@ -164,19 +141,6 @@ contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
}
}

/// @notice Sets `bot`'s special permissions in `creditManager` to `permissions`
function setBotSpecialPermissions(address bot, address creditManager, uint192 permissions)
external
override
configuratorOnly
{
BotInfo storage info = _botInfo[bot];
if (info.specialPermissions[creditManager] != permissions) {
info.specialPermissions[creditManager] = permissions;
emit SetBotSpecialPermissions(bot, creditManager, permissions);
}
}

/// @notice Sets `creditManager`'s status to `approved`
function setCreditManagerApprovedStatus(address creditManager, bool approved)
external
Expand All @@ -194,16 +158,10 @@ contract BotListV3 is ACLNonReentrantTrait, ContractsRegisterTrait, IBotListV3 {
// INTERNALS //
// --------- //

/// @dev Reverts if `creditManager` is not approved or caller is not a facade connected to `creditManager`
/// @dev Reverts if `creditManager` is not approved or caller is not a facade connected to it
function _revertIfCallerNotValidCreditFacade(address creditManager) internal view {
if (!approvedCreditManager[creditManager] || ICreditManagerV3(creditManager).creditFacade() != msg.sender) {
revert CallerNotCreditFacadeException();
}
}

/// @dev Removes `bot`'s permissions for `creditAccount` in `creditManager`
function _eraseBot(address bot, address creditManager, address creditAccount) internal {
delete _botInfo[bot].permissions[creditManager][creditAccount];
emit EraseBot(bot, creditManager, creditAccount);
}
}
33 changes: 11 additions & 22 deletions contracts/credit/CreditFacadeV3.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;

// THIRD-PARTY
Expand Down Expand Up @@ -72,7 +72,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
using SafeERC20 for IERC20;

/// @notice Contract version
uint256 public constant override version = 3_01;
uint256 public constant override version = 3_10;

/// @notice Maximum quota size, as a multiple of `maxDebt`
uint256 public constant override maxQuotaMultiplier = 2;
Expand Down Expand Up @@ -157,7 +157,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {

address addressProvider = ICreditManagerV3(_creditManager).addressProvider();
weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // U:[FA-1]
botList = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_BOT_LIST, 3_00); // U:[FA-1]
botList = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_BOT_LIST, 3_10); // U:[FA-1]

degenNFT = _degenNFT; // U:[FA-1]

Expand Down Expand Up @@ -255,7 +255,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
if (enabledTokensMask != 0) revert CloseAccountWithEnabledTokensException(); // U:[FA-11]

if (_flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG != 0) {
IBotListV3(botList).eraseAllBotPermissions(creditManager, creditAccount); // U:[FA-11]
IBotListV3(botList).eraseAllBotPermissions(creditAccount); // U:[FA-11]
}

ICreditManagerV3(creditManager).closeCreditAccount(creditAccount); // U:[FA-11]
Expand Down Expand Up @@ -370,8 +370,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
}

/// @notice Executes a batch of calls allowing bot to manage a credit account
/// - Performs a multicall (allowed calls are determined by permissions given by account's owner
/// or by DAO in case bot has special permissions in the credit manager)
/// - Performs a multicall (allowed calls are determined by permissions given by account's owner)
/// - Runs the collateral check
/// @param creditAccount Account to perform the calls on
/// @param calls List of calls to perform
Expand All @@ -387,16 +386,10 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
{
_getBorrowerOrRevert(creditAccount); // U:[FA-5]

(uint256 botPermissions, bool forbidden, bool hasSpecialPermissions) = IBotListV3(botList).getBotStatus({
bot: msg.sender,
creditManager: creditManager,
creditAccount: creditAccount
});
(uint256 botPermissions, bool forbidden) =
IBotListV3(botList).getBotStatus({bot: msg.sender, creditAccount: creditAccount});

if (
botPermissions == 0 || forbidden
|| (!hasSpecialPermissions && (_flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG == 0))
) {
if (forbidden || botPermissions == 0 || _flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG == 0) {
revert NotApprovedBotException(); // U:[FA-19]
}

Expand All @@ -408,7 +401,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
/// @param bot Bot to set permissions for
/// @param permissions A bit mask encoding bot permissions
/// @dev Reverts if `creditAccount` is not opened in connected credit manager by caller
/// @dev Reverts if `permissions` has unexpected bits enabled
/// @dev Reverts if `permissions` has unexpected bits enabled 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 creditAccount, address bot, uint192 permissions)
Expand All @@ -419,12 +412,8 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait {
{
if (permissions & ~ALL_PERMISSIONS != 0) revert UnexpectedPermissionsException(); // U:[FA-41]

uint256 remainingBots = IBotListV3(botList).setBotPermissions({
bot: bot,
creditManager: creditManager,
creditAccount: creditAccount,
permissions: permissions
}); // U:[FA-41]
uint256 remainingBots =
IBotListV3(botList).setBotPermissions({bot: bot, creditAccount: creditAccount, permissions: permissions}); // U:[FA-41]

if (remainingBots == 0) {
_setFlagFor({creditAccount: creditAccount, flag: BOT_PERMISSIONS_SET_FLAG, value: false}); // U:[FA-41]
Expand Down
Loading

0 comments on commit 97a88db

Please sign in to comment.